diff --git a/.ci/run_tests.sh b/.ci/run_tests.sh index 75662ccb2a16..5aed8e0c0ef1 100755 --- a/.ci/run_tests.sh +++ b/.ci/run_tests.sh @@ -2,15 +2,11 @@ set -e -# check for docker-compose and docker availability +# check for docker availability command -v docker > /dev/null || { echo "Please install docker" >&2 exit 1 } -command -v docker-compose > /dev/null || { - echo "Please install docker-compose" >&2 - exit 1 -} IMAGE_BUILD_DEPS=qgis/qgis3-build-deps:latest UPDATE_IMAGES=yes @@ -115,7 +111,7 @@ if test "$(docker images -q qgis3-build-deps-binary-image)" = ""; then fi if test "${INTERACTIVE}" = "no"; then - echo "--=[ Running tests via docker-compose" + echo "--=[ Running tests via docker compose" COMMAND=${QGIS_WORKSPACE_MOUNTPOINT}/.docker/docker-qgis-test.sh COMMAND_ARGS="${TESTS_TO_RUN}" else @@ -129,7 +125,7 @@ mkdir -p /tmp/minio_tests/test-bucket && chmod -R 777 /tmp/minio_tests # Create an empty webdav folder with appropriate permissions so www user can write inside it mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests -docker-compose \ +docker compose \ -f .docker/docker-compose-testing.yml \ run \ -w "${QGIS_WORKSPACE_MOUNTPOINT}" \ diff --git a/.docker/qgis3-qt5-build-deps.dockerfile b/.docker/qgis3-qt5-build-deps.dockerfile index eb73721c4877..4eb0263eba34 100644 --- a/.docker/qgis3-qt5-build-deps.dockerfile +++ b/.docker/qgis3-qt5-build-deps.dockerfile @@ -152,7 +152,7 @@ RUN apt-get update \ # HANA: client side # Install hdbsql tool -RUN curl -j -k -L -H "Cookie: eula_3_1_agreed=tools.hana.ondemand.com/developer-license-3_1.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ +RUN curl -j -k -L -H "Cookie: eula_3_2_agreed=tools.hana.ondemand.com/developer-license-3_2.txt" https://tools.hana.ondemand.com/additional/hanaclient-latest-linux-x64.tar.gz --output hanaclient-latest-linux-x64.tar.gz \ && tar -xvf hanaclient-latest-linux-x64.tar.gz \ && mkdir /usr/sap \ && ./client/hdbinst -a client --sapmnt=/usr/sap \ diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index a49d28af767d..000000000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: 🐳 Build Docker images for current branches - -# on commits to this file, schedule and dispatch runs, the workflow will build the 3 different Docker images (master, PR, LTR) -# on tags, it will build only the image of the given tag -# this is made by using a matrix defined in a dedicated job -on: - push: - tags: - - final-* - branches: - - master - paths: - - .github/workflows/build-docker.yml - schedule: - # runs every day - - cron: '0 0 * * *' - workflow_dispatch: - # POST https://api.github.com/repos/qgis/QGIS/actions/workflows/2264135/dispatches: - -permissions: - contents: read - -jobs: - define-strategy: - permissions: - contents: none - if: github.repository_owner == 'qgis' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - steps: - - id: matrix - run: | - if [[ "${GITHUB_REF}" =~ ^refs/tags ]]; then - echo "matrix={\"branch\":[\"${GITHUB_REF##*/}\"]}" >> $GITHUB_OUTPUT - else - echo "matrix={\"branch\":[\"master\", \"release-3_34\", \"release-3_36\"]}" >> $GITHUB_OUTPUT - fi - - build-docker: - if: github.repository_owner == 'qgis' - runs-on: ubuntu-latest - needs: define-strategy - - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - CC: /usr/lib/ccache/gcc - CXX: /usr/lib/ccache/g++ # Building SIP binding freezes with Clang in Docker, maybe a SIP issue, maybe not - DOCKER_BUILD_DEPS_FILE: qgis3-qt5-build-deps.dockerfile - - strategy: - fail-fast: false - matrix: ${{ fromJSON( needs.define-strategy.outputs.matrix ) }} - - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - tool-cache: true - large-packages: true - docker-images: false - swap-storage: true - - - name: Free additional space - run: | - df -h - rm -rf /tmp/workspace - rm -rf /usr/share/dotnet/sdk - sudo apt remove llvm-* ghc-* google-chrome-* dotnet-sdk-* - dpkg-query -Wf '${Installed-Size}\t${Package}\n' | sort -n | tail -n 100 - du -a /usr/share | sort -n -r | head -n 10 - du -a /usr/local/share | sort -n -r | head -n 10 - df -h - sudo apt clean - df -h - - - name: Cache - id: cache - uses: actions/cache@v4 - with: - path: ~/.ccache - key: docker-build-${{ matrix.branch }}-${{ github.sha }} - restore-keys: | - docker-build-${{ matrix.branch }}- - docker-build-master- - - - name: checkout ${{ matrix.branch }} - uses: actions/checkout@v4 - with: - ref: ${{ matrix.branch }} - - - name: Define vars - env: - branch: ${{ matrix.branch }} - run: | - export DOCKER_TAG=${branch//master/latest} - export DOCKER_DEPS_TAG=${DOCKER_TAG} - - # add vars for next steps - echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV - echo "DOCKER_DEPS_TAG=${DOCKER_DEPS_TAG}" >> $GITHUB_ENV - - echo "branch: ${branch}" - echo "docker tag: ${DOCKER_TAG}" - echo "docker deps tag: ${DOCKER_DEPS_TAG}" - - - name: Copy cache - run: | - [[ -d ~/.ccache ]] && echo "cache directory (~/.ccache) exists" || mkdir -p ~/.ccache - # copy ccache dir within QGIS source so it can be accessed from docker - cp -r ~/.ccache/. ./.ccache_image_build - - - name: QGIS deps Docker pull/rebuild - run: | - cd .docker - docker --version - docker pull "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" || true - docker build --cache-from "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" -t "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" -f ${DOCKER_BUILD_DEPS_FILE} . - echo "push to qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker push "qgis/qgis3-build-deps:${DOCKER_DEPS_TAG}" - - - name: Docker QGIS build - run: | - cd .docker - DOCKER_BUILD_ARGS="--build-arg DOCKER_DEPS_TAG --build-arg CC --build-arg CXX" - docker build ${DOCKER_BUILD_ARGS} \ - --cache-from "qgis/qgis:${DOCKER_TAG}" \ - -t "qgis/qgis:BUILDER" \ - -f qgis.dockerfile .. - - - name: Tag container and copy cache - run: | - docker run --name qgis_container qgis/qgis:BUILDER /bin/true - docker cp qgis_container:/QGIS/build_exit_value ./build_exit_value - - if [[ $(cat ./build_exit_value) != "OK" ]]; then - echo "Build failed, not pushing image" - exit 1 - fi - - echo "Copy build cache from Docker container to Travis cache directory" - rm -rf ~/.ccache/* - mkdir -p ~/.ccache - docker cp qgis_container:/QGIS/.ccache_image_build/. ~/.ccache - echo "Cache size: "$(du -sh ~/.ccache) - - - name: Finalize image - run: | - cd .docker - # enable experimental features in Docker to squash - echo '{ "experimental": true}' | sudo tee /etc/docker/daemon.json - sudo service docker restart - docker build ${DOCKER_BUILD_ARGS} \ - --cache-from "qgis/qgis:BUILDER" \ - --squash \ - -t "qgis/qgis:${DOCKER_TAG}" \ - -f qgis.dockerfile .. - - - name: Pushing image to docker hub - run: | - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker push "qgis/qgis:${DOCKER_TAG}" - - - name: Trigger PyQGIS API docs build for ${{ matrix.branch }} - if: success() && !startsWith(github.ref, 'refs/tags/') - env: - branch: ${{ matrix.branch }} - run: | - body='{ - "ref": "master", - "inputs": {"qgis_branch": "__QGIS_VERSION_BRANCH__"} - }' - body=$(sed "s/__QGIS_VERSION_BRANCH__/${branch}/;" <<< $body) - curl -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${GH_TOKEN}" \ - https://api.github.com/repos/qgis/pyqgis/actions/workflows/2246440/dispatches \ - -d "${body}" - - - diff --git a/.github/workflows/code_layout.yml b/.github/workflows/code_layout.yml index 36308e379482..6389bb3fc28a 100644 --- a/.github/workflows/code_layout.yml +++ b/.github/workflows/code_layout.yml @@ -162,7 +162,7 @@ jobs: silversearcher-ag - name: Retrieve changed files - uses: tj-actions/changed-files@v43 + uses: tj-actions/changed-files@v44 id: changed_files with: separator: " " diff --git a/.github/workflows/mingw-w64-msys2.yml b/.github/workflows/mingw-w64-msys2.yml index 9c6d2f1429e6..c667f99f521b 100644 --- a/.github/workflows/mingw-w64-msys2.yml +++ b/.github/workflows/mingw-w64-msys2.yml @@ -65,7 +65,6 @@ jobs: - name: Configure QGIS run: | - CXXFLAGS="-DQWT_POLAR_VERSION=0x060200" \ cmake \ -G"Ninja" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ @@ -74,7 +73,6 @@ jobs: -DWITH_DRACO=ON \ -DWITH_PDAL=OFF \ -DWITH_CUSTOM_WIDGETS=ON \ - -DWITH_QWTPOLAR=OFF \ -DWITH_BINDINGS=OFF \ -DWITH_GRASS=OFF \ -DUSE_CCACHE=ON \ diff --git a/.github/workflows/ogc.yml b/.github/workflows/ogc.yml index ecf941d38fa9..3cf6743a593b 100644 --- a/.github/workflows/ogc.yml +++ b/.github/workflows/ogc.yml @@ -44,7 +44,7 @@ jobs: uses: actions/checkout@v4 - name: Restore build cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: /home/runner/QGIS/.ccache key: build-ccache-ogc-${{ github.event.pull_request.base.ref || github.ref_name }} @@ -72,7 +72,7 @@ jobs: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} - name: Save build cache for push only - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 if: ${{ github.event_name == 'push' }} with: path: /home/runner/QGIS/.ccache @@ -88,7 +88,7 @@ jobs: - name: Run WMS 1.3.0 OGC tests run: | source venv/bin/activate && ./pyogctest/pyogctest.py -s wms130 -e - docker-compose -f .ci/ogc/docker-compose.yml up -d + docker compose -f .ci/ogc/docker-compose.yml up -d source venv/bin/activate && ./pyogctest/pyogctest.py -n ogc_qgis -s wms130 -v -u http://$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' qgis_server_nginx)/qgisserver_wms130 env: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} @@ -96,7 +96,7 @@ jobs: - name: Run OGC API Features 1.0 tests run: | cd data && git clone https://github.com/qgis/QGIS-Training-Data && cd - - docker-compose -f .ci/ogc/docker-compose.yml up -d + docker compose -f .ci/ogc/docker-compose.yml up -d source venv/bin/activate && ./pyogctest/pyogctest.py -n ogc_qgis -s ogcapif -v -u http://$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' qgis_server_nginx)/qgisserver_ogcapif env: DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 85278b3bad8e..80bfcdd21dd1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -116,7 +116,7 @@ jobs: echo QT_VERSION: ${QT_VERSION} - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -129,7 +129,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-build-deps-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true build-args: DISTRO_VERSION=${{ matrix.distro-version }} @@ -347,7 +347,7 @@ jobs: echo CTEST_BUILD_NAME: ${CTEST_BUILD_NAME} - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -360,7 +360,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true target: ${{ matrix.docker-target }} build-args: @@ -407,7 +407,7 @@ jobs: echo "DOCKERFILE=$DOCKERFILE" mkdir -p /tmp/webdav_tests && chmod 777 /tmp/webdav_tests mkdir -p /tmp/minio_tests/test-bucket && chmod -R 777 /tmp/minio_tests - docker-compose -f .docker/$DOCKERFILE run -e GITHUB_SHA=$GITHUB_SHA qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH + docker compose -f .docker/$DOCKERFILE run -e GITHUB_SHA=$GITHUB_SHA qgis-deps /root/QGIS/.docker/docker-qgis-test.sh $TEST_BATCH - name: Fix permissions on test report if: ${{ failure() }} @@ -458,7 +458,7 @@ jobs: fetch-depth: 2 - name: Login to Docker Hub - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} @@ -471,7 +471,7 @@ jobs: context: . file: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile tags: qgis/qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only:${{ github.event.pull_request.base.ref || github.ref_name }} - push: ${{ github.event_name == 'push' }} + push: ${{ github.event_name == 'push' && github.actor == 'qgis' }} pull: true target: ${{ matrix.docker-target }} build-args: diff --git a/.github/workflows/write_failure_comment.yml b/.github/workflows/write_failure_comment.yml index 9e85208e6407..38d676a1235d 100644 --- a/.github/workflows/write_failure_comment.yml +++ b/.github/workflows/write_failure_comment.yml @@ -1,9 +1,5 @@ name: Write test failure comment -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - on: workflow_run: workflows: [🧪 QGIS tests] diff --git a/debian/control b/debian/control index 851cf2852570..590317c5ab55 100644 --- a/debian/control +++ b/debian/control @@ -67,7 +67,6 @@ Build-Depends: qttools5-dev-tools, qttools5-dev, git, - doxygen, graphviz, xvfb, xauth, diff --git a/debian/control.in b/debian/control.in index 25e0a500788d..1e37feceb4e1 100644 --- a/debian/control.in +++ b/debian/control.in @@ -81,7 +81,7 @@ Build-Depends: protobuf-compiler, libzstd-dev, git, - doxygen, +#apidoc# doxygen, graphviz, xvfb, xauth, @@ -563,19 +563,19 @@ Description: QGIS server providing various OGC services display databases of geographic information. . This package contains the landing page service. - -Package: qgis-api-doc -Architecture: all -Section: doc -Depends: - ${misc:Depends} -Recommends: - qt5-doc-html -Description: QGIS API documentation - QGIS is a Geographic Information System (GIS) which manages, analyzes and - display databases of geographic information. - . - This package contains the QGIS API documentation. +#apidoc# +#apidoc#Package: qgis-api-doc +#apidoc#Architecture: all +#apidoc#Section: doc +#apidoc#Depends: +#apidoc# ${misc:Depends} +#apidoc#Recommends: +#apidoc# qt5-doc-html +#apidoc#Description: QGIS API documentation +#apidoc# QGIS is a Geographic Information System (GIS) which manages, analyzes and +#apidoc# display databases of geographic information. +#apidoc# . +#apidoc# This package contains the QGIS API documentation. #oracle# #oracle#Package: qgis-oracle-provider #oracle#Architecture: any diff --git a/debian/rules b/debian/rules index 4abb692fb603..71d1645447ec 100755 --- a/debian/rules +++ b/debian/rules @@ -88,8 +88,6 @@ CMAKE_OPTS := \ -DPEDANTIC=TRUE \ -DSERVER_SKIP_ECW=TRUE \ -DQGIS_CGIBIN_SUBDIR=/usr/lib/cgi-bin \ - -DWITH_APIDOC=TRUE \ - -DGENERATE_QHP=TRUE \ -DWITH_CUSTOM_WIDGETS=TRUE \ -DWITH_SERVER=TRUE \ -DWITH_SERVER_PLUGINS=TRUE \ @@ -134,6 +132,13 @@ ifneq (0,$(.SHELLSTATUS)) -DQT5_3DEXTRA_LIBRARY=/usr/lib/$(DEB_BUILD_MULTIARCH)/libQt53DExtras.so endif +ifneq (,$(WITH_APIDOC)) + CMAKE_OPTS += -DWITH_APIDOC=TRUE -DGENERATE_QHP=TRUE +else + CMAKE_OPTS += -DWITH_APIDOC=OFF -DGENERATE_QHP=OFF +endif + + ifneq (,$(WITH_ORACLE)) ifeq ($(DEB_BUILD_ARCH),amd64) ORACLE_INCLUDEDIR=/usr/include/oracle/21/client64/ @@ -195,12 +200,16 @@ endif CONTROL_EXPRESSIONS = $(DISTRIBUTION) grass$(GRASSVER) +ifneq (,$(WITH_APIDOC)) +CONTROL_EXPRESSIONS += apidoc +endif + ifneq (,$(WITH_ORACLE)) - CONTROL_EXPRESSIONS += oracle +CONTROL_EXPRESSIONS += oracle endif ifeq ($(shell pkg-config --exists pdal && dpkg --compare-versions $$(pkg-config --modversion pdal) ge 2.5 && echo 1 || echo 0),1) - CONTROL_EXPRESSIONS += pdal_wrench +CONTROL_EXPRESSIONS += pdal_wrench endif define gentemplate @@ -253,7 +262,9 @@ else ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) endif +ifneq (,$(WITH_APIDOC)) ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) apidoc +endif override_dh_auto_test: test-stamp @@ -280,8 +291,10 @@ endif override_dh_auto_install: DESTDIR=$(CURDIR)/debian/tmp ninja $(NINJA_OPTS) -C $(QGIS_BUILDDIR) install +ifneq (,$(WITH_APIDOC)) # remove unwanted files $(RM) $(CURDIR)/debian/tmp/usr/share/qgis/doc/api/installdox +endif # replace leaflet and jquery urls perl -i -p \ diff --git a/python/PyQt/CMakeLists.txt b/python/PyQt/CMakeLists.txt index a54b918070c2..8b062819d1a5 100644 --- a/python/PyQt/CMakeLists.txt +++ b/python/PyQt/CMakeLists.txt @@ -10,6 +10,9 @@ set(PYQT_COMPAT_FILES QtPrintSupport.py QtWebKit.py QtWebKitWidgets.py + QtWebEngineCore.py + QtWebEngineQuick.py + QtWebEngineWidgets.py QtNetwork.py QtXml.py QtSql.py diff --git a/python/PyQt/PyQt/QtWebEngineCore.py.in b/python/PyQt/PyQt/QtWebEngineCore.py.in new file mode 100644 index 000000000000..791bb3f6e381 --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineCore.py.in @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineCore.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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 PyQt@QT_VERSION_MAJOR@.QtWebEngineCore import * diff --git a/python/PyQt/PyQt/QtWebEngineQuick.py.in b/python/PyQt/PyQt/QtWebEngineQuick.py.in new file mode 100644 index 000000000000..5b940beb35bf --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineQuick.py.in @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineQuick.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" + +if @QT_VERSION_MAJOR@ == 5: + from PyQt5.QtWebEngine import * +else: + from PyQt@QT_VERSION_MAJOR@.QtWebEngineQuick import * diff --git a/python/PyQt/PyQt/QtWebEngineWidgets.py.in b/python/PyQt/PyQt/QtWebEngineWidgets.py.in new file mode 100644 index 000000000000..49364d6c6816 --- /dev/null +++ b/python/PyQt/PyQt/QtWebEngineWidgets.py.in @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + QtWebEngineWidgets.py + --------------------- + Date : April 2024 + Copyright : (C) 2024 David Koňařík + Email : dvdkon at konarici dot cz +*************************************************************************** +* * +* 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 PyQt@QT_VERSION_MAJOR@.QtWebEngineWidgets import * diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 5b55e1209b68..b5c63e785fd6 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -2116,7 +2116,10 @@ QgsRasterLayerTemporalProperties.FixedRangePerBand = Qgis.RasterTemporalMode.FixedRangePerBand QgsRasterLayerTemporalProperties.FixedRangePerBand.is_monkey_patched = True QgsRasterLayerTemporalProperties.FixedRangePerBand.__doc__ = "Layer has a fixed temporal range per band (since QGIS 3.38)" -Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ +QgsRasterLayerTemporalProperties.RepresentsTemporalValues = Qgis.RasterTemporalMode.RepresentsTemporalValues +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.is_monkey_patched = True +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.__doc__ = "Pixel values represent an datetime" +Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ + '\n' + '* ``RepresentsTemporalValues``: ' + Qgis.RasterTemporalMode.RepresentsTemporalValues.__doc__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod @@ -4904,6 +4907,7 @@ Qgis.SensorThingsEntity.ObservedProperty.__doc__ = "An ObservedProperty specifies the phenomenon of an Observation" Qgis.SensorThingsEntity.Observation.__doc__ = "An Observation is the act of measuring or otherwise determining the value of a property" Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ = "In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed" -Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ +Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 \"MultiDatastream extension\". (Since QGIS 3.38)" +Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__ # -- Qgis.SensorThingsEntity.baseClass = Qgis diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in index 56bc1221b191..ba77e7c03ba2 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1322,6 +1322,13 @@ geometries. bool contains( const QgsPointXY *p ) const; %Docstring Returns ``True`` if the geometry contains the point ``p``. +%End + + bool contains( double x, double y ) const; +%Docstring +Returns ``True`` if the geometry contains the point at (``x``, ``y``). + +.. versionadded:: 3.38 %End bool contains( const QgsGeometry &geometry ) const; diff --git a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index ebee1974cbfe..404571d1f5b8 100644 --- a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -986,6 +986,14 @@ Returns an empty string on failure. .. versionadded:: 3.30 %End + QString toOgcUrn() const; +%Docstring +Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) +Returns an empty string on failure. + +.. versionadded:: 3.38 +%End + void updateDefinition(); %Docstring diff --git a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index abfde62514bf..3df39d7b2121 100644 --- a/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/PyQt6/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,15 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); +%Docstring +Returns the geometry type for if the specified entity ``type``. + +If there are no restrictions on the geometry type an ntity can have :py:class:`Qgis`.GeometryType.Unknown will be returned. + +.. versionadded:: 3.38 %End static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ); diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index b065f97f1ecb..5b9871da60e9 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1258,6 +1258,7 @@ The development version TemporalRangeFromDataProvider, RedrawLayerOnly, FixedRangePerBand, + RepresentsTemporalValues, }; enum class TemporalIntervalMatchMethod /BaseType=IntEnum/ @@ -2751,6 +2752,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; diff --git a/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in b/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in index 2b9322784515..799f629dfdd9 100644 --- a/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in +++ b/python/PyQt6/core/auto_generated/qgsjsonutils.sip.in @@ -363,6 +363,7 @@ Returns a null geometry if the geometry could not be parsed. + }; /************************************************************************ diff --git a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in index a96b4c3d629a..75c525ddff91 100644 --- a/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/PyQt6/core/auto_generated/qgslayerdefinition.sip.in @@ -25,13 +25,34 @@ files also store the layer tree info for the exported layers, including group in #include "qgslayerdefinition.h" %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/ ); + + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + +:param path: file path to the qlr +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + +:return: - true in case of success + - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context ); + + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + +:param doc: the xml document +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param context: the read write context +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + +:return: - true in case of success + - errorMessage: the returned error message %End static bool exportLayerDefinition( const QString &path, const QList &selectedTreeNodes, QString &errorMessage /Out/ ); diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in index a5a1f1049395..92a28eb00b2e 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,6 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include +#include #include %End %ConvertToSubClassCode @@ -157,6 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 2c166a62b112..4d9e5a0728e3 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -132,6 +132,95 @@ Returns the band corresponding to the specified ``range``. This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. For other modes it will always return -1. +.. versionadded:: 3.38 +%End + + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns a filtered list of bands which match the specified ``range``. + +.. versionadded:: 3.38 +%End + + int bandNumber() const; +%Docstring +Returns the band number from which temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setBandNumber` + +.. versionadded:: 3.38 +%End + + void setBandNumber( int number ); +%Docstring +Sets the band number from which temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`bandNumber` + +.. versionadded:: 3.38 +%End + + QDateTime temporalRepresentationOffset() const; +%Docstring +Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationOffset( const QDateTime &offset ); +%Docstring +Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + const QgsInterval &temporalRepresentationScale() const; +%Docstring +Returns the scale, which is an interval factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScale( const QgsInterval &scale ); +%Docstring +Sets the scale, which is an interval factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScale` + .. versionadded:: 3.38 %End diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in new file mode 100644 index 000000000000..9d74477d06db --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerutils.sip.in @@ -0,0 +1,53 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsRasterLayerUtils +{ +%Docstring(signature="appended") +Contains utility functions for working with raster layers. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterlayerutils.h" +%End + public: + + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched /Out/ ); +%Docstring +Given a raster ``layer``, returns the band which should be used for +rendering the layer for a specified temporal and elevation range, +respecting any elevation and temporal settings which affect the rendered band. + +:param layer: Target raster layer +:param temporalRange: temporal range for rendering +:param elevationRange: elevation range for rendering + +:return: - Matched band, or -1 if the layer does not have any elevation + - matched: will be set to ``True`` if a band matching the temporal and elevation range was found + or temporal settings which affect the rendered band. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in new file mode 100644 index 000000000000..c31639035068 --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -0,0 +1,84 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastersinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterSingleColorRenderer: QgsRasterRenderer +{ +%Docstring(signature="appended") +Raster renderer which renders all data pixels using a single color. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrastersinglecolorrenderer.h" +%End + public: + + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); +%Docstring +Creates a single ``color`` renderer +%End + + + virtual QgsRasterSingleColorRenderer *clone() const /Factory/; + +%Docstring +QgsRasterSingleColorRenderer cannot be copied. Use :py:func:`~QgsRasterSingleColorRenderer.clone` instead. +%End + virtual Qgis::RasterRendererFlags flags() const; + + + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; +%Docstring +Creates an instance of the renderer based on definition from XML (used by the renderer registry) +%End + + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; + + + QColor color() const; +%Docstring +Returns the single color used by the renderer. + +.. seealso:: :py:func:`setColor` +%End + + void setColor( const QColor &color ); +%Docstring +Sets the single ``color`` used by the renderer. + +.. seealso:: :py:func:`color` +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + + + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + virtual QList usesBands() const; + + + private: + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastersinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/core.sip.in b/python/PyQt6/core/core.sip.in index a9a7b7a95325..165aaa4ecbb6 100644 --- a/python/PyQt6/core/core.sip.in +++ b/python/PyQt6/core/core.sip.in @@ -91,6 +91,14 @@ done: %End +%Feature HAVE_GUI +%Feature HAVE_QTSERIALPORT +%Feature HAVE_QTPRINTER +%Feature ANDROID +%Feature VECTOR_MAPPED_TYPE +%Feature HAVE_WEBENGINE_SIP +%Feature PYQT6 + %Import QtXml/QtXmlmod.sip %Import QtNetwork/QtNetworkmod.sip %Import QtSql/QtSqlmod.sip @@ -98,15 +106,10 @@ done: %Import QtPrintSupport/QtPrintSupportmod.sip %Import QtWidgets/QtWidgetsmod.sip %Import QtPositioning/QtPositioningmod.sip -%Import QtSerialPort/QtSerialPortmod.sip -%Feature HAVE_GUI -%Feature HAVE_QTSERIALPORT -%Feature HAVE_QTPRINTER -%Feature ANDROID -%Feature VECTOR_MAPPED_TYPE -%Feature HAVE_WEBENGINE_SIP -%Feature PYQT6 +%If (HAVE_QTSERIALPORT) +%Import QtSerialPort/QtSerialPortmod.sip +%End %Include conversions.sip %Include qgsexception.sip diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 71eb28b16c0e..1127fa0d7f2a 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -635,6 +635,7 @@ %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalproperties.sip +%Include auto_generated/raster/qgsrasterlayerutils.sip %Include auto_generated/raster/qgsrasterminmaxorigin.sip %Include auto_generated/raster/qgsrasternuller.sip %Include auto_generated/raster/qgsrasterpipe.sip @@ -653,6 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip +%Include auto_generated/raster/qgsrastersinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in b/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in index 5e09222255ad..1ef9ca3754ce 100644 --- a/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgskeyvaluewidget.sip.in @@ -38,6 +38,10 @@ Gets the edit value. :return: the QVariantMap %End + public slots: + + virtual void setReadOnly( bool readOnly ); + }; diff --git a/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in b/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in index d94f0d2c51cf..f6129b0b6af5 100644 --- a/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgslistwidget.sip.in @@ -46,6 +46,11 @@ Check the content is valid :return: ``True`` if valid %End + public slots: + + virtual void setReadOnly( bool readOnly ); + + }; diff --git a/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in b/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in index 3f09390e1215..b851922e5dfc 100644 --- a/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in +++ b/python/PyQt6/gui/auto_generated/qgstablewidgetbase.sip.in @@ -25,6 +25,26 @@ Child classes must call init(QAbstractTableModel*) from their constructor. explicit QgsTableWidgetBase( QWidget *parent ); %Docstring Constructor. +%End + + bool isReadOnly() const; +%Docstring +Returns ``True`` if the widget is shown in a read-only state. + +.. seealso:: :py:func:`setReadOnly` + +.. versionadded:: 3.38 +%End + + public slots: + + virtual void setReadOnly( bool readOnly ); +%Docstring +Sets whether the widget should be shown in a read-only state. + +.. seealso:: :py:func:`isReadOnly` + +.. versionadded:: 3.38 %End protected: diff --git a/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in b/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in index 55f3f1db35a6..249c4c584275 100644 --- a/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in +++ b/python/PyQt6/server/auto_generated/qgscapabilitiescache.sip.in @@ -39,9 +39,11 @@ Inserts new capabilities document (creates a copy of the document, does not take :param doc: the DOM document %End + public slots: + void removeCapabilitiesDocument( const QString &path ); %Docstring -Remove capabilities document +Removes capabilities document :param path: the project file path %End diff --git a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in index a1ed747f785f..145e0cc54a21 100644 --- a/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in +++ b/python/PyQt6/server/auto_generated/qgsconfigcache.sip.in @@ -78,6 +78,15 @@ Initialize from settings %End + signals: + + void projectRemovedFromCache( const QString &path ); +%Docstring +Emitted whenever a project is removed from the cache. + +.. versionadded:: 3.38 +%End + private: QgsConfigCache(); public slots: diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 4c38737929fe..5a2b2539999a 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -2077,7 +2077,10 @@ QgsRasterLayerTemporalProperties.FixedRangePerBand = Qgis.RasterTemporalMode.FixedRangePerBand QgsRasterLayerTemporalProperties.FixedRangePerBand.is_monkey_patched = True QgsRasterLayerTemporalProperties.FixedRangePerBand.__doc__ = "Layer has a fixed temporal range per band (since QGIS 3.38)" -Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ +QgsRasterLayerTemporalProperties.RepresentsTemporalValues = Qgis.RasterTemporalMode.RepresentsTemporalValues +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.is_monkey_patched = True +QgsRasterLayerTemporalProperties.RepresentsTemporalValues.__doc__ = "Pixel values represent an datetime" +Qgis.RasterTemporalMode.__doc__ = "Raster layer temporal modes\n\n.. versionadded:: 3.22\n\n" + '* ``ModeFixedTemporalRange``: ' + Qgis.RasterTemporalMode.FixedTemporalRange.__doc__ + '\n' + '* ``ModeTemporalRangeFromDataProvider``: ' + Qgis.RasterTemporalMode.TemporalRangeFromDataProvider.__doc__ + '\n' + '* ``ModeRedrawLayerOnly``: ' + Qgis.RasterTemporalMode.RedrawLayerOnly.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterTemporalMode.FixedRangePerBand.__doc__ + '\n' + '* ``RepresentsTemporalValues``: ' + Qgis.RasterTemporalMode.RepresentsTemporalValues.__doc__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod @@ -4826,7 +4829,8 @@ Qgis.SensorThingsEntity.ObservedProperty.__doc__ = "An ObservedProperty specifies the phenomenon of an Observation" Qgis.SensorThingsEntity.Observation.__doc__ = "An Observation is the act of measuring or otherwise determining the value of a property" Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ = "In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed" -Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ +Qgis.SensorThingsEntity.MultiDatastream.__doc__ = "A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 \"MultiDatastream extension\". (Since QGIS 3.38)" +Qgis.SensorThingsEntity.__doc__ = "OGC SensorThings API entity types.\n\n.. versionadded:: 3.36\n\n" + '* ``Invalid``: ' + Qgis.SensorThingsEntity.Invalid.__doc__ + '\n' + '* ``Thing``: ' + Qgis.SensorThingsEntity.Thing.__doc__ + '\n' + '* ``Location``: ' + Qgis.SensorThingsEntity.Location.__doc__ + '\n' + '* ``HistoricalLocation``: ' + Qgis.SensorThingsEntity.HistoricalLocation.__doc__ + '\n' + '* ``Datastream``: ' + Qgis.SensorThingsEntity.Datastream.__doc__ + '\n' + '* ``Sensor``: ' + Qgis.SensorThingsEntity.Sensor.__doc__ + '\n' + '* ``ObservedProperty``: ' + Qgis.SensorThingsEntity.ObservedProperty.__doc__ + '\n' + '* ``Observation``: ' + Qgis.SensorThingsEntity.Observation.__doc__ + '\n' + '* ``FeatureOfInterest``: ' + Qgis.SensorThingsEntity.FeatureOfInterest.__doc__ + '\n' + '* ``MultiDatastream``: ' + Qgis.SensorThingsEntity.MultiDatastream.__doc__ # -- Qgis.SensorThingsEntity.baseClass = Qgis from enum import Enum diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index 56bc1221b191..ba77e7c03ba2 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -1322,6 +1322,13 @@ geometries. bool contains( const QgsPointXY *p ) const; %Docstring Returns ``True`` if the geometry contains the point ``p``. +%End + + bool contains( double x, double y ) const; +%Docstring +Returns ``True`` if the geometry contains the point at (``x``, ``y``). + +.. versionadded:: 3.38 %End bool contains( const QgsGeometry &geometry ) const; diff --git a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index d4661ab1c028..80459e767c59 100644 --- a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -986,6 +986,14 @@ Returns an empty string on failure. .. versionadded:: 3.30 %End + QString toOgcUrn() const; +%Docstring +Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) +Returns an empty string on failure. + +.. versionadded:: 3.38 +%End + void updateDefinition(); %Docstring diff --git a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index abfde62514bf..3df39d7b2121 100644 --- a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,6 +55,15 @@ Returns the geometry field for a specified entity ``type``. static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); %Docstring Returns ``True`` if the specified entity ``type`` can have geometry attached. +%End + + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); +%Docstring +Returns the geometry type for if the specified entity ``type``. + +If there are no restrictions on the geometry type an ntity can have :py:class:`Qgis`.GeometryType.Unknown will be returned. + +.. versionadded:: 3.38 %End static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ); diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 8005b5ec3f02..6df2b9c00282 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1258,6 +1258,7 @@ The development version TemporalRangeFromDataProvider, RedrawLayerOnly, FixedRangePerBand, + RepresentsTemporalValues, }; enum class TemporalIntervalMatchMethod @@ -2751,6 +2752,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; diff --git a/python/core/auto_generated/qgsjsonutils.sip.in b/python/core/auto_generated/qgsjsonutils.sip.in index c45c146701cf..81b58724bc53 100644 --- a/python/core/auto_generated/qgsjsonutils.sip.in +++ b/python/core/auto_generated/qgsjsonutils.sip.in @@ -363,6 +363,7 @@ Returns a null geometry if the geometry could not be parsed. + }; /************************************************************************ diff --git a/python/core/auto_generated/qgslayerdefinition.sip.in b/python/core/auto_generated/qgslayerdefinition.sip.in index a96b4c3d629a..75c525ddff91 100644 --- a/python/core/auto_generated/qgslayerdefinition.sip.in +++ b/python/core/auto_generated/qgslayerdefinition.sip.in @@ -25,13 +25,34 @@ files also store the layer tree info for the exported layers, including group in #include "qgslayerdefinition.h" %End public: - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/ ); + + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + +:param path: file path to the qlr +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + +:return: - true in case of success + - errorMessage: the returned error message %End - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context ); + + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage /Out/, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = 0 ); %Docstring Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + +:param doc: the xml document +:param project: the current project +:param rootGroup: the layer tree group to insert the qlr content +:param context: the read write context +:param insertMethod: method for layer tree (since QGIS 3.38) +:param insertPoint: describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + +:return: - true in case of success + - errorMessage: the returned error message %End static bool exportLayerDefinition( const QString &path, const QList &selectedTreeNodes, QString &errorMessage /Out/ ); diff --git a/python/core/auto_generated/raster/qgsrasterinterface.sip.in b/python/core/auto_generated/raster/qgsrasterinterface.sip.in index 4d152aa92a6e..f1d62b2aea28 100644 --- a/python/core/auto_generated/raster/qgsrasterinterface.sip.in +++ b/python/core/auto_generated/raster/qgsrasterinterface.sip.in @@ -125,6 +125,7 @@ Base class for processing filters like renderers, reprojector, resampler etc. #include #include #include +#include #include %End %ConvertToSubClassCode @@ -157,6 +158,8 @@ Base class for processing filters like renderers, reprojector, resampler etc. sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 2c166a62b112..4d9e5a0728e3 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -132,6 +132,95 @@ Returns the band corresponding to the specified ``range``. This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. For other modes it will always return -1. +.. versionadded:: 3.38 +%End + + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns a filtered list of bands which match the specified ``range``. + +.. versionadded:: 3.38 +%End + + int bandNumber() const; +%Docstring +Returns the band number from which temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setBandNumber` + +.. versionadded:: 3.38 +%End + + void setBandNumber( int number ); +%Docstring +Sets the band number from which temporal values should be taken. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`bandNumber` + +.. versionadded:: 3.38 +%End + + QDateTime temporalRepresentationOffset() const; +%Docstring +Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationOffset( const QDateTime &offset ); +%Docstring +Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values +from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationOffset` + +.. versionadded:: 3.38 +%End + + const QgsInterval &temporalRepresentationScale() const; +%Docstring +Returns the scale, which is an interval factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`setTemporalRepresentationScale` + +.. versionadded:: 3.38 +%End + + void setTemporalRepresentationScale( const QgsInterval &scale ); +%Docstring +Sets the scale, which is an interval factor which should be applied to individual pixel +values from the layer. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.RepresentsTemporalValues. + +.. seealso:: :py:func:`temporalRepresentationScale` + .. versionadded:: 3.38 %End diff --git a/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in b/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in new file mode 100644 index 000000000000..9d74477d06db --- /dev/null +++ b/python/core/auto_generated/raster/qgsrasterlayerutils.sip.in @@ -0,0 +1,53 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsRasterLayerUtils +{ +%Docstring(signature="appended") +Contains utility functions for working with raster layers. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterlayerutils.h" +%End + public: + + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched /Out/ ); +%Docstring +Given a raster ``layer``, returns the band which should be used for +rendering the layer for a specified temporal and elevation range, +respecting any elevation and temporal settings which affect the rendered band. + +:param layer: Target raster layer +:param temporalRange: temporal range for rendering +:param elevationRange: elevation range for rendering + +:return: - Matched band, or -1 if the layer does not have any elevation + - matched: will be set to ``True`` if a band matching the temporal and elevation range was found + or temporal settings which affect the rendered band. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterlayerutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in new file mode 100644 index 000000000000..c31639035068 --- /dev/null +++ b/python/core/auto_generated/raster/qgsrastersinglecolorrenderer.sip.in @@ -0,0 +1,84 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastersinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterSingleColorRenderer: QgsRasterRenderer +{ +%Docstring(signature="appended") +Raster renderer which renders all data pixels using a single color. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsrastersinglecolorrenderer.h" +%End + public: + + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); +%Docstring +Creates a single ``color`` renderer +%End + + + virtual QgsRasterSingleColorRenderer *clone() const /Factory/; + +%Docstring +QgsRasterSingleColorRenderer cannot be copied. Use :py:func:`~QgsRasterSingleColorRenderer.clone` instead. +%End + virtual Qgis::RasterRendererFlags flags() const; + + + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; +%Docstring +Creates an instance of the renderer based on definition from XML (used by the renderer registry) +%End + + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; + + + QColor color() const; +%Docstring +Returns the single color used by the renderer. + +.. seealso:: :py:func:`setColor` +%End + + void setColor( const QColor &color ); +%Docstring +Sets the single ``color`` used by the renderer. + +.. seealso:: :py:func:`color` +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + + + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + virtual QList usesBands() const; + + + private: + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastersinglecolorrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core.sip.in b/python/core/core.sip.in index 10c6ff3f4c5c..8378dd455203 100644 --- a/python/core/core.sip.in +++ b/python/core/core.sip.in @@ -91,6 +91,13 @@ done: %End +%Feature HAVE_GUI +%Feature HAVE_QTSERIALPORT +%Feature HAVE_QTPRINTER +%Feature ANDROID +%Feature VECTOR_MAPPED_TYPE +%Feature HAVE_WEBENGINE_SIP + %Import QtXml/QtXmlmod.sip %Import QtNetwork/QtNetworkmod.sip %Import QtSql/QtSqlmod.sip @@ -98,14 +105,10 @@ done: %Import QtPrintSupport/QtPrintSupportmod.sip %Import QtWidgets/QtWidgetsmod.sip %Import QtPositioning/QtPositioningmod.sip -%Import QtSerialPort/QtSerialPortmod.sip -%Feature HAVE_GUI -%Feature HAVE_QTSERIALPORT -%Feature HAVE_QTPRINTER -%Feature ANDROID -%Feature VECTOR_MAPPED_TYPE -%Feature HAVE_WEBENGINE_SIP +%If (HAVE_QTSERIALPORT) +%Import QtSerialPort/QtSerialPortmod.sip +%End %Include conversions.sip %Include qgsexception.sip diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 71eb28b16c0e..1127fa0d7f2a 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -635,6 +635,7 @@ %Include auto_generated/raster/qgsrasterlayer.sip %Include auto_generated/raster/qgsrasterlayerelevationproperties.sip %Include auto_generated/raster/qgsrasterlayertemporalproperties.sip +%Include auto_generated/raster/qgsrasterlayerutils.sip %Include auto_generated/raster/qgsrasterminmaxorigin.sip %Include auto_generated/raster/qgsrasternuller.sip %Include auto_generated/raster/qgsrasterpipe.sip @@ -653,6 +654,7 @@ %Include auto_generated/raster/qgssinglebandcolordatarenderer.sip %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip +%Include auto_generated/raster/qgsrastersinglecolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip %Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip diff --git a/python/gui/auto_generated/qgskeyvaluewidget.sip.in b/python/gui/auto_generated/qgskeyvaluewidget.sip.in index 5e09222255ad..1ef9ca3754ce 100644 --- a/python/gui/auto_generated/qgskeyvaluewidget.sip.in +++ b/python/gui/auto_generated/qgskeyvaluewidget.sip.in @@ -38,6 +38,10 @@ Gets the edit value. :return: the QVariantMap %End + public slots: + + virtual void setReadOnly( bool readOnly ); + }; diff --git a/python/gui/auto_generated/qgslistwidget.sip.in b/python/gui/auto_generated/qgslistwidget.sip.in index d94f0d2c51cf..f6129b0b6af5 100644 --- a/python/gui/auto_generated/qgslistwidget.sip.in +++ b/python/gui/auto_generated/qgslistwidget.sip.in @@ -46,6 +46,11 @@ Check the content is valid :return: ``True`` if valid %End + public slots: + + virtual void setReadOnly( bool readOnly ); + + }; diff --git a/python/gui/auto_generated/qgstablewidgetbase.sip.in b/python/gui/auto_generated/qgstablewidgetbase.sip.in index 3f09390e1215..b851922e5dfc 100644 --- a/python/gui/auto_generated/qgstablewidgetbase.sip.in +++ b/python/gui/auto_generated/qgstablewidgetbase.sip.in @@ -25,6 +25,26 @@ Child classes must call init(QAbstractTableModel*) from their constructor. explicit QgsTableWidgetBase( QWidget *parent ); %Docstring Constructor. +%End + + bool isReadOnly() const; +%Docstring +Returns ``True`` if the widget is shown in a read-only state. + +.. seealso:: :py:func:`setReadOnly` + +.. versionadded:: 3.38 +%End + + public slots: + + virtual void setReadOnly( bool readOnly ); +%Docstring +Sets whether the widget should be shown in a read-only state. + +.. seealso:: :py:func:`isReadOnly` + +.. versionadded:: 3.38 %End protected: diff --git a/python/plugins/db_manager/db_plugins/data_model.py b/python/plugins/db_manager/db_plugins/data_model.py index 4842a79c37f8..1087035fe8ba 100644 --- a/python/plugins/db_manager/db_plugins/data_model.py +++ b/python/plugins/db_manager/db_plugins/data_model.py @@ -290,7 +290,7 @@ def getObject(self, row): match = regex.match(typestr) if match.hasMatch(): fld.dataType = match.captured(1).strip() - fld.modifier = regex.captured(2).strip() + fld.modifier = match.captured(2).strip() else: fld.modifier = None fld.dataType = typestr diff --git a/python/server/auto_generated/qgscapabilitiescache.sip.in b/python/server/auto_generated/qgscapabilitiescache.sip.in index 55f3f1db35a6..249c4c584275 100644 --- a/python/server/auto_generated/qgscapabilitiescache.sip.in +++ b/python/server/auto_generated/qgscapabilitiescache.sip.in @@ -39,9 +39,11 @@ Inserts new capabilities document (creates a copy of the document, does not take :param doc: the DOM document %End + public slots: + void removeCapabilitiesDocument( const QString &path ); %Docstring -Remove capabilities document +Removes capabilities document :param path: the project file path %End diff --git a/python/server/auto_generated/qgsconfigcache.sip.in b/python/server/auto_generated/qgsconfigcache.sip.in index a1ed747f785f..145e0cc54a21 100644 --- a/python/server/auto_generated/qgsconfigcache.sip.in +++ b/python/server/auto_generated/qgsconfigcache.sip.in @@ -78,6 +78,15 @@ Initialize from settings %End + signals: + + void projectRemovedFromCache( const QString &path ); +%Docstring +Emitted whenever a project is removed from the cache. + +.. versionadded:: 3.38 +%End + private: QgsConfigCache(); public slots: diff --git a/resources/server/src/landingpage/yarn.lock b/resources/server/src/landingpage/yarn.lock index 15e8d09e1958..26b998a0287a 100644 --- a/resources/server/src/landingpage/yarn.lock +++ b/resources/server/src/landingpage/yarn.lock @@ -2764,7 +2764,25 @@ bluebird@^3.1.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.1, body-parser@^1.19.0: +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.0: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -3337,6 +3355,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -3347,10 +3370,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-descriptor@^0.1.0: version "0.1.1" @@ -4230,16 +4253,16 @@ express-history-api-fallback@^2.2.1: integrity sha512-swxwm3aP8vrOOvlzOdZvHlSZtJGwHKaY94J6AkrAgCTmcbko3IRwbkhLv2wKV1WeZhjxX58aLMpP3atDBnKuZg== express@^4.17.1, express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -7156,6 +7179,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" diff --git a/src/analysis/vector/qgsgeometrysnapper.cpp b/src/analysis/vector/qgsgeometrysnapper.cpp index 3d6b9c34548c..1352679c733b 100644 --- a/src/analysis/vector/qgsgeometrysnapper.cpp +++ b/src/analysis/vector/qgsgeometrysnapper.cpp @@ -109,7 +109,7 @@ bool QgsSnapIndex::SegmentSnapItem::withinSquaredDistance( const QgsPoint &p, co QgsSnapIndex::QgsSnapIndex() { - mSTRTree = GEOSSTRtree_create_r( QgsGeos::getGEOSHandler(), ( size_t )10 ); + mSTRTree = GEOSSTRtree_create_r( QgsGeosContext::get(), ( size_t )10 ); } QgsSnapIndex::~QgsSnapIndex() @@ -117,14 +117,14 @@ QgsSnapIndex::~QgsSnapIndex() qDeleteAll( mCoordIdxs ); qDeleteAll( mSnapItems ); - GEOSSTRtree_destroy_r( QgsGeos::getGEOSHandler(), mSTRTree ); + GEOSSTRtree_destroy_r( QgsGeosContext::get(), mSTRTree ); } void QgsSnapIndex::addPoint( const CoordIdx *idx, bool isEndPoint ) { const QgsPoint p = idx->point(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr point( GEOSGeom_createPointFromXY_r( geosctxt, p.x(), p.y() ) ); PointSnapItem *item = new PointSnapItem( idx, isEndPoint ); @@ -137,7 +137,7 @@ void QgsSnapIndex::addSegment( const CoordIdx *idxFrom, const CoordIdx *idxTo ) const QgsPoint pointFrom = idxFrom->point(); const QgsPoint pointTo = idxTo->point(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 2, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, pointFrom.x(), pointFrom.y() ); @@ -191,7 +191,7 @@ void _GEOSQueryCallback( void *item, void *userdata ) QgsPoint QgsSnapIndex::getClosestSnapToPoint( const QgsPoint &startPoint, const QgsPoint &midPoint ) { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); // Look for intersections on segment from the target point to the point opposite to the point reference point // p2 = p1 + 2 * (q - p1) @@ -231,7 +231,7 @@ QgsPoint QgsSnapIndex::getClosestSnapToPoint( const QgsPoint &startPoint, const QgsSnapIndex::SnapItem *QgsSnapIndex::getSnapItem( const QgsPoint &pos, const double tolerance, QgsSnapIndex::PointSnapItem **pSnapPoint, QgsSnapIndex::SegmentSnapItem **pSnapSegment, bool endPointOnly ) const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 2, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, pos.x() - tolerance, pos.y() - tolerance ); diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 928d98718706..e9847f5041b6 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -386,15 +386,16 @@ find_package(${QT_VERSION_BASE} COMPONENTS UiTools REQUIRED) set (WITH_QWTPOLAR FALSE CACHE BOOL "Determines whether QwtPolar is available or whether functionality requiring QwtPolar should be disabled.") # Once we bump the minimum QWT VERSION to 6.2 or newer, we should get rid of WITH_QWTPOLAR -if(Qwt_VERSION_STRING VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) +if(QWT_VERSION_STR VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) add_definitions(-DWITH_QWTPOLAR) - if(Qwt_VERSION_STRING VERSION_LESS 6.2) + if(QWT_VERSION_STR VERSION_LESS 6.2) find_package(QwtPolar REQUIRED) else() set(FOUND_QwtPolar TRUE) set(QWTPOLAR_LIBRARY ${QWT_LIBRARY}) set(QWTPOLAR_INCLUDE_DIR ${QWT_INCLUDE_DIR}) + add_definitions(-DQWT_POLAR_VERSION=0x060200) endif() # If not found on the system, offer the possibility to build QwtPolar # internally @@ -403,7 +404,7 @@ if(Qwt_VERSION_STRING VERSION_GREATER_EQUAL 6.2 OR WITH_QWTPOLAR) else() set(DEFAULT_WITH_INTERNAL_QWTPOLAR FALSE) endif() - set (WITH_INTERNAL_QWTPOLAR DEFAULT_WITH_INTERNAL_QWTPOLAR CACHE BOOL "Use internal build of QwtPolar") + set (WITH_INTERNAL_QWTPOLAR ${DEFAULT_WITH_INTERNAL_QWTPOLAR} CACHE BOOL "Use internal build of QwtPolar") if(WITH_INTERNAL_QWTPOLAR) set(QGIS_APP_SRCS diff --git a/src/app/layers/qgsapplayerhandling.cpp b/src/app/layers/qgsapplayerhandling.cpp index 17ba4e979e18..43ed92f8fdf7 100644 --- a/src/app/layers/qgsapplayerhandling.cpp +++ b/src/app/layers/qgsapplayerhandling.cpp @@ -1209,7 +1209,7 @@ void QgsAppLayerHandling::addMapLayer( QgsMapLayer *mapLayer, bool addToLegend ) } } -void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) +void QgsAppLayerHandling::openLayerDefinition( const QString &filename, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QString errorMessage; QgsReadWriteContext context; @@ -1236,7 +1236,9 @@ void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) context.setPathResolver( QgsPathResolver( filename ) ); context.setProjectTranslator( QgsProject::instance() ); - loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context ); + QgsSettings settings; + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); + loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMessage, context, insertionMethod, insertPoint ); } } @@ -1260,7 +1262,7 @@ void QgsAppLayerHandling::openLayerDefinition( const QString &filename ) } } -void QgsAppLayerHandling::addLayerDefinition() +void QgsAppLayerHandling::addLayerDefinition( const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QgsSettings settings; QString lastUsedDir = settings.value( QStringLiteral( "UI/lastQLRDir" ), QDir::homePath() ).toString(); @@ -1272,7 +1274,7 @@ void QgsAppLayerHandling::addLayerDefinition() QFileInfo fi( path ); settings.setValue( QStringLiteral( "UI/lastQLRDir" ), fi.path() ); - openLayerDefinition( path ); + openLayerDefinition( path, insertPoint ); } QList< QgsMapLayer * > QgsAppLayerHandling::addDatabaseLayers( const QStringList &layerPathList, const QString &providerKey, bool &ok ) diff --git a/src/app/layers/qgsapplayerhandling.h b/src/app/layers/qgsapplayerhandling.h index 7e16fbbcf729..6df5b3f78ce8 100644 --- a/src/app/layers/qgsapplayerhandling.h +++ b/src/app/layers/qgsapplayerhandling.h @@ -20,6 +20,7 @@ #include "qgsconfig.h" #include "qgsmaplayer.h" #include "qgsvectorlayerref.h" +#include "qgslayertreeregistrybridge.h" #include @@ -149,10 +150,18 @@ class APP_EXPORT QgsAppLayerHandling //! Add a 'pre-made' map layer to the project static void addMapLayer( QgsMapLayer *mapLayer, bool addToLegend = true ); - static void openLayerDefinition( const QString &filename ); + /** + * Opens qlr + * \param filename file path to the qlr + * \param insertPoint describes where the qlr layers/groups shall be inserted + */ + static void openLayerDefinition( const QString &filename, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); - //! Add a Layer Definition file - static void addLayerDefinition(); + /** + * Add a Layer Definition file + * \param insertPoint describes where the qlr layers/groups shall be inserted + */ + static void addLayerDefinition( const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ); //! Add a list of database layers to the map static QList< QgsMapLayer * > addDatabaseLayers( const QStringList &layerPathList, const QString &providerKey, bool &ok ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 525ef313895c..84f643c23b3b 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -863,6 +863,12 @@ void QgisApp::annotationItemTypeAdded( int id ) } ); } +void QgisApp::addLayerDefinition() +{ + QgsLayerTreeRegistryBridge::InsertionPoint pt = layerTreeInsertionPoint(); + QgsAppLayerHandling::addLayerDefinition( &pt ); +} + /* * This function contains forced validation of CRS used in QGIS. * There are 4 options depending on the settings: @@ -1137,7 +1143,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers centralLayout->addWidget( mCentralContainer, 0, 0, 2, 1 ); mInfoBar->raise(); - connect( mMapCanvas, &QgsMapCanvas::layersChanged, this, &QgisApp::showMapCanvas ); + connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgisApp::showMapCanvas ); mCentralContainer->setCurrentIndex( mProjOpen ? 0 : 1 ); @@ -3030,7 +3036,7 @@ void QgisApp::createActions() connect( mActionShowRasterCalculator, &QAction::triggered, this, &QgisApp::showRasterCalculator ); connect( mActionShowMeshCalculator, &QAction::triggered, this, &QgisApp::showMeshCalculator ); connect( mActionEmbedLayers, &QAction::triggered, this, &QgisApp::embedLayers ); - connect( mActionAddLayerDefinition, &QAction::triggered, this, [] { QgsAppLayerHandling::addLayerDefinition(); } ); + connect( mActionAddLayerDefinition, &QAction::triggered, this, &QgisApp::addLayerDefinition ); connect( mActionAddOgrLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "ogr" ) ); } ); connect( mActionAddRasterLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "gdal" ) ); } ); connect( mActionAddMeshLayer, &QAction::triggered, this, [ = ] { dataSourceManager( QStringLiteral( "mdal" ) ); } ); @@ -7062,7 +7068,8 @@ QList< QgsMapLayer * > QgisApp::openFile( const QString &fileName, const QString } else if ( fi.suffix().compare( QLatin1String( "qlr" ), Qt::CaseInsensitive ) == 0 ) { - QgsAppLayerHandling::openLayerDefinition( fileName ); + QgsLayerTreeRegistryBridge::InsertionPoint p = layerTreeInsertionPoint(); + QgsAppLayerHandling::openLayerDefinition( fileName, &p ); } else if ( fi.suffix().compare( QLatin1String( "qpt" ), Qt::CaseInsensitive ) == 0 ) { @@ -7336,7 +7343,7 @@ void QgisApp::toggleReducedView( bool viewMapOnly ) for ( QDockWidget *dock : docks ) { - if ( dock->isVisible() && dockWidgetArea( dock ) != Qt::NoDockWidgetArea ) + if ( dock->isVisible() && !dock->isFloating() && dockWidgetArea( dock ) != Qt::NoDockWidgetArea ) { // remember the active docs docksTitle << dock->windowTitle(); @@ -10784,8 +10791,11 @@ void QgisApp::pasteLayer() root = QgsProject::instance()->layerTreeRoot(); } + QgsSettings settings; + Qgis::LayerTreeInsertionMethod insertionMethod = settings.enumValue( QStringLiteral( "/qgis/layerTreeInsertionMethod" ), Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup ); + QgsLayerTreeRegistryBridge::InsertionPoint insertionPoint = layerTreeInsertionPoint(); bool loaded = QgsLayerDefinition::loadLayerDefinition( doc, QgsProject::instance(), root, - errorMessage, readWriteContext ); + errorMessage, readWriteContext, insertionMethod, &insertionPoint ); if ( !loaded || !errorMessage.isEmpty() ) { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 745c52536a4f..a0a986622b9c 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2117,6 +2117,11 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void annotationItemTypeAdded( int id ); + /** + * Open a qlr file + */ + void addLayerDefinition(); + signals: /** diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index ff9f1cc5e5a2..6aaa2177c31a 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -33,6 +33,7 @@ #include #include +#include const int LAYER_COL = 0; const int OUTPUT_LAYER_ATTRIBUTE_COL = 1; @@ -731,6 +732,17 @@ QgsDxfExportDialog::QgsDxfExportDialog( QWidget *parent, Qt::WindowFlags f ) mEncoding->addItems( QgsDxfExport::encodings() ); mEncoding->setCurrentIndex( mEncoding->findText( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfEncoding" ), settings.value( QStringLiteral( "qgis/lastDxfEncoding" ), "CP1252" ).toString() ) ) ); + QPushButton *btnLoadSaveSettings = new QPushButton( tr( "Settings" ), this ); + QMenu *menuSettings = new QMenu( this ); + menuSettings->addAction( tr( "Load Settings from File…" ), this, &QgsDxfExportDialog::loadSettingsFromFile ); + menuSettings->addAction( tr( "Save Settings to File…" ), this, &QgsDxfExportDialog::saveSettingsToFile ); + btnLoadSaveSettings->setMenu( menuSettings ); + buttonBox->addButton( btnLoadSaveSettings, QDialogButtonBox::ResetRole ); + + mMessageBar = new QgsMessageBar(); + mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + mainLayout->insertWidget( 0, mMessageBar ); + mModel->loadLayersOutputAttribute( mModel->rootGroup() ); } @@ -797,6 +809,284 @@ void QgsDxfExportDialog::deselectDataDefinedBlocks() } +void QgsDxfExportDialog::loadSettingsFromFile() +{ + const QString fileName = QFileDialog::getOpenFileName( this, tr( "Load DXF Export Settings" ), + QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), + tr( "XML file" ) + " (*.xml)" ); + if ( fileName.isNull() ) + { + return; + } + + bool resultFlag = false; + + QDomDocument domDocument( QStringLiteral( "qgis" ) ); + + // location of problem associated with errorMsg + int line, column; + QString errorMessage; + + QFile file( fileName ); + if ( file.open( QFile::ReadOnly ) ) + { + QgsDebugMsgLevel( QStringLiteral( "file found %1" ).arg( fileName ), 2 ); + // read file + resultFlag = domDocument.setContent( &file, &errorMessage, &line, &column ); + if ( !resultFlag ) + errorMessage = tr( "%1 at line %2 column %3" ).arg( errorMessage ).arg( line ).arg( column ); + file.close(); + } + + if ( QMessageBox::question( this, + tr( "DXF Export - Load from XML File" ), + tr( "Are you sure you want to load settings from XML? This will change some values in the DXF Export dialog." ) ) == QMessageBox::Yes ) + { + resultFlag = loadSettingsFromXML( domDocument, errorMessage ); + if ( !resultFlag ) + { + mMessageBar->pushWarning( tr( "Load DXF Export Settings" ), tr( "Failed to load DXF Export settings file as %1. Details: %2" ).arg( fileName, errorMessage ) ); + } + else + { + QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( fileName ).path() ); + mMessageBar->pushMessage( QString(), tr( "DXF Export settings loaded!" ), Qgis::MessageLevel::Success, 0 ); + } + } +} + + +bool QgsDxfExportDialog::loadSettingsFromXML( QDomDocument &doc, QString &errorMessage ) const +{ + const QDomElement rootElement = doc.firstChildElement( QStringLiteral( "qgis" ) ); + if ( rootElement.isNull() ) + { + errorMessage = tr( "Root <qgis> element could not be found." ); + return false; + } + + const QDomElement dxfElement = rootElement.firstChildElement( QStringLiteral( "dxf_settings" ) ); + if ( dxfElement.isNull() ) + { + errorMessage = tr( "The XML file does not correspond to DXF Export settings. It must have a <dxf-settings> element." ); + return false; + } + + QDomElement element; + QVariant value; + + element = dxfElement.namedItem( QStringLiteral( "symbology_mode" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mSymbologyModeComboBox->setCurrentIndex( value.toInt() ); + + element = dxfElement.namedItem( QStringLiteral( "symbology_scale" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mScaleWidget->setScale( value.toDouble() ); + + element = dxfElement.namedItem( QStringLiteral( "encoding" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mEncoding->setCurrentText( value.toString() ); + + element = dxfElement.namedItem( QStringLiteral( "crs" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mCrsSelector->setCrs( value.value< QgsCoordinateReferenceSystem >() ); + + element = dxfElement.namedItem( QStringLiteral( "map_theme" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mVisibilityPresets->setCurrentText( value.toString() ); + + // layer settings + element = dxfElement.namedItem( QStringLiteral( "layers" ) ).toElement(); + QDomNodeList layerNodeList = element.elementsByTagName( QStringLiteral( "layer" ) ); + const QgsReadWriteContext rwContext = QgsReadWriteContext(); + + QgsVectorLayer *vl; + QgsVectorLayerRef vlRef; + + for ( int i = 0; i < layerNodeList.length(); i++ ) + { + element = layerNodeList.at( i ).toElement(); + if ( vlRef.readXml( element, rwContext ) ) + { + vl = vlRef.resolveWeakly( QgsProject::instance() ); + if ( vl ) + { + QgsLayerTreeLayer *treeNode = mLayerTreeGroup->findLayer( vl ); + QModelIndex idx = mModel->node2index( treeNode ); + + idx = mModel->index( idx.row(), OUTPUT_LAYER_ATTRIBUTE_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "attribute-index" ), QStringLiteral( "-1" ) ) ); + + idx = mModel->index( idx.row(), ALLOW_DD_SYMBOL_BLOCKS_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "use_symbol_blocks" ), QStringLiteral( "0" ) ), Qt::CheckStateRole ); + + idx = mModel->index( idx.row(), MAXIMUM_DD_SYMBOL_BLOCKS_COL, idx.parent() ); + mModel->setData( idx, element.attribute( QStringLiteral( "max_number_of_classes" ), QStringLiteral( "-1" ) ) ); + } + else + { + QgsDebugMsgLevel( QStringLiteral( " Layer '%1' found in the DXF Export settings XML file, but not present in the project." ).arg( element.attribute( QStringLiteral( "name" ) ) ), 1 ); + } + } + } + + element = dxfElement.namedItem( QStringLiteral( "use_layer_title" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mLayerTitleAsName->setChecked( value == true ); + + element = dxfElement.namedItem( QStringLiteral( "use_map_extent" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mMapExtentCheckBox->setChecked( value == true ); + + element = dxfElement.namedItem( QStringLiteral( "force_2d" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mForce2d->setChecked( value == true ); + + element = dxfElement.namedItem( QStringLiteral( "mtext" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mMTextCheckBox->setChecked( value == true ); + + element = dxfElement.namedItem( QStringLiteral( "selected_features_only" ) ).toElement(); + value = QgsXmlUtils::readVariant( element.firstChildElement() ); + if ( !value.isNull() ) + mSelectedFeaturesOnly->setChecked( value == true ); + + return true; +} + + +void QgsDxfExportDialog::saveSettingsToFile() +{ + QString outputFileName = QFileDialog::getSaveFileName( this, tr( "Save DXF Export Settings as XML" ), + QgsDxfExportDialog::settingsDxfLastSettingsDir->value(), + tr( "XML file" ) + " (*.xml)" ); + // return dialog focus on Mac + activateWindow(); + raise(); + if ( outputFileName.isEmpty() ) + { + return; + } + + //ensure the user never omitted the extension from the file name + if ( !outputFileName.endsWith( QStringLiteral( ".xml" ), Qt::CaseInsensitive ) ) + { + outputFileName += QStringLiteral( ".xml" ); + } + + QDomDocument domDocument; + + saveSettingsToXML( domDocument ); + + const QFileInfo fileInfo( outputFileName ); + const QFileInfo dirInfo( fileInfo.path() ); //excludes file name + if ( !dirInfo.isWritable() ) + { + mMessageBar->pushInfo( tr( "Save DXF Export Settings" ), tr( "The directory containing your dataset needs to be writable!" ) ); + return; + } + + QFile file( outputFileName ); + if ( file.open( QFile::WriteOnly | QFile::Truncate ) ) + { + QTextStream fileStream( &file ); + // save as utf-8 with 2 spaces for indents + domDocument.save( fileStream, 2 ); + file.close(); + mMessageBar->pushSuccess( tr( "Save DXF Export Settings" ), tr( "Created DXF Export settings file as %1" ).arg( outputFileName ) ); + QgsDxfExportDialog::settingsDxfLastSettingsDir->setValue( QFileInfo( outputFileName ).absolutePath() ); + return; + } + else + { + mMessageBar->pushWarning( tr( "Save DXF Export Settings" ), tr( "Failed to created DXF Export settings file as %1. Check file permissions and retry." ).arg( outputFileName ) ); + return; + } +} + + +void QgsDxfExportDialog::saveSettingsToXML( QDomDocument &doc ) const +{ + QDomImplementation DomImplementation; + const QDomDocumentType documentType = DomImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + QDomDocument domDocument( documentType ); + + QDomElement rootElement = domDocument.createElement( QStringLiteral( "qgis" ) ); + rootElement.setAttribute( QStringLiteral( "version" ), Qgis::version() ); + domDocument.appendChild( rootElement ); + + QDomElement dxfElement = domDocument.createElement( QStringLiteral( "dxf_settings" ) ); + rootElement.appendChild( dxfElement ); + + QDomElement symbologyModeElement = domDocument.createElement( QStringLiteral( "symbology_mode" ) ); + symbologyModeElement.appendChild( QgsXmlUtils::writeVariant( static_cast( symbologyMode() ), doc ) ); + dxfElement.appendChild( symbologyModeElement ); + + QDomElement symbologyScaleElement = domDocument.createElement( QStringLiteral( "symbology_scale" ) ); + symbologyScaleElement.appendChild( QgsXmlUtils::writeVariant( symbologyScale(), doc ) ); + dxfElement.appendChild( symbologyScaleElement ); + + QDomElement encodingElement = domDocument.createElement( QStringLiteral( "encoding" ) ); + encodingElement.appendChild( QgsXmlUtils::writeVariant( encoding(), doc ) ); + dxfElement.appendChild( encodingElement ); + + QDomElement crsElement = domDocument.createElement( QStringLiteral( "crs" ) ); + crsElement.appendChild( QgsXmlUtils::writeVariant( crs(), doc ) ); + dxfElement.appendChild( crsElement ); + + QDomElement mapThemeElement = domDocument.createElement( QStringLiteral( "map_theme" ) ); + mapThemeElement.appendChild( QgsXmlUtils::writeVariant( mapTheme(), doc ) ); + dxfElement.appendChild( mapThemeElement ); + + QDomElement layersElement = domDocument.createElement( QStringLiteral( "layers" ) ); + QgsVectorLayerRef vlRef; + const QgsReadWriteContext rwContext = QgsReadWriteContext(); + + for ( const auto dxfLayer : layers() ) + { + QDomElement layerElement = domDocument.createElement( QStringLiteral( "layer" ) ); + vlRef.setLayer( dxfLayer.layer() ); + vlRef.writeXml( layerElement, rwContext ); + layerElement.setAttribute( QStringLiteral( "attribute-index" ), dxfLayer.layerOutputAttributeIndex() ) ; + layerElement.setAttribute( QStringLiteral( "use_symbol_blocks" ), dxfLayer.buildDataDefinedBlocks() ) ; + layerElement.setAttribute( QStringLiteral( "max_number_of_classes" ), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ) ; + layersElement.appendChild( layerElement ); + } + dxfElement.appendChild( layersElement ); + + QDomElement titleAsNameElement = domDocument.createElement( QStringLiteral( "use_layer_title" ) ); + titleAsNameElement.appendChild( QgsXmlUtils::writeVariant( layerTitleAsName(), doc ) ); + dxfElement.appendChild( titleAsNameElement ); + + QDomElement useMapExtentElement = domDocument.createElement( QStringLiteral( "use_map_extent" ) ); + useMapExtentElement.appendChild( QgsXmlUtils::writeVariant( exportMapExtent(), doc ) ); + dxfElement.appendChild( useMapExtentElement ); + + QDomElement force2dElement = domDocument.createElement( QStringLiteral( "force_2d" ) ); + force2dElement.appendChild( QgsXmlUtils::writeVariant( force2d(), doc ) ); + dxfElement.appendChild( force2dElement ); + + QDomElement useMTextElement = domDocument.createElement( QStringLiteral( "mtext" ) ); + useMTextElement.appendChild( QgsXmlUtils::writeVariant( useMText(), doc ) ); + dxfElement.appendChild( useMTextElement ); + + QDomElement selectedFeatures = domDocument.createElement( QStringLiteral( "selected_features_only" ) ); + selectedFeatures.appendChild( QgsXmlUtils::writeVariant( selectedFeaturesOnly(), doc ) ); + dxfElement.appendChild( selectedFeatures ); + + doc = domDocument; +} + + QList< QgsDxfExport::DxfLayer > QgsDxfExportDialog::layers() const { return mModel->layers(); diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 973bea2722c3..a6fd724ddda0 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -24,6 +24,9 @@ #include "qgsdxfexport.h" #include "qgssettingstree.h" #include "qgssettingsentryimpl.h" +#include "qgsxmlutils.h" +#include "qgsvectorlayerref.h" +#include "qgsmessagebar.h" #include #include @@ -101,6 +104,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase public: static inline QgsSettingsTreeNode *sTreeAppDdxf = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "dxf" ) ); static const inline QgsSettingsEntryBool *settingsDxfEnableDDBlocks = new QgsSettingsEntryBool( QStringLiteral( "enable-datadefined-blocks" ), sTreeAppDdxf, false ); + static const inline QgsSettingsEntryString *settingsDxfLastSettingsDir = new QgsSettingsEntryString( QStringLiteral( "last-settings-dir" ), sTreeAppDdxf, QDir::homePath() ); QgsDxfExportDialog( QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); ~QgsDxfExportDialog() override; @@ -118,6 +122,8 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase QString mapTheme() const; QString encoding() const; QgsCoordinateReferenceSystem crs() const; + bool loadSettingsFromXML( QDomDocument &document, QString &errorMessage ) const; + void saveSettingsToXML( QDomDocument &document ) const; public slots: //! Change the selection of layers in the list @@ -125,6 +131,8 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase void deSelectAll(); void selectDataDefinedBlocks(); void deselectDataDefinedBlocks(); + void loadSettingsFromFile(); + void saveSettingsToFile(); private slots: void setOkEnabled(); @@ -139,6 +147,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase FieldSelectorDelegate *mFieldSelectorDelegate = nullptr; QgsVectorLayerAndAttributeModel *mModel = nullptr; QgsDxfExportLayerTreeView *mTreeView = nullptr; + QgsMessageBar *mMessageBar = nullptr; QgsCoordinateReferenceSystem mCRS; }; diff --git a/src/auth/README.md b/src/auth/README.md new file mode 100644 index 000000000000..196b05f882da --- /dev/null +++ b/src/auth/README.md @@ -0,0 +1,62 @@ +_Originally available in the [user manual](https://docs.qgis.org/3.28/en/docs/user_manual/auth_system/auth_considerations.html)_ + +# Security Considerations + +Once the master password is entered, the API is open to access authentication +configs in the authentication database, similar to how Firefox works. +However, in the initial implementation, no wall against PyQGIS access has been defined. +This may lead to issues where a user downloads/installs a malicious PyQGIS plugin +or standalone app that gains access to authentication credentials. + +The quick solution for initial release of feature is to just not include most +PyQGIS bindings for the authentication system. + +Another simple, though not robust, fix is to add a combobox +in Settings --> Options --> Authentication (defaults to "never"): + +``` + "Allow Python access to authentication system" + Choices: [ confirm once per session | always confirm | always allow | never] +``` + +Such an option's setting would need to be saved in a location non-accessible to Python, +e.g. the authentication database, and encrypted with the master password. + +* Another option may be to track which plugins the user has specifically + allowed to access the authentication system, though it may be tricky to deduce + which plugin is actually making the call. + +* Sandboxing plugins, possibly in their own virtual environments, + would reduce 'cross-plugin' hacking of authentication configs from another plugin + that is authorized. + This might mean limiting cross-plugin communication as well, + but maybe only between third-party plugins. + +* Another good solution is to issue code-signing certificates to vetted plugin authors. + Then validate the plugin's certificate upon loading. + If need be the user can also directly set an untrusted policy for the certificate associated + with the plugin using existing certificate management dialogs. + +* Alternatively, access to sensitive authentication system data from Python could never be allowed, + and only the use of QGIS core widgets, or duplicating authentication system integrations, + would allow the plugin to work with resources that have an authentication configuration, + while keeping master password and authentication config loading in the realm of the main app. + +The same security concerns apply to C++ plugins, though it will be harder to restrict access, +since there is no function binding to simply be removed as with Python. + +## Restrictions + +The confusing [licensing and exporting](https://www.openssl.org/docs/faq.html) +issues associated with OpenSSL apply. +In order for Qt to work with SSL certificates, it needs access to the OpenSSL libraries. +Depending upon how Qt was compiled, the default is to dynamically link +to the OpenSSL libs at run-time (to avoid the export limitations). + +QCA follows a similar tactic, whereby linking to QCA incurs no restrictions, +because the qca-ossl (OpenSSL) plugin is loaded at run-time. +The qca-ossl plugin is directly linked to the OpenSSL libs. Packagers would be the ones +needing to ensure any OpenSSL-linking restrictions are met, if they ship the plugin. +Maybe. I don't really know. I'm not a lawyer. + +The authentication system safely disables itself when ``qca-ossl`` is not found at run-time. diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 692753b4e273..85cbb80ae300 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -759,6 +759,7 @@ set(QGIS_CORE_SRCS raster/qgsrasterlayerprofilegenerator.cpp raster/qgsrasterlayerrenderer.cpp raster/qgsrasterlayertemporalproperties.cpp + raster/qgsrasterlayerutils.cpp raster/qgsrasterminmaxorigin.cpp raster/qgsrasternuller.cpp raster/qgsrasterpipe.cpp @@ -783,6 +784,7 @@ set(QGIS_CORE_SRCS raster/qgssinglebandcolordatarenderer.cpp raster/qgssinglebandgrayrenderer.cpp raster/qgssinglebandpseudocolorrenderer.cpp + raster/qgsrastersinglecolorrenderer.cpp raster/qgshillshaderenderer.cpp mesh/qgsmesh3daveraging.cpp @@ -1861,6 +1863,7 @@ set(QGIS_CORE_HDRS raster/qgsrasterlayerprofilegenerator.h raster/qgsrasterlayerrenderer.h raster/qgsrasterlayertemporalproperties.h + raster/qgsrasterlayerutils.h raster/qgsrasterminmaxorigin.h raster/qgsrasternuller.h raster/qgsrasterpipe.h @@ -1879,6 +1882,7 @@ set(QGIS_CORE_HDRS raster/qgssinglebandcolordatarenderer.h raster/qgssinglebandgrayrenderer.h raster/qgssinglebandpseudocolorrenderer.h + raster/qgsrastersinglecolorrenderer.h scalebar/qgsdoubleboxscalebarrenderer.h scalebar/qgshollowscalebarrenderer.h diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index 96b920c0d7d4..04f34a425973 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -2644,6 +2644,7 @@ void QgsDxfExport::createDDBlockInfo() } sctx.setFeature( &fet ); + sctx.renderContext().expressionContext().setFeature( fet ); DataDefinedBlockInfo blockInfo; blockInfo.blockName = QStringLiteral( "symbolLayer%1class%2" ).arg( symbolLayerNr ).arg( symbolHash ); diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index b61d36c158de..9cf1f2445f1c 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1492,6 +1492,19 @@ bool QgsGeometry::contains( const QgsPointXY *p ) const return geos.contains( &pt, &mLastError ); } +bool QgsGeometry::contains( double x, double y ) const +{ + if ( !d->geometry ) + { + return false; + } + + QgsPoint pt( x, y ); + QgsGeos geos( d->geometry.get() ); + mLastError.clear(); + return geos.contains( &pt, &mLastError ); +} + bool QgsGeometry::contains( const QgsGeometry &geometry ) const { if ( !d->geometry || geometry.isNull() ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index fff7878c646c..ecd16457f87c 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -1417,6 +1417,13 @@ class CORE_EXPORT QgsGeometry */ bool contains( const QgsPointXY *p ) const; + /** + * Returns TRUE if the geometry contains the point at (\a x, \a y). + * + * \since QGIS 3.38 + */ + bool contains( double x, double y ) const; + /** * Returns TRUE if the geometry completely contains another \a geometry. * diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index 6285cf0d4718..00c404bc63d9 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -94,45 +94,59 @@ static void printGEOSNotice( const char *fmt, ... ) #endif } -class GEOSInit -{ - public: - GEOSContextHandle_t ctxt; +// +// QgsGeosContext +// - GEOSInit() - { - ctxt = initGEOS_r( printGEOSNotice, throwGEOSException ); - } +thread_local QgsGeosContext QgsGeosContext::sGeosContext; - ~GEOSInit() - { - finishGEOS_r( ctxt ); - } +QgsGeosContext::QgsGeosContext() +{ +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=5 ) + mContext = GEOS_init_r(); + GEOSContext_setNoticeHandler_r( mContext, printGEOSNotice ); + GEOSContext_setErrorHandler_r( mContext, throwGEOSException ); +#else + mContext = initGEOS_r( printGEOSNotice, throwGEOSException ); +#endif +} - GEOSInit( const GEOSInit &rh ) = delete; - GEOSInit &operator=( const GEOSInit &rh ) = delete; -}; +QgsGeosContext::~QgsGeosContext() +{ +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=5 ) + GEOS_finish_r( mContext ); +#else + finishGEOS_r( mContext ); +#endif +} -Q_GLOBAL_STATIC( GEOSInit, geosinit ) +GEOSContextHandle_t QgsGeosContext::get() +{ + return sGeosContext.mContext; +} + +// +// geos +// void geos::GeosDeleter::operator()( GEOSGeometry *geom ) const { - GEOSGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSGeom_destroy_r( QgsGeosContext::get(), geom ); } void geos::GeosDeleter::operator()( const GEOSPreparedGeometry *geom ) const { - GEOSPreparedGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSPreparedGeom_destroy_r( QgsGeosContext::get(), geom ); } void geos::GeosDeleter::operator()( GEOSBufferParams *params ) const { - GEOSBufferParams_destroy_r( geosinit()->ctxt, params ); + GEOSBufferParams_destroy_r( QgsGeosContext::get(), params ); } void geos::GeosDeleter::operator()( GEOSCoordSequence *sequence ) const { - GEOSCoordSeq_destroy_r( geosinit()->ctxt, sequence ); + GEOSCoordSeq_destroy_r( QgsGeosContext::get(), sequence ); } @@ -150,7 +164,7 @@ QgsGeos::QgsGeos( const QgsAbstractGeometry *geometry, double precision, bool al QgsGeometry QgsGeos::geometryFromGeos( GEOSGeometry *geos ) { QgsGeometry g( QgsGeos::fromGeos( geos ) ); - GEOSGeom_destroy_r( QgsGeos::getGEOSHandler(), geos ); + GEOSGeom_destroy_r( QgsGeosContext::get(), geos ); return g; } @@ -167,6 +181,8 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m return nullptr; } + GEOSContextHandle_t context = QgsGeosContext::get(); + #if GEOS_VERSION_MAJOR==3 && GEOS_VERSION_MINOR<10 if ( method != Qgis::MakeValidMethod::Linework ) throw QgsNotSupportedException( QObject::tr( "The structured method to make geometries valid requires a QGIS build based on GEOS 3.10 or later" ) ); @@ -176,32 +192,32 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m geos::unique_ptr geos; try { - geos.reset( GEOSMakeValid_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMakeValid_r( context, mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) #else - GEOSMakeValidParams *params = GEOSMakeValidParams_create_r( geosinit()->ctxt ); + GEOSMakeValidParams *params = GEOSMakeValidParams_create_r( context ); switch ( method ) { case Qgis::MakeValidMethod::Linework: - GEOSMakeValidParams_setMethod_r( geosinit()->ctxt, params, GEOS_MAKE_VALID_LINEWORK ); + GEOSMakeValidParams_setMethod_r( context, params, GEOS_MAKE_VALID_LINEWORK ); break; case Qgis::MakeValidMethod::Structure: - GEOSMakeValidParams_setMethod_r( geosinit()->ctxt, params, GEOS_MAKE_VALID_STRUCTURE ); + GEOSMakeValidParams_setMethod_r( context, params, GEOS_MAKE_VALID_STRUCTURE ); break; } - GEOSMakeValidParams_setKeepCollapsed_r( geosinit()->ctxt, + GEOSMakeValidParams_setKeepCollapsed_r( context, params, keepCollapsed ? 1 : 0 ); geos::unique_ptr geos; try { - geos.reset( GEOSMakeValidWithParams_r( geosinit()->ctxt, mGeos.get(), params ) ); - GEOSMakeValidParams_destroy_r( geosinit()->ctxt, params ); + geos.reset( GEOSMakeValidWithParams_r( context, mGeos.get(), params ) ); + GEOSMakeValidParams_destroy_r( context, params ); } catch ( GEOSException &e ) { @@ -209,7 +225,7 @@ std::unique_ptr QgsGeos::makeValid( Qgis::MakeValidMethod m { *errorMsg = e.what(); } - GEOSMakeValidParams_destroy_r( geosinit()->ctxt, params ); + GEOSMakeValidParams_destroy_r( context, params ); return nullptr; } #endif @@ -258,7 +274,7 @@ void QgsGeos::prepareGeometry() } if ( mGeos ) { - mGeosPrepared.reset( GEOSPrepare_r( geosinit()->ctxt, mGeos.get() ) ); + mGeosPrepared.reset( GEOSPrepare_r( QgsGeosContext::get(), mGeos.get() ) ); } } @@ -296,7 +312,7 @@ std::unique_ptr QgsGeos::clip( const QgsRectangle &rect, QS try { - geos::unique_ptr opGeom( GEOSClipByRect_r( geosinit()->ctxt, mGeos.get(), rect.xMinimum(), rect.yMinimum(), rect.xMaximum(), rect.yMaximum() ) ); + geos::unique_ptr opGeom( GEOSClipByRect_r( QgsGeosContext::get(), mGeos.get(), rect.xMinimum(), rect.yMinimum(), rect.xMaximum(), rect.yMaximum() ) ); return fromGeos( opGeom.get() ); } catch ( GEOSException &e ) @@ -310,12 +326,10 @@ std::unique_ptr QgsGeos::clip( const QgsRectangle &rect, QS } } - - - void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, int depth, QgsGeometryCollection *parts, const QgsRectangle &clipRect, double gridSize ) const { - int partType = GEOSGeomTypeId_r( geosinit()->ctxt, currentPart ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int partType = GEOSGeomTypeId_r( context, currentPart ); if ( qgsDoubleNear( clipRect.width(), 0.0 ) && qgsDoubleNear( clipRect.height(), 0.0 ) ) { if ( partType == GEOS_POINT ) @@ -331,10 +345,10 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, if ( partType == GEOS_MULTILINESTRING || partType == GEOS_MULTIPOLYGON || partType == GEOS_GEOMETRYCOLLECTION ) { - int partCount = GEOSGetNumGeometries_r( geosinit()->ctxt, currentPart ); + int partCount = GEOSGetNumGeometries_r( context, currentPart ); for ( int i = 0; i < partCount; ++i ) { - subdivideRecursive( GEOSGetGeometryN_r( geosinit()->ctxt, currentPart, i ), maxNodes, depth, parts, clipRect, gridSize ); + subdivideRecursive( GEOSGetGeometryN_r( context, currentPart, i ), maxNodes, depth, parts, clipRect, gridSize ); } return; } @@ -345,7 +359,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, return; } - int vertexCount = GEOSGetNumCoordinates_r( geosinit()->ctxt, currentPart ); + int vertexCount = GEOSGetNumCoordinates_r( context, currentPart ); if ( vertexCount == 0 ) { return; @@ -387,8 +401,8 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, halfClipRect2.setXMaximum( halfClipRect2.xMaximum() + std::numeric_limits::epsilon() ); } - geos::unique_ptr clipPart1( GEOSClipByRect_r( geosinit()->ctxt, currentPart, halfClipRect1.xMinimum(), halfClipRect1.yMinimum(), halfClipRect1.xMaximum(), halfClipRect1.yMaximum() ) ); - geos::unique_ptr clipPart2( GEOSClipByRect_r( geosinit()->ctxt, currentPart, halfClipRect2.xMinimum(), halfClipRect2.yMinimum(), halfClipRect2.xMaximum(), halfClipRect2.yMaximum() ) ); + geos::unique_ptr clipPart1( GEOSClipByRect_r( context, currentPart, halfClipRect1.xMinimum(), halfClipRect1.yMinimum(), halfClipRect1.xMaximum(), halfClipRect1.yMaximum() ) ); + geos::unique_ptr clipPart2( GEOSClipByRect_r( context, currentPart, halfClipRect2.xMinimum(), halfClipRect2.yMinimum(), halfClipRect2.xMaximum(), halfClipRect2.yMaximum() ) ); ++depth; @@ -396,7 +410,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, { if ( gridSize > 0 ) { - clipPart1.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), clipPart1.get(), gridSize ) ); + clipPart1.reset( GEOSIntersectionPrec_r( context, mGeos.get(), clipPart1.get(), gridSize ) ); } subdivideRecursive( clipPart1.get(), maxNodes, depth, parts, halfClipRect1, gridSize ); } @@ -404,7 +418,7 @@ void QgsGeos::subdivideRecursive( const GEOSGeometry *currentPart, int maxNodes, { if ( gridSize > 0 ) { - clipPart2.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), clipPart2.get(), gridSize ) ); + clipPart2.reset( GEOSIntersectionPrec_r( context, mGeos.get(), clipPart2.get(), gridSize ) ); } subdivideRecursive( clipPart2.get(), maxNodes, depth, parts, halfClipRect2, gridSize ); } @@ -447,17 +461,18 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geo geosGeometries.emplace_back( asGeos( g, mPrecision ) ); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geomUnion; try { geos::unique_ptr geomCollection = createGeosCollection( GEOS_GEOMETRYCOLLECTION, geosGeometries ); if ( parameters.gridSize() > 0 ) { - geomUnion.reset( GEOSUnaryUnionPrec_r( geosinit()->ctxt, geomCollection.get(), parameters.gridSize() ) ); + geomUnion.reset( GEOSUnaryUnionPrec_r( context, geomCollection.get(), parameters.gridSize() ) ); } else { - geomUnion.reset( GEOSUnaryUnion_r( geosinit()->ctxt, geomCollection.get() ) ); + geomUnion.reset( GEOSUnaryUnion_r( context, geomCollection.get() ) ); } } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -478,6 +493,7 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QSt geosGeometries.emplace_back( asGeos( g.constGet(), mPrecision ) ); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geomUnion; try { @@ -485,11 +501,11 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QSt if ( parameters.gridSize() > 0 ) { - geomUnion.reset( GEOSUnaryUnionPrec_r( geosinit()->ctxt, geomCollection.get(), parameters.gridSize() ) ); + geomUnion.reset( GEOSUnaryUnionPrec_r( context, geomCollection.get(), parameters.gridSize() ) ); } else { - geomUnion.reset( GEOSUnaryUnion_r( geosinit()->ctxt, geomCollection.get() ) ); + geomUnion.reset( GEOSUnaryUnion_r( context, geomCollection.get() ) ); } } @@ -518,15 +534,16 @@ double QgsGeos::distance( const QgsAbstractGeometry *geom, QString *errorMsg ) c return distance; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); } else { - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), otherGeosGeom.get(), &distance ); } } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -546,15 +563,16 @@ double QgsGeos::distance( double x, double y, QString *errorMsg ) const if ( !point ) return distance; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), point.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), point.get(), &distance ); } else { - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), point.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), point.get(), &distance ); } } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -580,23 +598,23 @@ bool QgsGeos::distanceWithin( const QgsAbstractGeometry *geom, double maxdist, Q // distance double distance; - + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) { #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - return GEOSPreparedDistanceWithin_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), maxdist ); + return GEOSPreparedDistanceWithin_r( context, mGeosPrepared.get(), otherGeosGeom.get(), maxdist ); #else - GEOSPreparedDistance_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); + GEOSPreparedDistance_r( context, mGeosPrepared.get(), otherGeosGeom.get(), &distance ); #endif } else { #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - return GEOSDistanceWithin_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), maxdist ); + return GEOSDistanceWithin_r( context, mGeos.get(), otherGeosGeom.get(), maxdist ); #else - GEOSDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSDistance_r( context, mGeos.get(), otherGeosGeom.get(), &distance ); #endif } } @@ -612,14 +630,15 @@ bool QgsGeos::contains( double x, double y, QString *errorMsg ) const return false; bool result = false; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( mGeosPrepared ) //use faster version with prepared geometry { - return GEOSPreparedContains_r( geosinit()->ctxt, mGeosPrepared.get(), point.get() ) == 1; + return GEOSPreparedContains_r( context, mGeosPrepared.get(), point.get() ) == 1; } - result = ( GEOSContains_r( geosinit()->ctxt, mGeos.get(), point.get() ) == 1 ); + result = ( GEOSContains_r( context, mGeos.get(), point.get() ) == 1 ); } catch ( GEOSException &e ) { @@ -650,7 +669,7 @@ double QgsGeos::hausdorffDistance( const QgsAbstractGeometry *geom, QString *err try { - GEOSHausdorffDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSHausdorffDistance_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -673,7 +692,7 @@ double QgsGeos::hausdorffDistanceDensify( const QgsAbstractGeometry *geom, doubl try { - GEOSHausdorffDistanceDensify_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); + GEOSHausdorffDistanceDensify_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -696,7 +715,7 @@ double QgsGeos::frechetDistance( const QgsAbstractGeometry *geom, QString *error try { - GEOSFrechetDistance_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), &distance ); + GEOSFrechetDistance_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -719,7 +738,7 @@ double QgsGeos::frechetDistanceDensify( const QgsAbstractGeometry *geom, double try { - GEOSFrechetDistanceDensify_r( geosinit()->ctxt, mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); + GEOSFrechetDistanceDensify_r( QgsGeosContext::get(), mGeos.get(), otherGeosGeom.get(), densifyFraction, &distance ); } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -775,13 +794,14 @@ QString QgsGeos::relate( const QgsAbstractGeometry *geom, QString *errorMsg ) co } QString result; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - char *r = GEOSRelate_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ); + char *r = GEOSRelate_r( context, mGeos.get(), geosGeom.get() ); if ( r ) { result = QString( r ); - GEOSFree_r( geosinit()->ctxt, r ); + GEOSFree_r( context, r ); } } catch ( GEOSException &e ) @@ -810,9 +830,10 @@ bool QgsGeos::relatePattern( const QgsAbstractGeometry *geom, const QString &pat } bool result = false; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - result = ( GEOSRelatePattern_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), pattern.toLocal8Bit().constData() ) == 1 ); + result = ( GEOSRelatePattern_r( context, mGeos.get(), geosGeom.get(), pattern.toLocal8Bit().constData() ) == 1 ); } catch ( GEOSException &e ) { @@ -836,7 +857,7 @@ double QgsGeos::area( QString *errorMsg ) const try { - if ( GEOSArea_r( geosinit()->ctxt, mGeos.get(), &area ) != 1 ) + if ( GEOSArea_r( QgsGeosContext::get(), mGeos.get(), &area ) != 1 ) return -1.0; } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -852,7 +873,7 @@ double QgsGeos::length( QString *errorMsg ) const } try { - if ( GEOSLength_r( geosinit()->ctxt, mGeos.get(), &length ) != 1 ) + if ( GEOSLength_r( QgsGeosContext::get(), mGeos.get(), &length ) != 1 ) return -1.0; } CATCH_GEOS_WITH_ERRMSG( -1.0 ) @@ -878,7 +899,8 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitGeometry( const QgsLineSt return SplitCannotSplitPoint; //cannot split points } - if ( !GEOSisValid_r( geosinit()->ctxt, mGeos.get() ) ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( !GEOSisValid_r( context, mGeos.get() ) ) return InvalidBaseGeometry; //make sure splitLine is valid @@ -904,7 +926,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitGeometry( const QgsLineSt return InvalidInput; } - if ( !GEOSisValid_r( geosinit()->ctxt, splitLineGeos.get() ) || !GEOSisSimple_r( geosinit()->ctxt, splitLineGeos.get() ) ) + if ( !GEOSisValid_r( context, splitLineGeos.get() ) || !GEOSisSimple_r( context, splitLineGeos.get() ) ) { return InvalidInput; } @@ -950,21 +972,22 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { testPoints.clear(); - geos::unique_ptr intersectionGeom( GEOSIntersection_r( geosinit()->ctxt, mGeos.get(), splitLine ) ); + geos::unique_ptr intersectionGeom( GEOSIntersection_r( context, mGeos.get(), splitLine ) ); if ( !intersectionGeom ) return false; bool simple = false; int nIntersectGeoms = 1; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, intersectionGeom.get() ) == GEOS_LINESTRING - || GEOSGeomTypeId_r( geosinit()->ctxt, intersectionGeom.get() ) == GEOS_POINT ) + if ( GEOSGeomTypeId_r( context, intersectionGeom.get() ) == GEOS_LINESTRING + || GEOSGeomTypeId_r( context, intersectionGeom.get() ) == GEOS_POINT ) simple = true; if ( !simple ) - nIntersectGeoms = GEOSGetNumGeometries_r( geosinit()->ctxt, intersectionGeom.get() ); + nIntersectGeoms = GEOSGetNumGeometries_r( context, intersectionGeom.get() ); for ( int i = 0; i < nIntersectGeoms; ++i ) { @@ -972,16 +995,16 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin if ( simple ) currentIntersectGeom = intersectionGeom.get(); else - currentIntersectGeom = GEOSGetGeometryN_r( geosinit()->ctxt, intersectionGeom.get(), i ); + currentIntersectGeom = GEOSGetGeometryN_r( context, intersectionGeom.get(), i ); - const GEOSCoordSequence *lineSequence = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, currentIntersectGeom ); + const GEOSCoordSequence *lineSequence = GEOSGeom_getCoordSeq_r( context, currentIntersectGeom ); unsigned int sequenceSize = 0; double x, y, z; - if ( GEOSCoordSeq_getSize_r( geosinit()->ctxt, lineSequence, &sequenceSize ) != 0 ) + if ( GEOSCoordSeq_getSize_r( context, lineSequence, &sequenceSize ) != 0 ) { for ( unsigned int i = 0; i < sequenceSize; ++i ) { - if ( GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, lineSequence, i, &x, &y, &z ) ) + if ( GEOSCoordSeq_getXYZ_r( context, lineSequence, i, &x, &y, &z ) ) { testPoints.push_back( QgsPoint( x, y, z ) ); } @@ -996,7 +1019,8 @@ bool QgsGeos::topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPoin geos::unique_ptr QgsGeos::linePointDifference( GEOSGeometry *GEOSsplitPoint ) const { - int type = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int type = GEOSGeomTypeId_r( context, mGeos.get() ); std::unique_ptr< QgsMultiCurve > multiCurve; if ( type == GEOS_MULTILINESTRING ) @@ -1145,14 +1169,14 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( const GEO if ( !mGeos ) return InvalidBaseGeometry; - GEOSContextHandle_t geosctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); - geos::unique_ptr intersectGeom( GEOSIntersection_r( geosctxt, splitLine, mGeos.get() ) ); - if ( !intersectGeom || GEOSisEmpty_r( geosctxt, intersectGeom.get() ) ) + geos::unique_ptr intersectGeom( GEOSIntersection_r( context, splitLine, mGeos.get() ) ); + if ( !intersectGeom || GEOSisEmpty_r( context, intersectGeom.get() ) ) return NothingHappened; //check that split line has no linear intersection - const int linearIntersect = GEOSRelatePattern_r( geosctxt, mGeos.get(), splitLine, "1********" ); + const int linearIntersect = GEOSRelatePattern_r( context, mGeos.get(), splitLine, "1********" ); if ( linearIntersect > 0 ) return InvalidInput; @@ -1163,18 +1187,18 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( const GEO std::vector lineGeoms; - const int splitType = GEOSGeomTypeId_r( geosctxt, splitGeom.get() ); + const int splitType = GEOSGeomTypeId_r( context, splitGeom.get() ); if ( splitType == GEOS_MULTILINESTRING ) { - const int nGeoms = GEOSGetNumGeometries_r( geosctxt, splitGeom.get() ); + const int nGeoms = GEOSGetNumGeometries_r( context, splitGeom.get() ); lineGeoms.reserve( nGeoms ); for ( int i = 0; i < nGeoms; ++i ) - lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, GEOSGetGeometryN_r( geosctxt, splitGeom.get(), i ) ) ); + lineGeoms.emplace_back( GEOSGeom_clone_r( context, GEOSGetGeometryN_r( context, splitGeom.get(), i ) ) ); } else { - lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, splitGeom.get() ) ); + lineGeoms.emplace_back( GEOSGeom_clone_r( context, splitGeom.get() ) ); } mergeGeometriesMultiTypeSplit( lineGeoms ); @@ -1200,10 +1224,10 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE if ( !mGeosPrepared ) return EngineError; - GEOSContextHandle_t geosctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); //first test if linestring intersects geometry. If not, return straight away - if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( geosctxt, mGeosPrepared.get(), splitLine ) ) + if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( context, mGeosPrepared.get(), splitLine ) ) return NothingHappened; //first union all the polygon rings together (to get them noded, see JTS developer guide) @@ -1212,7 +1236,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE return NodedGeometryError; //an error occurred during noding const GEOSGeometry *noded = nodedGeometry.get(); - geos::unique_ptr polygons( GEOSPolygonize_r( geosctxt, &noded, 1 ) ); + geos::unique_ptr polygons( GEOSPolygonize_r( context, &noded, 1 ) ); if ( !polygons ) { return InvalidBaseGeometry; @@ -1232,11 +1256,11 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE // those would be also returned by polygonize and we need to skip them) for ( int i = 0; i < numberOfGeometriesPolygon; i++ ) { - const GEOSGeometry *polygon = GEOSGetGeometryN_r( geosctxt, polygons.get(), i ); + const GEOSGeometry *polygon = GEOSGetGeometryN_r( context, polygons.get(), i ); - geos::unique_ptr pointOnSurface( GEOSPointOnSurface_r( geosctxt, polygon ) ); - if ( pointOnSurface && GEOSPreparedIntersects_r( geosctxt, mGeosPrepared.get(), pointOnSurface.get() ) ) - testedGeometries.emplace_back( GEOSGeom_clone_r( geosctxt, polygon ) ); + geos::unique_ptr pointOnSurface( GEOSPointOnSurface_r( context, polygon ) ); + if ( pointOnSurface && GEOSPreparedIntersects_r( context, mGeosPrepared.get(), pointOnSurface.get() ) ) + testedGeometries.emplace_back( GEOSGeom_clone_r( context, polygon ) ); } const size_t nGeometriesThis = numberOfGeometries( mGeos.get() ); //original number of geometries @@ -1253,7 +1277,7 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GE mergeGeometriesMultiTypeSplit( testedGeometries ); size_t i; - for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( geosctxt, testedGeometries[i].get() ); ++i ) + for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( context, testedGeometries[i].get() ); ++i ) ; if ( i < testedGeometries.size() ) @@ -1275,13 +1299,14 @@ geos::unique_ptr QgsGeos::nodeGeometries( const GEOSGeometry *splitLine, const G return nullptr; geos::unique_ptr geometryBoundary; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, geom ) == GEOS_POLYGON || GEOSGeomTypeId_r( geosinit()->ctxt, geom ) == GEOS_MULTIPOLYGON ) - geometryBoundary.reset( GEOSBoundary_r( geosinit()->ctxt, geom ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, geom ) == GEOS_POLYGON || GEOSGeomTypeId_r( context, geom ) == GEOS_MULTIPOLYGON ) + geometryBoundary.reset( GEOSBoundary_r( context, geom ) ); else - geometryBoundary.reset( GEOSGeom_clone_r( geosinit()->ctxt, geom ) ); + geometryBoundary.reset( GEOSGeom_clone_r( context, geom ) ); - geos::unique_ptr splitLineClone( GEOSGeom_clone_r( geosinit()->ctxt, splitLine ) ); - geos::unique_ptr unionGeometry( GEOSUnion_r( geosinit()->ctxt, splitLineClone.get(), geometryBoundary.get() ) ); + geos::unique_ptr splitLineClone( GEOSGeom_clone_r( context, splitLine ) ); + geos::unique_ptr unionGeometry( GEOSUnion_r( context, splitLineClone.get(), geometryBoundary.get() ) ); return unionGeometry; } @@ -1292,7 +1317,8 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( std::vector &split return 1; //convert mGeos to geometry collection - int type = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int type = GEOSGeomTypeId_r( context, mGeos.get() ); if ( type != GEOS_GEOMETRYCOLLECTION && type != GEOS_MULTILINESTRING && type != GEOS_MULTIPOLYGON && @@ -1308,9 +1334,9 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( std::vector &split { //is this geometry a part of the original multitype? bool isPart = false; - for ( int j = 0; j < GEOSGetNumGeometries_r( geosinit()->ctxt, mGeos.get() ); j++ ) + for ( int j = 0; j < GEOSGetNumGeometries_r( context, mGeos.get() ); j++ ) { - if ( GEOSEquals_r( geosinit()->ctxt, splitResult[i].get(), GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), j ) ) ) + if ( GEOSEquals_r( context, splitResult[i].get(), GEOSGetGeometryN_r( context, mGeos.get(), j ) ) ) { isPart = true; break; @@ -1352,11 +1378,12 @@ geos::unique_ptr QgsGeos::createGeosCollection( int typeId, std::vector geomarr; geomarr.reserve( geoms.size() ); + GEOSContextHandle_t context = QgsGeosContext::get(); for ( geos::unique_ptr &geomUniquePtr : geoms ) { if ( geomUniquePtr ) { - if ( !GEOSisEmpty_r( geosinit()->ctxt, geomUniquePtr.get() ) ) + if ( !GEOSisEmpty_r( context, geomUniquePtr.get() ) ) { // don't add empty parts to a geos collection, it can cause crashes in GEOS // transfer ownership of the geometries to GEOSGeom_createCollection_r() @@ -1368,13 +1395,13 @@ geos::unique_ptr QgsGeos::createGeosCollection( int typeId, std::vectorctxt, typeId, geomarr.data(), geomarr.size() ) ); + geomRes.reset( GEOSGeom_createCollection_r( context, typeId, geomarr.data(), geomarr.size() ) ); } catch ( GEOSException & ) { for ( GEOSGeometry *geom : geomarr ) { - GEOSGeom_destroy_r( geosinit()->ctxt, geom ); + GEOSGeom_destroy_r( context, geom ); } } @@ -1388,21 +1415,22 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos return nullptr; } - int nCoordDims = GEOSGeom_getCoordinateDimension_r( geosinit()->ctxt, geos ); - int nDims = GEOSGeom_getDimensions_r( geosinit()->ctxt, geos ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int nCoordDims = GEOSGeom_getCoordinateDimension_r( context, geos ); + int nDims = GEOSGeom_getDimensions_r( context, geos ); bool hasZ = ( nCoordDims == 3 ); bool hasM = ( ( nDims - nCoordDims ) == 1 ); - switch ( GEOSGeomTypeId_r( geosinit()->ctxt, geos ) ) + switch ( GEOSGeomTypeId_r( context, geos ) ) { case GEOS_POINT: // a point { - if ( GEOSisEmpty_r( geosinit()->ctxt, geos ) ) + if ( GEOSisEmpty_r( context, geos ) ) return nullptr; - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, geos ); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, geos ); unsigned int nPoints = 0; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); return nPoints > 0 ? std::unique_ptr( coordSeqPoint( cs, 0, hasZ, hasM ).clone() ) : nullptr; } case GEOS_LINESTRING: @@ -1416,15 +1444,15 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_MULTIPOINT: { std::unique_ptr< QgsMultiPoint > multiPoint( new QgsMultiPoint() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiPoint->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, GEOSGetGeometryN_r( context, geos, i ) ); if ( cs ) { unsigned int nPoints = 0; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); if ( nPoints > 0 ) multiPoint->addGeometry( coordSeqPoint( cs, 0, hasZ, hasM ).clone() ); } @@ -1434,11 +1462,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_MULTILINESTRING: { std::unique_ptr< QgsMultiLineString > multiLineString( new QgsMultiLineString() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiLineString->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsLineString >line( sequenceToLinestring( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ), hasZ, hasM ) ); + std::unique_ptr< QgsLineString >line( sequenceToLinestring( GEOSGetGeometryN_r( context, geos, i ), hasZ, hasM ) ); if ( line ) { multiLineString->addGeometry( line.release() ); @@ -1450,11 +1478,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos { std::unique_ptr< QgsMultiPolygon > multiPolygon( new QgsMultiPolygon() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); multiPolygon->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsPolygon > poly = fromGeosPolygon( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ); + std::unique_ptr< QgsPolygon > poly = fromGeosPolygon( GEOSGetGeometryN_r( context, geos, i ) ); if ( poly ) { multiPolygon->addGeometry( poly.release() ); @@ -1465,11 +1493,11 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos case GEOS_GEOMETRYCOLLECTION: { std::unique_ptr< QgsGeometryCollection > geomCollection( new QgsGeometryCollection() ); - int nParts = GEOSGetNumGeometries_r( geosinit()->ctxt, geos ); + int nParts = GEOSGetNumGeometries_r( context, geos ); geomCollection->reserve( nParts ); for ( int i = 0; i < nParts; ++i ) { - std::unique_ptr< QgsAbstractGeometry > geom( fromGeos( GEOSGetGeometryN_r( geosinit()->ctxt, geos, i ) ) ); + std::unique_ptr< QgsAbstractGeometry > geom( fromGeos( GEOSGetGeometryN_r( context, geos, i ) ) ); if ( geom ) { geomCollection->addGeometry( geom.release() ); @@ -1483,30 +1511,31 @@ std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos std::unique_ptr QgsGeos::fromGeosPolygon( const GEOSGeometry *geos ) { - if ( GEOSGeomTypeId_r( geosinit()->ctxt, geos ) != GEOS_POLYGON ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, geos ) != GEOS_POLYGON ) { return nullptr; } - int nCoordDims = GEOSGeom_getCoordinateDimension_r( geosinit()->ctxt, geos ); - int nDims = GEOSGeom_getDimensions_r( geosinit()->ctxt, geos ); + int nCoordDims = GEOSGeom_getCoordinateDimension_r( context, geos ); + int nDims = GEOSGeom_getDimensions_r( context, geos ); bool hasZ = ( nCoordDims == 3 ); bool hasM = ( ( nDims - nCoordDims ) == 1 ); std::unique_ptr< QgsPolygon > polygon( new QgsPolygon() ); - const GEOSGeometry *ring = GEOSGetExteriorRing_r( geosinit()->ctxt, geos ); + const GEOSGeometry *ring = GEOSGetExteriorRing_r( context, geos ); if ( ring ) { polygon->setExteriorRing( sequenceToLinestring( ring, hasZ, hasM ).release() ); } QVector interiorRings; - const int ringCount = GEOSGetNumInteriorRings_r( geosinit()->ctxt, geos ); + const int ringCount = GEOSGetNumInteriorRings_r( context, geos ); interiorRings.reserve( ringCount ); for ( int i = 0; i < ringCount; ++i ) { - ring = GEOSGetInteriorRingN_r( geosinit()->ctxt, geos, i ); + ring = GEOSGetInteriorRingN_r( context, geos, i ); if ( ring ) { interiorRings.push_back( sequenceToLinestring( ring, hasZ, hasM ).release() ); @@ -1519,10 +1548,11 @@ std::unique_ptr QgsGeos::fromGeosPolygon( const GEOSGeometry *geos ) std::unique_ptr QgsGeos::sequenceToLinestring( const GEOSGeometry *geos, bool hasZ, bool hasM ) { - const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, geos ); + GEOSContextHandle_t context = QgsGeosContext::get(); + const GEOSCoordSequence *cs = GEOSGeom_getCoordSeq_r( context, geos ); unsigned int nPoints; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, cs, &nPoints ); + GEOSCoordSeq_getSize_r( context, cs, &nPoints ); QVector< double > xOut( nPoints ); QVector< double > yOut( nPoints ); @@ -1539,17 +1569,17 @@ std::unique_ptr QgsGeos::sequenceToLinestring( const GEOSGeometry double *m = mOut.data(); #if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) - GEOSCoordSeq_copyToArrays_r( geosinit()->ctxt, cs, x, y, hasZ ? z : nullptr, hasM ? m : nullptr ); + GEOSCoordSeq_copyToArrays_r( context, cs, x, y, hasZ ? z : nullptr, hasM ? m : nullptr ); #else for ( unsigned int i = 0; i < nPoints; ++i ) { if ( hasZ ) - GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, cs, i, x++, y++, z++ ); + GEOSCoordSeq_getXYZ_r( context, cs, i, x++, y++, z++ ); else - GEOSCoordSeq_getXY_r( geosinit()->ctxt, cs, i, x++, y++ ); + GEOSCoordSeq_getXY_r( context, cs, i, x++, y++ ); if ( hasM ) { - GEOSCoordSeq_getOrdinate_r( geosinit()->ctxt, cs, i, 3, m++ ); + GEOSCoordSeq_getOrdinate_r( context, cs, i, 3, m++ ); } } #endif @@ -1562,13 +1592,14 @@ int QgsGeos::numberOfGeometries( GEOSGeometry *g ) if ( !g ) return 0; - int geometryType = GEOSGeomTypeId_r( geosinit()->ctxt, g ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int geometryType = GEOSGeomTypeId_r( context, g ); if ( geometryType == GEOS_POINT || geometryType == GEOS_LINESTRING || geometryType == GEOS_LINEARRING || geometryType == GEOS_POLYGON ) return 1; //calling GEOSGetNumGeometries is save for multi types and collections also in geos2 - return GEOSGetNumGeometries_r( geosinit()->ctxt, g ); + return GEOSGetNumGeometries_r( context, g ); } QgsPoint QgsGeos::coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, bool hasM ) @@ -1578,16 +1609,18 @@ QgsPoint QgsGeos::coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, return QgsPoint(); } + GEOSContextHandle_t context = QgsGeosContext::get(); + double x, y; double z = 0; double m = 0; if ( hasZ ) - GEOSCoordSeq_getXYZ_r( geosinit()->ctxt, cs, i, &x, &y, &z ); + GEOSCoordSeq_getXYZ_r( context, cs, i, &x, &y, &z ); else - GEOSCoordSeq_getXY_r( geosinit()->ctxt, cs, i, &x, &y ); + GEOSCoordSeq_getXY_r( context, cs, i, &x, &y ); if ( hasM ) { - GEOSCoordSeq_getOrdinate_r( geosinit()->ctxt, cs, i, 3, &m ); + GEOSCoordSeq_getOrdinate_r( context, cs, i, 3, &m ); } Qgis::WkbType t = Qgis::WkbType::Point; @@ -1702,6 +1735,7 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry const double gridSize = parameters.gridSize(); + GEOSContextHandle_t context = QgsGeosContext::get(); try { geos::unique_ptr opGeom; @@ -1710,22 +1744,22 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry case OverlayIntersection: if ( gridSize > 0 ) { - opGeom.reset( GEOSIntersectionPrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSIntersectionPrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSIntersection_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSIntersection_r( context, mGeos.get(), geosGeom.get() ) ); } break; case OverlayDifference: if ( gridSize > 0 ) { - opGeom.reset( GEOSDifferencePrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSDifferencePrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSDifference_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSDifference_r( context, mGeos.get(), geosGeom.get() ) ); } break; @@ -1734,16 +1768,16 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry geos::unique_ptr unionGeometry; if ( gridSize > 0 ) { - unionGeometry.reset( GEOSUnionPrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + unionGeometry.reset( GEOSUnionPrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - unionGeometry.reset( GEOSUnion_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + unionGeometry.reset( GEOSUnion_r( context, mGeos.get(), geosGeom.get() ) ); } - if ( unionGeometry && GEOSGeomTypeId_r( geosinit()->ctxt, unionGeometry.get() ) == GEOS_MULTILINESTRING ) + if ( unionGeometry && GEOSGeomTypeId_r( context, unionGeometry.get() ) == GEOS_MULTILINESTRING ) { - geos::unique_ptr mergedLines( GEOSLineMerge_r( geosinit()->ctxt, unionGeometry.get() ) ); + geos::unique_ptr mergedLines( GEOSLineMerge_r( context, unionGeometry.get() ) ); if ( mergedLines ) { unionGeometry = std::move( mergedLines ); @@ -1757,11 +1791,11 @@ std::unique_ptr QgsGeos::overlay( const QgsAbstractGeometry case OverlaySymDifference: if ( gridSize > 0 ) { - opGeom.reset( GEOSSymDifferencePrec_r( geosinit()->ctxt, mGeos.get(), geosGeom.get(), gridSize ) ); + opGeom.reset( GEOSSymDifferencePrec_r( context, mGeos.get(), geosGeom.get(), gridSize ) ); } else { - opGeom.reset( GEOSSymDifference_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) ); + opGeom.reset( GEOSSymDifference_r( context, mGeos.get(), geosGeom.get() ) ); } break; } @@ -1791,6 +1825,7 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); bool result = false; try { @@ -1799,25 +1834,25 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er switch ( r ) { case RelationIntersects: - result = ( GEOSPreparedIntersects_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedIntersects_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationTouches: - result = ( GEOSPreparedTouches_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedTouches_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationCrosses: - result = ( GEOSPreparedCrosses_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedCrosses_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationWithin: - result = ( GEOSPreparedWithin_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedWithin_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationContains: - result = ( GEOSPreparedContains_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedContains_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationDisjoint: - result = ( GEOSPreparedDisjoint_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedDisjoint_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; case RelationOverlaps: - result = ( GEOSPreparedOverlaps_r( geosinit()->ctxt, mGeosPrepared.get(), geosGeom.get() ) == 1 ); + result = ( GEOSPreparedOverlaps_r( context, mGeosPrepared.get(), geosGeom.get() ) == 1 ); break; } return result; @@ -1826,25 +1861,25 @@ bool QgsGeos::relation( const QgsAbstractGeometry *geom, Relation r, QString *er switch ( r ) { case RelationIntersects: - result = ( GEOSIntersects_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSIntersects_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationTouches: - result = ( GEOSTouches_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSTouches_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationCrosses: - result = ( GEOSCrosses_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSCrosses_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationWithin: - result = ( GEOSWithin_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSWithin_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationContains: - result = ( GEOSContains_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSContains_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationDisjoint: - result = ( GEOSDisjoint_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSDisjoint_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; case RelationOverlaps: - result = ( GEOSOverlaps_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ) == 1 ); + result = ( GEOSOverlaps_r( context, mGeos.get(), geosGeom.get() ) == 1 ); break; } } @@ -1871,7 +1906,7 @@ QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, QString *er geos::unique_ptr geos; try { - geos.reset( GEOSBuffer_r( geosinit()->ctxt, mGeos.get(), distance, segments ) ); + geos.reset( GEOSBuffer_r( QgsGeosContext::get(), mGeos.get(), distance, segments ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1887,7 +1922,7 @@ QgsAbstractGeometry *QgsGeos::buffer( double distance, int segments, Qgis::EndCa geos::unique_ptr geos; try { - geos.reset( GEOSBufferWithStyle_r( geosinit()->ctxt, mGeos.get(), distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) ); + geos.reset( GEOSBufferWithStyle_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( endCapStyle ), static_cast< int >( joinStyle ), miterLimit ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1902,7 +1937,7 @@ QgsAbstractGeometry *QgsGeos::simplify( double tolerance, QString *errorMsg ) co geos::unique_ptr geos; try { - geos.reset( GEOSTopologyPreserveSimplify_r( geosinit()->ctxt, mGeos.get(), tolerance ) ); + geos.reset( GEOSTopologyPreserveSimplify_r( QgsGeosContext::get(), mGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1917,7 +1952,7 @@ QgsAbstractGeometry *QgsGeos::interpolate( double distance, QString *errorMsg ) geos::unique_ptr geos; try { - geos.reset( GEOSInterpolate_r( geosinit()->ctxt, mGeos.get(), distance ) ); + geos.reset( GEOSInterpolate_r( QgsGeosContext::get(), mGeos.get(), distance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1934,15 +1969,16 @@ QgsPoint *QgsGeos::centroid( QString *errorMsg ) const double x; double y; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSGetCentroid_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSGetCentroid_r( context, mGeos.get() ) ); if ( !geos ) return nullptr; - GEOSGeomGetX_r( geosinit()->ctxt, geos.get(), &x ); - GEOSGeomGetY_r( geosinit()->ctxt, geos.get(), &y ); + GEOSGeomGetX_r( context, geos.get(), &x ); + GEOSGeomGetY_r( context, geos.get(), &y ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -1958,7 +1994,7 @@ QgsAbstractGeometry *QgsGeos::envelope( QString *errorMsg ) const geos::unique_ptr geos; try { - geos.reset( GEOSEnvelope_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSEnvelope_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ).release(); @@ -1974,18 +2010,19 @@ QgsPoint *QgsGeos::pointOnSurface( QString *errorMsg ) const double x; double y; + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geos; try { - geos.reset( GEOSPointOnSurface_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSPointOnSurface_r( context, mGeos.get() ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return nullptr; } - GEOSGeomGetX_r( geosinit()->ctxt, geos.get(), &x ); - GEOSGeomGetY_r( geosinit()->ctxt, geos.get(), &y ); + GEOSGeomGetX_r( context, geos.get(), &x ); + GEOSGeomGetY_r( context, geos.get(), &y ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) @@ -2001,7 +2038,7 @@ QgsAbstractGeometry *QgsGeos::convexHull( QString *errorMsg ) const try { - geos::unique_ptr cHull( GEOSConvexHull_r( geosinit()->ctxt, mGeos.get() ) ); + geos::unique_ptr cHull( GEOSConvexHull_r( QgsGeosContext::get(), mGeos.get() ) ); std::unique_ptr< QgsAbstractGeometry > cHullGeom = fromGeos( cHull.get() ); return cHullGeom.release(); } @@ -2023,7 +2060,7 @@ QgsAbstractGeometry *QgsGeos::concaveHull( double targetPercent, bool allowHoles try { - geos::unique_ptr concaveHull( GEOSConcaveHull_r( geosinit()->ctxt, mGeos.get(), targetPercent, allowHoles ) ); + geos::unique_ptr concaveHull( GEOSConcaveHull_r( QgsGeosContext::get(), mGeos.get(), targetPercent, allowHoles ) ); std::unique_ptr< QgsAbstractGeometry > concaveHullGeom = fromGeos( concaveHull.get() ); return concaveHullGeom.release(); } @@ -2046,17 +2083,18 @@ Qgis::CoverageValidityResult QgsGeos::validateCoverage( double gapWidth, std::un return Qgis::CoverageValidityResult::Error; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { GEOSGeometry *invalidEdgesGeos = nullptr; - const int result = GEOSCoverageIsValid_r( geosinit()->ctxt, mGeos.get(), gapWidth, invalidEdges ? &invalidEdgesGeos : nullptr ); + const int result = GEOSCoverageIsValid_r( context, mGeos.get(), gapWidth, invalidEdges ? &invalidEdgesGeos : nullptr ); if ( invalidEdges && invalidEdgesGeos ) { *invalidEdges = fromGeos( invalidEdgesGeos ); } if ( invalidEdgesGeos ) { - GEOSGeom_destroy_r( geosinit()->ctxt, invalidEdgesGeos ); + GEOSGeom_destroy_r( context, invalidEdgesGeos ); invalidEdgesGeos = nullptr; } @@ -2092,7 +2130,7 @@ std::unique_ptr QgsGeos::simplifyCoverageVW( double toleran try { - geos::unique_ptr simplified( GEOSCoverageSimplifyVW_r( geosinit()->ctxt, mGeos.get(), tolerance, preserveBoundary ? 1 : 0 ) ); + geos::unique_ptr simplified( GEOSCoverageSimplifyVW_r( QgsGeosContext::get(), mGeos.get(), tolerance, preserveBoundary ? 1 : 0 ) ); std::unique_ptr< QgsAbstractGeometry > simplifiedGeom = fromGeos( simplified.get() ); return simplifiedGeom; } @@ -2111,7 +2149,7 @@ std::unique_ptr QgsGeos::unionCoverage( QString *errorMsg ) try { - geos::unique_ptr unioned( GEOSCoverageUnion_r( geosinit()->ctxt, mGeos.get() ) ); + geos::unique_ptr unioned( GEOSCoverageUnion_r( QgsGeosContext::get(), mGeos.get() ) ); std::unique_ptr< QgsAbstractGeometry > result = fromGeos( unioned.get() ); return result; } @@ -2127,18 +2165,19 @@ bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, Qgs return false; } + GEOSContextHandle_t context = QgsGeosContext::get(); try { GEOSGeometry *g1 = nullptr; char *r = nullptr; - char res = GEOSisValidDetail_r( geosinit()->ctxt, mGeos.get(), allowSelfTouchingHoles ? GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE : 0, &r, &g1 ); + char res = GEOSisValidDetail_r( context, mGeos.get(), allowSelfTouchingHoles ? GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE : 0, &r, &g1 ); const bool invalid = res != 1; QString error; if ( r ) { error = QString( r ); - GEOSFree_r( geosinit()->ctxt, r ); + GEOSFree_r( context, r ); } if ( invalid && errorMsg ) @@ -2170,7 +2209,7 @@ bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, Qgs } else if ( g1 ) { - GEOSGeom_destroy_r( geosinit()->ctxt, g1 ); + GEOSGeom_destroy_r( context, g1 ); } } return !invalid; @@ -2192,7 +2231,7 @@ bool QgsGeos::isEqual( const QgsAbstractGeometry *geom, QString *errorMsg ) cons { return false; } - bool equal = GEOSEquals_r( geosinit()->ctxt, mGeos.get(), geosGeom.get() ); + bool equal = GEOSEquals_r( QgsGeosContext::get(), mGeos.get(), geosGeom.get() ); return equal; } CATCH_GEOS_WITH_ERRMSG( false ) @@ -2207,7 +2246,7 @@ bool QgsGeos::isEmpty( QString *errorMsg ) const try { - return GEOSisEmpty_r( geosinit()->ctxt, mGeos.get() ); + return GEOSisEmpty_r( QgsGeosContext::get(), mGeos.get() ); } CATCH_GEOS_WITH_ERRMSG( false ) } @@ -2221,14 +2260,14 @@ bool QgsGeos::isSimple( QString *errorMsg ) const try { - return GEOSisSimple_r( geosinit()->ctxt, mGeos.get() ); + return GEOSisSimple_r( QgsGeosContext::get(), mGeos.get() ); } CATCH_GEOS_WITH_ERRMSG( false ) } GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, double precision, bool forceClose ) { - GEOSContextHandle_t ctxt = geosinit()->ctxt; + GEOSContextHandle_t context = QgsGeosContext::get(); std::unique_ptr< QgsLineString > segmentized; const QgsLineString *line = qgsgeometry_cast( curve ); @@ -2257,7 +2296,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou // use optimised method if we don't have to force close an open ring try { - coordSeq = GEOSCoordSeq_copyFromArrays_r( ctxt, line->xData(), line->yData(), line->zData(), nullptr, numPoints ); + coordSeq = GEOSCoordSeq_copyFromArrays_r( context, line->xData(), line->yData(), line->zData(), nullptr, numPoints ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for %1 points" ).arg( numPoints ) ); @@ -2279,7 +2318,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou z.append( z.at( 0 ) ); try { - coordSeq = GEOSCoordSeq_copyFromArrays_r( ctxt, x.constData(), y.constData(), !hasZ ? nullptr : z.constData(), nullptr, numPoints + 1 ); + coordSeq = GEOSCoordSeq_copyFromArrays_r( context, x.constData(), y.constData(), !hasZ ? nullptr : z.constData(), nullptr, numPoints + 1 ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create closed coordinate sequence for %1 points" ).arg( numPoints + 1 ) ); @@ -2312,7 +2351,7 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou try { - coordSeq = GEOSCoordSeq_create_r( ctxt, numOutPoints, coordDims ); + coordSeq = GEOSCoordSeq_create_r( context, numOutPoints, coordDims ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for %1 points in %2 dimensions" ).arg( numPoints ).arg( coordDims ) ); @@ -2338,15 +2377,15 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou } if ( hasZ ) { - GEOSCoordSeq_setXYZ_r( ctxt, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision, std::round( *zData++ / precision ) * precision ); + GEOSCoordSeq_setXYZ_r( context, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision, std::round( *zData++ / precision ) * precision ); } else { - GEOSCoordSeq_setXY_r( ctxt, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision ); + GEOSCoordSeq_setXY_r( context, coordSeq, i, std::round( *xData++ / precision ) * precision, std::round( *yData++ / precision ) * precision ); } if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( ctxt, coordSeq, i, 3, *mData++ ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, i, 3, *mData++ ); } } } @@ -2364,15 +2403,15 @@ GEOSCoordSequence *QgsGeos::createCoordinateSequence( const QgsCurve *curve, dou } if ( hasZ ) { - GEOSCoordSeq_setXYZ_r( ctxt, coordSeq, i, *xData++, *yData++, *zData++ ); + GEOSCoordSeq_setXYZ_r( context, coordSeq, i, *xData++, *yData++, *zData++ ); } else { - GEOSCoordSeq_setXY_r( ctxt, coordSeq, i, *xData++, *yData++ ); + GEOSCoordSeq_setXY_r( context, coordSeq, i, *xData++, *yData++ ); } if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( ctxt, coordSeq, i, 3, *mData++ ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, i, 3, *mData++ ); } } } @@ -2397,19 +2436,20 @@ geos::unique_ptr QgsGeos::createGeosPointXY( double x, double y, bool hasZ, doub Q_UNUSED( m ) geos::unique_ptr geosPoint; + GEOSContextHandle_t context = QgsGeosContext::get(); try { if ( coordDims == 2 ) { // optimised constructor if ( precision > 0. ) - geosPoint.reset( GEOSGeom_createPointFromXY_r( geosinit()->ctxt, std::round( x / precision ) * precision, std::round( y / precision ) * precision ) ); + geosPoint.reset( GEOSGeom_createPointFromXY_r( context, std::round( x / precision ) * precision, std::round( y / precision ) * precision ) ); else - geosPoint.reset( GEOSGeom_createPointFromXY_r( geosinit()->ctxt, x, y ) ); + geosPoint.reset( GEOSGeom_createPointFromXY_r( context, x, y ) ); return geosPoint; } - GEOSCoordSequence *coordSeq = GEOSCoordSeq_create_r( geosinit()->ctxt, 1, coordDims ); + GEOSCoordSequence *coordSeq = GEOSCoordSeq_create_r( context, 1, coordDims ); if ( !coordSeq ) { QgsDebugError( QStringLiteral( "GEOS Exception: Could not create coordinate sequence for point with %1 dimensions" ).arg( coordDims ) ); @@ -2417,29 +2457,29 @@ geos::unique_ptr QgsGeos::createGeosPointXY( double x, double y, bool hasZ, doub } if ( precision > 0. ) { - GEOSCoordSeq_setX_r( geosinit()->ctxt, coordSeq, 0, std::round( x / precision ) * precision ); - GEOSCoordSeq_setY_r( geosinit()->ctxt, coordSeq, 0, std::round( y / precision ) * precision ); + GEOSCoordSeq_setX_r( context, coordSeq, 0, std::round( x / precision ) * precision ); + GEOSCoordSeq_setY_r( context, coordSeq, 0, std::round( y / precision ) * precision ); if ( hasZ ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 2, std::round( z / precision ) * precision ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 2, std::round( z / precision ) * precision ); } } else { - GEOSCoordSeq_setX_r( geosinit()->ctxt, coordSeq, 0, x ); - GEOSCoordSeq_setY_r( geosinit()->ctxt, coordSeq, 0, y ); + GEOSCoordSeq_setX_r( context, coordSeq, 0, x ); + GEOSCoordSeq_setY_r( context, coordSeq, 0, y ); if ( hasZ ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 2, z ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 2, z ); } } #if 0 //disabled until geos supports m-coordinates if ( hasM ) { - GEOSCoordSeq_setOrdinate_r( geosinit()->ctxt, coordSeq, 0, 3, m ); + GEOSCoordSeq_setOrdinate_r( context, coordSeq, 0, 3, m ); } #endif - geosPoint.reset( GEOSGeom_createPoint_r( geosinit()->ctxt, coordSeq ) ); + geosPoint.reset( GEOSGeom_createPoint_r( context, coordSeq ) ); } CATCH_GEOS( nullptr ) return geosPoint; @@ -2458,7 +2498,7 @@ geos::unique_ptr QgsGeos::createGeosLinestring( const QgsAbstractGeometry *curve geos::unique_ptr geosGeom; try { - geosGeom.reset( GEOSGeom_createLineString_r( geosinit()->ctxt, coordSeq ) ); + geosGeom.reset( GEOSGeom_createLineString_r( QgsGeosContext::get(), coordSeq ) ); } CATCH_GEOS( nullptr ) return geosGeom; @@ -2476,10 +2516,11 @@ geos::unique_ptr QgsGeos::createGeosPolygon( const QgsAbstractGeometry *poly, do return nullptr; } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geosPolygon; try { - geos::unique_ptr exteriorRingGeos( GEOSGeom_createLinearRing_r( geosinit()->ctxt, createCoordinateSequence( exteriorRing, precision, true ) ) ); + geos::unique_ptr exteriorRingGeos( GEOSGeom_createLinearRing_r( context, createCoordinateSequence( exteriorRing, precision, true ) ) ); int nHoles = polygon->numInteriorRings(); GEOSGeometry **holes = nullptr; @@ -2491,9 +2532,9 @@ geos::unique_ptr QgsGeos::createGeosPolygon( const QgsAbstractGeometry *poly, do for ( int i = 0; i < nHoles; ++i ) { const QgsCurve *interiorRing = polygon->interiorRing( i ); - holes[i] = GEOSGeom_createLinearRing_r( geosinit()->ctxt, createCoordinateSequence( interiorRing, precision, true ) ); + holes[i] = GEOSGeom_createLinearRing_r( context, createCoordinateSequence( interiorRing, precision, true ) ); } - geosPolygon.reset( GEOSGeom_createPolygon_r( geosinit()->ctxt, exteriorRingGeos.release(), holes, nHoles ) ); + geosPolygon.reset( GEOSGeom_createPolygon_r( context, exteriorRingGeos.release(), holes, nHoles ) ); delete[] holes; } CATCH_GEOS( nullptr ) @@ -2513,7 +2554,7 @@ QgsAbstractGeometry *QgsGeos::offsetCurve( double distance, int segments, Qgis:: // https://github.com/qgis/QGIS/issues/53165#issuecomment-1563470832 if ( segments < 8 ) segments = 8; - offset.reset( GEOSOffsetCurve_r( geosinit()->ctxt, mGeos.get(), distance, segments, static_cast< int >( joinStyle ), miterLimit ) ); + offset.reset( GEOSOffsetCurve_r( QgsGeosContext::get(), mGeos.get(), distance, segments, static_cast< int >( joinStyle ), miterLimit ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) std::unique_ptr< QgsAbstractGeometry > offsetGeom = fromGeos( offset.get() ); @@ -2528,19 +2569,20 @@ std::unique_ptr QgsGeos::singleSidedBuffer( double distance } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos::buffer_params_unique_ptr bp( GEOSBufferParams_create_r( geosinit()->ctxt ) ); - GEOSBufferParams_setSingleSided_r( geosinit()->ctxt, bp.get(), 1 ); - GEOSBufferParams_setQuadrantSegments_r( geosinit()->ctxt, bp.get(), segments ); - GEOSBufferParams_setJoinStyle_r( geosinit()->ctxt, bp.get(), static_cast< int >( joinStyle ) ); - GEOSBufferParams_setMitreLimit_r( geosinit()->ctxt, bp.get(), miterLimit ); //#spellok + geos::buffer_params_unique_ptr bp( GEOSBufferParams_create_r( context ) ); + GEOSBufferParams_setSingleSided_r( context, bp.get(), 1 ); + GEOSBufferParams_setQuadrantSegments_r( context, bp.get(), segments ); + GEOSBufferParams_setJoinStyle_r( context, bp.get(), static_cast< int >( joinStyle ) ); + GEOSBufferParams_setMitreLimit_r( context, bp.get(), miterLimit ); //#spellok if ( side == Qgis::BufferSide::Right ) { distance = -distance; } - geos.reset( GEOSBufferWithParams_r( geosinit()->ctxt, mGeos.get(), bp.get(), distance ) ); + geos.reset( GEOSBufferWithParams_r( context, mGeos.get(), bp.get(), distance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2556,7 +2598,7 @@ std::unique_ptr QgsGeos::maximumInscribedCircle( double tol geos::unique_ptr geos; try { - geos.reset( GEOSMaximumInscribedCircle_r( geosinit()->ctxt, mGeos.get(), tolerance ) ); + geos.reset( GEOSMaximumInscribedCircle_r( QgsGeosContext::get(), mGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2576,7 +2618,7 @@ std::unique_ptr QgsGeos::largestEmptyCircle( double toleran if ( boundary ) boundaryGeos = asGeos( boundary ); - geos.reset( GEOSLargestEmptyCircle_r( geosinit()->ctxt, mGeos.get(), boundaryGeos.get(), tolerance ) ); + geos.reset( GEOSLargestEmptyCircle_r( QgsGeosContext::get(), mGeos.get(), boundaryGeos.get(), tolerance ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2592,7 +2634,7 @@ std::unique_ptr QgsGeos::minimumWidth( QString *errorMsg ) geos::unique_ptr geos; try { - geos.reset( GEOSMinimumWidth_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMinimumWidth_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2609,7 +2651,7 @@ double QgsGeos::minimumClearance( QString *errorMsg ) const double res = 0; try { - if ( GEOSMinimumClearance_r( geosinit()->ctxt, mGeos.get(), &res ) != 0 ) + if ( GEOSMinimumClearance_r( QgsGeosContext::get(), mGeos.get(), &res ) != 0 ) return std::numeric_limits< double >::quiet_NaN(); } CATCH_GEOS_WITH_ERRMSG( std::numeric_limits< double >::quiet_NaN() ) @@ -2626,7 +2668,7 @@ std::unique_ptr QgsGeos::minimumClearanceLine( QString *err geos::unique_ptr geos; try { - geos.reset( GEOSMinimumClearanceLine_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSMinimumClearanceLine_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2642,7 +2684,7 @@ std::unique_ptr QgsGeos::node( QString *errorMsg ) const geos::unique_ptr geos; try { - geos.reset( GEOSNode_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSNode_r( QgsGeosContext::get(), mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2662,7 +2704,7 @@ std::unique_ptr QgsGeos::sharedPaths( const QgsAbstractGeom if ( !otherGeos ) return nullptr; - geos.reset( GEOSSharedPaths_r( geosinit()->ctxt, mGeos.get(), otherGeos.get() ) ); + geos.reset( GEOSSharedPaths_r( QgsGeosContext::get(), mGeos.get(), otherGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( nullptr ) return fromGeos( geos.get() ); @@ -2684,8 +2726,9 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri geos::unique_ptr reshapeLineGeos = createGeosLinestring( &reshapeWithLine, mPrecision ); + GEOSContextHandle_t context = QgsGeosContext::get(); //single or multi? - int numGeoms = GEOSGetNumGeometries_r( geosinit()->ctxt, mGeos.get() ); + int numGeoms = GEOSGetNumGeometries_r( context, mGeos.get() ); if ( numGeoms == -1 ) { if ( errorCode ) @@ -2696,7 +2739,7 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri } bool isMultiGeom = false; - int geosTypeId = GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ); + int geosTypeId = GEOSGeomTypeId_r( context, mGeos.get() ); if ( geosTypeId == GEOS_MULTILINESTRING || geosTypeId == GEOS_MULTIPOLYGON ) isMultiGeom = true; @@ -2732,9 +2775,9 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri for ( int i = 0; i < numGeoms; ++i ) { if ( isLine ) - currentReshapeGeometry = reshapeLine( GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); + currentReshapeGeometry = reshapeLine( GEOSGetGeometryN_r( context, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); else - currentReshapeGeometry = reshapePolygon( GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); + currentReshapeGeometry = reshapePolygon( GEOSGetGeometryN_r( context, mGeos.get(), i ), reshapeLineGeos.get(), mPrecision ); if ( currentReshapeGeometry ) { @@ -2743,18 +2786,18 @@ std::unique_ptr QgsGeos::reshapeGeometry( const QgsLineStri } else { - newGeoms[i] = GEOSGeom_clone_r( geosinit()->ctxt, GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), i ) ); + newGeoms[i] = GEOSGeom_clone_r( context, GEOSGetGeometryN_r( context, mGeos.get(), i ) ); } } geos::unique_ptr newMultiGeom; if ( isLine ) { - newMultiGeom.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, newGeoms, numGeoms ) ); + newMultiGeom.reset( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, newGeoms, numGeoms ) ); } else //multipolygon { - newMultiGeom.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTIPOLYGON, newGeoms, numGeoms ) ); + newMultiGeom.reset( GEOSGeom_createCollection_r( context, GEOS_MULTIPOLYGON, newGeoms, numGeoms ) ); } delete[] newGeoms; @@ -2791,13 +2834,14 @@ QgsGeometry QgsGeos::mergeLines( QString *errorMsg ) const return QgsGeometry(); } - if ( GEOSGeomTypeId_r( geosinit()->ctxt, mGeos.get() ) != GEOS_MULTILINESTRING ) + GEOSContextHandle_t context = QgsGeosContext::get(); + if ( GEOSGeomTypeId_r( context, mGeos.get() ) != GEOS_MULTILINESTRING ) return QgsGeometry(); geos::unique_ptr geos; try { - geos.reset( GEOSLineMerge_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSLineMerge_r( context, mGeos.get() ) ); } CATCH_GEOS_WITH_ERRMSG( QgsGeometry() ) return QgsGeometry( fromGeos( geos.get() ) ); @@ -2816,6 +2860,7 @@ QgsGeometry QgsGeos::closestPoint( const QgsGeometry &other, QString *errorMsg ) return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); double nx = 0.0; double ny = 0.0; try @@ -2823,15 +2868,15 @@ QgsGeometry QgsGeos::closestPoint( const QgsGeometry &other, QString *errorMsg ) geos::coord_sequence_unique_ptr nearestCoord; if ( mGeosPrepared ) // use faster version with prepared geometry { - nearestCoord.reset( GEOSPreparedNearestPoints_r( geosinit()->ctxt, mGeosPrepared.get(), otherGeom.get() ) ); + nearestCoord.reset( GEOSPreparedNearestPoints_r( context, mGeosPrepared.get(), otherGeom.get() ) ); } else { - nearestCoord.reset( GEOSNearestPoints_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ) ); + nearestCoord.reset( GEOSNearestPoints_r( context, mGeos.get(), otherGeom.get() ) ); } - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 0, &nx ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 0, &ny ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 0, &nx ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 0, &ny ); } catch ( GEOSException &e ) { @@ -2867,13 +2912,14 @@ QgsGeometry QgsGeos::shortestLine( const QgsAbstractGeometry *other, QString *er return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); double nx1 = 0.0; double ny1 = 0.0; double nx2 = 0.0; double ny2 = 0.0; try { - geos::coord_sequence_unique_ptr nearestCoord( GEOSNearestPoints_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ) ); + geos::coord_sequence_unique_ptr nearestCoord( GEOSNearestPoints_r( context, mGeos.get(), otherGeom.get() ) ); if ( !nearestCoord ) { @@ -2882,10 +2928,10 @@ QgsGeometry QgsGeos::shortestLine( const QgsAbstractGeometry *other, QString *er return QgsGeometry(); } - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 0, &nx1 ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 0, &ny1 ); - ( void )GEOSCoordSeq_getX_r( geosinit()->ctxt, nearestCoord.get(), 1, &nx2 ); - ( void )GEOSCoordSeq_getY_r( geosinit()->ctxt, nearestCoord.get(), 1, &ny2 ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 0, &nx1 ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 0, &ny1 ); + ( void )GEOSCoordSeq_getX_r( context, nearestCoord.get(), 1, &nx2 ); + ( void )GEOSCoordSeq_getY_r( context, nearestCoord.get(), 1, &ny2 ); } catch ( GEOSException &e ) { @@ -2919,7 +2965,7 @@ double QgsGeos::lineLocatePoint( const QgsPoint &point, QString *errorMsg ) cons double distance = -1; try { - distance = GEOSProject_r( geosinit()->ctxt, mGeos.get(), otherGeom.get() ); + distance = GEOSProject_r( QgsGeosContext::get(), mGeos.get(), otherGeom.get() ); } catch ( GEOSException &e ) { @@ -2948,7 +2994,7 @@ double QgsGeos::lineLocatePoint( double x, double y, QString *errorMsg ) const double distance = -1; try { - distance = GEOSProject_r( geosinit()->ctxt, mGeos.get(), point.get() ); + distance = GEOSProject_r( QgsGeosContext::get(), mGeos.get(), point.get() ); } catch ( GEOSException &e ) { @@ -2977,12 +3023,13 @@ QgsGeometry QgsGeos::polygonize( const QVector &geo } } + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos::unique_ptr result( GEOSPolygonize_r( geosinit()->ctxt, lineGeosGeometries, validLines ) ); + geos::unique_ptr result( GEOSPolygonize_r( context, lineGeosGeometries, validLines ) ); for ( int i = 0; i < validLines; ++i ) { - GEOSGeom_destroy_r( geosinit()->ctxt, lineGeosGeometries[i] ); + GEOSGeom_destroy_r( context, lineGeosGeometries[i] ); } delete[] lineGeosGeometries; return QgsGeometry( fromGeos( result.get() ) ); @@ -2995,7 +3042,7 @@ QgsGeometry QgsGeos::polygonize( const QVector &geo } for ( int i = 0; i < validLines; ++i ) { - GEOSGeom_destroy_r( geosinit()->ctxt, lineGeosGeometries[i] ); + GEOSGeom_destroy_r( context, lineGeosGeometries[i] ); } delete[] lineGeosGeometries; return QgsGeometry(); @@ -3020,11 +3067,12 @@ QgsGeometry QgsGeos::voronoiDiagram( const QgsAbstractGeometry *extent, double t } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSVoronoiDiagram_r( geosinit()->ctxt, mGeos.get(), extentGeosGeom.get(), tolerance, edgesOnly ) ); + geos.reset( GEOSVoronoiDiagram_r( context, mGeos.get(), extentGeosGeom.get(), tolerance, edgesOnly ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return QgsGeometry(); } @@ -3041,12 +3089,13 @@ QgsGeometry QgsGeos::delaunayTriangulation( double tolerance, bool edgesOnly, QS return QgsGeometry(); } + GEOSContextHandle_t context = QgsGeosContext::get(); geos::unique_ptr geos; try { - geos.reset( GEOSDelaunayTriangulation_r( geosinit()->ctxt, mGeos.get(), tolerance, edgesOnly ) ); + geos.reset( GEOSDelaunayTriangulation_r( context, mGeos.get(), tolerance, edgesOnly ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return QgsGeometry(); } @@ -3068,11 +3117,12 @@ std::unique_ptr QgsGeos::constrainedDelaunayTriangulation( } geos::unique_ptr geos; + GEOSContextHandle_t context = QgsGeosContext::get(); try { - geos.reset( GEOSConstrainedDelaunayTriangulation_r( geosinit()->ctxt, mGeos.get() ) ); + geos.reset( GEOSConstrainedDelaunayTriangulation_r( context, mGeos.get() ) ); - if ( !geos || GEOSisEmpty_r( geosinit()->ctxt, geos.get() ) != 0 ) + if ( !geos || GEOSisEmpty_r( context, geos.get() ) != 0 ) { return nullptr; } @@ -3094,21 +3144,22 @@ std::unique_ptr QgsGeos::constrainedDelaunayTriangulation( //! Extract coordinates of linestring's endpoints. Returns false on error. static bool _linestringEndpoints( const GEOSGeometry *linestring, double &x1, double &y1, double &x2, double &y2 ) { - const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, linestring ); + GEOSContextHandle_t context = QgsGeosContext::get(); + const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( context, linestring ); if ( !coordSeq ) return false; unsigned int coordSeqSize; - if ( GEOSCoordSeq_getSize_r( geosinit()->ctxt, coordSeq, &coordSeqSize ) == 0 ) + if ( GEOSCoordSeq_getSize_r( context, coordSeq, &coordSeqSize ) == 0 ) return false; if ( coordSeqSize < 2 ) return false; - GEOSCoordSeq_getX_r( geosinit()->ctxt, coordSeq, 0, &x1 ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, coordSeq, 0, &y1 ); - GEOSCoordSeq_getX_r( geosinit()->ctxt, coordSeq, coordSeqSize - 1, &x2 ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, coordSeq, coordSeqSize - 1, &y2 ); + GEOSCoordSeq_getX_r( context, coordSeq, 0, &x1 ); + GEOSCoordSeq_getY_r( context, coordSeq, 0, &y1 ); + GEOSCoordSeq_getX_r( context, coordSeq, coordSeqSize - 1, &x2 ); + GEOSCoordSeq_getY_r( context, coordSeq, coordSeqSize - 1, &y2 ); return true; } @@ -3131,14 +3182,15 @@ static geos::unique_ptr _mergeLinestrings( const GEOSGeometry *line1, const GEOS ( intersectionPoint.x() == rx1 && intersectionPoint.y() == ry1 ) || ( intersectionPoint.x() == rx2 && intersectionPoint.y() == ry2 ); + GEOSContextHandle_t context = QgsGeosContext::get(); // the intersection must be at the begin/end of both lines if ( intersectionAtOrigLineEndpoint && intersectionAtReshapeLineEndpoint ) { - geos::unique_ptr g1( GEOSGeom_clone_r( geosinit()->ctxt, line1 ) ); - geos::unique_ptr g2( GEOSGeom_clone_r( geosinit()->ctxt, line2 ) ); + geos::unique_ptr g1( GEOSGeom_clone_r( context, line1 ) ); + geos::unique_ptr g2( GEOSGeom_clone_r( context, line2 ) ); GEOSGeometry *geoms[2] = { g1.release(), g2.release() }; - geos::unique_ptr multiGeom( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, geoms, 2 ) ); - geos::unique_ptr res( GEOSLineMerge_r( geosinit()->ctxt, multiGeom.get() ) ); + geos::unique_ptr multiGeom( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, geoms, 2 ) ); + geos::unique_ptr res( GEOSLineMerge_r( context, multiGeom.get() ) ); return res; } else @@ -3155,23 +3207,24 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome bool oneIntersection = false; QgsPointXY oneIntersectionPoint; + GEOSContextHandle_t context = QgsGeosContext::get(); try { //make sure there are at least two intersection between line and reshape geometry - geos::unique_ptr intersectGeom( GEOSIntersection_r( geosinit()->ctxt, line, reshapeLineGeos ) ); + geos::unique_ptr intersectGeom( GEOSIntersection_r( context, line, reshapeLineGeos ) ); if ( intersectGeom ) { - const int geomType = GEOSGeomTypeId_r( geosinit()->ctxt, intersectGeom.get() ); - atLeastTwoIntersections = ( geomType == GEOS_MULTIPOINT && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 1 ) - || ( geomType == GEOS_GEOMETRYCOLLECTION && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 0 ) // a collection implies at least two points! - || ( geomType == GEOS_MULTILINESTRING && GEOSGetNumGeometries_r( geosinit()->ctxt, intersectGeom.get() ) > 0 ); + const int geomType = GEOSGeomTypeId_r( context, intersectGeom.get() ); + atLeastTwoIntersections = ( geomType == GEOS_MULTIPOINT && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 1 ) + || ( geomType == GEOS_GEOMETRYCOLLECTION && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 0 ) // a collection implies at least two points! + || ( geomType == GEOS_MULTILINESTRING && GEOSGetNumGeometries_r( context, intersectGeom.get() ) > 0 ); // one point is enough when extending line at its endpoint - if ( GEOSGeomTypeId_r( geosinit()->ctxt, intersectGeom.get() ) == GEOS_POINT ) + if ( GEOSGeomTypeId_r( context, intersectGeom.get() ) == GEOS_POINT ) { - const GEOSCoordSequence *intersectionCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, intersectGeom.get() ); + const GEOSCoordSequence *intersectionCoordSeq = GEOSGeom_getCoordSeq_r( context, intersectGeom.get() ); double xi, yi; - GEOSCoordSeq_getX_r( geosinit()->ctxt, intersectionCoordSeq, 0, &xi ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, intersectionCoordSeq, 0, &yi ); + GEOSCoordSeq_getX_r( context, intersectionCoordSeq, 0, &xi ); + GEOSCoordSeq_getY_r( context, intersectionCoordSeq, 0, &yi ); oneIntersection = true; oneIntersectionPoint = QgsPointXY( xi, yi ); } @@ -3198,8 +3251,8 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome geos::unique_ptr endLineVertex = createGeosPointXY( x2, y2, false, 0, false, 0, 2, precision ); bool isRing = false; - if ( GEOSGeomTypeId_r( geosinit()->ctxt, line ) == GEOS_LINEARRING - || GEOSEquals_r( geosinit()->ctxt, beginLineVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSGeomTypeId_r( context, line ) == GEOS_LINEARRING + || GEOSEquals_r( context, beginLineVertex.get(), endLineVertex.get() ) == 1 ) isRing = true; //node line and reshape line @@ -3210,18 +3263,18 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } //and merge them together - geos::unique_ptr mergedLines( GEOSLineMerge_r( geosinit()->ctxt, nodedGeometry.get() ) ); + geos::unique_ptr mergedLines( GEOSLineMerge_r( context, nodedGeometry.get() ) ); if ( !mergedLines ) { return nullptr; } - int numMergedLines = GEOSGetNumGeometries_r( geosinit()->ctxt, mergedLines.get() ); + int numMergedLines = GEOSGetNumGeometries_r( context, mergedLines.get() ); if ( numMergedLines < 2 ) //some special cases. Normally it is >2 { if ( numMergedLines == 1 ) //reshape line is from begin to endpoint. So we keep the reshapeline { - geos::unique_ptr result( GEOSGeom_clone_r( geosinit()->ctxt, reshapeLineGeos ) ); + geos::unique_ptr result( GEOSGeom_clone_r( context, reshapeLineGeos ) ); return result; } else @@ -3233,7 +3286,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome for ( int i = 0; i < numMergedLines; ++i ) { - const GEOSGeometry *currentGeom = GEOSGetGeometryN_r( geosinit()->ctxt, mergedLines.get(), i ); + const GEOSGeometry *currentGeom = GEOSGetGeometryN_r( context, mergedLines.get(), i ); // have we already added this part? bool alreadyAdded = false; @@ -3241,7 +3294,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome double bufferDistance = std::pow( 10.0L, geomDigits( currentGeom ) - 11 ); for ( const GEOSGeometry *other : std::as_const( resultLineParts ) ) { - GEOSHausdorffDistance_r( geosinit()->ctxt, currentGeom, other, &distance ); + GEOSHausdorffDistance_r( context, currentGeom, other, &distance ); if ( distance < bufferDistance ) { alreadyAdded = true; @@ -3251,18 +3304,18 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome if ( alreadyAdded ) continue; - const GEOSCoordSequence *currentCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, currentGeom ); + const GEOSCoordSequence *currentCoordSeq = GEOSGeom_getCoordSeq_r( context, currentGeom ); unsigned int currentCoordSeqSize; - GEOSCoordSeq_getSize_r( geosinit()->ctxt, currentCoordSeq, ¤tCoordSeqSize ); + GEOSCoordSeq_getSize_r( context, currentCoordSeq, ¤tCoordSeqSize ); if ( currentCoordSeqSize < 2 ) continue; //get the two endpoints of the current line merge result double xBegin, xEnd, yBegin, yEnd; - GEOSCoordSeq_getX_r( geosinit()->ctxt, currentCoordSeq, 0, &xBegin ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, currentCoordSeq, 0, &yBegin ); - GEOSCoordSeq_getX_r( geosinit()->ctxt, currentCoordSeq, currentCoordSeqSize - 1, &xEnd ); - GEOSCoordSeq_getY_r( geosinit()->ctxt, currentCoordSeq, currentCoordSeqSize - 1, &yEnd ); + GEOSCoordSeq_getX_r( context, currentCoordSeq, 0, &xBegin ); + GEOSCoordSeq_getY_r( context, currentCoordSeq, 0, &yBegin ); + GEOSCoordSeq_getX_r( context, currentCoordSeq, currentCoordSeqSize - 1, &xEnd ); + GEOSCoordSeq_getY_r( context, currentCoordSeq, currentCoordSeqSize - 1, &yEnd ); geos::unique_ptr beginCurrentGeomVertex = createGeosPointXY( xBegin, yBegin, false, 0, false, 0, 2, precision ); geos::unique_ptr endCurrentGeomVertex = createGeosPointXY( xEnd, yEnd, false, 0, false, 0, 2, precision ); @@ -3276,12 +3329,12 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome //check how many endpoints equal the endpoints of the original line int nEndpointsSameAsOriginalLine = 0; - if ( GEOSEquals_r( geosinit()->ctxt, beginCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 - || GEOSEquals_r( geosinit()->ctxt, beginCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSEquals_r( context, beginCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 + || GEOSEquals_r( context, beginCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) nEndpointsSameAsOriginalLine += 1; - if ( GEOSEquals_r( geosinit()->ctxt, endCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 - || GEOSEquals_r( geosinit()->ctxt, endCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) + if ( GEOSEquals_r( context, endCurrentGeomVertex.get(), beginLineVertex.get() ) == 1 + || GEOSEquals_r( context, endCurrentGeomVertex.get(), endLineVertex.get() ) == 1 ) nEndpointsSameAsOriginalLine += 1; //check if the current geometry overlaps the original geometry (GEOSOverlap does not seem to work with linestrings) @@ -3296,24 +3349,24 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome //logic to decide if this part belongs to the result if ( !isRing && nEndpointsSameAsOriginalLine == 1 && nEndpointsOnOriginalLine == 2 && currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } //for closed rings, we take one segment from the candidate list else if ( isRing && nEndpointsOnOriginalLine == 2 && currentGeomOverlapsOriginalGeom ) { - probableParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + probableParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( nEndpointsOnOriginalLine == 2 && !currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( nEndpointsSameAsOriginalLine == 2 && !currentGeomOverlapsOriginalGeom ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } else if ( currentGeomOverlapsOriginalGeom && currentGeomOverlapsReshapeLine ) { - resultLineParts.push_back( GEOSGeom_clone_r( geosinit()->ctxt, currentGeom ) ); + resultLineParts.push_back( GEOSGeom_clone_r( context, currentGeom ) ); } } @@ -3327,7 +3380,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome for ( int i = 0; i < probableParts.size(); ++i ) { currentGeom = probableParts.at( i ); - GEOSLength_r( geosinit()->ctxt, currentGeom, ¤tLength ); + GEOSLength_r( context, currentGeom, ¤tLength ); if ( currentLength > maxLength ) { maxLength = currentLength; @@ -3335,7 +3388,7 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } else { - GEOSGeom_destroy_r( geosinit()->ctxt, currentGeom ); + GEOSGeom_destroy_r( context, currentGeom ); } } resultLineParts.push_back( maxGeom.release() ); @@ -3358,15 +3411,15 @@ geos::unique_ptr QgsGeos::reshapeLine( const GEOSGeometry *line, const GEOSGeome } //create multiline from resultLineParts - geos::unique_ptr multiLineGeom( GEOSGeom_createCollection_r( geosinit()->ctxt, GEOS_MULTILINESTRING, lineArray, resultLineParts.size() ) ); + geos::unique_ptr multiLineGeom( GEOSGeom_createCollection_r( context, GEOS_MULTILINESTRING, lineArray, resultLineParts.size() ) ); delete [] lineArray; //then do a linemerge with the newly combined partstrings - result.reset( GEOSLineMerge_r( geosinit()->ctxt, multiLineGeom.get() ) ); + result.reset( GEOSLineMerge_r( context, multiLineGeom.get() ) ); } //now test if the result is a linestring. Otherwise something went wrong - if ( GEOSGeomTypeId_r( geosinit()->ctxt, result.get() ) != GEOS_LINESTRING ) + if ( GEOSGeomTypeId_r( context, result.get() ) != GEOS_LINESTRING ) { return nullptr; } @@ -3381,13 +3434,14 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO int lastIntersectingRing = -2; const GEOSGeometry *lastIntersectingGeom = nullptr; - int nRings = GEOSGetNumInteriorRings_r( geosinit()->ctxt, polygon ); + GEOSContextHandle_t context = QgsGeosContext::get(); + int nRings = GEOSGetNumInteriorRings_r( context, polygon ); if ( nRings < 0 ) return nullptr; //does outer ring intersect? - const GEOSGeometry *outerRing = GEOSGetExteriorRing_r( geosinit()->ctxt, polygon ); - if ( GEOSIntersects_r( geosinit()->ctxt, outerRing, reshapeLineGeos ) == 1 ) + const GEOSGeometry *outerRing = GEOSGetExteriorRing_r( context, polygon ); + if ( GEOSIntersects_r( context, outerRing, reshapeLineGeos ) == 1 ) { ++nIntersections; lastIntersectingRing = -1; @@ -3401,8 +3455,8 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO { for ( int i = 0; i < nRings; ++i ) { - innerRings[i] = GEOSGetInteriorRingN_r( geosinit()->ctxt, polygon, i ); - if ( GEOSIntersects_r( geosinit()->ctxt, innerRings[i], reshapeLineGeos ) == 1 ) + innerRings[i] = GEOSGetInteriorRingN_r( context, polygon, i ); + if ( GEOSIntersects_r( context, innerRings[i], reshapeLineGeos ) == 1 ) { ++nIntersections; lastIntersectingRing = i; @@ -3431,12 +3485,12 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO //if reshaping took place, we need to reassemble the polygon and its rings GEOSGeometry *newRing = nullptr; - const GEOSCoordSequence *reshapeSequence = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, reshapeResult.get() ); - GEOSCoordSequence *newCoordSequence = GEOSCoordSeq_clone_r( geosinit()->ctxt, reshapeSequence ); + const GEOSCoordSequence *reshapeSequence = GEOSGeom_getCoordSeq_r( context, reshapeResult.get() ); + GEOSCoordSequence *newCoordSequence = GEOSCoordSeq_clone_r( context, reshapeSequence ); reshapeResult.reset(); - newRing = GEOSGeom_createLinearRing_r( geosinit()->ctxt, newCoordSequence ); + newRing = GEOSGeom_createLinearRing_r( context, newCoordSequence ); if ( !newRing ) { delete [] innerRings; @@ -3447,13 +3501,13 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO if ( lastIntersectingRing == -1 ) newOuterRing = newRing; else - newOuterRing = GEOSGeom_clone_r( geosinit()->ctxt, outerRing ); + newOuterRing = GEOSGeom_clone_r( context, outerRing ); //check if all the rings are still inside the outer boundary QVector ringList; if ( nRings > 0 ) { - GEOSGeometry *outerRingPoly = GEOSGeom_createPolygon_r( geosinit()->ctxt, GEOSGeom_clone_r( geosinit()->ctxt, newOuterRing ), nullptr, 0 ); + GEOSGeometry *outerRingPoly = GEOSGeom_createPolygon_r( context, GEOSGeom_clone_r( context, newOuterRing ), nullptr, 0 ); if ( outerRingPoly ) { ringList.reserve( nRings ); @@ -3463,16 +3517,16 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO if ( lastIntersectingRing == i ) currentRing = newRing; else - currentRing = GEOSGeom_clone_r( geosinit()->ctxt, innerRings[i] ); + currentRing = GEOSGeom_clone_r( context, innerRings[i] ); //possibly a ring is no longer contained in the result polygon after reshape - if ( GEOSContains_r( geosinit()->ctxt, outerRingPoly, currentRing ) == 1 ) + if ( GEOSContains_r( context, outerRingPoly, currentRing ) == 1 ) ringList.push_back( currentRing ); else - GEOSGeom_destroy_r( geosinit()->ctxt, currentRing ); + GEOSGeom_destroy_r( context, currentRing ); } } - GEOSGeom_destroy_r( geosinit()->ctxt, outerRingPoly ); + GEOSGeom_destroy_r( context, outerRingPoly ); } GEOSGeometry **newInnerRings = new GEOSGeometry*[ringList.size()]; @@ -3481,7 +3535,7 @@ geos::unique_ptr QgsGeos::reshapePolygon( const GEOSGeometry *polygon, const GEO delete [] innerRings; - geos::unique_ptr reshapedPolygon( GEOSGeom_createPolygon_r( geosinit()->ctxt, newOuterRing, newInnerRings, ringList.size() ) ); + geos::unique_ptr reshapedPolygon( GEOSGeom_createPolygon_r( context, newOuterRing, newInnerRings, ringList.size() ) ); delete[] newInnerRings; return reshapedPolygon; @@ -3496,18 +3550,19 @@ int QgsGeos::lineContainedInLine( const GEOSGeometry *line1, const GEOSGeometry double bufferDistance = std::pow( 10.0L, geomDigits( line2 ) - 11 ); - geos::unique_ptr bufferGeom( GEOSBuffer_r( geosinit()->ctxt, line2, bufferDistance, DEFAULT_QUADRANT_SEGMENTS ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr bufferGeom( GEOSBuffer_r( context, line2, bufferDistance, DEFAULT_QUADRANT_SEGMENTS ) ); if ( !bufferGeom ) return -2; - geos::unique_ptr intersectionGeom( GEOSIntersection_r( geosinit()->ctxt, bufferGeom.get(), line1 ) ); + geos::unique_ptr intersectionGeom( GEOSIntersection_r( context, bufferGeom.get(), line1 ) ); //compare ratio between line1Length and intersectGeomLength (usually close to 1 if line1 is contained in line2) double intersectGeomLength; double line1Length; - GEOSLength_r( geosinit()->ctxt, intersectionGeom.get(), &intersectGeomLength ); - GEOSLength_r( geosinit()->ctxt, line1, &line1Length ); + GEOSLength_r( context, intersectionGeom.get(), &intersectGeomLength ); + GEOSLength_r( context, line1, &line1Length ); double intersectRatio = line1Length / intersectGeomLength; if ( intersectRatio > 0.9 && intersectRatio < 1.1 ) @@ -3523,12 +3578,13 @@ int QgsGeos::pointContainedInLine( const GEOSGeometry *point, const GEOSGeometry double bufferDistance = std::pow( 10.0L, geomDigits( line ) - 11 ); - geos::unique_ptr lineBuffer( GEOSBuffer_r( geosinit()->ctxt, line, bufferDistance, 8 ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr lineBuffer( GEOSBuffer_r( context, line, bufferDistance, 8 ) ); if ( !lineBuffer ) return -2; bool contained = false; - if ( GEOSContains_r( geosinit()->ctxt, lineBuffer.get(), point ) == 1 ) + if ( GEOSContains_r( context, lineBuffer.get(), point ) == 1 ) contained = true; return contained; @@ -3536,35 +3592,36 @@ int QgsGeos::pointContainedInLine( const GEOSGeometry *point, const GEOSGeometry int QgsGeos::geomDigits( const GEOSGeometry *geom ) { - geos::unique_ptr bbox( GEOSEnvelope_r( geosinit()->ctxt, geom ) ); + GEOSContextHandle_t context = QgsGeosContext::get(); + geos::unique_ptr bbox( GEOSEnvelope_r( context, geom ) ); if ( !bbox.get() ) return -1; - const GEOSGeometry *bBoxRing = GEOSGetExteriorRing_r( geosinit()->ctxt, bbox.get() ); + const GEOSGeometry *bBoxRing = GEOSGetExteriorRing_r( context, bbox.get() ); if ( !bBoxRing ) return -1; - const GEOSCoordSequence *bBoxCoordSeq = GEOSGeom_getCoordSeq_r( geosinit()->ctxt, bBoxRing ); + const GEOSCoordSequence *bBoxCoordSeq = GEOSGeom_getCoordSeq_r( context, bBoxRing ); if ( !bBoxCoordSeq ) return -1; unsigned int nCoords = 0; - if ( !GEOSCoordSeq_getSize_r( geosinit()->ctxt, bBoxCoordSeq, &nCoords ) ) + if ( !GEOSCoordSeq_getSize_r( context, bBoxCoordSeq, &nCoords ) ) return -1; int maxDigits = -1; for ( unsigned int i = 0; i < nCoords - 1; ++i ) { double t; - GEOSCoordSeq_getX_r( geosinit()->ctxt, bBoxCoordSeq, i, &t ); + GEOSCoordSeq_getX_r( context, bBoxCoordSeq, i, &t ); int digits; digits = std::ceil( std::log10( std::fabs( t ) ) ); if ( digits > maxDigits ) maxDigits = digits; - GEOSCoordSeq_getY_r( geosinit()->ctxt, bBoxCoordSeq, i, &t ); + GEOSCoordSeq_getY_r( context, bBoxCoordSeq, i, &t ); digits = std::ceil( std::log10( std::fabs( t ) ) ); if ( digits > maxDigits ) maxDigits = digits; @@ -3572,8 +3629,3 @@ int QgsGeos::geomDigits( const GEOSGeometry *geom ) return maxDigits; } - -GEOSContextHandle_t QgsGeos::getGEOSHandler() -{ - return geosinit()->ctxt; -} diff --git a/src/core/geometry/qgsgeos.h b/src/core/geometry/qgsgeos.h index 1e59b29c1180..92ad193c3159 100644 --- a/src/core/geometry/qgsgeos.h +++ b/src/core/geometry/qgsgeos.h @@ -28,6 +28,36 @@ class QgsPolygon; class QgsGeometry; class QgsGeometryCollection; + +/** + * \class QgsGeosContext + * \ingroup core + * \brief Used to create and store a proj context object, correctly freeing the context upon destruction. + * \note Not available in Python bindings + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsGeosContext +{ + public: + + QgsGeosContext(); + ~QgsGeosContext(); + + /** + * Returns a thread local instance of a GEOS context, safe for use in the current thread. + */ + static GEOSContextHandle_t get(); + + private: + GEOSContextHandle_t mContext = nullptr; + + /** + * Thread local GEOS context storage. A new GEOS context will be created + * for every thread. + */ + static thread_local QgsGeosContext sGeosContext; +}; + /** * Contains geos related utilities and functions. * \note not available in Python bindings. @@ -660,9 +690,6 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine static geos::unique_ptr asGeos( const QgsAbstractGeometry *geometry, double precision = 0, bool allowInvalidSubGeom = true ); static QgsPoint coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, bool hasM ); - static GEOSContextHandle_t getGEOSHandler(); - - private: mutable geos::unique_ptr mGeos; geos::prepared_unique_ptr mGeosPrepared; diff --git a/src/core/labeling/qgslabelfeature.cpp b/src/core/labeling/qgslabelfeature.cpp index 4d0b699348b1..00a4b4bdd3ab 100644 --- a/src/core/labeling/qgslabelfeature.cpp +++ b/src/core/labeling/qgslabelfeature.cpp @@ -51,7 +51,7 @@ void QgsLabelFeature::setPermissibleZone( const QgsGeometry &geometry ) if ( !mPermissibleZoneGeos ) return; - mPermissibleZoneGeosPrepared.reset( GEOSPrepare_r( QgsGeos::getGEOSHandler(), mPermissibleZoneGeos.get() ) ); + mPermissibleZoneGeosPrepared.reset( GEOSPrepare_r( QgsGeosContext::get(), mPermissibleZoneGeos.get() ) ); } QgsFeature QgsLabelFeature::feature() const diff --git a/src/core/pal/feature.cpp b/src/core/pal/feature.cpp index 6fa3f04ab220..dc0fabe3fa0c 100644 --- a/src/core/pal/feature.cpp +++ b/src/core/pal/feature.cpp @@ -93,7 +93,7 @@ FeaturePart::~FeaturePart() void FeaturePart::extractCoords( const GEOSGeometry *geom ) { const GEOSCoordSequence *coordSeq = nullptr; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); type = GEOSGeomTypeId_r( geosctxt, geom ); @@ -410,7 +410,7 @@ std::unique_ptr FeaturePart::createCandidatePointOnSurface( Point double px, py; try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr pointGeom( GEOSPointOnSurface_r( geosctxt, mapShape->geos() ) ); if ( pointGeom ) { @@ -1995,7 +1995,7 @@ std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector if ( !mGeos ) createGeosGeom(); - GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t ctxt = QgsGeosContext::get(); int geomType = GEOSGeomTypeId_r( ctxt, mGeos ); double sizeCost = 0; @@ -2320,7 +2320,7 @@ bool FeaturePart::isConnected( FeaturePart *p2 ) const double p2otherX = p2startTouches ? x2last : x2first; const double p2otherY = p2startTouches ? y2last : y2first; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY ); @@ -2345,7 +2345,7 @@ bool FeaturePart::mergeWithFeaturePart( FeaturePart *other ) if ( !other->mGeos ) other->createGeosGeom(); - GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t ctxt = QgsGeosContext::get(); try { GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos ); diff --git a/src/core/pal/geomfunction.cpp b/src/core/pal/geomfunction.cpp index 550151f24ec8..c23be07d8c9c 100644 --- a/src/core/pal/geomfunction.cpp +++ b/src/core/pal/geomfunction.cpp @@ -308,7 +308,7 @@ bool GeomFunction::containsCandidate( const GEOSPreparedGeometry *geom, double x try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 5, 2 ); GEOSCoordSeq_setXY_r( geosctxt, coord, 0, x, y ); diff --git a/src/core/pal/labelposition.cpp b/src/core/pal/labelposition.cpp index 724eaaafaba7..ba1a576c58d8 100644 --- a/src/core/pal/labelposition.cpp +++ b/src/core/pal/labelposition.cpp @@ -169,7 +169,7 @@ LabelPosition::~LabelPosition() { if ( mPreparedOuterBoundsGeos ) { - GEOSPreparedGeom_destroy_r( QgsGeos::getGEOSHandler(), mPreparedOuterBoundsGeos ); + GEOSPreparedGeom_destroy_r( QgsGeosContext::get(), mPreparedOuterBoundsGeos ); mPreparedOuterBoundsGeos = nullptr; } } @@ -182,7 +182,7 @@ bool LabelPosition::intersects( const GEOSPreparedGeometry *geometry ) try { - if ( GEOSPreparedIntersects_r( QgsGeos::getGEOSHandler(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) + if ( GEOSPreparedIntersects_r( QgsGeosContext::get(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) { return true; } @@ -209,7 +209,7 @@ bool LabelPosition::within( const GEOSPreparedGeometry *geometry ) try { - if ( GEOSPreparedContains_r( QgsGeos::getGEOSHandler(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) != 1 ) + if ( GEOSPreparedContains_r( QgsGeosContext::get(), geometry, mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) != 1 ) { return false; } @@ -260,7 +260,7 @@ bool LabelPosition::isInConflict( const LabelPosition *lp ) const if ( !mOuterBoundsGeos && !mGeos ) createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { const bool result = ( GEOSPreparedIntersects_r( geosctxt, lp->preparedOuterBoundsGeom() ? lp->preparedOuterBoundsGeom() : lp->preparedGeom(), @@ -289,7 +289,7 @@ bool LabelPosition::isInConflictMultiPart( const LabelPosition *lp ) const if ( !lp->mMultipartGeos ) lp->createMultiPartGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { const bool result = ( GEOSPreparedIntersects_r( geosctxt, preparedMultiPartGeom(), lp->mMultipartGeos ) == 1 ); @@ -311,7 +311,7 @@ void LabelPosition::createOuterBoundsGeom() if ( outerBounds.isNull() ) return; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const double beta = this->alpha + M_PI_2; @@ -356,7 +356,7 @@ void LabelPosition::createOuterBoundsGeom() mOuterBoundsGeos.reset( GEOSGeom_createPolygon_r( geosctxt, GEOSGeom_createLinearRing_r( geosctxt, coord ), nullptr, 0 ) ); - mPreparedOuterBoundsGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mOuterBoundsGeos.get() ); + mPreparedOuterBoundsGeos = GEOSPrepare_r( QgsGeosContext::get(), mOuterBoundsGeos.get() ); auto xminmax = std::minmax_element( mOuterBoundsX.begin(), mOuterBoundsX.end() ); mOuterBoundsXMin = *xminmax.first; @@ -505,7 +505,7 @@ void LabelPosition::insertIntoIndex( PalRtree &index ) void LabelPosition::createMultiPartGeosGeom() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); std::vector< const GEOSGeometry * > geometries; const LabelPosition *tmp1 = this; @@ -535,7 +535,7 @@ const GEOSPreparedGeometry *LabelPosition::preparedMultiPartGeom() const if ( !mMultipartPreparedGeos ) { - mMultipartPreparedGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mMultipartGeos ); + mMultipartPreparedGeos = GEOSPrepare_r( QgsGeosContext::get(), mMultipartGeos ); } return mMultipartPreparedGeos; } @@ -549,7 +549,7 @@ double LabelPosition::getDistanceToPoint( double xp, double yp, bool useOuterBou { // this method may consider the label's outer bounds! - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); //first check if inside, if so then distance is -1 bool contains = false; @@ -655,7 +655,7 @@ bool LabelPosition::crossesLine( PointSet *line ) const if ( !line->mGeos ) line->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, line->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) @@ -686,7 +686,7 @@ bool LabelPosition::crossesBoundary( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 @@ -726,7 +726,7 @@ bool LabelPosition::intersectsWithPolygon( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mOuterBoundsGeos ? mOuterBoundsGeos.get() : mGeos ) == 1 ) @@ -759,7 +759,7 @@ double LabelPosition::polygonIntersectionCostForParts( PointSet *polygon ) const if ( !polygon->mGeos ) polygon->createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); double cost = 0; try { diff --git a/src/core/pal/layer.cpp b/src/core/pal/layer.cpp index 1b11ce8aba31..7a275564396d 100644 --- a/src/core/pal/layer.cpp +++ b/src/core/pal/layer.cpp @@ -107,7 +107,7 @@ bool Layer::registerFeature( QgsLabelFeature *lf ) throw InternalException::UnknownGeometry(); } - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const bool featureGeomIsObstacleGeom = lf->obstacleSettings().obstacleGeometry().isNull(); @@ -364,7 +364,7 @@ int Layer::connectedFeatureId( QgsFeatureId featureId ) const void Layer::chopFeaturesAtRepeatDistance() { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); std::deque< std::unique_ptr< FeaturePart > > newFeatureParts; while ( !mFeatureParts.empty() ) { diff --git a/src/core/pal/pal.cpp b/src/core/pal/pal.cpp index 029de6d62ca2..051de8e4d358 100644 --- a/src/core/pal/pal.cpp +++ b/src/core/pal/pal.cpp @@ -134,7 +134,7 @@ std::unique_ptr Pal::extractProblem( const QgsRectangle &extent, const // prepare map boundary geos::unique_ptr mapBoundaryGeos( QgsGeos::asGeos( mapBoundary ) ); - geos::prepared_unique_ptr mapBoundaryPrepared( GEOSPrepare_r( QgsGeos::getGEOSHandler(), mapBoundaryGeos.get() ) ); + geos::prepared_unique_ptr mapBoundaryPrepared( GEOSPrepare_r( QgsGeosContext::get(), mapBoundaryGeos.get() ) ); int obstacleCount = 0; diff --git a/src/core/pal/pointset.cpp b/src/core/pal/pointset.cpp index 1cc41f07f14b..ae6b20a32ea7 100644 --- a/src/core/pal/pointset.cpp +++ b/src/core/pal/pointset.cpp @@ -92,14 +92,14 @@ PointSet::PointSet( const PointSet &ps ) if ( ps.mGeos ) { - mGeos = GEOSGeom_clone_r( QgsGeos::getGEOSHandler(), ps.mGeos ); + mGeos = GEOSGeom_clone_r( QgsGeosContext::get(), ps.mGeos ); mOwnsGeom = true; } } void PointSet::createGeosGeom() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); bool needClose = false; if ( type == GEOS_POLYGON && ( !qgsDoubleNear( x[0], x[ nbPoints - 1] ) || !qgsDoubleNear( y[0], y[ nbPoints - 1 ] ) ) ) @@ -159,14 +159,14 @@ const GEOSPreparedGeometry *PointSet::preparedGeom() const if ( !mPreparedGeom ) { - mPreparedGeom = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mGeos ); + mPreparedGeom = GEOSPrepare_r( QgsGeosContext::get(), mGeos ); } return mPreparedGeom; } void PointSet::invalidateGeos() const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); if ( mOwnsGeom ) // delete old geometry if we own it GEOSGeom_destroy_r( geosctxt, mGeos ); mOwnsGeom = false; @@ -201,7 +201,7 @@ void PointSet::invalidateGeos() const PointSet::~PointSet() { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); if ( mGeos && mOwnsGeom ) { @@ -270,7 +270,7 @@ std::unique_ptr PointSet::clone() const bool PointSet::containsPoint( double x, double y ) const { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { geos::unique_ptr point( GEOSGeom_createPointFromXY_r( geosctxt, x, y ) ); @@ -551,7 +551,7 @@ void PointSet::offsetCurveByDistance( double distance ) if ( !mGeos || type != GEOS_LINESTRING ) return; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr newGeos = nullptr; try { @@ -866,7 +866,7 @@ double PointSet::minDistanceToPoint( double px, double py, double *rx, double *r if ( !mGeos ) return 0; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { geos::unique_ptr geosPt( GEOSGeom_createPointFromXY_r( geosctxt, px, py ) ); @@ -925,7 +925,7 @@ void PointSet::getCentroid( double &px, double &py, bool forceInside ) const try { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); geos::unique_ptr centroidGeom( GEOSGetCentroid_r( geosctxt, mGeos ) ); if ( centroidGeom ) { @@ -1020,7 +1020,7 @@ geos::unique_ptr PointSet::interpolatePoint( double distance ) const try { - geos::unique_ptr res( GEOSInterpolate_r( QgsGeos::getGEOSHandler(), thisGeos, distance ) ); + geos::unique_ptr res( GEOSInterpolate_r( QgsGeosContext::get(), thisGeos, distance ) ); return res; } catch ( GEOSException &e ) @@ -1039,7 +1039,7 @@ double PointSet::lineLocatePoint( const GEOSGeometry *point ) const double distance = -1; try { - distance = GEOSProject_r( QgsGeos::getGEOSHandler(), thisGeos, point ); + distance = GEOSProject_r( QgsGeosContext::get(), thisGeos, point ); } catch ( GEOSException &e ) { @@ -1069,7 +1069,7 @@ double PointSet::length() const if ( !mGeos ) return -1; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { @@ -1095,7 +1095,7 @@ double PointSet::area() const if ( !mGeos ) return -1; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { @@ -1121,7 +1121,7 @@ QString PointSet::toWkt() const if ( !mGeos ) createGeosGeom(); - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); try { diff --git a/src/core/pal/util.cpp b/src/core/pal/util.cpp index 085b35066022..7bd781f4145d 100644 --- a/src/core/pal/util.cpp +++ b/src/core/pal/util.cpp @@ -44,7 +44,7 @@ QLinkedList *pal::Util::unmulti( const GEOSGeometry *the_g int nGeom; int i; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); while ( !queue->isEmpty() ) { diff --git a/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp b/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp index c38090e5c64a..12c71799f1cc 100644 --- a/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp +++ b/src/core/pointcloud/qgspointcloudlayerprofilegenerator.cpp @@ -37,18 +37,18 @@ QgsPointCloudLayerProfileResults::QgsPointCloudLayerProfileResults() { - mPointIndex = GEOSSTRtree_create_r( QgsGeos::getGEOSHandler(), ( size_t )10 ); + mPointIndex = GEOSSTRtree_create_r( QgsGeosContext::get(), ( size_t )10 ); } QgsPointCloudLayerProfileResults::~QgsPointCloudLayerProfileResults() { - GEOSSTRtree_destroy_r( QgsGeos::getGEOSHandler(), mPointIndex ); + GEOSSTRtree_destroy_r( QgsGeosContext::get(), mPointIndex ); mPointIndex = nullptr; } void QgsPointCloudLayerProfileResults::finalize( QgsFeedback *feedback ) { - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const std::size_t size = results.size(); PointResult *pointData = results.data(); @@ -240,7 +240,7 @@ QgsProfileSnapResult QgsPointCloudLayerProfileResults::snapPoint( const QgsProfi { QgsProfileSnapResult result; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); const double minDistance = point.distance() - context.maximumPointDistanceDelta; const double maxDistance = point.distance() + context.maximumPointDistanceDelta; diff --git a/src/core/proj/qgscoordinatereferencesystem.cpp b/src/core/proj/qgscoordinatereferencesystem.cpp index 660a0972a609..ba05991fbac8 100644 --- a/src/core/proj/qgscoordinatereferencesystem.cpp +++ b/src/core/proj/qgscoordinatereferencesystem.cpp @@ -1613,6 +1613,30 @@ QString QgsCoordinateReferenceSystem::toOgcUri() const return QString(); } +QString QgsCoordinateReferenceSystem::toOgcUrn() const +{ + const auto parts { authid().split( ':' ) }; + if ( parts.length() == 2 ) + { + if ( parts[0] == QLatin1String( "EPSG" ) ) + return QStringLiteral( "urn:ogc:def:crs:EPSG:0:%1" ).arg( parts[1] ); + else if ( parts[0] == QLatin1String( "OGC" ) ) + { + return QStringLiteral( "urn:ogc:def:crs:OGC:1.3:%1" ).arg( parts[1] ); + } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URN %1: (not OGC or EPSG)" ).arg( authid() ), QStringLiteral( "CRS" ), Qgis::MessageLevel::Critical ); + } + } + else + { + QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URN: %1" ).arg( authid() ), QStringLiteral( "CRS" ), Qgis::MessageLevel::Critical ); + } + return QString(); +} + + void QgsCoordinateReferenceSystem::updateDefinition() { if ( !d->mIsValid ) diff --git a/src/core/proj/qgscoordinatereferencesystem.h b/src/core/proj/qgscoordinatereferencesystem.h index 654d5f3743fc..e4b97b0af0a5 100644 --- a/src/core/proj/qgscoordinatereferencesystem.h +++ b/src/core/proj/qgscoordinatereferencesystem.h @@ -906,6 +906,14 @@ class CORE_EXPORT QgsCoordinateReferenceSystem */ QString toOgcUri() const; + /** + * Returns the crs as OGC URN (format: urn:ogc:def:crs:OGC:1.3:CRS84) + * Returns an empty string on failure. + * + * \since QGIS 3.38 + */ + QString toOgcUrn() const; + // Mutators ----------------------------------- /** diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index 7f9d7a0b9a45..1040abb66fc7 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -4631,7 +4631,7 @@ QString QgsGdalProviderMetadata::getStyleById( const QString &uri, const QString bool QgsGdalProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause ) { gdal::dataset_unique_ptr ds; - ds.reset( QgsGdalProviderBase::gdalOpen( uri, GDAL_OF_READONLY ) ); + ds.reset( QgsGdalProviderBase::gdalOpen( uri, GDAL_OF_UPDATE ) ); if ( !ds ) { errCause = QObject::tr( "Cannot open %1." ).arg( uri ); diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 7987ffcbc52d..f607029335c0 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -1031,7 +1031,7 @@ void QgsOgrProvider::loadMetadata() QgsSqliteUtils::quotedString( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ), QgsSqliteUtils::quotedString( QStringLiteral( "table" ) ) ); - if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) ) + if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toUtf8().constData() ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) @@ -1593,6 +1593,24 @@ static int strictToInt( const QVariant &v, bool *ok ) return 0; } +// Converts a string with values "0", "false", "1", "true" to 0 / 1 +static int stringToBool( const QString &strVal, bool *ok ) +{ + if ( strVal.compare( QLatin1String( "0" ) ) == 0 || strVal.compare( QLatin1String( "false" ), Qt::CaseInsensitive ) == 0 ) + { + *ok = true; + return 0; + } + else if ( strVal.compare( QLatin1String( "1" ) ) == 0 || strVal.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 ) + { + *ok = true; + return 1; + } + *ok = false; + return 0; +} + + bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId incrementalFeatureId ) { bool returnValue = true; @@ -1688,12 +1706,29 @@ bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId } else { + bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: - OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) ); + { + if ( OGR_Fld_GetSubType( fldDef ) == OFSTBoolean && qType == QVariant::String ) + { + // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + const QString strVal = attrVal.toString(); + OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, stringToBool( strVal, &ok ) ); + if ( !ok ) + { + pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( strVal ) ); + errorEmitted = true; + } + } + else + { + OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) ); + } break; + } case OFTInteger64: OGR_F_SetFieldInteger64( feature.get(), ogrAttributeId, attrVal.toLongLong( &ok ) ); @@ -1895,7 +1930,10 @@ bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId if ( !ok ) { - pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( qType ) ); + if ( !errorEmitted ) + { + pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( qgisAttributeId ) .arg( f.id() ).arg( attrVal.typeName() ) ); + } returnValue = false; } } @@ -2656,12 +2694,29 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_ } else { + bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: - OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) ); + { + if ( OGR_Fld_GetSubType( fd ) == OFSTBoolean && qType == QVariant::String ) + { + // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + const QString strVal = it2->toString(); + OGR_F_SetFieldInteger( of.get(), f, stringToBool( strVal, &ok ) ); + if ( !ok ) + { + pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( strVal ) ); + errorEmitted = true; + } + } + else + { + OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) ); + } break; + } case OFTInteger64: OGR_F_SetFieldInteger64( of.get(), f, it2->toLongLong( &ok ) ); break; @@ -2857,7 +2912,10 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_ if ( !ok ) { - pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( qType ) ); + if ( !errorEmitted ) + { + pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( it2->typeName() ) ); + } returnValue = false; } } diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.cpp b/src/core/providers/ogr/qgsogrprovidermetadata.cpp index 1f19bcd38824..01e3d930aac9 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.cpp +++ b/src/core/providers/ogr/qgsogrprovidermetadata.cpp @@ -418,7 +418,7 @@ bool QgsOgrProviderMetadata::saveStyle( bool QgsOgrProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause ) { QString filePath; - QgsOgrLayerUniquePtr userLayer = LoadDataSourceAndLayer( uri, false, filePath, errCause ); + QgsOgrLayerUniquePtr userLayer = LoadDataSourceAndLayer( uri, true, filePath, errCause ); if ( !userLayer ) return false; QRecursiveMutex *mutex = nullptr; diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp index 2c9cfe152399..387f7e8925af 100644 --- a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp @@ -86,6 +86,7 @@ QVector QgsSensorThingsConnectionItem::createChildren() Qgis::SensorThingsEntity::ObservedProperty, Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::FeatureOfInterest, + Qgis::SensorThingsEntity::MultiDatastream, } ) { QVariantMap entityUriParts = connectionUriParts; @@ -138,13 +139,28 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() QVector children; int sortKey = 1; - for ( const Qgis::WkbType wkbType : - { - Qgis::WkbType::Point, - Qgis::WkbType::MultiPoint, - Qgis::WkbType::MultiLineString, - Qgis::WkbType::MultiPolygon - } ) + QList< Qgis::WkbType > compatibleTypes; + // we always expose "no geometry" types for these, even though they have a restricted fixed type + // according to the spec. This is because not all services respect the mandated geometry types! + switch ( QgsSensorThingsUtils::geometryTypeForEntity( mEntityType ) ) + { + case Qgis::GeometryType::Point: + compatibleTypes << Qgis::WkbType::Point << Qgis::WkbType::MultiPoint << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Line: + compatibleTypes << Qgis::WkbType::MultiLineString << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Polygon: + compatibleTypes << Qgis::WkbType::MultiPolygon << Qgis::WkbType::NoGeometry; + break; + case Qgis::GeometryType::Unknown: + compatibleTypes << Qgis::WkbType::Point << Qgis::WkbType::MultiPoint << Qgis::WkbType::MultiLineString << Qgis::WkbType::MultiPolygon; + break; + case Qgis::GeometryType::Null: + compatibleTypes << Qgis::WkbType::NoGeometry;; + } + + for ( const Qgis::WkbType wkbType : std::as_const( compatibleTypes ) ) { QVariantMap geometryUriParts = mEntityUriParts; QString name; @@ -171,6 +187,11 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() name = tr( "Polygons" ); layerType = Qgis::BrowserLayerType::Polygon; break; + case Qgis::WkbType::NoGeometry: + geometryUriParts.remove( QStringLiteral( "geometryType" ) ); + name = tr( "No Geometry" ); + layerType = Qgis::BrowserLayerType::TableLayer; + break; default: break; } diff --git a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp index c290bb9b00d8..7442a50a57a4 100644 --- a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp @@ -18,7 +18,6 @@ #include "qgssensorthingsprovider.h" #include "qgssensorthingsutils.h" #include "qgsapplication.h" -#include "qgsnetworkaccessmanager.h" #include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsthreadingutils.h" @@ -26,6 +25,7 @@ #include "qgssensorthingsfeatureiterator.h" #include "qgssensorthingsdataitems.h" #include "qgssensorthingsconnection.h" +#include "qgsmessagelog.h" #include #include @@ -99,7 +99,28 @@ QgsSensorThingsProvider::QgsSensorThingsProvider( const QString &uri, const Prov if ( !foundMatchingEntity ) { - appendError( QgsErrorMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), QStringLiteral( "SensorThings" ) ) ); + switch ( mSharedData->mEntityType ) + { + + case Qgis::SensorThingsEntity::Invalid: + case Qgis::SensorThingsEntity::Thing: + case Qgis::SensorThingsEntity::Location: + case Qgis::SensorThingsEntity::HistoricalLocation: + case Qgis::SensorThingsEntity::Datastream: + case Qgis::SensorThingsEntity::Sensor: + case Qgis::SensorThingsEntity::ObservedProperty: + case Qgis::SensorThingsEntity::Observation: + case Qgis::SensorThingsEntity::FeatureOfInterest: + appendError( QgsErrorMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), QStringLiteral( "SensorThings" ) ) ); + QgsMessageLog::logMessage( tr( "Could not find url for %1" ).arg( qgsEnumValueToKey( mSharedData->mEntityType ) ), tr( "SensorThings" ) ); + break; + + case Qgis::SensorThingsEntity::MultiDatastream: + appendError( QgsErrorMessage( tr( "MultiDatastreams are not supported by this connection" ), QStringLiteral( "SensorThings" ) ) ); + QgsMessageLog::logMessage( tr( "MultiDatastreams are not supported by this connection" ), tr( "SensorThings" ) ); + break; + } + return; } } @@ -306,8 +327,7 @@ QgsSensorThingsProviderMetadata::QgsSensorThingsProviderMetadata(): QIcon QgsSensorThingsProviderMetadata::icon() const { - // TODO - return QgsApplication::getThemeIcon( QStringLiteral( "mIconAfs.svg" ) ); + return QgsApplication::getThemeIcon( QStringLiteral( "mIconSensorThings.svg" ) ); } QList QgsSensorThingsProviderMetadata::dataItemProviders() const diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 198ee4aef852..99409a57172d 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -45,25 +45,36 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri ) if ( QgsSensorThingsUtils::entityTypeHasGeometry( mEntityType ) ) { - const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString(); - if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) + if ( uriParts.contains( QStringLiteral( "geometryType" ) ) ) { - mGeometryType = Qgis::WkbType::PointZ; - } - else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) - { - mGeometryType = Qgis::WkbType::MultiPointZ; - } - else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) - { - mGeometryType = Qgis::WkbType::MultiLineStringZ; + const QString geometryType = uriParts.value( QStringLiteral( "geometryType" ) ).toString(); + if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::PointZ; + } + else if ( geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiPointZ; + } + else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiLineStringZ; + } + else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) + { + mGeometryType = Qgis::WkbType::MultiPolygonZ; + } + + if ( mGeometryType != Qgis::WkbType::NoGeometry ) + { + // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 + mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); + } } - else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) + else { - mGeometryType = Qgis::WkbType::MultiPolygonZ; + mGeometryType = Qgis::WkbType::NoGeometry; } - // geometry is always GeoJSON spec (for now, at least), so CRS will always be WGS84 - mSourceCRS = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); } else { @@ -484,6 +495,14 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee return QgsJsonUtils::jsonToVariant( json[tag] ); }; + auto getVariantList = []( const basic_json<> &json, const char *tag ) -> QVariant + { + if ( !json.contains( tag ) ) + return QVariant(); + + return QgsJsonUtils::jsonToVariant( json[tag] ); + }; + auto getStringList = []( const basic_json<> &json, const char *tag ) -> QVariant { if ( !json.contains( tag ) ) @@ -508,7 +527,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee return QVariant(); }; - auto getDateTimeRange = []( const basic_json<> &json, const char *tag ) -> std::pair< QVariant, QVariant > + auto getDateTimeRange = []( const basic_json<> &json, const char *tag, bool allowInstant = false ) -> std::pair< QVariant, QVariant > { if ( !json.contains( tag ) ) return { QVariant(), QVariant() }; @@ -526,6 +545,11 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee QDateTime::fromString( rangeParts.at( 1 ), Qt::ISODateWithMs ) }; } + else if ( allowInstant ) + { + const QDateTime instant = QDateTime::fromString( rangeString, Qt::ISODateWithMs ); + return { instant, instant }; + } } return { QVariant(), QVariant() }; @@ -631,7 +655,7 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee case Qgis::SensorThingsEntity::Observation: { - std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" ); + std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime", true ); std::pair< QVariant, QVariant > validTime = getDateTimeRange( featureData, "validTime" ); feature.setAttributes( QgsAttributes() @@ -659,6 +683,28 @@ bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFee << properties ); break; + + case Qgis::SensorThingsEntity::MultiDatastream: + { + std::pair< QVariant, QVariant > phenomenonTime = getDateTimeRange( featureData, "phenomenonTime" ); + std::pair< QVariant, QVariant > resultTime = getDateTimeRange( featureData, "resultTime" ); + feature.setAttributes( + QgsAttributes() + << iotId + << selfLink + << getString( featureData, "name" ) + << getString( featureData, "description" ) + << getVariantList( featureData, "unitOfMeasurements" ) + << getString( featureData, "observationType" ) + << getStringList( featureData, "multiObservationDataTypes" ) + << properties + << phenomenonTime.first + << phenomenonTime.second + << resultTime.first + << resultTime.second + ); + break; + } } // NOLINTEND(bugprone-branch-clone) diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.cpp b/src/core/providers/sensorthings/qgssensorthingsutils.cpp index b4fd025fbcbd..87f9ba757a44 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsutils.cpp @@ -45,6 +45,8 @@ Qgis::SensorThingsEntity QgsSensorThingsUtils::stringToEntity( const QString &ty return Qgis::SensorThingsEntity::Observation; if ( trimmed.compare( QLatin1String( "FeatureOfInterest" ), Qt::CaseInsensitive ) == 0 ) return Qgis::SensorThingsEntity::FeatureOfInterest; + if ( trimmed.compare( QLatin1String( "MultiDatastream" ), Qt::CaseInsensitive ) == 0 ) + return Qgis::SensorThingsEntity::MultiDatastream; return Qgis::SensorThingsEntity::Invalid; } @@ -71,6 +73,8 @@ QString QgsSensorThingsUtils::displayString( Qgis::SensorThingsEntity type, bool return plural ? QObject::tr( "Observations" ) : QObject::tr( "Observation" ); case Qgis::SensorThingsEntity::FeatureOfInterest: return plural ? QObject::tr( "Features of Interest" ) : QObject::tr( "Feature of Interest" ); + case Qgis::SensorThingsEntity::MultiDatastream: + return plural ? QObject::tr( "MultiDatastreams" ) : QObject::tr( "MultiDatastream" ); } BUILTIN_UNREACHABLE } @@ -94,6 +98,8 @@ Qgis::SensorThingsEntity QgsSensorThingsUtils::entitySetStringToEntity( const QS return Qgis::SensorThingsEntity::Observation; if ( trimmed.compare( QLatin1String( "FeaturesOfInterest" ), Qt::CaseInsensitive ) == 0 ) return Qgis::SensorThingsEntity::FeatureOfInterest; + if ( trimmed.compare( QLatin1String( "MultiDatastreams" ), Qt::CaseInsensitive ) == 0 ) + return Qgis::SensorThingsEntity::MultiDatastream; return Qgis::SensorThingsEntity::Invalid; } @@ -180,6 +186,20 @@ QgsFields QgsSensorThingsUtils::fieldsForEntityType( Qgis::SensorThingsEntity ty fields.append( QgsField( QStringLiteral( "description" ), QVariant::String ) ); fields.append( QgsField( QStringLiteral( "properties" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); break; + + case Qgis::SensorThingsEntity::MultiDatastream: + // https://docs.ogc.org/is/18-088/18-088.html#multidatastream-extension + fields.append( QgsField( QStringLiteral( "name" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "description" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "unitOfMeasurements" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "observationType" ), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "multiObservationDataTypes" ), QVariant::StringList, QString(), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "properties" ), QVariant::Map, QStringLiteral( "json" ), 0, 0, QString(), QVariant::String ) ); + fields.append( QgsField( QStringLiteral( "phenomenonTimeStart" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "phenomenonTimeEnd" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "resultTimeStart" ), QVariant::DateTime ) ); + fields.append( QgsField( QStringLiteral( "resultTimeEnd" ), QVariant::DateTime ) ); + break; } return fields; @@ -203,6 +223,9 @@ QString QgsSensorThingsUtils::geometryFieldForEntityType( Qgis::SensorThingsEnti case Qgis::SensorThingsEntity::FeatureOfInterest: return QStringLiteral( "feature" ); + + case Qgis::SensorThingsEntity::MultiDatastream: + return QStringLiteral( "observedArea" ); } BUILTIN_UNREACHABLE } @@ -222,11 +245,35 @@ bool QgsSensorThingsUtils::entityTypeHasGeometry( Qgis::SensorThingsEntity type case Qgis::SensorThingsEntity::Location: case Qgis::SensorThingsEntity::FeatureOfInterest: + case Qgis::SensorThingsEntity::MultiDatastream: return true; } BUILTIN_UNREACHABLE } +Qgis::GeometryType QgsSensorThingsUtils::geometryTypeForEntity( Qgis::SensorThingsEntity type ) +{ + switch ( type ) + { + case Qgis::SensorThingsEntity::Invalid: + case Qgis::SensorThingsEntity::Thing: + case Qgis::SensorThingsEntity::HistoricalLocation: + case Qgis::SensorThingsEntity::Datastream: + case Qgis::SensorThingsEntity::Sensor: + case Qgis::SensorThingsEntity::Observation: + case Qgis::SensorThingsEntity::ObservedProperty: + return Qgis::GeometryType::Null; + + case Qgis::SensorThingsEntity::Location: + case Qgis::SensorThingsEntity::FeatureOfInterest: + return Qgis::GeometryType::Unknown; + + case Qgis::SensorThingsEntity::MultiDatastream: + return Qgis::GeometryType::Polygon; + } + BUILTIN_UNREACHABLE +} + QString QgsSensorThingsUtils::filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ) { QString geometryTypeString; diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.h b/src/core/providers/sensorthings/qgssensorthingsutils.h index c1cd617f2f8e..873abe1afa91 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.h +++ b/src/core/providers/sensorthings/qgssensorthingsutils.h @@ -76,6 +76,15 @@ class CORE_EXPORT QgsSensorThingsUtils */ static bool entityTypeHasGeometry( Qgis::SensorThingsEntity type ); + /** + * Returns the geometry type for if the specified entity \a type. + * + * If there are no restrictions on the geometry type an ntity can have Qgis::GeometryType::Unknown will be returned. + * + * \since QGIS 3.38 + */ + static Qgis::GeometryType geometryTypeForEntity( Qgis::SensorThingsEntity type ); + /** * Returns a filter string which restricts results to those matching the specified * \a entityType and \a wkbType. diff --git a/src/core/qgis.h b/src/core/qgis.h index 3993ca3e9a32..c5b0d0eb0eee 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -2145,6 +2145,7 @@ class CORE_EXPORT Qgis TemporalRangeFromDataProvider SIP_MONKEYPATCH_COMPAT_NAME( ModeTemporalRangeFromDataProvider ) = 1, //!< Mode when raster layer delegates temporal range handling to the dataprovider. RedrawLayerOnly SIP_MONKEYPATCH_COMPAT_NAME( ModeRedrawLayerOnly ) = 2, //!< Redraw the layer when temporal range changes, but don't apply any filtering. Useful when raster symbology expressions depend on the time range. (since QGIS 3.22) FixedRangePerBand = 3, //!< Layer has a fixed temporal range per band (since QGIS 3.38) + RepresentsTemporalValues = 4, //!< Pixel values represent an datetime }; Q_ENUM( RasterTemporalMode ) @@ -4872,6 +4873,7 @@ class CORE_EXPORT Qgis ObservedProperty, //!< An ObservedProperty specifies the phenomenon of an Observation Observation, //!< An Observation is the act of measuring or otherwise determining the value of a property FeatureOfInterest, //!< In the context of the Internet of Things, many Observations’ FeatureOfInterest can be the Location of the Thing. For example, the FeatureOfInterest of a wifi-connect thermostat can be the Location of the thermostat (i.e., the living room where the thermostat is located in). In the case of remote sensing, the FeatureOfInterest can be the geographical area or volume that is being sensed + MultiDatastream, //!< A MultiDatastream groups a collection of Observations and the Observations in a MultiDatastream have a complex result type. Implemented in the SensorThings version 1.1 "MultiDatastream extension". (Since QGIS 3.38) }; Q_ENUM( SensorThingsEntity ) diff --git a/src/core/qgsjsonutils.cpp b/src/core/qgsjsonutils.cpp index 644ba640e4b9..0585b2666d38 100644 --- a/src/core/qgsjsonutils.cpp +++ b/src/core/qgsjsonutils.cpp @@ -245,6 +245,9 @@ json QgsJsonExporter::exportFeaturesToJsonObject( const QgsFeatureList &features { "type", "FeatureCollection" }, { "features", json::array() } }; + + QgsJsonUtils::addCrsInfo( data, mDestinationCrs ); + for ( const QgsFeature &feature : std::as_const( features ) ) { data["features"].push_back( exportFeatureToJsonObject( feature ) ); @@ -912,3 +915,14 @@ json QgsJsonUtils::exportAttributesToJsonObject( const QgsFeature &feature, QgsV } return attrs; } + +void QgsJsonUtils::addCrsInfo( json &value, const QgsCoordinateReferenceSystem &crs ) +{ + // When user request EPSG:4326 we return a compliant CRS84 lon/lat GeoJSON + // so no need to add CRS information + if ( crs.authid() == "OGC:CRS84" || crs.authid() == "EPSG:4326" ) + return; + + value["crs"]["type"] = "name"; + value["crs"]["properties"]["name"] = crs.toOgcUrn().toStdString(); +} diff --git a/src/core/qgsjsonutils.h b/src/core/qgsjsonutils.h index 3e6aa0a7dbc6..6e1b793e8df2 100644 --- a/src/core/qgsjsonutils.h +++ b/src/core/qgsjsonutils.h @@ -423,6 +423,15 @@ class CORE_EXPORT QgsJsonUtils */ static QVariant jsonToVariant( const json &value ) SIP_SKIP; + /** + * Add \a crs information entry in \a json object regarding old GeoJSON specification format + * if it differs from OGC:CRS84 or EPSG:4326. + * According to new specification RFC 7946, coordinate reference system for all GeoJSON coordinates + * is assumed to be OGC:CRS84 but when user specifically request a different CRS, this method + * adds this information in the JSON output + */ + static void addCrsInfo( json &value, const QgsCoordinateReferenceSystem &crs ) SIP_SKIP; + }; #endif // QGSJSONUTILS_H diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp index 307218d1f831..40e74703b328 100644 --- a/src/core/qgslayerdefinition.cpp +++ b/src/core/qgslayerdefinition.cpp @@ -38,7 +38,7 @@ #include "qgslayertreegroup.h" #include "qgslayertreelayer.h" -bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage ) +bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, Qgis::LayerTreeInsertionMethod insertMethod, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { QFile file( path ); if ( !file.open( QIODevice::ReadOnly ) ) @@ -62,10 +62,10 @@ bool QgsLayerDefinition::loadLayerDefinition( const QString &path, QgsProject *p context.setPathResolver( QgsPathResolver( path ) ); context.setProjectTranslator( project ); - return loadLayerDefinition( doc, project, rootGroup, errorMessage, context ); + return loadLayerDefinition( doc, project, rootGroup, errorMessage, context, insertMethod, insertPoint ); } -bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context ) +bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint ) { errorMessage.clear(); @@ -195,7 +195,24 @@ bool QgsLayerDefinition::loadLayerDefinition( QDomDocument doc, QgsProject *proj root->abandonChildren(); delete root; - rootGroup->insertChildNodes( -1, nodes ); + switch ( insertMethod ) + { + case Qgis::LayerTreeInsertionMethod::AboveInsertionPoint: + if ( insertPoint ) + { + insertPoint->group->insertChildNodes( insertPoint->position, nodes ); + } + else + { + rootGroup->insertChildNodes( -1, nodes ); + } + break; + case Qgis::LayerTreeInsertionMethod::TopOfTree: + rootGroup->insertChildNodes( 0, nodes ); + break; + default: //Keep current behavior for Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup + rootGroup->insertChildNodes( -1, nodes ); + } return true; } diff --git a/src/core/qgslayerdefinition.h b/src/core/qgslayerdefinition.h index 97d1c472a470..5d9f812f91b1 100644 --- a/src/core/qgslayerdefinition.h +++ b/src/core/qgslayerdefinition.h @@ -19,6 +19,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" +#include "qgslayertreeregistrybridge.h" #include #include @@ -44,10 +45,31 @@ class QgsProject; class CORE_EXPORT QgsLayerDefinition { public: - //! Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup - static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT ); - //! Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup - static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context ); + + /** + * Loads the QLR at path into QGIS. New layers are added to given project into layer tree specified by rootGroup + * \param path file path to the qlr + * \param project the current project + * \param rootGroup the layer tree group to insert the qlr content + * \param errorMessage the returned error message + * \param insertMethod method for layer tree (since QGIS 3.38) + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + * \return true in case of success + */ + static bool loadLayerDefinition( const QString &path, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); + + /** + * Loads the QLR from the XML document. New layers are added to given project into layer tree specified by rootGroup + * \param doc the xml document + * \param project the current project + * \param rootGroup the layer tree group to insert the qlr content + * \param errorMessage the returned error message + * \param context the read write context + * \param insertMethod method for layer tree (since QGIS 3.38) + * \param insertPoint describes where in rootGroup the qlr layers/groups shall be inserted (since QGIS 3.38) + * \return true in case of success + */ + static bool loadLayerDefinition( QDomDocument doc, QgsProject *project, QgsLayerTreeGroup *rootGroup, QString &errorMessage SIP_OUT, QgsReadWriteContext &context, Qgis::LayerTreeInsertionMethod insertMethod = Qgis::LayerTreeInsertionMethod::OptimalInInsertionGroup, const QgsLayerTreeRegistryBridge::InsertionPoint *insertPoint = nullptr ); /** * Exports the selected layer tree nodes to a QLR file. diff --git a/src/core/qgstemporalnavigationobject.cpp b/src/core/qgstemporalnavigationobject.cpp index 7a8485e626c7..6edef7a8712b 100644 --- a/src/core/qgstemporalnavigationobject.cpp +++ b/src/core/qgstemporalnavigationobject.cpp @@ -270,7 +270,15 @@ double QgsTemporalNavigationObject::framesPerSecond() const void QgsTemporalNavigationObject::setTemporalRangeCumulative( bool state ) { + if ( mCumulativeTemporalRange == state ) + return; + mCumulativeTemporalRange = state; + + if ( !mBlockUpdateTemporalRangeSignal && mNavigationMode == Qgis::TemporalNavigationMode::Animated ) + { + emit updateTemporalRange( dateTimeRangeForFrameNumber( mCurrentFrameNumber ) ); + } } bool QgsTemporalNavigationObject::temporalRangeCumulative() const diff --git a/src/core/qgstracer.cpp b/src/core/qgstracer.cpp index 778498289b69..94cb662f730a 100644 --- a/src/core/qgstracer.cpp +++ b/src/core/qgstracer.cpp @@ -558,7 +558,7 @@ bool QgsTracer::initGraph() t2a.start(); // GEOSNode_r may throw an exception geos::unique_ptr allGeomGeos( QgsGeos::asGeos( allGeom ) ); - geos::unique_ptr allNoded( GEOSNode_r( QgsGeos::getGEOSHandler(), allGeomGeos.get() ) ); + geos::unique_ptr allNoded( GEOSNode_r( QgsGeosContext::get(), allGeomGeos.get() ) ); timeNodingCall = t2a.elapsed(); QgsGeometry noded = QgsGeos::geometryFromGeos( allNoded.release() ); diff --git a/src/core/qgsunittypes.cpp b/src/core/qgsunittypes.cpp index 2c60ce5db534..6d6d124934cf 100644 --- a/src/core/qgsunittypes.cpp +++ b/src/core/qgsunittypes.cpp @@ -1611,15 +1611,15 @@ double QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit fromUnit, Qgis::Te case Qgis::TemporalUnit::Days: return 1 / 86400000.0; case Qgis::TemporalUnit::Weeks: - return 1 / 60480000.0; + return 1 / 604800000.0; case Qgis::TemporalUnit::Months: - return 1 / 259200000.0; + return 1 / 2592000000.0; case Qgis::TemporalUnit::Years: - return 1 / 3155760000.0; - case Qgis::TemporalUnit::Decades: return 1 / 31557600000.0; - case Qgis::TemporalUnit::Centuries: + case Qgis::TemporalUnit::Decades: return 1 / 315576000000.0; + case Qgis::TemporalUnit::Centuries: + return 1 / 3155760000000.0; case Qgis::TemporalUnit::Unknown: case Qgis::TemporalUnit::IrregularStep: return 1.0; diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 0b79f62570de..fc29a0236ea2 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -41,21 +41,33 @@ QgsPalettedRasterRenderer::QgsPalettedRasterRenderer( QgsRasterInterface *input, : QgsRasterRenderer( input, QStringLiteral( "paletted" ) ) , mBand( bandNumber ) { + + QHash>> classData; + // Prepare for the worst case, where we have to store all the values for each class + classData.reserve( classes.size() ); + // This is to keep the ordering of the labels, because hash is fast but unordered + QVector labels; + labels.reserve( classes.size() ); + for ( const Class &klass : std::as_const( classes ) ) { - MultiValueClassData::iterator it = std::find_if( mMultiValueClassData.begin(), mMultiValueClassData.end(), [&klass]( const MultiValueClass & val ) -> bool - { - return val.label == klass.label && val.color == klass.color ; - } ); - if ( it != mMultiValueClassData.end() ) + if ( !classData.contains( klass.label ) ) { - it->values.push_back( klass.value ); + labels.push_back( klass.label ); } - else + classData[klass.label][klass.color].push_back( klass.value ); + } + + mMultiValueClassData.reserve( classData.size() ); + + for ( auto labelIt = labels.constBegin(); labelIt != labels.constEnd(); ++labelIt ) + { + for ( auto colorIt = classData[*labelIt].constBegin(); colorIt != classData[*labelIt].constEnd(); ++colorIt ) { - mMultiValueClassData.push_back( MultiValueClass{ { klass.value }, klass.color, klass.label } ); + mMultiValueClassData.push_back( MultiValueClass{ colorIt.value(), colorIt.key(), *labelIt } ); } } + updateArrays(); } diff --git a/src/core/raster/qgsrasterinterface.h b/src/core/raster/qgsrasterinterface.h index 22c4e2e200d9..ff4b2f2ddeba 100644 --- a/src/core/raster/qgsrasterinterface.h +++ b/src/core/raster/qgsrasterinterface.h @@ -151,6 +151,7 @@ class CORE_EXPORT QgsRasterInterface #include #include #include +#include #include #endif @@ -186,6 +187,8 @@ class CORE_EXPORT QgsRasterInterface sipType = sipType_QgsSingleBandGrayRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsSingleBandPseudoColorRenderer; + else if ( dynamic_cast( sipCpp ) ) + sipType = sipType_QgsRasterSingleColorRenderer; else if ( dynamic_cast( sipCpp ) ) sipType = sipType_QgsRasterContourRenderer; else diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index cb489e3616f6..3a9e8f824678 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -34,6 +34,9 @@ #include "qgsruntimeprofiler.h" #include "qgsapplication.h" #include "qgsrastertransparency.h" +#include "qgsrasterlayerutils.h" +#include "qgsinterval.h" +#include "qgsunittypes.h" #include #include @@ -272,26 +275,57 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mPipe->evaluateDataDefinedProperties( rendererContext.expressionContext() ); const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< const QgsRasterLayerTemporalProperties * >( layer->temporalProperties() ); + const QgsRasterLayerElevationProperties *elevationProperties = qobject_cast( layer->elevationProperties() ); + + if ( ( temporalProperties->isActive() && renderContext()->isTemporal() ) + || ( elevationProperties->hasElevation() && !renderContext()->zRange().isInfinite() ) ) + { + // temporal and/or elevation band filtering may be applicable + bool matched = false; + const int matchedBand = QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( + layer, + rendererContext.temporalRange(), + rendererContext.zRange(), + matched + ); + if ( matched && matchedBand > 0 ) + { + mPipe->renderer()->setInputBand( matchedBand ); + } + } + if ( temporalProperties->isActive() && renderContext()->isTemporal() ) { switch ( temporalProperties->mode() ) { case Qgis::RasterTemporalMode::FixedTemporalRange: case Qgis::RasterTemporalMode::RedrawLayerOnly: - break; - case Qgis::RasterTemporalMode::FixedRangePerBand: - { - const int matchingBand = temporalProperties->bandForTemporalRange( layer, rendererContext.temporalRange() ); + break; - // this is guaranteed, as we won't ever be creating a renderer if this condition is not met, but let's be ultra safe! - if ( matchingBand > 0 ) + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + if ( mPipe->renderer()->usesBands().contains( temporalProperties->bandNumber() ) ) { - mPipe->renderer()->setInputBand( matchingBand ); + // if layer has elevation settings and we are only rendering a temporal range => we need to filter pixels by temporal values + std::unique_ptr< QgsRasterTransparency > transparency; + if ( const QgsRasterTransparency *rendererTransparency = mPipe->renderer()->rasterTransparency() ) + transparency = std::make_unique< QgsRasterTransparency >( *rendererTransparency ); + else + transparency = std::make_unique< QgsRasterTransparency >(); + + QVector transparentPixels = transparency->transparentSingleValuePixelList(); + + const QDateTime &offset = temporalProperties->temporalRepresentationOffset(); + const QgsInterval &scale = temporalProperties->temporalRepresentationScale(); + const double adjustedLower = static_cast< double >( offset.msecsTo( rendererContext.temporalRange().begin() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); + const double adjustedUpper = static_cast< double >( offset.msecsTo( rendererContext.temporalRange().end() ) ) * QgsUnitTypes::fromUnitToUnitFactor( Qgis::TemporalUnit::Milliseconds, scale.originalUnit() ) / scale.originalDuration(); + transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( std::numeric_limits::lowest(), adjustedLower, 0, true, !rendererContext.zRange().includeLower() ) ); + transparentPixels.append( QgsRasterTransparency::TransparentSingleValuePixel( adjustedUpper, std::numeric_limits::max(), 0, !rendererContext.zRange().includeUpper(), true ) ); + + transparency->setTransparentSingleValuePixelList( transparentPixels ); + mPipe->renderer()->setRasterTransparency( transparency.release() ); } - break; - } case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: // in this mode we need to pass on the desired render temporal range to the data provider @@ -311,17 +345,16 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); - if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) + if ( elevationProperties && elevationProperties->hasElevation() ) { - QgsRasterLayerElevationProperties *elevProp = qobject_cast( layer->elevationProperties() ); mDrawElevationMap = true; - mElevationScale = elevProp->zScale(); - mElevationOffset = elevProp->zOffset(); - mElevationBand = elevProp->bandNumber(); + mElevationScale = elevationProperties->zScale(); + mElevationOffset = elevationProperties->zOffset(); + mElevationBand = elevationProperties->bandNumber(); if ( !rendererContext.zRange().isInfinite() ) { - switch ( elevProp->mode() ) + switch ( elevationProperties->mode() ) { case Qgis::RasterElevationMode::FixedElevationRange: // don't need to handle anything here -- the layer renderer will never be created if the @@ -330,16 +363,8 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender case Qgis::RasterElevationMode::FixedRangePerBand: case Qgis::RasterElevationMode::DynamicRangePerBand: - { - const int matchingBand = elevProp->bandForElevationRange( layer, rendererContext.zRange() ); - - // this is guaranteed, as we won't ever be creating a renderer if this condition is not met, but let's be ultra safe! - if ( matchingBand > 0 ) - { - mPipe->renderer()->setInputBand( matchingBand ); - } + // temporal/elevation band based filtering was already handled earlier in this method break; - } case Qgis::RasterElevationMode::RepresentsElevationSurface: { diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index a2320f1bbe27..659ab8aee834 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -22,6 +22,7 @@ QgsRasterLayerTemporalProperties::QgsRasterLayerTemporalProperties( QObject *parent, bool enabled ) : QgsMapLayerTemporalProperties( parent, enabled ) { + mTemporalRepresentationScale.setDays( 1.0 ); } bool QgsRasterLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTimeRange &range ) const @@ -44,6 +45,7 @@ bool QgsRasterLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTi return false; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: return true; @@ -95,6 +97,7 @@ QgsDateTimeRange QgsRasterLayerTemporalProperties::calculateTemporalExtent( QgsM return QgsDateTimeRange( begin, end, includeBeginning, includeEnd ); } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; } @@ -131,6 +134,7 @@ QList QgsRasterLayerTemporalProperties::allTemporalRanges( Qgs return ranges.empty() ? QList< QgsDateTimeRange > { rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange() } : ranges; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; } @@ -160,6 +164,7 @@ QgsTemporalProperty::Flags QgsRasterLayerTemporalProperties::flags() const case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: case Qgis::RasterTemporalMode::FixedRangePerBand: + case Qgis::RasterTemporalMode::RepresentsTemporalValues: return QgsTemporalProperty::Flags(); } BUILTIN_UNREACHABLE @@ -230,10 +235,90 @@ int QgsRasterLayerTemporalProperties::bandForTemporalRange( QgsRasterLayer *, co } return currentMatchingBand; } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + return mBandNumber; } BUILTIN_UNREACHABLE } +QList QgsRasterLayerTemporalProperties::filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const +{ + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::RedrawLayerOnly: + { + const int bandCount = layer->bandCount(); + QList< int > res; + res.reserve( bandCount ); + for ( int i = 1; i <= bandCount; ++i ) + res.append( i ); + return res; + } + + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + QList res; + res.reserve( mRangePerBand.size() ); + // find the latest-most band which matches the map range + QgsDateTimeRange currentMatchingRange; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + { + res.append( it.key() ); + } + } + return res; + } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + return QList() << mBandNumber; + } + BUILTIN_UNREACHABLE +} + +int QgsRasterLayerTemporalProperties::bandNumber() const +{ + return mBandNumber; +} + +void QgsRasterLayerTemporalProperties::setBandNumber( int number ) +{ + if ( mBandNumber == number ) + return; + + mBandNumber = number; +} + +QDateTime QgsRasterLayerTemporalProperties::temporalRepresentationOffset() const +{ + return mTemporalRepresentationOffset; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationOffset( const QDateTime &offset ) +{ + if ( mTemporalRepresentationOffset == offset ) + return; + + mTemporalRepresentationOffset = offset; +} + +const QgsInterval &QgsRasterLayerTemporalProperties::temporalRepresentationScale() const +{ + return mTemporalRepresentationScale; +} + +void QgsRasterLayerTemporalProperties::setTemporalRepresentationScale( const QgsInterval &scale ) +{ + if ( mTemporalRepresentationScale == scale ) + return; + + mTemporalRepresentationScale = scale; +} + bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) @@ -244,6 +329,7 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons setIsActive( temporalNode.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt() ); mMode = static_cast< Qgis::RasterTemporalMode >( temporalNode.attribute( QStringLiteral( "mode" ), QStringLiteral( "0" ) ). toInt() ); + mBandNumber = temporalNode.attribute( QStringLiteral( "bandNumber" ), QStringLiteral( "1" ) ).toInt(); mIntervalHandlingMethod = static_cast< Qgis::TemporalIntervalMatchMethod >( temporalNode.attribute( QStringLiteral( "fetchMode" ), QStringLiteral( "0" ) ). toInt() ); switch ( mMode ) @@ -281,6 +367,14 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons break; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + mTemporalRepresentationOffset = QDateTime::fromString( temporalNode.attribute( QStringLiteral( "temporalRepresentationOffset" ) ), Qt::ISODate ); + mTemporalRepresentationScale = QgsInterval( temporalNode.attribute( QStringLiteral( "temporalRepresentationScale" ), QStringLiteral( "1" ) ).toDouble(), + static_cast< Qgis::TemporalUnit >( temporalNode.attribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QStringLiteral( "4" ) ).toInt() ) ); + break; + } + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: break; @@ -298,6 +392,7 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD QDomElement temporalElement = document.createElement( QStringLiteral( "temporal" ) ); temporalElement.setAttribute( QStringLiteral( "enabled" ), isActive() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); temporalElement.setAttribute( QStringLiteral( "mode" ), QString::number( static_cast< int >( mMode ) ) ); + temporalElement.setAttribute( QStringLiteral( "bandNumber" ), QString::number( mBandNumber ) ); temporalElement.setAttribute( QStringLiteral( "fetchMode" ), QString::number( static_cast< int >( mIntervalHandlingMethod ) ) ); switch ( mMode ) @@ -338,6 +433,14 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD break; } + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationOffset" ), mTemporalRepresentationOffset.toString( Qt::ISODate ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScale" ), QString::number( mTemporalRepresentationScale.originalDuration() ) ); + temporalElement.setAttribute( QStringLiteral( "temporalRepresentationScaleUnit" ), QString::number( static_cast< int >( mTemporalRepresentationScale.originalUnit() ) ) ); + break; + } + case Qgis::RasterTemporalMode::RedrawLayerOnly: case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: break; diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 77fb62e7034b..f4cff37b4b82 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -22,6 +22,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" +#include "qgsinterval.h" #include "qgsrange.h" #include "qgsmaplayertemporalproperties.h" @@ -137,6 +138,71 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP */ int bandForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; + /** + * Returns a filtered list of bands which match the specified \a range. + * + * \since QGIS 3.38 + */ + QList< int > filteredBandsForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; + + /** + * Returns the band number from which temporal values should be taken. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setBandNumber() + * \since QGIS 3.38 + */ + int bandNumber() const; + + /** + * Sets the band number from which temporal values should be taken. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see bandNumber() + * \since QGIS 3.38 + */ + void setBandNumber( int number ); + + /** + * Returns the temporal offset, which is a fixed datetime which should be added to individual pixel values + * from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationOffset() + * \since QGIS 3.38 + */ + QDateTime temporalRepresentationOffset() const; + + /** + * Sets the temporal offset, which is a fixed datetime which should be added to individual pixel values + * from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationOffset() + * \since QGIS 3.38 + */ + void setTemporalRepresentationOffset( const QDateTime &offset ); + + /** + * Returns the scale, which is an interval factor which should be applied to individual pixel + * values from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see setTemporalRepresentationScale() + * \since QGIS 3.38 + */ + const QgsInterval &temporalRepresentationScale() const; + + /** + * Sets the scale, which is an interval factor which should be applied to individual pixel + * values from the layer. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::RepresentsTemporalValues. + * \see temporalRepresentationScale() + * \since QGIS 3.38 + */ + void setTemporalRepresentationScale( const QgsInterval &scale ); + QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; @@ -155,6 +221,11 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP QgsDateTimeRange mFixedRange; QMap< int, QgsDateTimeRange > mRangePerBand; + + int mBandNumber = 1; + + QDateTime mTemporalRepresentationOffset; + QgsInterval mTemporalRepresentationScale; }; #endif // QGSRASTERLAYERTEMPORALPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerutils.cpp b/src/core/raster/qgsrasterlayerutils.cpp new file mode 100644 index 000000000000..2ffd4d62845b --- /dev/null +++ b/src/core/raster/qgsrasterlayerutils.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + qgsrasterlayerutils.cpp + ------------------------- + begin : March 2024 + copyright : (C) 2024 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 "qgsrasterlayerutils.h" +#include "qgsrasterlayer.h" +#include "qgsrasterlayerelevationproperties.h" +#include "qgsrasterlayertemporalproperties.h" +#include "qgsexpressioncontext.h" +#include "qgsexpressioncontextutils.h" + +int QgsRasterLayerUtils::renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched ) +{ + matched = true; + const QgsRasterLayerElevationProperties *elevationProperties = qobject_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() ); + const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< QgsRasterLayerTemporalProperties *>( layer->temporalProperties() ); + + // neither active + if ( ( !temporalProperties->isActive() || temporalRange.isInfinite() ) + && ( !elevationProperties->hasElevation() || elevationRange.isInfinite() ) ) + { + return -1; + } + + // only elevation properties enabled + if ( !temporalProperties->isActive() || temporalRange.isInfinite() ) + { + const int band = elevationProperties->bandForElevationRange( layer, elevationRange ); + matched = band > 0; + return band; + } + + // only temporal properties enabled + if ( !elevationProperties->hasElevation() || elevationRange.isInfinite() ) + { + const int band = temporalProperties->bandForTemporalRange( layer, temporalRange ); + matched = band > 0; + return band; + } + + // both elevation and temporal properties enabled + + // first find bands matching the temporal range + QList< int > temporalBands; + switch ( temporalProperties->mode() ) + { + case Qgis::RasterTemporalMode::RedrawLayerOnly: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::FixedTemporalRange: + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + temporalBands << temporalProperties->filteredBandsForTemporalRange( layer, temporalRange ); + break; + } + + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + { + temporalBands << temporalProperties->bandNumber(); + break; + } + } + + if ( temporalBands.empty() ) + { + matched = false; + return -1; + } + + switch ( elevationProperties->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + case Qgis::RasterElevationMode::RepresentsElevationSurface: + return temporalBands.at( 0 ); + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + // find the top-most band which matches the map range + int currentMatchingBand = -1; + matched = false; + QgsDoubleRange currentMatchingRange; + const QMap rangePerBand = elevationProperties->fixedRangePerBand(); + for ( int band : temporalBands ) + { + const QgsDoubleRange rangeForBand = rangePerBand.value( band ); + if ( rangeForBand.overlaps( elevationRange ) ) + { + if ( currentMatchingRange.isInfinite() + || ( rangeForBand.includeUpper() && rangeForBand.upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && rangeForBand.upper() >= currentMatchingRange.upper() ) ) + { + matched = true; + currentMatchingBand = band; + currentMatchingRange = rangeForBand; + } + } + } + return currentMatchingBand; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( layer ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = elevationProperties->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = elevationProperties->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + + int currentMatchingBand = -1; + matched = false; + QgsDoubleRange currentMatchingRange; + + for ( int band : temporalBands ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), layer->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), layer->dataProvider()->bandDescription( band ) ); + + bool ok = false; + const double lower = lowerProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + continue; + const double upper = upperProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + continue; + + const QgsDoubleRange bandRange = QgsDoubleRange( lower, upper ); + if ( bandRange.overlaps( elevationRange ) ) + { + if ( currentMatchingRange.isInfinite() + || ( bandRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingBand = band; + currentMatchingRange = bandRange; + matched = true; + } + } + } + return currentMatchingBand; + } + return -1; + } + } + BUILTIN_UNREACHABLE; +} diff --git a/src/core/raster/qgsrasterlayerutils.h b/src/core/raster/qgsrasterlayerutils.h new file mode 100644 index 000000000000..a19295c15c36 --- /dev/null +++ b/src/core/raster/qgsrasterlayerutils.h @@ -0,0 +1,59 @@ +/*************************************************************************** + qgsrasterlayerutils.h + ------------------------- + begin : March 2024 + copyright : (C) 2024 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 QGSRASTERLAYERUTILS_H +#define QGSRASTERLAYERUTILS_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsrange.h" + +class QgsRasterLayer; + +/** + * \class QgsRasterLayerUtils + * \ingroup core + * \brief Contains utility functions for working with raster layers. + * + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsRasterLayerUtils +{ + public: + + /** + * Given a raster \a layer, returns the band which should be used for + * rendering the layer for a specified temporal and elevation range, + * respecting any elevation and temporal settings which affect the rendered band. + * + * \param layer Target raster layer + * \param temporalRange temporal range for rendering + * \param elevationRange elevation range for rendering + * \param matched will be set to TRUE if a band matching the temporal and elevation range was found + * + * \returns Matched band, or -1 if the layer does not have any elevation + * or temporal settings which affect the rendered band. + */ + static int renderedBandForElevationAndTemporalRange( + QgsRasterLayer *layer, + const QgsDateTimeRange &temporalRange, + const QgsDoubleRange &elevationRange, + bool &matched SIP_OUT ); + +}; + +#endif //QGSRASTERLAYERUTILS_H diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index 772b362d5e28..b905caccf6ea 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -26,6 +26,7 @@ #include "qgssinglebandcolordatarenderer.h" #include "qgssinglebandgrayrenderer.h" #include "qgssinglebandpseudocolorrenderer.h" +#include "qgsrastersinglecolorrenderer.h" #include "qgshillshaderenderer.h" #include "qgsapplication.h" #include "qgssettings.h" @@ -62,6 +63,8 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() QgsSingleBandPseudoColorRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlebandcolordata" ), QObject::tr( "Singleband color data" ), QgsSingleBandColorDataRenderer::create, nullptr ) ); + insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlecolor" ), QObject::tr( "Single color" ), + QgsRasterSingleColorRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "hillshade" ), QObject::tr( "Hillshade" ), QgsHillshadeRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "contour" ), QObject::tr( "Contours" ), diff --git a/src/core/raster/qgsrastersinglecolorrenderer.cpp b/src/core/raster/qgsrastersinglecolorrenderer.cpp new file mode 100644 index 000000000000..bf2f6414c3cb --- /dev/null +++ b/src/core/raster/qgsrastersinglecolorrenderer.cpp @@ -0,0 +1,185 @@ +/*************************************************************************** + qgsrastersinglecolorrenderer.cpp + ----------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot 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 "qgsrastersinglecolorrenderer.h" +#include "qgsrastertransparency.h" +#include "qgscolorutils.h" + +#include +#include + +QgsRasterSingleColorRenderer::QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ) + : QgsRasterRenderer( input, QStringLiteral( "singlecolor" ) ) + , mInputBand( band ) + , mColor( color ) +{ +} + +QgsRasterSingleColorRenderer *QgsRasterSingleColorRenderer::clone() const +{ + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( nullptr, mInputBand, mColor ); + renderer->copyCommonProperties( this ); + return renderer; +} + +Qgis::RasterRendererFlags QgsRasterSingleColorRenderer::flags() const +{ + return Qgis::RasterRendererFlag::InternalLayerOpacityHandling; +} + +QgsRasterRenderer *QgsRasterSingleColorRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) +{ + if ( elem.isNull() ) + { + return nullptr; + } + + const QColor color = QgsColorUtils::colorFromString( elem.attribute( QStringLiteral( "color" ), QStringLiteral( "0,0,0" ) ) ); + const int band = elem.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + QgsRasterSingleColorRenderer *r = new QgsRasterSingleColorRenderer( input, band, color ); + r->readXml( elem ); + + return r; +} + +QgsRasterBlock *QgsRasterSingleColorRenderer::block( int, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) +{ + QgsDebugMsgLevel( QStringLiteral( "width = %1 height = %2" ).arg( width ).arg( height ), 4 ); + + std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); + if ( !mInput || mInputBand == -1 ) + { + return outputBlock.release(); + } + + const std::shared_ptr< QgsRasterBlock > inputBlock( mInput->block( mInputBand, extent, width, height, feedback ) ); + if ( !inputBlock || inputBlock->isEmpty() ) + { + QgsDebugError( QStringLiteral( "No raster data!" ) ); + return outputBlock.release(); + } + + std::shared_ptr< QgsRasterBlock > alphaBlock; + if ( mAlphaBand > 0 ) + { + alphaBlock = inputBlock; + } + + if ( !outputBlock->reset( Qgis::DataType::ARGB32_Premultiplied, width, height ) ) + { + return outputBlock.release(); + } + + const QRgb defaultColor = renderColorForNodataPixel(); + const QRgb rendererColor = qRgba( mColor.red(), mColor.green(), mColor.blue(), mColor.alpha() ); + + bool isNoData = false; + const qgssize blockSize = static_cast< qgssize >( width ) * height; + for ( qgssize i = 0; i < blockSize; i++ ) + { + double value = inputBlock->valueAndNoData( i, isNoData ); + if ( isNoData ) + { + outputBlock->setColor( i, defaultColor ); + continue; + } + + double currentAlpha = mOpacity; + if ( mRasterTransparency ) + { + currentAlpha *= mRasterTransparency->opacityForValue( value ); + } + if ( mAlphaBand > 0 ) + { + const double alpha = alphaBlock->value( i ); + if ( alpha == 0 ) + { + outputBlock->setColor( i, defaultColor ); + continue; + } + else + { + currentAlpha *= alpha / 255.0; + } + } + + if ( qgsDoubleNear( currentAlpha, 1.0 ) ) + { + outputBlock->setColor( i, rendererColor ); + } + else + { + outputBlock->setColor( i, qRgba( static_cast( currentAlpha * mColor.red() ), + static_cast( currentAlpha * mColor.green() ), + static_cast( currentAlpha * mColor.blue() ), + static_cast( currentAlpha * mColor.alpha() ) ) ); + } + } + + return outputBlock.release(); +} + +void QgsRasterSingleColorRenderer::setColor( const QColor &color ) +{ + mColor = color;; +} + +QColor QgsRasterSingleColorRenderer::color() const +{ + return mColor; +} + +void QgsRasterSingleColorRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const +{ + if ( parentElem.isNull() ) + { + return; + } + + QDomElement rasterRendererElem = doc.createElement( QStringLiteral( "rasterrenderer" ) ); + _writeXml( doc, rasterRendererElem ); + + rasterRendererElem.setAttribute( QStringLiteral( "color" ), QgsColorUtils::colorToString( mColor ) ); + rasterRendererElem.setAttribute( QStringLiteral( "band" ), mInputBand ); + + parentElem.appendChild( rasterRendererElem ); +} + +int QgsRasterSingleColorRenderer::inputBand() const +{ + return mInputBand; +} + +bool QgsRasterSingleColorRenderer::setInputBand( int band ) +{ + if ( !mInput || ( band > 0 && band <= mInput->bandCount() ) ) + { + mInputBand = band; + return true; + } + return false; +} + +QList QgsRasterSingleColorRenderer::usesBands() const +{ + QList bands; + if ( mInputBand != -1 ) + { + bands << mInputBand; + } + return bands; +} diff --git a/src/core/raster/qgsrastersinglecolorrenderer.h b/src/core/raster/qgsrastersinglecolorrenderer.h new file mode 100644 index 000000000000..50f7fa94df2c --- /dev/null +++ b/src/core/raster/qgsrastersinglecolorrenderer.h @@ -0,0 +1,82 @@ +/*************************************************************************** + qgsrastersinglecolorrenderer.h + --------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot 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 QGSRASTERSINGLECOLORRENDERER_H +#define QGSRASTERSINGLECOLORRENDERER_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsrasterrenderer.h" + +#include + +class QDomElement; + +/** + * \ingroup core + * \brief Raster renderer which renders all data pixels using a single color. + * \since QGIS 3.38 + */ +class CORE_EXPORT QgsRasterSingleColorRenderer: public QgsRasterRenderer +{ + public: + + //! Creates a single \a color renderer + QgsRasterSingleColorRenderer( QgsRasterInterface *input, int band, const QColor &color ); + + //! QgsRasterSingleColorRenderer cannot be copied. Use clone() instead. + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ) = delete; + //! QgsRasterSingleColorRenderer cannot be copied. Use clone() instead. + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ) = delete; + + QgsRasterSingleColorRenderer *clone() const override SIP_FACTORY; + Qgis::RasterRendererFlags flags() const override; + + //! Creates an instance of the renderer based on definition from XML (used by the renderer registry) + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; + + QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; + + /** + * Returns the single color used by the renderer. + * \see setColor() + */ + QColor color() const; + + /** + * Sets the single \a color used by the renderer. + * \see color() + */ + void setColor( const QColor &color ); + + void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; + + int inputBand() const override; + bool setInputBand( int band ) override; + QList usesBands() const override; + + private: +#ifdef SIP_RUN + QgsRasterSingleColorRenderer( const QgsRasterSingleColorRenderer & ); + const QgsRasterSingleColorRenderer &operator=( const QgsRasterSingleColorRenderer & ); +#endif + + int mInputBand = -1; + QColor mColor; +}; + +#endif // QGSRASTERSINGLECOLORRENDERER_H diff --git a/src/core/web/qgswebenginepage.cpp b/src/core/web/qgswebenginepage.cpp index f56e0c1b916b..309483ca4b58 100644 --- a/src/core/web/qgswebenginepage.cpp +++ b/src/core/web/qgswebenginepage.cpp @@ -18,11 +18,11 @@ #include "qgswebenginepage.h" #include "qgsconfig.h" #include +#include #include #ifdef HAVE_PDF4QT #include "qgspdfrenderer.h" -#include #include #else #include "qgsexception.h" diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 09b327700271..89df2a440e22 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -24,6 +24,7 @@ set(QGIS_GUI_SRCS raster/qgsrasterrendererwidget.cpp raster/qgssinglebandgrayrendererwidget.cpp raster/qgssinglebandpseudocolorrendererwidget.cpp + raster/qgsrastersinglecolorrendererwidget.cpp raster/qgsrendererrasterpropertieswidget.cpp raster/qgsrastertransparencywidget.cpp raster/qgshillshaderendererwidget.cpp @@ -1456,6 +1457,7 @@ set(QGIS_GUI_HDRS raster/qgsrendererrasterpropertieswidget.h raster/qgssinglebandgrayrendererwidget.h raster/qgssinglebandpseudocolorrendererwidget.h + raster/qgsrastersinglecolorrendererwidget.h raster/qgsrasterlayerproperties.h raster/qgsrasterlayertemporalpropertieswidget.h raster/qgsresamplingutils.h diff --git a/src/gui/editorwidgets/qgsjsoneditwidget.cpp b/src/gui/editorwidgets/qgsjsoneditwidget.cpp index 5981300aa944..24424c8181be 100644 --- a/src/gui/editorwidgets/qgsjsoneditwidget.cpp +++ b/src/gui/editorwidgets/qgsjsoneditwidget.cpp @@ -26,8 +26,8 @@ QgsJsonEditWidget::QgsJsonEditWidget( QWidget *parent ) : QWidget( parent ) - , mCopyValueAction( new QAction( tr( "Copy value" ), this ) ) - , mCopyKeyAction( new QAction( tr( "Copy key" ), this ) ) + , mCopyValueAction( new QAction( tr( "Copy Value" ), this ) ) + , mCopyKeyAction( new QAction( tr( "Copy Key" ), this ) ) { setupUi( this ); @@ -40,8 +40,6 @@ QgsJsonEditWidget::QgsJsonEditWidget( QWidget *parent ) mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETINDICATORCURRENT, SCINTILLA_UNDERLINE_INDICATOR_INDEX ); mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETMOUSEDWELLTIME, 400 ); - mTreeWidget->setStyleSheet( QStringLiteral( "font-family: %1;" ).arg( QgsCodeEditor::getMonospaceFont().family() ) ); - mTreeWidget->setContextMenuPolicy( Qt::ActionsContextMenu ); mTreeWidget->addAction( mCopyValueAction ); mTreeWidget->addAction( mCopyKeyAction ); @@ -261,6 +259,7 @@ void QgsJsonEditWidget::refreshTreeView( const QJsonDocument &jsonDocument ) { const QJsonValue jsonValue = jsonDocument.object().value( key ); QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << key ); + treeWidgetItem->setFont( 0, monospaceFont() ); refreshTreeViewItem( treeWidgetItem, jsonValue ); mTreeWidget->addTopLevelItem( treeWidgetItem ); mTreeWidget->expandItem( treeWidgetItem ); @@ -281,6 +280,7 @@ void QgsJsonEditWidget::refreshTreeView( const QJsonDocument &jsonDocument ) for ( auto index = decltype( arraySize ) {0}; index < arraySize; index++ ) { QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << QString::number( index ) ); + treeWidgetItem->setFont( 0, monospaceFont() ); if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) ) { refreshTreeViewItem( treeWidgetItem, array.at( index ) ); @@ -334,6 +334,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co { QLabel *label = new QLabel( QString( "%1" ).arg( jsonValueString ) ); label->setOpenExternalLinks( true ); + label->setFont( monospaceFont() ); mTreeWidget->setItemWidget( treeWidgetItem, static_cast( TreeWidgetColumn::Value ), label ); mClickableLinkList.append( jsonValueString ); @@ -359,6 +360,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co for ( auto index = decltype( arraySize ) {0}; index < arraySize; index++ ) { QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << QString::number( index ) ); + treeWidgetItemChild->setFont( 0, monospaceFont() ); if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) ) { refreshTreeViewItem( treeWidgetItemChild, jsonArray.at( index ) ); @@ -382,6 +384,7 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co for ( const QString &key : keys ) { QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << key ); + treeWidgetItemChild->setFont( 0, monospaceFont() ); refreshTreeViewItem( treeWidgetItemChild, jsonObject.value( key ) ); treeWidgetItem->addChild( treeWidgetItemChild ); treeWidgetItem->setExpanded( true ); @@ -399,7 +402,17 @@ void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, co void QgsJsonEditWidget::refreshTreeViewItemValue( QTreeWidgetItem *treeWidgetItem, const QString &jsonValueString, const QColor &textColor ) { QLabel *label = new QLabel( jsonValueString ); + label->setFont( monospaceFont() ); + if ( textColor.isValid() ) label->setStyleSheet( QStringLiteral( "color: %1;" ).arg( textColor.name() ) ); mTreeWidget->setItemWidget( treeWidgetItem, static_cast( TreeWidgetColumn::Value ), label ); } + +QFont QgsJsonEditWidget::monospaceFont() const +{ + QFont f = QgsCodeEditor::getMonospaceFont(); + // use standard widget font size, not code editor font size + f.setPointSize( font().pointSize() ); + return f; +} diff --git a/src/gui/editorwidgets/qgsjsoneditwidget.h b/src/gui/editorwidgets/qgsjsoneditwidget.h index 89bf1cb421a9..34fd071fcc22 100644 --- a/src/gui/editorwidgets/qgsjsoneditwidget.h +++ b/src/gui/editorwidgets/qgsjsoneditwidget.h @@ -116,6 +116,8 @@ class GUI_EXPORT QgsJsonEditWidget : public QWidget, private Ui::QgsJsonEditWidg void refreshTreeViewItem( QTreeWidgetItem *treeWidgetItemParent, const QJsonValue &jsonValue ); void refreshTreeViewItemValue( QTreeWidgetItem *treeWidgetItem, const QString &jsonValueString, const QColor &textColor ); + QFont monospaceFont() const; + QString mJsonText; FormatJson mFormatJsonMode = FormatJson::Indented; diff --git a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp index da9dd095852a..f44dc213e7ea 100644 --- a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.cpp @@ -33,6 +33,14 @@ void QgsKeyValueWidgetWrapper::showIndeterminateState() mWidget->setMap( QVariantMap() ); } +void QgsKeyValueWidgetWrapper::setEnabled( bool enabled ) +{ + if ( mWidget ) + { + mWidget->setReadOnly( !enabled ); + } +} + QWidget *QgsKeyValueWidgetWrapper::createWidget( QWidget *parent ) { if ( isInTable( parent ) ) diff --git a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h index 1863f50fd346..e7146488b51c 100644 --- a/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h +++ b/src/gui/editorwidgets/qgskeyvaluewidgetwrapper.h @@ -49,6 +49,8 @@ class GUI_EXPORT QgsKeyValueWidgetWrapper : public QgsEditorWidgetWrapper public: QVariant value() const override; void showIndeterminateState() override; + public slots: + void setEnabled( bool enabled ) override; protected: QWidget *createWidget( QWidget *parent ) override; diff --git a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp index d9ad07c1e380..a5e8fc69bbde 100644 --- a/src/gui/editorwidgets/qgslistwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgslistwidgetwrapper.cpp @@ -27,22 +27,29 @@ void QgsListWidgetWrapper::showIndeterminateState() mWidget->setList( QVariantList() ); } +void QgsListWidgetWrapper::setEnabled( bool enabled ) +{ + if ( mWidget ) + { + mWidget->setReadOnly( !enabled ); + } +} + QWidget *QgsListWidgetWrapper::createWidget( QWidget *parent ) { + QFrame *ret = new QFrame( parent ); + ret->setFrameShape( QFrame::StyledPanel ); + QHBoxLayout *layout = new QHBoxLayout( ret ); + layout->setContentsMargins( 0, 0, 0, 0 ); + QgsListWidget *widget = new QgsListWidget( field().subType(), ret ); + layout->addWidget( widget ); + if ( isInTable( parent ) ) { - // if to be put in a table, draw a border and set a decent size - QFrame *ret = new QFrame( parent ); - ret->setFrameShape( QFrame::StyledPanel ); - QHBoxLayout *layout = new QHBoxLayout( ret ); - layout->addWidget( new QgsListWidget( field().subType(), ret ) ); + // if to be put in a table, set a decent size ret->setMinimumSize( QSize( 320, 110 ) ); - return ret; - } - else - { - return new QgsListWidget( field().subType(), parent ); } + return ret; } void QgsListWidgetWrapper::initWidget( QWidget *editor ) diff --git a/src/gui/editorwidgets/qgslistwidgetwrapper.h b/src/gui/editorwidgets/qgslistwidgetwrapper.h index 820677e22c0f..571d1439974b 100644 --- a/src/gui/editorwidgets/qgslistwidgetwrapper.h +++ b/src/gui/editorwidgets/qgslistwidgetwrapper.h @@ -50,6 +50,9 @@ class GUI_EXPORT QgsListWidgetWrapper : public QgsEditorWidgetWrapper QVariant value() const override; void showIndeterminateState() override; + public slots: + void setEnabled( bool enabled ) override; + protected: QWidget *createWidget( QWidget *parent ) override; void initWidget( QWidget *editor ) override; diff --git a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp index dcc0e73705ef..a3b87d359741 100644 --- a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp +++ b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp @@ -48,7 +48,9 @@ QgsProcessingDxfLayerDetailsWidget::QgsProcessingDxfLayerDetailsWidget( const QV return; mFieldsComboBox->setLayer( mLayer ); - mFieldsComboBox->setCurrentIndex( layer.layerOutputAttributeIndex() ); + + if ( mLayer->fields().exists( layer.layerOutputAttributeIndex() ) ) + mFieldsComboBox->setField( mLayer->fields().at( layer.layerOutputAttributeIndex() ).name() ); connect( mFieldsComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsPanelWidget::widgetChanged ); } diff --git a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp index b04c6c668d99..be0159091253 100644 --- a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp +++ b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp @@ -85,6 +85,24 @@ void QgsGeoPackageItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu if ( QgsGeoPackageCollectionItem *collectionItem = qobject_cast< QgsGeoPackageCollectionItem * >( item ) ) { + if ( !( item->capabilities2() & Qgis::BrowserItemCapability::ItemRepresentsFile ) ) + { + // add a refresh action, but ONLY if the collection item isn't representing a file + // (if so, then QgsAppFileItemGuiProvider will add the Refresh item) + QAction *actionRefresh = new QAction( QObject::tr( "Refresh" ), menu ); + connect( actionRefresh, &QAction::triggered, collectionItem, [collectionItem] { collectionItem->refresh(); } ); + if ( !menu->actions().empty() ) + { + QAction *firstAction = menu->actions().at( 0 ); + menu->insertAction( firstAction, actionRefresh ); + menu->insertSeparator( firstAction ); + } + else + { + menu->addAction( actionRefresh ); + } + } + menu->addSeparator(); if ( QgsOgrDbConnection::connectionList( QStringLiteral( "GPKG" ) ).contains( collectionItem->name() ) ) diff --git a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp index 8f4af395ed13..fd73b8fac82c 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp +++ b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp @@ -57,6 +57,7 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent ) Qgis::SensorThingsEntity::ObservedProperty, Qgis::SensorThingsEntity::Observation, Qgis::SensorThingsEntity::FeatureOfInterest, + Qgis::SensorThingsEntity::MultiDatastream, } ) { mComboEntityType->addItem( QgsSensorThingsUtils::displayString( type, true ), QVariant::fromValue( type ) ); @@ -190,6 +191,9 @@ QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connection case Qgis::WkbType::MultiPolygon: parts.insert( QStringLiteral( "geometryType" ), QStringLiteral( "polygon" ) ); break; + case Qgis::WkbType::NoGeometry: + parts.remove( QStringLiteral( "geometryType" ) ); + break; default: break; } @@ -297,8 +301,10 @@ void QgsSensorThingsSourceWidget::rebuildGeometryTypes( Qgis::SensorThingsEntity mPropertiesTask = nullptr; } - mRetrieveTypesButton->setEnabled( QgsSensorThingsUtils::entityTypeHasGeometry( type ) && !mSourceParts.value( QStringLiteral( "url" ) ).toString().isEmpty() ); - if ( QgsSensorThingsUtils::entityTypeHasGeometry( type ) && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::Point ) ) < 0 ) + mRetrieveTypesButton->setEnabled( QgsSensorThingsUtils::geometryTypeForEntity( type ) == Qgis::GeometryType::Unknown + && !mSourceParts.value( QStringLiteral( "url" ) ).toString().isEmpty() ); + const Qgis::GeometryType geometryTypeForEntity = QgsSensorThingsUtils::geometryTypeForEntity( type ); + if ( geometryTypeForEntity == Qgis::GeometryType::Unknown && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::Point ) ) < 0 ) { mComboGeometryType->clear(); mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::Point ), tr( "Point" ), QVariant::fromValue( Qgis::WkbType::Point ) ); @@ -307,11 +313,37 @@ void QgsSensorThingsSourceWidget::rebuildGeometryTypes( Qgis::SensorThingsEntity mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPolygon ), tr( "Polygon" ), QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ); setCurrentGeometryTypeFromString( mSourceParts.value( QStringLiteral( "geometryType" ) ).toString() ); } - else if ( !QgsSensorThingsUtils::entityTypeHasGeometry( type ) && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) < 0 ) + else if ( geometryTypeForEntity == Qgis::GeometryType::Null && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) < 0 ) { mComboGeometryType->clear(); mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::NoGeometry ), tr( "No Geometry" ), QVariant::fromValue( Qgis::WkbType::NoGeometry ) ); } + else if ( ( geometryTypeForEntity != Qgis::GeometryType::Null && geometryTypeForEntity != Qgis::GeometryType::Unknown ) + && mComboGeometryType->findData( QVariant::fromValue( geometryTypeForEntity ) ) < 0 ) + { + mComboGeometryType->clear(); + switch ( geometryTypeForEntity ) + { + case Qgis::GeometryType::Point: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::Point ), tr( "Point" ), QVariant::fromValue( Qgis::WkbType::Point ) ); + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPoint ), tr( "Multipoint" ), QVariant::fromValue( Qgis::WkbType::MultiPoint ) ); + break; + case Qgis::GeometryType::Line: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiLineString ), tr( "Line" ), QVariant::fromValue( Qgis::WkbType::MultiLineString ) ); + break; + + case Qgis::GeometryType::Polygon: + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::MultiPolygon ), tr( "Polygon" ), QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ); + break; + + case Qgis::GeometryType::Unknown: + case Qgis::GeometryType::Null: + break; + } + // we always add a "no geometry" option here, as some services don't correctly respect the mandated geometry types for eg MultiDatastreams + mComboGeometryType->addItem( QgsIconUtils::iconForWkbType( Qgis::WkbType::NoGeometry ), tr( "No Geometry" ), QVariant::fromValue( Qgis::WkbType::NoGeometry ) ); + setCurrentGeometryTypeFromString( mSourceParts.value( QStringLiteral( "geometryType" ) ).toString() ); mComboGeometryType->setCurrentIndex( 0 ); + } } void QgsSensorThingsSourceWidget::setCurrentGeometryTypeFromString( const QString &geometryType ) @@ -332,6 +364,10 @@ void QgsSensorThingsSourceWidget::setCurrentGeometryTypeFromString( const QStrin { mComboGeometryType->setCurrentIndex( mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::MultiPolygon ) ) ); } + else if ( geometryType.isEmpty() && mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) >= 0 ) + { + mComboGeometryType->setCurrentIndex( mComboGeometryType->findData( QVariant::fromValue( Qgis::WkbType::NoGeometry ) ) ); + } else if ( geometryType.isEmpty() && mComboGeometryType->currentIndex() < 0 ) { mComboGeometryType->setCurrentIndex( 0 ); diff --git a/src/gui/qgskeyvaluewidget.cpp b/src/gui/qgskeyvaluewidget.cpp index d0972cc2b33a..07ec875eb918 100644 --- a/src/gui/qgskeyvaluewidget.cpp +++ b/src/gui/qgskeyvaluewidget.cpp @@ -28,6 +28,12 @@ void QgsKeyValueWidget::setMap( const QVariantMap &map ) mModel.setMap( map ); } +void QgsKeyValueWidget::setReadOnly( bool readOnly ) +{ + mModel.setReadOnly( readOnly ); + QgsTableWidgetBase::setReadOnly( readOnly ); +} + ///@cond PRIVATE void QgsKeyValueModel::setMap( const QVariantMap &map ) { @@ -96,6 +102,9 @@ QVariant QgsKeyValueModel::data( const QModelIndex &index, int role ) const bool QgsKeyValueModel::setData( const QModelIndex &index, const QVariant &value, int role ) { + if ( mReadOnly ) + return false; + if ( index.row() < 0 || index.row() >= mLines.count() || role != Qt::EditRole ) { return false; @@ -114,11 +123,17 @@ bool QgsKeyValueModel::setData( const QModelIndex &index, const QVariant &value, Qt::ItemFlags QgsKeyValueModel::flags( const QModelIndex &index ) const { - return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + if ( !mReadOnly ) + return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + else + return QAbstractTableModel::flags( index ); } bool QgsKeyValueModel::insertRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginInsertRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -131,10 +146,18 @@ bool QgsKeyValueModel::insertRows( int position, int rows, const QModelIndex &pa bool QgsKeyValueModel::removeRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginRemoveRows( QModelIndex(), position, position + rows - 1 ); mLines.remove( position, rows ); endRemoveRows(); return true; } + +void QgsKeyValueModel::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; +} ///@endcond diff --git a/src/gui/qgskeyvaluewidget.h b/src/gui/qgskeyvaluewidget.h index e50531760c51..1945a07c3126 100644 --- a/src/gui/qgskeyvaluewidget.h +++ b/src/gui/qgskeyvaluewidget.h @@ -48,10 +48,11 @@ class GUI_EXPORT QgsKeyValueModel : public QAbstractTableModel Qt::ItemFlags flags( const QModelIndex &index ) const override; bool insertRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; - + void setReadOnly( bool readOnly ); typedef QPair Line; private: + bool mReadOnly = false; QVector mLines; }; ///@endcond @@ -83,6 +84,9 @@ class GUI_EXPORT QgsKeyValueWidget: public QgsTableWidgetBase */ QVariantMap map() const { return mModel.map(); } + public slots: + + void setReadOnly( bool readOnly ) override; private: QgsKeyValueModel mModel; }; diff --git a/src/gui/qgslistwidget.cpp b/src/gui/qgslistwidget.cpp index b44d73ef723e..c60caebc9f8d 100644 --- a/src/gui/qgslistwidget.cpp +++ b/src/gui/qgslistwidget.cpp @@ -29,6 +29,12 @@ void QgsListWidget::setList( const QVariantList &list ) mModel.setList( list ); } +void QgsListWidget::setReadOnly( bool readOnly ) +{ + mModel.setReadOnly( readOnly ); + QgsTableWidgetBase::setReadOnly( readOnly ); +} + ///@cond PRIVATE QgsListModel::QgsListModel( QVariant::Type subType, QObject *parent ) : @@ -101,6 +107,9 @@ QVariant QgsListModel::data( const QModelIndex &index, int role ) const bool QgsListModel::setData( const QModelIndex &index, const QVariant &value, int role ) { + if ( mReadOnly ) + return false; + if ( index.row() < 0 || index.row() >= mLines.count() || index.column() != 0 || role != Qt::EditRole ) { @@ -113,11 +122,17 @@ bool QgsListModel::setData( const QModelIndex &index, const QVariant &value, int Qt::ItemFlags QgsListModel::flags( const QModelIndex &index ) const { - return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + if ( !mReadOnly ) + return QAbstractTableModel::flags( index ) | Qt::ItemIsEditable; + else + return QAbstractTableModel::flags( index ); } bool QgsListModel::insertRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginInsertRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -130,6 +145,9 @@ bool QgsListModel::insertRows( int position, int rows, const QModelIndex &parent bool QgsListModel::removeRows( int position, int rows, const QModelIndex &parent ) { + if ( mReadOnly ) + return false; + Q_UNUSED( parent ) beginRemoveRows( QModelIndex(), position, position + rows - 1 ); for ( int i = 0; i < rows; ++i ) @@ -137,4 +155,9 @@ bool QgsListModel::removeRows( int position, int rows, const QModelIndex &parent endRemoveRows(); return true; } + +void QgsListModel::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; +} ///@endcond diff --git a/src/gui/qgslistwidget.h b/src/gui/qgslistwidget.h index 10161451eba8..2a96d652e58d 100644 --- a/src/gui/qgslistwidget.h +++ b/src/gui/qgslistwidget.h @@ -48,8 +48,9 @@ class GUI_EXPORT QgsListModel : public QAbstractTableModel Qt::ItemFlags flags( const QModelIndex &index ) const override; bool insertRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; - + void setReadOnly( bool readOnly ); private: + bool mReadOnly = false; QVariantList mLines; QVariant::Type mSubType; }; @@ -89,6 +90,10 @@ class GUI_EXPORT QgsListWidget: public QgsTableWidgetBase */ bool valid() const { return mModel.valid(); } + public slots: + + void setReadOnly( bool readOnly ) override; + private: QgsListModel mModel; QVariant::Type mSubType; diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 176629800ef2..264bbe77ea17 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -1059,7 +1059,9 @@ void QgsMapCanvas::clearTemporalCache() continue; if ( !alreadyInvalidatedThisLayer ) + { mCache->invalidateCacheForLayer( layer ); + } } else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) { diff --git a/src/gui/qgstablewidgetbase.cpp b/src/gui/qgstablewidgetbase.cpp index d517a1682465..f870acd5ef49 100644 --- a/src/gui/qgstablewidgetbase.cpp +++ b/src/gui/qgstablewidgetbase.cpp @@ -34,6 +34,9 @@ void QgsTableWidgetBase::init( QAbstractTableModel *model ) void QgsTableWidgetBase::addButton_clicked() { + if ( mReadOnly ) + return; + const QItemSelectionModel *select = tableView->selectionModel(); const int pos = select->hasSelection() ? select->selectedRows()[0].row() : 0; QAbstractItemModel *model = tableView->model(); @@ -46,6 +49,9 @@ void QgsTableWidgetBase::addButton_clicked() void QgsTableWidgetBase::removeButton_clicked() { + if ( mReadOnly ) + return; + const QItemSelectionModel *select = tableView->selectionModel(); // The UI is configured to have single row selection. if ( select->hasSelection() ) @@ -58,3 +64,22 @@ void QgsTableWidgetBase::onSelectionChanged() { removeButton->setEnabled( tableView->selectionModel()->hasSelection() ); } + +void QgsTableWidgetBase::setReadOnly( bool readOnly ) +{ + mReadOnly = readOnly; + + addButton->setEnabled( !mReadOnly ); + removeButton->setEnabled( !mReadOnly && tableView->selectionModel()->hasSelection() ); + + if ( mReadOnly ) + { + mWidgetActions->hide(); + layout()->setSpacing( 0 ); + } + else + { + mWidgetActions->show(); + layout()->setSpacing( 6 ); + } +} diff --git a/src/gui/qgstablewidgetbase.h b/src/gui/qgstablewidgetbase.h index c9d9c6ca4463..7c4a384bd504 100644 --- a/src/gui/qgstablewidgetbase.h +++ b/src/gui/qgstablewidgetbase.h @@ -39,6 +39,24 @@ class GUI_EXPORT QgsTableWidgetBase: public QWidget, protected Ui::QgsTableWidge */ explicit QgsTableWidgetBase( QWidget *parent ); + /** + * Returns TRUE if the widget is shown in a read-only state. + * + * \see setReadOnly() + * \since QGIS 3.38 + */ + bool isReadOnly() const { return mReadOnly; } + + public slots: + + /** + * Sets whether the widget should be shown in a read-only state. + * + * \see isReadOnly() + * \since QGIS 3.38 + */ + virtual void setReadOnly( bool readOnly ); + protected: /** @@ -71,6 +89,10 @@ class GUI_EXPORT QgsTableWidgetBase: public QWidget, protected Ui::QgsTableWidge */ void onSelectionChanged(); + private: + + bool mReadOnly = false; + friend class TestQgsKeyValueWidget; friend class TestQgsListWidget; diff --git a/src/gui/raster/qgsrasterlayerproperties.cpp b/src/gui/raster/qgsrasterlayerproperties.cpp index be2d2436b3bb..c27bf6e03d3c 100644 --- a/src/gui/raster/qgsrasterlayerproperties.cpp +++ b/src/gui/raster/qgsrasterlayerproperties.cpp @@ -49,6 +49,7 @@ #include "qgsrastertransparency.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" #include "qgshuesaturationfilter.h" #include "qgshillshaderendererwidget.h" #include "qgssettings.h" @@ -462,6 +463,7 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsRasterSingleColorRendererWidget::create ); //fill available renderers into combo box QgsRasterRendererRegistryEntry entry; diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp index 05f6ff1b006f..0e63cad44d50 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -23,6 +23,8 @@ #include "qgsdatetimeedit.h" #include "qgsexpressionbuilderdialog.h" #include "qgsexpressioncontextutils.h" +#include "qgsunittypes.h" + #include #include @@ -48,8 +50,27 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( } mModeComboBox->addItem( tr( "Fixed Time Range" ), QVariant::fromValue( Qgis::RasterTemporalMode::FixedTemporalRange ) ); mModeComboBox->addItem( tr( "Fixed Time Range Per Band" ), QVariant::fromValue( Qgis::RasterTemporalMode::FixedRangePerBand ) ); + mModeComboBox->addItem( tr( "Represents Temporal Values" ), QVariant::fromValue( Qgis::RasterTemporalMode::RepresentsTemporalValues ) ); mModeComboBox->addItem( tr( "Redraw Layer Only" ), QVariant::fromValue( Qgis::RasterTemporalMode::RedrawLayerOnly ) ); + for ( const Qgis::TemporalUnit unit : + { + Qgis::TemporalUnit::Milliseconds, + Qgis::TemporalUnit::Seconds, + Qgis::TemporalUnit::Minutes, + Qgis::TemporalUnit::Hours, + Qgis::TemporalUnit::Days, + Qgis::TemporalUnit::Weeks, + Qgis::TemporalUnit::Months, + Qgis::TemporalUnit::Years, + Qgis::TemporalUnit::Decades, + Qgis::TemporalUnit::Centuries, + } ) + { + mScaleUnitComboBox->addItem( QgsUnitTypes::toString( unit ), static_cast< int >( unit ) ); + } + mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( Qgis::TemporalUnit::Days ) ) ); + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); mFixedRangePerBandModel = new QgsRasterBandFixedTemporalRangeModel( this ); @@ -65,6 +86,7 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( mStartTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); mEndTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); + mOffsetDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); QMenu *calculateFixedRangePerBandMenu = new QMenu( mCalculateFixedRangePerBandButton ); mCalculateFixedRangePerBandButton->setMenu( calculateFixedRangePerBandMenu ); @@ -93,6 +115,7 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< QgsRasterLayerTemporalProperties * >( mLayer->temporalProperties() ); temporalProperties->setMode( mModeComboBox->currentData().value< Qgis::RasterTemporalMode >() ); + temporalProperties->setBandNumber( mBandComboBox->currentBand() ); const QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), mEndTemporalDateTimeEdit->dateTime() ); @@ -100,6 +123,11 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() temporalProperties->setFixedRangePerBand( mFixedRangePerBandModel->rangeData() ); + temporalProperties->setTemporalRepresentationOffset( mOffsetDateTimeEdit->dateTime() ); + + const QgsInterval scale( mScaleSpinBox->value(), static_cast< Qgis::TemporalUnit >( mScaleUnitComboBox->currentData().toInt() ) ); + temporalProperties->setTemporalRepresentationScale( scale ); + for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) { widget->apply(); @@ -124,8 +152,14 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() case Qgis::RasterTemporalMode::FixedRangePerBand: mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); break; + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + mStackedWidget->setCurrentWidget( mPageRepresentsTemporalValues ); + break; } + mBandComboBox->setLayer( mLayer ); + mBandComboBox->setBand( temporalProperties->bandNumber() ); + mStartTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().begin() ); mEndTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().end() ); @@ -134,6 +168,11 @@ void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() mBandRangesTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); mBandRangesTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + mOffsetDateTimeEdit->setDateTime( temporalProperties->temporalRepresentationOffset() ); + + mScaleSpinBox->setValue( temporalProperties->temporalRepresentationScale().originalDuration() ); + mScaleUnitComboBox->setCurrentIndex( mScaleUnitComboBox->findData( static_cast< int >( temporalProperties->temporalRepresentationScale().originalUnit() ) ) ); + mTemporalGroupBox->setChecked( temporalProperties->isActive() ); for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) @@ -174,6 +213,9 @@ void QgsRasterLayerTemporalPropertiesWidget::modeChanged() case Qgis::RasterTemporalMode::FixedRangePerBand: mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); break; + case Qgis::RasterTemporalMode::RepresentsTemporalValues: + mStackedWidget->setCurrentWidget( mPageRepresentsTemporalValues ); + break; } } } diff --git a/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp new file mode 100644 index 000000000000..eef41fb13967 --- /dev/null +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + qgsrastersinglecolorrendererwidget.cpp + --------------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot 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 "qgsrastersinglecolorrendererwidget.h" +#include "qgsrastersinglecolorrenderer.h" +#include "qgsrasterlayer.h" +#include "qgsrasterdataprovider.h" + +QgsRasterSingleColorRendererWidget::QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent ) + : QgsRasterRendererWidget( layer, extent ) +{ + setupUi( this ); + + if ( mRasterLayer ) + { + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return; + } + + mBandComboBox->setLayer( layer ); + + connect( mBandComboBox, &QgsRasterBandComboBox::bandChanged, this, [ = ]( int ) { emit widgetChanged(); } ); + connect( mColorButton, &QgsColorButton::colorChanged, this, [ = ]( const QColor & ) { emit widgetChanged(); } ); + + setFromRenderer( layer->renderer() ); + } +} + +QgsRasterRenderer *QgsRasterSingleColorRendererWidget::renderer() +{ + if ( !mRasterLayer ) + { + return nullptr; + } + + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return nullptr; + } + + QgsRasterSingleColorRenderer *renderer = new QgsRasterSingleColorRenderer( provider, mBandComboBox->currentBand(), mColorButton->color() ); + return renderer; +} + +void QgsRasterSingleColorRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) +{ + const QgsRasterSingleColorRenderer *scr = dynamic_cast( r ); + if ( scr ) + { + mBandComboBox->setBand( scr->inputBand() ); + mColorButton->setColor( scr->color() ); + } + else + { + mBandComboBox->setBand( 1 ); + mColorButton->setColor( QColor( 0, 0, 0 ) ); + } +} diff --git a/src/gui/raster/qgsrastersinglecolorrendererwidget.h b/src/gui/raster/qgsrastersinglecolorrendererwidget.h new file mode 100644 index 000000000000..23197e736a39 --- /dev/null +++ b/src/gui/raster/qgsrastersinglecolorrendererwidget.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsrastersinglecolorrendererwidget.h + --------------------------------- + begin : April 2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu at opengis dot 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 QGSRASTERSINGLECOLORRENDERERWIDGET_H +#define QGSRASTERSINGLECOLORRENDERERWIDGET_H + +#define SIP_NO_FILE + +#include "ui_qgsrastersinglecolorrendererwidgetbase.h" + +#include "qgsrasterrendererwidget.h" +#include "qgis_gui.h" + +/** + * \brief Renderer widget for the single color renderer. + * \ingroup gui + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsRasterSingleColorRendererWidget: public QgsRasterRendererWidget, private Ui::QgsRasterSingleColorRendererWidgetBase +{ + Q_OBJECT + public: + //! Constructs the widget + QgsRasterSingleColorRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + + //! Widget creation function (use by the renderer registry) + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) SIP_FACTORY { return new QgsRasterSingleColorRendererWidget( layer, extent ); } + + QgsRasterRenderer *renderer() SIP_FACTORY override; + + /** + * Sets the widget state from the specified renderer. + */ + void setFromRenderer( const QgsRasterRenderer *r ); + +}; + +#endif // QGSRASTERSINGLECOLORRENDERERWIDGET_H diff --git a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp index 95cf5e7a4f77..9daaa656b03a 100644 --- a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp +++ b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp @@ -24,6 +24,7 @@ #include "qgsrasterrendererregistry.h" #include "qgssinglebandgrayrendererwidget.h" #include "qgssinglebandpseudocolorrendererwidget.h" +#include "qgsrastersinglecolorrendererwidget.h" #include "qgsmultibandcolorrendererwidget.h" #include "qgspalettedrendererwidget.h" #include "qgshillshaderendererwidget.h" @@ -43,6 +44,7 @@ void QgsRendererRasterPropertiesWidget::initRendererWidgetFunctions() QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "multibandcolor" ), QgsMultiBandColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandpseudocolor" ), QgsSingleBandPseudoColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlecolor" ), QgsRasterSingleColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); diff --git a/src/plugins/topology/topolTest.cpp b/src/plugins/topology/topolTest.cpp index 32dda6580d4c..b59480b7bdfc 100644 --- a/src/plugins/topology/topolTest.cpp +++ b/src/plugins/topology/topolTest.cpp @@ -470,7 +470,7 @@ ErrorList topolTest::checkGaps( QgsVectorLayer *layer1, QgsVectorLayer *layer2, int i = 0; ErrorList errorList; - GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler(); + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); // could be enabled for lines and points too // so duplicate rule may be removed? diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index 5ac23a33e53f..298cd5dc9d0a 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -213,6 +213,11 @@ bool QgsWmsSettings::parseUri( const QString &uriString ) } } + if ( uri.hasParam( QStringLiteral( "filter" ) ) ) + { + mFilter = uri.param( QStringLiteral( "filter" ) ); + } + mImageMimeType = uri.param( QStringLiteral( "format" ) ); QgsDebugMsgLevel( "Setting image encoding to " + mImageMimeType + '.', 2 ); diff --git a/src/providers/wms/qgswmscapabilities.h b/src/providers/wms/qgswmscapabilities.h index f56a5126d103..e954a140d708 100644 --- a/src/providers/wms/qgswmscapabilities.h +++ b/src/providers/wms/qgswmscapabilities.h @@ -874,6 +874,8 @@ class QgsWmsSettings bool mSmoothPixmapTransform; enum QgsWmsDpiMode mDpiMode; + QString mFilter; + /** * Active sublayers managed by this provider in a draw function, in order from bottom to top * (some may not be visible in a draw function, cf. activeSubLayerVisibility) diff --git a/src/providers/wms/qgswmsprovider.cpp b/src/providers/wms/qgswmsprovider.cpp index 46aa775d326c..48d202a7e43e 100644 --- a/src/providers/wms/qgswmsprovider.cpp +++ b/src/providers/wms/qgswmsprovider.cpp @@ -1298,6 +1298,11 @@ QUrl QgsWmsProvider::createRequestUrlWMS( const QgsRectangle &viewExtent, int pi setQueryItem( query, QStringLiteral( "OPACITIES" ), mSettings.mOpacities.join( ',' ) ); } + if ( !mSettings.mFilter.isEmpty() ) + { + setQueryItem( query, QStringLiteral( "FILTER" ), mSettings.mFilter ); + } + // For WMS-T layers if ( temporalCapabilities() && temporalCapabilities()->hasTemporalCapabilities() ) diff --git a/src/server/qgscapabilitiescache.h b/src/server/qgscapabilitiescache.h index cea69332b5ef..7ea9a0560444 100644 --- a/src/server/qgscapabilitiescache.h +++ b/src/server/qgscapabilitiescache.h @@ -53,8 +53,10 @@ class SERVER_EXPORT QgsCapabilitiesCache : public QObject */ void insertCapabilitiesDocument( const QString &configFilePath, const QString &key, const QDomDocument *doc ); + public slots: + /** - * Remove capabilities document + * Removes capabilities document * \param path the project file path */ void removeCapabilitiesDocument( const QString &path ); diff --git a/src/server/qgsconfigcache.cpp b/src/server/qgsconfigcache.cpp index 9c0728c96602..6731eb6fab81 100644 --- a/src/server/qgsconfigcache.cpp +++ b/src/server/qgsconfigcache.cpp @@ -249,6 +249,8 @@ void QgsConfigCache::removeEntry( const QString &path ) mXmlDocumentCache.remove( path ); mStrategy->entryRemoved( path ); + + emit projectRemovedFromCache( path ); } // slots diff --git a/src/server/qgsconfigcache.h b/src/server/qgsconfigcache.h index 00fe0adcdf88..b0910a34320a 100644 --- a/src/server/qgsconfigcache.h +++ b/src/server/qgsconfigcache.h @@ -128,6 +128,14 @@ class SERVER_EXPORT QgsConfigCache : public QObject //! Initialize with a strategy implementation. QgsConfigCache( QgsAbstractCacheStrategy *strategy ) SIP_SKIP; + signals: + + /** + * Emitted whenever a project is removed from the cache. + * \since QGIS 3.38 + */ + void projectRemovedFromCache( const QString &path ); + private: // SIP require this QgsConfigCache() SIP_FORCE; diff --git a/src/server/qgsserver.cpp b/src/server/qgsserver.cpp index 37cb497b60ef..52475fb5d5c3 100644 --- a/src/server/qgsserver.cpp +++ b/src/server/qgsserver.cpp @@ -368,6 +368,8 @@ bool QgsServer::init() // Initialize config cache QgsConfigCache::initialize( sSettings ); + QObject::connect( QgsConfigCache::instance(), &QgsConfigCache::projectRemovedFromCache, sCapabilitiesCache, &QgsCapabilitiesCache::removeCapabilitiesDocument ); + sInitialized = true; QgsMessageLog::logMessage( QStringLiteral( "Server initialized" ), QStringLiteral( "Server" ), Qgis::MessageLevel::Info ); return true; diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index 681d3b7dd288..bd7503a19296 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -1187,6 +1187,21 @@ namespace QgsWfs fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); fcString += " \"bbox\": [ " + qgsDoubleToString( rect->xMinimum(), prec ) + ", " + qgsDoubleToString( rect->yMinimum(), prec ) + ", " + qgsDoubleToString( rect->xMaximum(), prec ) + ", " + qgsDoubleToString( rect->yMaximum(), prec ) + "],\n"; + + const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + const QgsCoordinateReferenceSystem destinationCrs { srsName.isEmpty( ) ? QStringLiteral( "EPSG:4326" ) : srsName }; + if ( ! destinationCrs.isValid() ) + { + throw QgsRequestNotWellFormedException( QStringLiteral( "srsName error: '%1' is not valid." ).arg( srsName ) ); + } + + json value; + QgsJsonUtils::addCrsInfo( value, destinationCrs ); + for ( const auto &it : value.items() ) + { + fcString += " \"" + QString::fromStdString( it.key() ) + "\": " + QString::fromStdString( it.value().dump() ) + ",\n"; + } + fcString += QLatin1String( " \"features\": [\n" ); response.write( fcString.toUtf8() ); } diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 5cb762617780..a1c774ba12bc 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -1316,7 +1316,7 @@ namespace QgsWms else if ( infoFormat == QgsWmsParameters::Format::HTML ) ba = convertFeatureInfoToHtml( result ); else if ( infoFormat == QgsWmsParameters::Format::JSON ) - ba = convertFeatureInfoToJson( layers, result ); + ba = convertFeatureInfoToJson( layers, result, mapSettings.destinationCrs() ); else ba = result.toByteArray(); @@ -2813,7 +2813,7 @@ namespace QgsWms return featureInfoString.toUtf8(); } - QByteArray QgsRenderer::convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc ) const + QByteArray QgsRenderer::convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc, const QgsCoordinateReferenceSystem &destCRS ) const { json json { @@ -2915,6 +2915,8 @@ namespace QgsWms exporter.setIncludeGeometry( withGeometry ); exporter.setTransformGeometries( false ); + QgsJsonUtils::addCrsInfo( json, destCRS ); + for ( const auto &feature : std::as_const( features ) ) { const QString id = QStringLiteral( "%1.%2" ).arg( layerName ).arg( fidMap.value( feature.id() ) ); diff --git a/src/server/services/wms/qgswmsrenderer.h b/src/server/services/wms/qgswmsrenderer.h index 9efbd00e7cfb..1ad582d8069a 100644 --- a/src/server/services/wms/qgswmsrenderer.h +++ b/src/server/services/wms/qgswmsrenderer.h @@ -323,7 +323,7 @@ namespace QgsWms QByteArray convertFeatureInfoToText( const QDomDocument &doc ) const; //! Converts a feature info xml document to json - QByteArray convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc ) const; + QByteArray convertFeatureInfoToJson( const QList &layers, const QDomDocument &doc, const QgsCoordinateReferenceSystem &destCRS ) const; QDomElement createFeatureGML( const QgsFeature *feat, diff --git a/src/ui/editorwidgets/qgsjsoneditwidget.ui b/src/ui/editorwidgets/qgsjsoneditwidget.ui index d591fec8239b..ac384ea88bac 100644 --- a/src/ui/editorwidgets/qgsjsoneditwidget.ui +++ b/src/ui/editorwidgets/qgsjsoneditwidget.ui @@ -26,72 +26,7 @@ 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Show text - - - ... - - - - :/images/themes/default/mIconFieldText.svg:/images/themes/default/mIconFieldText.svg - - - true - - - - - - - Show tree - - - ... - - - - :/images/themes/default/mIconTreeView.svg:/images/themes/default/mIconTreeView.svg - - - true - - - - - - - + 1 @@ -150,6 +85,84 @@ + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Show text + + + ... + + + + :/images/themes/default/mIconFieldText.svg:/images/themes/default/mIconFieldText.svg + + + true + + + true + + + + + + + Show tree + + + ... + + + + :/images/themes/default/mIconTreeView.svg:/images/themes/default/mIconTreeView.svg + + + true + + + true + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + @@ -159,6 +172,11 @@
qgscodeeditorjson.h
+ + mTreeWidget + mTextToolButton + mTreeToolButton + diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 2a3acbc211d8..679c46bf1b2c 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -13,93 +13,121 @@ DXF Export - - - - - - 0 - 0 - - - - Symbology mode - - - - - - - - 0 - 0 - - - - Save as - - - - - - - - - - - 0 - 0 - - - - Symbology scale - - - - - - + + + + + + + + 0 + 0 + + + + Save as + + + + + + + + + + + 0 + 0 + + + + Symbology mode + + + + + + + + + + + 0 + 0 + + + + Symbology scale + + + + + + + Qt::StrongFocus + + + true + + + + + + + + 0 + 0 + + + + Encoding + + + + + + + + + + + 0 + 0 + + + + CRS + + + + + + + Qt::StrongFocus + + + + + + + + 0 + 0 + + + + Map themes + + + + + + - - - - Qt::StrongFocus - - - true - - - - - - - Qt::StrongFocus - - - - - - - - - - - - - - 0 - 0 - - - - CRS - - - @@ -119,45 +147,19 @@ - Select Data DefinedBlocks + Select Data Defined Blocks - Deselect Data DefinedBlocks + Deselect Data Defined Blocks - - - - - 0 - 0 - - - - Encoding - - - - - - - - 0 - 0 - - - - Map themes - - - @@ -224,6 +226,8 @@ + + @@ -253,9 +257,12 @@ mVisibilityPresets mSelectAllButton mDeselectAllButton + mSelectDataDefinedBlocks + mDeselectDataDefinedBlocks mLayerTitleAsName mMTextCheckBox mMapExtentCheckBox + mSelectedFeaturesOnly mForce2d buttonBox diff --git a/src/ui/qgsrastersinglecolorrendererwidgetbase.ui b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui new file mode 100644 index 000000000000..b8b1276227d9 --- /dev/null +++ b/src/ui/qgsrastersinglecolorrendererwidgetbase.ui @@ -0,0 +1,101 @@ + + + QgsRasterSingleColorRendererWidgetBase + + + + 0 + 0 + 395 + 409 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Band + + + + + + + + + + Color + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 16777215 + 16777215 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsColorButton + QToolButton +
qgscolorbutton.h
+
+ + QgsRasterBandComboBox + QComboBox +
qgsrasterbandcombobox.h
+
+
+ + +
diff --git a/src/ui/qgstablewidgetuibase.ui b/src/ui/qgstablewidgetuibase.ui index 254a6e1b192b..53cff3e9bdd6 100644 --- a/src/ui/qgstablewidgetuibase.ui +++ b/src/ui/qgstablewidgetuibase.ui @@ -32,54 +32,6 @@ 0 - - - - - - Add entry - - - - - - - :/images/themes/default/mActionAdd.svg:/images/themes/default/mActionAdd.svg - - - - - - - false - - - Remove entry - - - - - - - :/images/themes/default/mActionRemove.svg:/images/themes/default/mActionRemove.svg - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - @@ -105,8 +57,75 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add entry + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + false + + + Remove entry + + + + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + +
+ + tableView + addButton + removeButton + diff --git a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui index fed537432da6..7e2855573185 100644 --- a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui @@ -157,6 +157,19 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti
+ + + + Qt::Vertical + + + + 40 + 20 + + + + @@ -207,7 +220,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + @@ -217,7 +230,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + @@ -233,7 +246,7 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - + 0 @@ -270,6 +283,140 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti + + + + Qt::Vertical + + + + 40 + 20 + + + + + + + + + + 0 + 0 + + + + + + + Scale + + + + + + + Offset + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + false + + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The pixel values in the layer represent a temporal value. </span></p><p> The offset is used to define the starting datetime while scaling identifies the scale – e.g. 1 day or 1 week - of each pixel value.</p></body></html> + + + true + + + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + + + + Band + + + + + + + Qt::Vertical + + + + 40 + 20 + + + + diff --git a/tests/README.md b/tests/README.md index cab7b5bcbe64..cbd64786c041 100644 --- a/tests/README.md +++ b/tests/README.md @@ -61,7 +61,7 @@ Some tests require a specific PostgreSQL server configuration to bring up such server would be to (tweak $srcdir appropriately): QGIS_WORKSPACE=${srcdir} \ - docker-compose -f .docker/docker-compose-testing-postgres.yml up -d postgres + docker compose -f .docker/docker-compose-testing-postgres.yml up -d postgres export PGHOST=`docker inspect docker_postgres_1 | jq -r .[0].NetworkSettings.Networks.docker_default.IPAddress` export PGUSER=docker export PGPASSWORD=docker diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 7f16e73f0c6c..5cbb5b6b8c01 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -152,6 +152,8 @@ class TestQgsGeometry : public QgsTest void isSimple_data(); void isSimple(); + void contains(); + void reshapeGeometryLineMerge(); void createCollectionOfType(); @@ -590,7 +592,7 @@ void TestQgsGeometry::geos() polyWithEmptyParts.addGeometry( new QgsPolygon( new QgsLineString() ) ); polyWithEmptyParts.addGeometry( new QgsPolygon( new QgsLineString( QVector< QgsPoint >() << QgsPoint( 10, 0 ) << QgsPoint( 10, 1 ) << QgsPoint( 11, 1 ) << QgsPoint( 10, 0 ) ) ) ); asGeos = QgsGeos::asGeos( &polyWithEmptyParts ); - QCOMPARE( GEOSGetNumGeometries_r( QgsGeos::getGEOSHandler(), asGeos.get() ), 2 ); + QCOMPARE( GEOSGetNumGeometries_r( QgsGeosContext::get(), asGeos.get() ), 2 ); res = QgsGeometry( QgsGeos::fromGeos( asGeos.get() ) ); QCOMPARE( res.asWkt(), QStringLiteral( "MultiPolygon (((0 0, 0 1, 1 1, 0 0)),((10 0, 10 1, 11 1, 10 0)))" ) ); @@ -1347,7 +1349,7 @@ void TestQgsGeometry::simplifyCheck1() initPainterTest(); QVERIFY( mpPolylineGeometryD->simplify( 0.5 ) ); // should be a single polygon as A intersect B - QgsGeometry *mypSimplifyGeometry = mpPolylineGeometryD->simplify( 0.5 ); + QgsGeometry *mypSimplifyGeometry = mpPolylineGeometryD->simplify( 0.5 ); qDebug( "Geometry Type: %s", Qgis::WkbType::displayString( mypSimplifyGeometry->wkbType() ) ); QVERIFY( mypSimplifyGeometry->wkbType() == Qgis::WkbType::LineString ); QgsPolyline myLine = mypSimplifyGeometry->asPolyline(); @@ -1369,7 +1371,7 @@ void TestQgsGeometry::intersectionCheck1() QVERIFY( engine->intersects( mpPolygonGeometryB.constGet() ) ); // should be a single polygon as A intersect B - QgsGeometry mypIntersectionGeometry = mpPolygonGeometryA.intersection( mpPolygonGeometryB ); + QgsGeometry mypIntersectionGeometry = mpPolygonGeometryA.intersection( mpPolygonGeometryB ); QVERIFY( mypIntersectionGeometry.wkbType() == Qgis::WkbType::Polygon ); QgsPolygonXY myPolygon = mypIntersectionGeometry.asPolygon(); QVERIFY( myPolygon.size() > 0 ); //check that the union created a feature @@ -1460,7 +1462,7 @@ void TestQgsGeometry::unionCheck1() { initPainterTest(); // should be a multipolygon with 2 parts as A does not intersect C - QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryC ); + QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryC ); QVERIFY( mypUnionGeometry.wkbType() == Qgis::WkbType::MultiPolygon ); QgsMultiPolygonXY myMultiPolygon = mypUnionGeometry.asMultiPolygon(); QVERIFY( myMultiPolygon.size() > 0 ); //check that the union did not fail @@ -1472,7 +1474,7 @@ void TestQgsGeometry::unionCheck2() { initPainterTest(); // should be a single polygon as A intersect B - QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryB ); + QgsGeometry mypUnionGeometry = mpPolygonGeometryA.combine( mpPolygonGeometryB ); QVERIFY( mypUnionGeometry.wkbType() == Qgis::WkbType::Polygon ); QgsPolygonXY myPolygon = mypUnionGeometry.asPolygon(); QVERIFY( myPolygon.size() > 0 ); //check that the union created a feature @@ -2076,6 +2078,21 @@ void TestQgsGeometry::isSimple() QCOMPARE( res, simple ); } +void TestQgsGeometry::contains() +{ + QgsGeometry geomTest = QgsGeometry::fromWkt( QStringLiteral( "Polygon((0 0, 5 5, -2.1 12.1, -7.1 7.1))" ) ); + + QgsPointXY pointInside( 1, 2 ); + QVERIFY( geomTest.contains( &pointInside ) ); + QVERIFY( geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(1 2)" ) ) ) ); + QVERIFY( geomTest.contains( pointInside.x(), pointInside.y() ) ); + + QgsPointXY pointOutside( 3, 1 ); + QVERIFY( !geomTest.contains( &pointOutside ) ); + QVERIFY( !geomTest.contains( QgsGeometry::fromWkt( QStringLiteral( "Point(3 1)" ) ) ) ); + QVERIFY( !geomTest.contains( pointOutside.x(), pointOutside.y() ) ); +} + void TestQgsGeometry::reshapeGeometryLineMerge() { int res; diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index e1d438b2253b..adfe17feb894 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -169,6 +169,7 @@ void TestQgsDxfExport::init() const QString blueMarkerSvgPath = QgsSymbolLayerUtils::svgSymbolNameToPath( QStringLiteral( "/symbol/blue-marker.svg" ), QgsPathResolver() ); QString expressionString = QString( "CASE WHEN \"CLASS\" = 'B52' THEN '%1' WHEN \"CLASS\" = 'Biplane' THEN '%2' WHEN \"CLASS\" = 'Jet' THEN '%3' END" ).arg( planeSvgPath ).arg( planeOrangeSvgPath ).arg( blueMarkerSvgPath ); ddProperties.setProperty( QgsSymbolLayer::Property::Name, QgsProperty::fromExpression( expressionString ) ); + ddProperties.setProperty( QgsSymbolLayer::Property::Angle, QgsProperty::fromExpression( "Heading" ) ); svgSymbolLayer->setDataDefinedProperties( ddProperties ); QgsSymbolLayerList ddSymbolLayerList; ddSymbolLayerList << svgSymbolLayer; @@ -276,7 +277,10 @@ void TestQgsDxfExport::testPointsDataDefinedSizeSymbol() dxfBuffer.close(); QString dxfString = QString::fromLatin1( dxfByteArray ); + //test if data defined blocks have been created QVERIFY( dxfString.contains( QStringLiteral( "symbolLayer0class" ) ) ); + //test a rotation for a referenced block + QVERIFY( dxfString.contains( QStringLiteral( "50\n5.0" ) ) ); } void TestQgsDxfExport::testLines() diff --git a/tests/src/core/testqgslayerdefinition.cpp b/tests/src/core/testqgslayerdefinition.cpp index 6bf8f69032fe..d1b35e0bb013 100644 --- a/tests/src/core/testqgslayerdefinition.cpp +++ b/tests/src/core/testqgslayerdefinition.cpp @@ -41,6 +41,11 @@ class TestQgsLayerDefinition: public QObject */ void testFindLayers(); + /** + * Tests loading a qlr placing the content at the top of the layer tree + */ + void testLoadTopOfTree(); + /** * test that export does not crash: regression #18981 * https://github.com/qgis/QGIS/issues/26812 - Save QLR crashes QGIS 3 @@ -90,6 +95,17 @@ void TestQgsLayerDefinition::testFindLayers() QCOMPARE( QgsProject::instance()->layerTreeRoot()->findLayers().at( 1 )->name(), QStringLiteral( "NewMemory" ) ); } +void TestQgsLayerDefinition::testLoadTopOfTree() +{ + QString errorMsg; + QgsLayerDefinition::loadLayerDefinition( TEST_DATA_DIR + QStringLiteral( "/vector_and_raster.qlr" ), QgsProject::instance(), QgsProject::instance()->layerTreeRoot(), errorMsg, Qgis::LayerTreeInsertionMethod::TopOfTree ); + //test if new layers are on top + QList orderedLayers = QgsProject::instance()->layerTreeRoot()->layerOrder(); + QCOMPARE( orderedLayers.length(), 3 ); + QVERIFY( orderedLayers.at( 1 )->name() == QStringLiteral( "rgb256x256" ) ); + QVERIFY( orderedLayers.at( 0 )->name() == QStringLiteral( "memoryLayer" ) ); +} + void TestQgsLayerDefinition::testExportDoesNotCrash() { QString errorMessage; diff --git a/tests/src/core/testqgstemporalnavigationobject.cpp b/tests/src/core/testqgstemporalnavigationobject.cpp index 3588666fdd9e..b390e54a1601 100644 --- a/tests/src/core/testqgstemporalnavigationobject.cpp +++ b/tests/src/core/testqgstemporalnavigationobject.cpp @@ -272,13 +272,14 @@ void TestQgsTemporalNavigationObject::frameSettings() // Test if, when changing to Cumulative mode, the dateTimeRange for frame 2 (with 2 hours frames) is indeed the full range navigationObject->setTemporalRangeCumulative( true ); + QCOMPARE( temporalRangeSignal.count(), 8 ); QCOMPARE( navigationObject->dateTimeRangeForFrameNumber( 1 ), QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ), QTime( 8, 0, 0 ) ), QDateTime( QDate( 2020, 1, 1 ), QTime( 12, 0, 0 ) ), true, false ) ); - QCOMPARE( temporalRangeSignal.count(), 7 ); + QCOMPARE( temporalRangeSignal.count(), 8 ); navigationObject->setTemporalRangeCumulative( false ); // interval which doesn't fit exactly into overall range diff --git a/tests/src/gui/testqgslistwidget.cpp b/tests/src/gui/testqgslistwidget.cpp index b10192e54012..dcdd32e33ce4 100644 --- a/tests/src/gui/testqgslistwidget.cpp +++ b/tests/src/gui/testqgslistwidget.cpp @@ -73,7 +73,7 @@ class TestQgsListWidget : public QObject QVERIFY( wrapper ); const QSignalSpy spy( wrapper, SIGNAL( valueChanged( const QVariant & ) ) ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( wrapper->widget() ); + QgsListWidget *widget = wrapper->widget()->findChild(); QVERIFY( widget ); QStringList initial; @@ -108,7 +108,7 @@ class TestQgsListWidget : public QObject QVERIFY( wrapper ); QSignalSpy spy( wrapper, SIGNAL( valueChanged( const QVariant & ) ) ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( wrapper->widget() ); + QgsListWidget *widget = wrapper->widget()->findChild(); QVERIFY( widget ); QVariantList initial; @@ -160,7 +160,7 @@ class TestQgsListWidget : public QObject QVERIFY( vl_array_int->isValid( ) ); QgsListWidgetWrapper w_array_int( vl_array_int, vl_array_int->fields().indexOf( QLatin1String( "location" ) ), nullptr, nullptr ); - QgsListWidget *widget = qobject_cast< QgsListWidget * >( w_array_int.widget( ) ); + QgsListWidget *widget = w_array_int.widget( )->findChild(); vl_array_int->startEditing( ); QVariantList newList; @@ -204,7 +204,7 @@ class TestQgsListWidget : public QObject QVERIFY( vl_array_str->isValid() ); QgsListWidgetWrapper w_array_str( vl_array_str, vl_array_str->fields().indexOf( QLatin1String( "value" ) ), nullptr, nullptr ); - widget = qobject_cast< QgsListWidget * >( w_array_str.widget( ) ); + widget = w_array_str.widget( )->findChild(); vl_array_str->startEditing( ); QVariantList newListStr; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index b0e4fcbcd97a..7b1402a7b326 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -263,6 +263,7 @@ ADD_PYTHON_TEST(PyQgsRasterLayerElevationProperties test_qgsrasterlayerelevation ADD_PYTHON_TEST(PyQgsRasterLayerProfileGenerator test_qgsrasterlayerprofilegenerator.py) ADD_PYTHON_TEST(PyQgsRasterLayerRenderer test_qgsrasterlayerrenderer.py) ADD_PYTHON_TEST(PyQgsRasterLayerTemporalProperties test_qgsrasterlayertemporalproperties.py) +ADD_PYTHON_TEST(PyQgsRasterLayerUtils test_qgsrasterlayerutils.py) ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py) ADD_PYTHON_TEST(PyQgsRasterLineSymbolLayer test_qgsrasterlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsRasterPipe test_qgsrasterpipe.py) @@ -290,6 +291,7 @@ ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarend ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) +ADD_PYTHON_TEST(PyQgsRasterSingleColorRenderer test_qgsrastersinglecolorrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) ADD_PYTHON_TEST(PyQgsSvgCache test_qgssvgcache.py) diff --git a/tests/src/python/test_authmanager_password_postgres.py b/tests/src/python/test_authmanager_password_postgres.py index dcfc0f590ec5..ab27daae3104 100644 --- a/tests/src/python/test_authmanager_password_postgres.py +++ b/tests/src/python/test_authmanager_password_postgres.py @@ -9,7 +9,7 @@ It uses a docker container as postgres/postgis server with certificates from tests/testdata/auth_system/certs_keys_2048 -Use docker-compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server +Use docker compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server TODO: - Document how to restore the server data diff --git a/tests/src/python/test_authmanager_pki_postgres.py b/tests/src/python/test_authmanager_pki_postgres.py index 457958b4c4a8..3ab2b6a47b01 100644 --- a/tests/src/python/test_authmanager_pki_postgres.py +++ b/tests/src/python/test_authmanager_pki_postgres.py @@ -9,7 +9,7 @@ It uses a docker container as postgres/postgis server with certificates from tests/testdata/auth_system/certs_keys_2048 -Use docker-compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server. +Use docker compose -f .docker/docker-compose-testing-postgres.yml up postgres to start the server. TODO: - Document how to restore the server data diff --git a/tests/src/python/test_provider_ogr_gpkg.py b/tests/src/python/test_provider_ogr_gpkg.py index 94b21bb54acb..570bc45efee4 100644 --- a/tests/src/python/test_provider_ogr_gpkg.py +++ b/tests/src/python/test_provider_ogr_gpkg.py @@ -2860,6 +2860,9 @@ def testChangeAttributeValuesErrors(self): lyr.CreateField(ogr.FieldDefn('datetime_field', ogr.OFTDateTime)) lyr.CreateField(ogr.FieldDefn('date_field', ogr.OFTDateTime)) lyr.CreateField(ogr.FieldDefn('string_field', ogr.OFTString)) + fld_defn = ogr.FieldDefn('bool_field', ogr.OFTInteger) + fld_defn.SetSubType(ogr.OFSTBoolean) + lyr.CreateField(fld_defn) f = ogr.Feature(lyr.GetLayerDefn()) lyr.CreateFeature(f) ds = None @@ -2886,6 +2889,9 @@ def testChangeAttributeValuesErrors(self): self.assertFalse(vl.dataProvider().changeAttributeValues({1: {4: "not a datetime"}})) self.assertFalse(vl.dataProvider().changeAttributeValues({1: {5: "not a date"}})) + # wrong value for attribute 7 of feature 1: wrong + self.assertFalse(vl.dataProvider().changeAttributeValues({1: {7: "wrong"}})) + # OK # int_field self.assertTrue(vl.dataProvider().changeAttributeValues({1: {1: 1}})) @@ -2903,6 +2909,89 @@ def testChangeAttributeValuesErrors(self): # string_field self.assertTrue(vl.dataProvider().changeAttributeValues({1: {6: "foo"}})) self.assertTrue(vl.dataProvider().changeAttributeValues({1: {6: 12345}})) + # bool field + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: True}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: False}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: 1}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: 0}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "true"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "false"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "1"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], True) + self.assertTrue(vl.dataProvider().changeAttributeValues({1: {7: "0"}})) + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()][0], False) + + def testAttributeBoolean(self): + + tmpfile = os.path.join(self.basetestpath, 'testAttributeBoolean.gpkg') + ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile) + lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint) + fld_defn = ogr.FieldDefn('bool_field', ogr.OFTInteger) + fld_defn.SetSubType(ogr.OFSTBoolean) + lyr.CreateField(fld_defn) + ds = None + + vl = QgsVectorLayer(f'{tmpfile}' + "|layername=" + "test", 'test', 'ogr') + + f = QgsFeature(vl.fields()) + f.setAttribute(1, False) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, True) + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, 0) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, 1) + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + # Test compatibility with use case of https://github.com/qgis/QGIS/issues/55517 + f = QgsFeature(vl.fields()) + f.setAttribute(1, "false") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "true") + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "0") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "1") + vl.dataProvider().addFeatures([f]) + self.assertTrue(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, "invalid") + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertFalse(ret) + + f = QgsFeature(vl.fields()) + f.setAttribute(1, [1]) + ret, _ = vl.dataProvider().addFeatures([f]) + self.assertFalse(ret) + + self.assertEqual([feat["bool_field"] for feat in vl.getFeatures()], + [False, True, False, True, False, True, False, True]) def testExtent(self): # 2D points diff --git a/tests/src/python/test_provider_sensorthings.py b/tests/src/python/test_provider_sensorthings.py index a3fe8e306dd0..b5c03fe038cd 100644 --- a/tests/src/python/test_provider_sensorthings.py +++ b/tests/src/python/test_provider_sensorthings.py @@ -31,7 +31,8 @@ def sanitize(endpoint, x): for prefix in ('/Locations', '/HistoricalLocations', '/Things', - '/FeaturesOfInterest'): + '/FeaturesOfInterest', + '/MultiDatastreams'): if x.startswith(prefix): x = x[len(prefix):] endpoint = endpoint + "_" + prefix[1:] @@ -99,6 +100,10 @@ def test_filter_for_wkb_type(self): QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.LineString), "location/type eq 'LineString' or location/geometry/type eq 'LineString'" ) + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.MultiDatastream, Qgis.WkbType.Polygon), + "observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'" + ) def test_utils_string_to_entity(self): self.assertEqual( @@ -136,6 +141,10 @@ def test_utils_string_to_entity(self): QgsSensorThingsUtils.stringToEntity(" FeatureOfInterest "), Qgis.SensorThingsEntity.FeatureOfInterest, ) + self.assertEqual( + QgsSensorThingsUtils.stringToEntity(" MultiDataStream "), + Qgis.SensorThingsEntity.MultiDatastream, + ) def test_utils_string_to_entityset(self): self.assertEqual( @@ -174,6 +183,10 @@ def test_utils_string_to_entityset(self): QgsSensorThingsUtils.entitySetStringToEntity(" FeaturesOfInterest "), Qgis.SensorThingsEntity.FeatureOfInterest, ) + self.assertEqual( + QgsSensorThingsUtils.entitySetStringToEntity(" MultidataStreams "), + Qgis.SensorThingsEntity.MultiDatastream, + ) def test_filter_for_extent(self): """ @@ -2973,6 +2986,598 @@ def test_feature_of_interest(self): ['Point (16.4 48.2)', 'Point (16.5 48.2)', 'Point (16.5 48.2)'], ) + def test_multidatastream_no_geometry(self): + """ + Test a layer retrieving 'MultiDatastream' entities from a service without geometry + """ + with tempfile.TemporaryDirectory() as temp_dir: + base_path = temp_dir.replace("\\", "/") + endpoint = base_path + "/fake_qgis_http_endpoint" + with open(sanitize(endpoint, ""), "wt", encoding="utf8") as f: + f.write( + """ +{ + "value": [ + { + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=0&$count=true"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(1)", + "@iot.id": 1, + "name": "MultiDatastream 1", + "description": "Desc 1", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + } + ], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-12T04:00:00Z", + "resultTime": "2017-12-31T23:30:00Z/2017-12-31T23:31:00Z", + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/MultiDatastreams(2)", + "@iot.id": 2, + "name": "MultiDatastream 2", + "description": "Desc 2", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2018-12-31T23:00:00Z/2019-01-12T04:00:00Z", + "resultTime": "2018-12-31T23:30:00Z/2018-12-31T23:31:00Z", + "properties": { + "owner": "owner 2" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(3)", + "@iot.id": 3, + "name": "MultiDatastream 3", + "description": "Desc 3", + "unitOfMeasurements": [{ + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2020-12-31T23:00:00Z/2021-01-12T04:00:00Z", + "resultTime": "2020-12-31T23:30:00Z/2020-12-31T23:31:00Z", + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' pageSize=2 entity='MultiDatastream'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.NoGeometry) + self.assertEqual(vl.featureCount(), 3) + self.assertFalse(vl.crs().isValid()) + self.assertIn("Entity TypeMultiDatastream", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/MultiDatastreams"', + vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "unitOfMeasurements", + "observationType", + "multiObservationDataTypes", + "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "resultTimeStart", + "resultTimeEnd", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, + QVariant.String, + QVariant.StringList, + QVariant.Map, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + ], + ) + + # test retrieving all features from layer + features = list(vl.getFeatures()) + self.assertEqual([f.id() for f in features], [0, 1, 2]) + self.assertEqual([f["id"] for f in features], ["1", "2", "3"]) + self.assertEqual( + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["MultiDatastream 1", "MultiDatastream 2", "MultiDatastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["unitOfMeasurements"] for f in features], + [ + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + ], + ) + self.assertEqual( + [f["observationType"] for f in features], + [ + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + ], + ) + self.assertEqual( + [f["multiObservationDataTypes"] for f in features], + [ + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ], + ) + self.assertEqual( + [f["phenomenonTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["phenomenonTimeEnd"] for f in features], + [ + QDateTime(QDate(2018, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2019, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2021, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeEnd"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], + ) + + def test_multidatastream_polygons(self): + """ + Test a layer retrieving 'MultiDatastream' entities from a service using polygons + """ + with tempfile.TemporaryDirectory() as temp_dir: + base_path = temp_dir.replace("\\", "/") + endpoint = base_path + "/fake_qgis_http_endpoint" + with open(sanitize(endpoint, ""), "wt", encoding="utf8") as f: + f.write( + """ +{ + "value": [ + { + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=0&$count=true&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(1)", + "@iot.id": 1, + "name": "MultiDatastream 1", + "description": "Desc 1", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + } + ], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-12T04:00:00Z", + "resultTime": "2017-12-31T23:30:00Z/2017-12-31T23:31:00Z", + "properties": { + "owner": "owner 1" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [100, 0], [101, 0], [101, 1], [100, 1], [100, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/MultiDatastreams(2)", + "@iot.id": 2, + "name": "MultiDatastream 2", + "description": "Desc 2", + "unitOfMeasurements": [ + { + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2018-12-31T23:00:00Z/2019-01-12T04:00:00Z", + "resultTime": "2018-12-31T23:30:00Z/2018-12-31T23:31:00Z", + "properties": { + "owner": "owner 2" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [102, 0], [103, 0], [103, 1], [102, 1], [102, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/MultiDatastreams(3)", + "@iot.id": 3, + "name": "MultiDatastream 3", + "description": "Desc 3", + "unitOfMeasurements": [{ + "name": "ug.m-3", + "symbol": "ug.m-3", + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3" + }], + "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "multiObservationDataTypes": ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + "phenomenonTime": "2020-12-31T23:00:00Z/2021-01-12T04:00:00Z", + "resultTime": "2020-12-31T23:30:00Z/2020-12-31T23:31:00Z", + "properties": { + "owner": "owner 3" + }, + "observedArea": { + "type": "Polygon", + "coordinates": [ + [ + [103, 0], [104, 0], [104, 1], [103, 1], [103, 0] + ] + ] + }, + "Things@iot.navigationLink": "endpoint/MultiDatastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' pageSize=2 type=MultiPolygonZ entity='MultiDatastream'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.MultiPolygonZ) + self.assertEqual(vl.featureCount(), 3) + self.assertEqual(vl.crs().authid(), 'EPSG:4326') + self.assertIn("Entity TypeMultiDatastream", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/MultiDatastreams"', + vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "unitOfMeasurements", + "observationType", + "multiObservationDataTypes", + "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "resultTimeStart", + "resultTimeEnd", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, + QVariant.String, + QVariant.StringList, + QVariant.Map, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + QVariant.DateTime, + ], + ) + + # test retrieving all features from layer + features = list(vl.getFeatures()) + self.assertEqual([f.id() for f in features], [0, 1, 2]) + self.assertEqual([f["id"] for f in features], ["1", "2", "3"]) + self.assertEqual( + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["MultiDatastream 1", "MultiDatastream 2", "MultiDatastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["unitOfMeasurements"] for f in features], + [ + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + [{ + "definition": "http://dd.eionet.europa.eu/vocabulary/uom/concentration/ug.m-3", + "name": "ug.m-3", + "symbol": "ug.m-3", + }], + ], + ) + self.assertEqual( + [f["observationType"] for f in features], + [ + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + ], + ) + self.assertEqual( + [f["multiObservationDataTypes"] for f in features], + [ + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ["http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement"], + ], + ) + self.assertEqual( + [f["phenomenonTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["phenomenonTimeEnd"] for f in features], + [ + QDateTime(QDate(2018, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2019, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2021, 1, 12), QTime(4, 0, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 30, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultTimeEnd"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2018, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(23, 31, 0, 0), + Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], + ) + self.assertEqual( + [f.geometry().asWkt() for f in features], + ['Polygon ((100 0, 101 0, 101 1, 100 1, 100 0))', + 'Polygon ((102 0, 103 0, 103 1, 102 1, 102 0))', + 'Polygon ((103 0, 104 0, 104 1, 103 1, 103 0))'], + ) + def testDecodeUri(self): """ Test decoding a SensorThings uri diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 975dc6810df6..c8b7ec3df4d5 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -1113,8 +1113,15 @@ def testContains(self): QgsPointXY(2, 2), QgsPointXY(0, 2), QgsPointXY(0, 0)]]) - myPoint = QgsGeometry.fromPointXY(QgsPointXY(1, 1)) - self.assertTrue(QgsGeometry.contains(myPoly, myPoint)) + pointInside = QgsPointXY(1, 1) + self.assertTrue(myPoly.contains(pointInside)) + self.assertTrue(myPoly.contains(QgsGeometry.fromPointXY(pointInside))) + self.assertTrue(myPoly.contains(pointInside.x(), pointInside.y())) + + pointOutside = QgsPointXY(3, 3) + self.assertFalse(myPoly.contains(pointOutside)) + self.assertFalse(myPoly.contains(QgsGeometry.fromPointXY(pointOutside))) + self.assertFalse(myPoly.contains(pointOutside.x(), pointOutside.y())) def testTouches(self): myLine = QgsGeometry.fromPolylineXY([ diff --git a/tests/src/python/test_qgsrasterlayerrenderer.py b/tests/src/python/test_qgsrasterlayerrenderer.py index 727a55affca5..222e7c6303a2 100644 --- a/tests/src/python/test_qgsrasterlayerrenderer.py +++ b/tests/src/python/test_qgsrasterlayerrenderer.py @@ -21,6 +21,7 @@ Qgis, QgsCoordinateReferenceSystem, QgsGeometry, + QgsInterval, QgsMapClippingRegion, QgsMapSettings, QgsRasterLayer, @@ -517,6 +518,57 @@ def test_render_fixed_range_per_band_with_temporal_range_filter(self): map_settings) ) + def test_render_represents_temporal_values(self): + """ + Test rendering a raster with its temporal properties' mode set + to represents temporal values + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'scaleoffset.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 1) + raster_layer.setRenderer(renderer) + + # set layer as temporal enabled + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.RepresentsTemporalValues + ) + raster_layer.temporalProperties().setBandNumber(1) + raster_layer.temporalProperties().setTemporalRepresentationOffset(QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + raster_layer.temporalProperties().setTemporalRepresentationScale(QgsInterval(1, Qgis.TemporalUnit.Days)) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(raster_layer.crs()) + map_settings.setExtent(raster_layer.extent()) + map_settings.setLayers([raster_layer]) + + # no filter on map settings + map_settings.setIsTemporal(False) + self.assertTrue( + self.render_map_settings_check( + 'No temporal range filter on map settings on represents temporal values mode', + 'represents_temporal_values_no_filter', + map_settings) + ) + + # map settings matches part of the overall range + map_settings.setIsTemporal(True) + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2024, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2024, 1, 5), + QTime(23, 59, 59)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings on represents temporal values mode', + 'represents_temporal_values_filter', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterlayertemporalproperties.py b/tests/src/python/test_qgsrasterlayertemporalproperties.py index 90cc2dfaa96c..ac5588cd903c 100644 --- a/tests/src/python/test_qgsrasterlayertemporalproperties.py +++ b/tests/src/python/test_qgsrasterlayertemporalproperties.py @@ -16,6 +16,7 @@ from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgis, + QgsInterval, QgsRasterLayerTemporalProperties, QgsReadWriteContext, QgsDateTimeRange @@ -273,6 +274,54 @@ def test_basic_fixed_range_per_band(self): includeBeginning=False, includeEnd=True)}) + def test_basic_represents_temporal_value(self): + """ + Basic tests for the class using the RepresentsTemporalValue mode + """ + props = QgsRasterLayerTemporalProperties(None) + props.setMode(Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props.mode(), + Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props.bandNumber(), 1) + self.assertEqual(props.temporalRepresentationScale(), QgsInterval(1, Qgis.TemporalUnit.Days)) + self.assertEqual(props.temporalRepresentationOffset(), QDateTime()) + self.assertFalse(props.isActive()) + + props.setBandNumber(2) + props.setTemporalRepresentationScale(QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) + props.setTemporalRepresentationOffset(QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + props.setIsActive(True) + self.assertEqual(props.bandNumber(), 2) + self.assertEqual(props.temporalRepresentationScale(), QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) + self.assertEqual(props.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + self.assertTrue(props.isActive()) + + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 4), + QTime(12, 13, 14)) + )), 2) + self.assertEqual(props.filteredBandsForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 4), + QTime(12, 13, 14)) + )), [2]) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerTemporalProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterTemporalMode.RepresentsTemporalValues) + self.assertEqual(props.bandNumber(), 2) + self.assertEqual(props2.temporalRepresentationScale(), QgsInterval(2.5, Qgis.TemporalUnit.Weeks)) + self.assertEqual(props2.temporalRepresentationOffset(), QDateTime(QDate(2024, 1, 1), QTime(0, 0, 0))) + self.assertTrue(props2.isActive()) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterlayerutils.py b/tests/src/python/test_qgsrasterlayerutils.py new file mode 100644 index 000000000000..4b4b47fc061c --- /dev/null +++ b/tests/src/python/test_qgsrasterlayerutils.py @@ -0,0 +1,397 @@ +"""QGIS Unit tests for QgsRasterLayerUtils + +.. 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. +""" + +import os + +from qgis.PyQt.QtCore import ( + QDate, + QTime, + QDateTime +) +from qgis.core import ( + Qgis, + QgsRasterLayerUtils, + QgsRasterLayer, + QgsDoubleRange, + QgsDateTimeRange +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsRasterLayerUtils(QgisTestCase): + + def test_rendered_band_for_elevation_and_temporal_ranges(self): + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + # no temporal or elevation properties + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # only elevation properties enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedRangePerBand + ) + raster_layer.elevationProperties().setFixedRangePerBand( + {1: QgsDoubleRange(1, 5), + 2: QgsDoubleRange(4, 10), + 3: QgsDoubleRange(11, 15), + 4: QgsDoubleRange(1, 5), + 5: QgsDoubleRange(4, 10), + 6: QgsDoubleRange(11, 16), + 7: QgsDoubleRange(1, 5), + 8: QgsDoubleRange(4, 10), + 9: QgsDoubleRange(11, 15), + } + ) + # no matching elevation + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + # matching elevation + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(1, 3) + ) + self.assertEqual(band, 7) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + + # specify infinite elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange() + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # only temporal properties enabled + raster_layer.elevationProperties().setEnabled(False) + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.FixedRangePerBand + ) + raster_layer.temporalProperties().setFixedRangePerBand( + { + 1: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 2: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 3: QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 12, 31), + QTime(23, 59, 59)) + ), + 4: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 5: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 6: QgsDateTimeRange( + QDateTime(QDate(2021, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 12, 31), + QTime(23, 59, 59)) + ), + 7: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ), + 8: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ), + 9: QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 12, 31), + QTime(23, 59, 59)) + ) + } + ) + + # no matching time range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ), + QgsDoubleRange(30, 50) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + # matching time range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59)) + ), + QgsDoubleRange(1, 3) + ) + self.assertEqual(band, 3) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59)) + ), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + # specify infinite temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange(QDateTime(), QDateTime()), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, -1) + self.assertTrue(matched) + + # with both elevation and temporal handling enabled + raster_layer.elevationProperties().setEnabled(True) + + # specify infinite temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange(QDateTime(), QDateTime()), + QgsDoubleRange(5, 8) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + + # specify infinite elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange() + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 1) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 2) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2020, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2020, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 3) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 4) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 5) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2021, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2021, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 6) + self.assertTrue(matched) + + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(2, 3) + ) + self.assertEqual(band, 7) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(3, 7) + ) + self.assertEqual(band, 8) + self.assertTrue(matched) + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, 9) + self.assertTrue(matched) + + # outside temporal range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2023, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(11, + 13) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + + # outside elevation range + band, matched = QgsRasterLayerUtils.renderedBandForElevationAndTemporalRange( + raster_layer, + QgsDateTimeRange( + QDateTime(QDate(2022, 6, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2022, 6, 30), + QTime(23, 59, 59))), + QgsDoubleRange(111, + 113) + ) + self.assertEqual(band, -1) + self.assertFalse(matched) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrastersinglecolorrenderer.py b/tests/src/python/test_qgsrastersinglecolorrenderer.py new file mode 100644 index 000000000000..6ef5a3a420d1 --- /dev/null +++ b/tests/src/python/test_qgsrastersinglecolorrenderer.py @@ -0,0 +1,62 @@ +"""QGIS Unit tests for QgsRasterSingleColorRenderer. + +.. 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. +""" + +import os + +from qgis.PyQt.QtCore import QFileInfo +from qgis.PyQt.QtGui import QColor +from qgis.core import ( + QgsRasterLayer, + QgsRasterSingleColorRenderer, + QgsMapSettings, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() + + +class TestQgsRasterSingleBandGrayRenderer(QgisTestCase): + + def testRenderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat-int16-b1.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsRasterSingleColorRenderer(layer.dataProvider(), + 1, + QColor(255, 0, 0)) + + self.assertEqual(renderer.inputBand(), 1) + self.assertEqual(renderer.usesBands(), [1]) + self.assertEqual(renderer.color(), QColor(255, 0, 0)) + renderer.setColor(QColor(0, 255, 0)) + self.assertEqual(renderer.color(), QColor(0, 255, 0)) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'raster_single_color_renderer', + 'raster_single_color_renderer', + ms) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsserver_wfs.py b/tests/src/python/test_qgsserver_wfs.py index f97be0916b7d..ec5ffcfcb948 100644 --- a/tests/src/python/test_qgsserver_wfs.py +++ b/tests/src/python/test_qgsserver_wfs.py @@ -787,6 +787,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual(jdata['features'][0]['geometry']['coordinates'], [807305, 5592878]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:3857") query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -803,6 +804,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [7, 44]) + self.assertFalse('crs' in jdata) query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -834,6 +836,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [361806, 4964192]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:32632") query_string = "?" + "&".join(["%s=%s" % i for i in list({ "SERVICE": "WFS", @@ -851,6 +854,7 @@ def test_getFeatureFeatureJsonCrs(self): jdata['features'][0]['geometry'] jdata['features'][0]['geometry']['coordinates'] self.assertEqual([int(i) for i in jdata['features'][0]['geometry']['coordinates']], [812191, 5589555]) + self.assertEqual(jdata['crs']['properties']['name'], "urn:ogc:def:crs:EPSG:0:3857") def test_insert_srsName(self): """Test srsName is respected when insering""" diff --git a/tests/src/python/test_qgsserver_wms_getfeatureinfo.py b/tests/src/python/test_qgsserver_wms_getfeatureinfo.py index 2416bec2f3be..0d634f74b2b8 100644 --- a/tests/src/python/test_qgsserver_wms_getfeatureinfo.py +++ b/tests/src/python/test_qgsserver_wms_getfeatureinfo.py @@ -615,6 +615,30 @@ def testGetFeatureInfoJSON(self): 'wms_getfeatureinfo_raster_json', normalizeJson=True) + # simple test with geometry with underlying layer in 4326 and CRS is EPSG:4326 + self.wms_request_compare('GetFeatureInfo', + '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + + 'info_format=application%2Fjson&transparent=true&' + + 'width=600&height=400&srs=EPSG:4326&' + + 'bbox=44.9014173,8.2034387,44.9015094,8.2036094&' + + 'query_layers=testlayer2&X=203&Y=116&' + + 'with_geometry=true', + 'wms_getfeatureinfo_geometry_CRS84_json', + 'test_project.qgs', + normalizeJson=True) + + # simple test with geometry with underlying layer in 4326 and CRS is CRS84 + self.wms_request_compare('GetFeatureInfo', + '&layers=testlayer%20%C3%A8%C3%A9&styles=&' + + 'info_format=application%2Fjson&transparent=true&' + + 'width=600&height=400&srs=OGC:CRS84&' + + 'bbox=8.2034387,44.9014173,8.2036094,44.9015094&' + + 'query_layers=testlayer2&X=203&Y=116&' + + 'with_geometry=true', + 'wms_getfeatureinfo_geometry_CRS84_json', + 'test_project.qgs', + normalizeJson=True) + def testGetFeatureInfoGroupedLayers(self): """Test that we can get feature info from the top and group layers""" diff --git a/tests/src/python/test_qgsunittypes.py b/tests/src/python/test_qgsunittypes.py index efc06d2f7777..37659ca8e98b 100644 --- a/tests/src/python/test_qgsunittypes.py +++ b/tests/src/python/test_qgsunittypes.py @@ -958,11 +958,11 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalSeconds: { QgsUnitTypes.TemporalUnit.TemporalMilliseconds: 1000.0, QgsUnitTypes.TemporalUnit.TemporalSeconds: 1, - QgsUnitTypes.TemporalUnit.TemporalMinutes: 0.016666675200000136831, - QgsUnitTypes.TemporalUnit.TemporalHours: 0.00027777792000000228051, + QgsUnitTypes.TemporalUnit.TemporalMinutes: 0.016666666666666666, + QgsUnitTypes.TemporalUnit.TemporalHours: 0.0002777777777777778, QgsUnitTypes.TemporalUnit.TemporalDays: 1.157408000000009502e-5, QgsUnitTypes.TemporalUnit.TemporalWeeks: 1.653440000000013514e-6, - QgsUnitTypes.TemporalUnit.TemporalMonths: 3.805172816248999917e-7, + QgsUnitTypes.TemporalUnit.TemporalMonths: 3.8580246913580245e-7, QgsUnitTypes.TemporalUnit.TemporalYears: 3.170980821917834046e-8, QgsUnitTypes.TemporalUnit.TemporalDecades: 3.170980821917834046e-9, QgsUnitTypes.TemporalUnit.TemporalCenturies: 3.170980821917834149e-10, @@ -975,10 +975,10 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalMinutes: 1, QgsUnitTypes.TemporalUnit.TemporalHours: 0.016666666666666666, QgsUnitTypes.TemporalUnit.TemporalDays: 0.0006944444444444445, - QgsUnitTypes.TemporalUnit.TemporalWeeks: 9.921893245713293505e-5, + QgsUnitTypes.TemporalUnit.TemporalWeeks: 9.92063492063492e-5, QgsUnitTypes.TemporalUnit.TemporalMonths: 2.3148148148148147e-05, - QgsUnitTypes.TemporalUnit.TemporalYears: 1.902828841643645226e-6, - QgsUnitTypes.TemporalUnit.TemporalDecades: 1.902828841643645332e-7, + QgsUnitTypes.TemporalUnit.TemporalYears: 1.901285268841737e-6, + QgsUnitTypes.TemporalUnit.TemporalDecades: 1.901285268841737e-7, QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.9028288416436452e-8, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, @@ -988,12 +988,12 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalSeconds: 3600, QgsUnitTypes.TemporalUnit.TemporalMinutes: 60, QgsUnitTypes.TemporalUnit.TemporalHours: 1, - QgsUnitTypes.TemporalUnit.TemporalDays: 0.041666700000240003421, - QgsUnitTypes.TemporalUnit.TemporalWeeks: 0.0059523857143200008604, + QgsUnitTypes.TemporalUnit.TemporalDays: 0.041666666666666664, + QgsUnitTypes.TemporalUnit.TemporalWeeks: 0.005952380952380952, QgsUnitTypes.TemporalUnit.TemporalMonths: 0.001388888888888889, QgsUnitTypes.TemporalUnit.TemporalYears: 0.00011407711613050422, - QgsUnitTypes.TemporalUnit.TemporalDecades: 1.141553424664109737e-5, - QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.141553424664109737e-6, + QgsUnitTypes.TemporalUnit.TemporalDecades: 1.1407711613050422e-5, + QgsUnitTypes.TemporalUnit.TemporalCenturies: 1.1407711613050422e-6, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, }, @@ -1007,7 +1007,7 @@ def testTemporalFromUnitToUnitFactor(self): QgsUnitTypes.TemporalUnit.TemporalMonths: 0.03333333333333333, QgsUnitTypes.TemporalUnit.TemporalYears: 0.0027378507871321013, QgsUnitTypes.TemporalUnit.TemporalDecades: 0.0002737850787132101, - QgsUnitTypes.TemporalUnit.TemporalCenturies: 2.739723287683189167e-5, + QgsUnitTypes.TemporalUnit.TemporalCenturies: 2.7378507871321012e-5, QgsUnitTypes.TemporalUnit.TemporalUnknownUnit: 1.0, QgsUnitTypes.TemporalUnit.TemporalIrregularStep: 1.0, }, @@ -1089,6 +1089,7 @@ def testTemporalFromUnitToUnitFactor(self): res = QgsUnitTypes.fromUnitToUnitFactor(from_unit, to_unit) self.assertAlmostEqual(res, expected_factor, + places=10, msg='got {:.15f}, expected {:.15f} when converting from {} to {}'.format(res, expected_factor, QgsUnitTypes.toString(from_unit), QgsUnitTypes.toString(to_unit))) diff --git a/tests/testdata/control_images/expected_raster_single_color_renderer/expected_raster_single_color_renderer.png b/tests/testdata/control_images/expected_raster_single_color_renderer/expected_raster_single_color_renderer.png new file mode 100644 index 000000000000..ac13c4815f88 Binary files /dev/null and b/tests/testdata/control_images/expected_raster_single_color_renderer/expected_raster_single_color_renderer.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png new file mode 100644 index 000000000000..ebd0fd00bcdf Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_filter/expected_represents_temporal_values_filter.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_no_filter/expected_represents_temporal_values_no_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_no_filter/expected_represents_temporal_values_no_filter.png new file mode 100644 index 000000000000..3fd3383173ca Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_represents_temporal_values_no_filter/expected_represents_temporal_values_no_filter.png differ diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt index c6e43c20d560..445b318673b2 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_alias_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt index 1535a9702c4f..d8984c9030cc 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_exclude_attribute_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt index b910446e47c8..3b9f8db9af7c 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geojson.txt @@ -2,6 +2,13 @@ Content-Type: application/geo+json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt new file mode 100644 index 000000000000..1125b3ee9e3d --- /dev/null +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_CRS84_json.txt @@ -0,0 +1,24 @@ +***** +Content-Type: application/json; charset=utf-8 + +{ + "features": [ + { + "geometry": { + "coordinates": [ + 8.2035, + 44.9015 + ], + "type": "Point" + }, + "id": "testlayer2.0", + "properties": { + "id": 1, + "name": "one", + "utf8nameè": "one èé" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt index f248b083459c..5204b6fe23f0 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_geometry_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": { diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt index 4ba7c4ca5db5..41cc8910893c 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null, diff --git a/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt b/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt index b761b71e1d83..a8709da01519 100644 --- a/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt +++ b/tests/testdata/qgis_server/wms_getfeatureinfo_multiple_json.txt @@ -2,6 +2,13 @@ Content-Type: application/json; charset=utf-8 { + "crs": + { + "properties": { + "name": "urn:ogc:def:crs:EPSG:0:3857" + }, + "type": "name" + }, "features": [ { "geometry": null,