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/.ci/test_blocklist_qt6.txt b/.ci/test_blocklist_qt6.txt index a81734d8aee2..6ef77939c2ed 100644 --- a/.ci/test_blocklist_qt6.txt +++ b/.ci/test_blocklist_qt6.txt @@ -3,6 +3,7 @@ test_core_compositionconverter test_core_expression test_core_labelingengine test_core_layoutpicture +# test_core_ogcutils runs fine locally on Fedora:rawhide but not on CI test_core_ogcutils test_core_vectortilelayer test_gui_processinggui @@ -41,15 +42,12 @@ PyQgsPythonProvider PyQgsAnnotation PyQgsAuthenticationSystem PyQgsBlockingProcess -PyQgsBookmarkModel PyQgsCodeEditor PyQgsDelimitedTextProvider PyQgsEditWidgets PyQgsElevationProfileCanvas PyQgsProject -PyQgsFieldModel PyQgsFloatingWidget -PyQgsJsonUtils PyQgsLayoutHtml PyQgsLineSymbolLayers PyQgsMapBoxGlStyleConverter @@ -61,7 +59,6 @@ PyQgsRasterAttributeTable PyQgsRasterLayerRenderer PyQgsShapefileProvider PyQgsTextRenderer -PyQgsOGRProvider PyQgsSpatialiteProvider PyQgsSymbolLayerReadSld PyQgsVectorLayerCache @@ -81,4 +78,5 @@ ProcessingQgisAlgorithmsTestPt3 ProcessingQgisAlgorithmsTestPt4 ProcessingGdalAlgorithmsVectorTest ProcessingGrassAlgorithmsImageryTest +# PyQgsProviderRegistry runs fine locally on Fedora:rawhide but not on CI PyQgsProviderRegistry diff --git a/.docker/docker-qgis-build.sh b/.docker/docker-qgis-build.sh index 61a631ffc21d..5596eeb93359 100755 --- a/.docker/docker-qgis-build.sh +++ b/.docker/docker-qgis-build.sh @@ -35,16 +35,11 @@ pushd ${CTEST_BUILD_DIR} > /dev/null echo "${bold}Running cmake...${endbold}" echo "::group::cmake" -if [[ -f "/usr/lib64/ccache/clang" ]]; then - export CC=/usr/lib64/ccache/clang - export CXX=/usr/lib64/ccache/clang++ -else - export CC=/usr/lib/ccache/clang - export CXX=/usr/lib/ccache/clang++ -fi - BUILD_TYPE=Release +export CC=/usr/bin/clang +export CXX=/usr/bin/clang++ + if [[ "${WITH_CLAZY}" = "ON" ]]; then # In release mode, all variables in QgsDebugMsg would be considered unused BUILD_TYPE=Debug @@ -75,7 +70,7 @@ fi cmake \ -GNinja \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DUSE_CCACHE=OFF \ + -DUSE_CCACHE=ON \ -DBUILD_WITH_QT6=${BUILD_WITH_QT6} \ -DWITH_DESKTOP=ON \ -DWITH_ANALYSIS=ON \ diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index 5d168a6816c8..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_28\"]}" >> $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.0.0 - 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 a0d7aeb186c1..6389bb3fc28a 100644 --- a/.github/workflows/code_layout.yml +++ b/.github/workflows/code_layout.yml @@ -154,17 +154,24 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Requirements run: | sudo apt install -y \ expect \ silversearcher-ag - - uses: tj-actions/changed-files@v42 + + - name: Retrieve changed files + uses: tj-actions/changed-files@v44 id: changed_files with: - separator: ' ' + separator: " " + - name: Spell Test - run: ./scripts/spell_check/check_spelling.sh -r "${{ steps.changed_files.outputs.all_changed_files }}" + if: steps.changed_files.outputs.any_changed == 'true' + env: + ALL_CHANGED_FILES: ${{ steps.changed_files.outputs.all_changed_files }} + run: echo "$ALL_CHANGED_FILES" | ./scripts/spell_check/check_spelling.sh sip_check: runs-on: ubuntu-latest diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 46cc454d6c7a..97776a563129 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -38,36 +38,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Prepare build cache for pull request - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name == 'pull_request' + - name: Restore build cache + uses: actions/cache/restore@v4 with: path: ${{ env.CCACHE_DIR }} - key: build-mac-ccache-${{ github.actor }}-${{ github.head_ref }}-${{ github.sha }} - # The head_ref or source branch of the pull request in a workflow run. - # The base_ref or target branch of the pull request in a workflow run. + key: build-ccache-mac-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | - build-mac-ccache-${{ github.actor }}-${{ github.head_ref }}- - build-mac-ccache-refs/heads/${{ github.base_ref }}- - build-mac-ccache-refs/heads/master- - - - name: Prepare build cache for branch/tag - # use a fork of actions/cache@v2 to upload cache even when the build or test failed - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name != 'pull_request' - with: - path: ${{ env.CCACHE_DIR }} - # The branch or tag ref that triggered the workflow run. For branches this in the format refs/heads/, and for tags it is refs/tags/ - key: build-mac-ccache-${{ github.ref }}-${{ github.sha }} - restore-keys: | - build-mac-ccache-${{ github.ref }}- - build-mac-ccache-refs/heads/master- + build-ccache-mac-master - - # Qt caching - name: Cache Qt id: cache-qt - uses: actions/cache@v4.0.0 + uses: actions/cache@v4 with: path: ${{ env.DEPS_CACHE_DIR }}/Qt/${{ env.QT_VERSION }} key: mac-qt-${{ env.QT_VERSION }} @@ -90,7 +71,7 @@ jobs: # QGIS-deps caching - name: Cache qgis-deps id: cache-deps - uses: actions/cache@v4.0.0 + uses: actions/cache@v4 with: path: ${{ env.DEPS_CACHE_DIR }}/QGIS/qgis-deps-${{ env.QGIS_DEPS_VERSION }}.${{ env.QGIS_DEPS_PATCH_VERSION }} key: mac-qgis-deps-${{ env.QGIS_DEPS_VERSION }}.${{ env.QGIS_DEPS_PATCH_VERSION }} @@ -109,8 +90,6 @@ jobs: mkdir -p ${DEPS_CACHE_DIR} mkdir -p ${DEPS_CACHE_DIR}/QGIS - - - name: Install Qt and deps env: QT_ALREADY_CACHED: ${{ steps.cache-qt.outputs.cache-hit }} @@ -150,3 +129,10 @@ jobs: run: | cd ${BUILD_DIR} make -j $(sysctl -n hw.ncpu) + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: ${{ env.CCACHE_DIR }} + key: build-ccache-mac-${{ github.ref_name }}-${{ github.run_id }} diff --git a/.github/workflows/mingw-w64-msys2.yml b/.github/workflows/mingw-w64-msys2.yml index d855d96846ab..c667f99f521b 100644 --- a/.github/workflows/mingw-w64-msys2.yml +++ b/.github/workflows/mingw-w64-msys2.yml @@ -55,34 +55,16 @@ jobs: update: true release: false - - name: Prepare build cache for pull request - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name == 'pull_request' + - name: Restore build cache + uses: actions/cache/restore@v4 with: path: build - key: mingw-w64-msys2-ccache-${{ github.actor }}-${{ github.head_ref }}-${{ github.sha }} - # The head_ref or source branch of the pull request in a workflow run. - # The base_ref or target branch of the pull request in a workflow run. + key: build-ccache-mingw64-msys2-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | - mingw-w64-msys2-ccache-${{ github.actor }}-${{ github.head_ref }}- - mingw-w64-msys2-ccache-${{ github.base_ref }}- - mingw-w64-msys2-ccache-refs/heads/master- - - - name: Prepare build cache for branch/tag - # use a fork of actions/cache@v2 to upload cache even when the build or test failed - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name != 'pull_request' - with: - path: build - # The branch or tag ref that triggered the workflow run. For branches this in the format refs/heads/, and for tags it is refs/tags/ - key: mingw-w64-msys2-ccache-${{ github.ref }}-${{ github.sha }} - restore-keys: | - mingw-w64-msys2-ccache-${{ github.ref }}- - mingw-w64-msys2-ccache-refs/heads/master- + build-ccache-mingw64-msys2-master - name: Configure QGIS run: | - CXXFLAGS="-DQWT_POLAR_VERSION=0x060200" \ cmake \ -G"Ninja" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ @@ -91,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 \ @@ -102,3 +83,10 @@ jobs: run: | cmake --build build ccache -s + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: build + key: build-ccache-mingw64-msys2-${{ github.ref_name }}-${{ github.run_id }} diff --git a/.github/workflows/mingw64.yml b/.github/workflows/mingw64.yml index d9b7d37ad987..1815b1ee4769 100644 --- a/.github/workflows/mingw64.yml +++ b/.github/workflows/mingw64.yml @@ -63,34 +63,23 @@ jobs: - name: Create ccache dir run: mkdir -p /w/.ccache/QGIS - - name: Prepare build cache for pull request - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name == 'pull_request' + - name: Restore build cache + uses: actions/cache/restore@v4 with: path: /w/.ccache/QGIS - key: mingw64-ccache-${{ github.actor }}-${{ github.head_ref }}-${{ github.sha }} - # The head_ref or source branch of the pull request in a workflow run. - # The base_ref or target branch of the pull request in a workflow run. + key: build-ccache-mingw64-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | - mingw64-ccache-${{ github.actor }}-${{ github.head_ref }}- - mingw64-ccache-${{ github.base_ref }}- - mingw64-ccache-refs/heads/master- - - - name: Prepare build cache for branch/tag - # use a fork of actions/cache@v2 to upload cache even when the build or test failed - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name != 'pull_request' - with: - path: /w/.ccache/QGIS - # The branch or tag ref that triggered the workflow run. For branches this in the format refs/heads/, and for tags it is refs/tags/ - key: mingw64-ccache-${{ github.ref }}-${{ github.sha }} - restore-keys: | - mingw64-ccache-${{ github.ref }}- - mingw64-ccache-refs/heads/master- + build-ccache-mingw64-master - name: Build QGIS Application - run: | - CCACHE_DIR=/w/.ccache/QGIS ./ms-windows/mingw/build.sh x86_64 nodebug 4 + run: CCACHE_DIR=/w/.ccache/QGIS ./ms-windows/mingw/build.sh x86_64 nodebug 4 + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: /w/.ccache/QGIS + key: build-ccache-mingw64-${{ github.ref_name }}-${{ github.run_id }} - name: Create Portable zip run: | diff --git a/.github/workflows/ogc.yml b/.github/workflows/ogc.yml index c257c01c1582..3cf6743a593b 100644 --- a/.github/workflows/ogc.yml +++ b/.github/workflows/ogc.yml @@ -43,50 +43,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Prepare build cache for pull request - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name == 'pull_request' + - name: Restore build cache + uses: actions/cache/restore@v4 with: path: /home/runner/QGIS/.ccache - key: build-ccache-ogc-${{ github.actor }}-${{ github.head_ref }}-${{ github.sha }} - # The head_ref or source branch of the pull request in a workflow run. - # The base_ref or target branch of the pull request in a workflow run. + key: build-ccache-ogc-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | - build-ccache-ogc-${{ github.actor }}-${{ github.head_ref }}- - build-ccache-ogc-refs/heads/${{ github.base_ref }}- - build-ccache-ogc-refs/heads/master- + build-ccache-ogc-master - - name: Prepare build cache for branch/tag - # use a fork of actions/cache@v2 to upload cache even when the build or test failed - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name != 'pull_request' - with: - path: /home/runner/QGIS/.ccache - # The branch or tag ref that triggered the workflow run. For branches this in the format refs/heads/, and for tags it is refs/tags/ - key: build-ccache-ogc-${{ github.ref }}-${{ github.sha }} - restore-keys: | - build-ccache-ogc-${{ github.ref }}- - build-ccache-ogc-refs/heads/master- + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Build Docker Container with Build Environment + - name: Build Docker Container id: docker-build - uses: whoan/docker-build-with-cache-action@v8 + uses: docker/build-push-action@v5 with: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - image_name: qgis-deps-ogc + tags: qgis/qgis-deps-ogc:${{ github.event.pull_request.base.ref || github.ref_name }} context: .ci/ogc - dockerfile: Dockerfile - push_git_tag: true - push_image_and_stages: on:push - pull_image_and_stages: ${{ github.event_name != 'workflow_dispatch' }} + file: .ci/ogc/Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + load: true - name: Run build run: | docker run -v $(pwd):/usr/src/qgis -v /home/runner/QGIS/.ccache:/root/.ccache ${DOCKER_IMAGE} /usr/src/qgis/.ci/ogc/build.sh env: - DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} + DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} + + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: /home/runner/QGIS/.ccache + key: build-ccache-ogc-${{ github.ref_name }}-${{ github.run_id }} - name: Install pyogctest run: | @@ -98,15 +88,15 @@ 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.FULL_IMAGE_NAME }} + DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} - 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.FULL_IMAGE_NAME }} + DOCKER_IMAGE: ${{ steps.docker-build.outputs.imageid }} diff --git a/.github/workflows/pr-needs-documentation.yml b/.github/workflows/pr-needs-documentation.yml index 5638d7a351d8..593187145d60 100644 --- a/.github/workflows/pr-needs-documentation.yml +++ b/.github/workflows/pr-needs-documentation.yml @@ -91,25 +91,17 @@ jobs: LABEL=$(sed -r 's/^([[:digit:]]\.[[:digit:]]+)(\.[[:digit:]]+)?$/\1/' <<< ${MILESTONE}) echo ${LABEL} echo "label=${LABEL}" >> $GITHUB_OUTPUT - # get the PR body # get the PR body - - name: Get PR body as JSON - id: get_pr_info - uses: octokit/request-action@v2.x - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - route: GET /repos/qgis/QGIS/pulls/:pull_number - pull_number: ${{ github.event.pull_request.number }} - - # extract body from json output - name: Get PR body as text id: get_pr_body - uses: gr2m/get-json-paths-action@v1.x - with: - json: ${{ steps.get_pr_info.outputs.data }} - body: "body" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BODY: "${{ github.event.pull_request.body }}" + run: | + echo 'body<> $GITHUB_OUTPUT + echo "$PR_BODY" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT # get commits from the PR - name: Get PR commits @@ -118,7 +110,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - route: GET /repos/qgis/QGIS/pulls/:pull_number/commits + route: GET /repos/qgis/QGIS/pulls/{pull_number}/commits pull_number: ${{ github.event.pull_request.number }} # extracts the matching commits @@ -133,7 +125,7 @@ jobs: # create the documentation issue - name: Create Documentation issue id: doc_issue - uses: maxkomarychev/oction-create-issue@v0.7.1 + uses: dacbd/create-issue-action@v2.0.0 with: token: ${{ secrets.GH_TOKEN_BOT }} owner: qgis @@ -144,7 +136,7 @@ jobs: # the token is in clear, so no rights are given to qgis-bot body: | ### Request for documentation - From pull request QGIS/qgis#${{ github.event.pull_request.number }} + From pull request qgis/QGIS#${{ github.event.pull_request.number }} Author: @${{ github.event.pull_request.user.login }} QGIS version: ${{ steps.milestone2label.outputs.label }} @@ -160,7 +152,7 @@ jobs: - name: Create comment uses: peter-evans/create-or-update-comment@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_TOKEN_BOT }} issue-number: ${{ github.event.pull_request.number }} body: | @${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0eec7d03169e..80bfcdd21dd1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -33,8 +33,6 @@ permissions: jobs: build: env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} QGIS_WORKSPACE: ${{ github.workspace }} # used in docker compose RUN_FLAKY_TESTS: ${{ contains( github.event.pull_request.labels.*.name, 'run flaky tests') }} @@ -47,7 +45,6 @@ jobs: - distro-version: '22.04' qt-version: 5 run-tests: true - docker-tag-suffix: '' with-qt6: OFF with-qt5: ON with-3d: ON @@ -64,7 +61,6 @@ jobs: - distro-version: '39' qt-version: 6 run-tests: true - docker-tag-suffix: '' with-qt6: ON with-qt5: OFF with-3d: ON @@ -107,7 +103,7 @@ jobs: GITHUB_PR_NUMBER: ${{github.event.number}} run: | # Be aware that these instructions are duplicated in run-tests job - DOCKER_TAG=$(echo $( [[ ${GITHUB_EVENT_NAME} == pull_request ]] && echo ${GITHUB_BASE_REF} || echo ${GITHUB_REF##*/} ) | sed 's/^master$/latest/')${{ matrix.docker-tag-suffix }} + DOCKER_TAG=$(echo $( [[ ${GITHUB_EVENT_NAME} == pull_request ]] && echo ${GITHUB_BASE_REF} || echo ${GITHUB_REF##*/} ) | sed 's/^master$/latest/') CTEST_BUILD_NAME=$( [[ ${GITHUB_EVENT_NAME} == pull_request ]] && echo "PR${GITHUB_PR_NUMBER}" || echo ${GITHUB_REF##*/} )"_${GITHUB_SHA}" echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV echo "CTEST_BUILD_NAME=${CTEST_BUILD_NAME}" >> $GITHUB_ENV @@ -119,57 +115,35 @@ jobs: echo CTEST_BUILD_NAME: ${CTEST_BUILD_NAME} echo QT_VERSION: ${QT_VERSION} + - name: Login to Docker Hub + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build Docker Container with Build Environment id: docker-build - uses: whoan/docker-build-with-cache-action@v8 + uses: docker/build-push-action@v5 with: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - image_name: qgis3-build-deps-${{ matrix.distro-version }} - dockerfile: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile - build_extra_args: "--build-arg=DISTRO_VERSION=${{ matrix.distro-version }}" - push_git_tag: true - push_image_and_stages: on:push - pull_image_and_stages: ${{ github.event_name != 'workflow_dispatch' }} + 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' && github.actor == 'qgis' }} + pull: true + build-args: + DISTRO_VERSION=${{ matrix.distro-version }} - name: Tag image - run: | - docker tag ${DOCKER_IMAGE} qgis3-build-deps - env: - DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} - - - name: Push Qt5 deps image to Docker hub - if: ${{ github.repository_owner == 'qgis' && github.event_name != 'pull_request' && matrix.qt-version == '5' }} - run: | - docker tag qgis3-build-deps qgis/qgis3-build-deps:${DOCKER_TAG} - docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}" - docker push "qgis/qgis3-build-deps:${DOCKER_TAG}" + run: docker tag ${{ steps.docker-build.outputs.imageid }} qgis3-build-deps - - name: Prepare build cache for pull request - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name == 'pull_request' + - name: Restore build cache + uses: actions/cache/restore@v4 with: path: /home/runner/QGIS/.ccache - key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.actor }}-${{ github.head_ref }}-${{ github.sha }} - # The head_ref or source branch of the pull request in a workflow run. - # The base_ref or target branch of the pull request in a workflow run. + key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.event.pull_request.base.ref || github.ref_name }} restore-keys: | - build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.actor }}-${{ github.head_ref }}- - build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-refs/heads/${{ github.base_ref }}- - build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-refs/heads/master- - - - name: Prepare build cache for branch/tag - # use a fork of actions/cache@v2 to upload cache even when the build or test failed - uses: pat-s/always-upload-cache@v3.0.11 - if: github.event_name != 'pull_request' - with: - path: /home/runner/QGIS/.ccache - # The branch or tag ref that triggered the workflow run. For branches this in the format refs/heads/, and for tags it is refs/tags/ - key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.ref }}-${{ github.sha }} - restore-keys: | - build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.ref }}- - build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-refs/heads/master- + build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-master - name: Compile QGIS id: compile @@ -178,6 +152,7 @@ jobs: -v $(pwd):/root/QGIS \ -v /home/runner/QGIS/.ccache:/root/.ccache \ --env-file .docker/docker-variables.env \ + --env CCACHE_DIR=/root/.ccache \ --env PUSH_TO_CDASH=true \ --env WITH_QT5=${{ matrix.with-qt5 }} \ --env BUILD_WITH_QT6=${{ matrix.with-qt6 }} \ @@ -193,6 +168,13 @@ jobs: qgis3-build-deps \ /root/QGIS/.docker/docker-qgis-build.sh + - name: Save build cache for push only + uses: actions/cache/save@v4 + if: ${{ github.event_name == 'push' }} + with: + path: /home/runner/QGIS/.ccache + key: build-ccache-${{ matrix.distro-version }}-qt${{ matrix.qt-version }}-${{ github.ref_name }}-${{ github.run_id }} + - name: Push artifact id: push_artifact if: ${{ matrix.run-tests }} @@ -301,8 +283,6 @@ jobs: run-tests: name: Run tests env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} QGIS_WORKSPACE: ${{ github.workspace }} # used in docker compose runs-on: ubuntu-latest @@ -366,25 +346,28 @@ jobs: run: | echo CTEST_BUILD_NAME: ${CTEST_BUILD_NAME} + - name: Login to Docker Hub + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build Docker Container with Testing Environment id: docker-build - uses: whoan/docker-build-with-cache-action@v8 + uses: docker/build-push-action@v5 with: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - image_name: qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only - dockerfile: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile - build_extra_args: "--target ${{ matrix.docker-target }} --build-arg=DISTRO_VERSION=${{ matrix.distro-version }}" - push_git_tag: true - push_image_and_stages: on:push - pull_image_and_stages: ${{ github.event_name != 'workflow_dispatch' }} + 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' && github.actor == 'qgis' }} + pull: true + target: ${{ matrix.docker-target }} + build-args: + DISTRO_VERSION=${{ matrix.distro-version }} - name: Tag image - run: | - docker tag ${DOCKER_IMAGE} qgis3-build-deps-binary-image - env: - DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} + run: docker tag ${{ steps.docker-build.outputs.imageid }} qgis3-build-deps-binary-image - name: Print disk space run: | @@ -424,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() }} @@ -474,25 +457,28 @@ jobs: with: fetch-depth: 2 + - name: Login to Docker Hub + if: ${{ github.event_name == 'push' && github.actor == 'qgis' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build Docker Container with Testing Environment id: docker-build - uses: whoan/docker-build-with-cache-action@v8 + uses: docker/build-push-action@v5 with: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ghcr.io - image_name: qgis3-qt${{ matrix.qt-version }}-build-deps-bin-only - dockerfile: .docker/qgis3-qt${{ matrix.qt-version }}-build-deps.dockerfile - build_extra_args: "--build-arg=DISTRO_VERSION=${{ matrix.distro-version }}" - push_git_tag: true - push_image_and_stages: on:push - pull_image_and_stages: ${{ github.event_name != 'workflow_dispatch' }} + 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' && github.actor == 'qgis' }} + pull: true + target: ${{ matrix.docker-target }} + build-args: + DISTRO_VERSION=${{ matrix.distro-version }} - name: Tag image - run: | - docker tag ${DOCKER_IMAGE} qgis3-build-deps - env: - DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }} + run: docker tag ${{ steps.docker-build.outputs.imageid }} qgis3-build-deps-binary-image - name: Download build artifact uses: actions/download-artifact@v4 @@ -511,5 +497,5 @@ jobs: -v $(pwd):/root/QGIS \ -v /home/runner/QGIS/.ccache:/root/.ccache \ --env-file .docker/docker-variables.env \ - qgis3-build-deps \ + qgis3-build-deps-binary-image \ /root/QGIS/.docker/docker-qgis-clangtidy.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index c44705325ad2..aefb09e594dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ set(CMAKE_LINK_DEPENDS_NO_SHARED ON) ############################################################# # Project and version set(CPACK_PACKAGE_VERSION_MAJOR "3") -set(CPACK_PACKAGE_VERSION_MINOR "36") +set(CPACK_PACKAGE_VERSION_MINOR "37") set(CPACK_PACKAGE_VERSION_PATCH "0") set(COMPLETE_VERSION ${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}) set(RELEASE_NAME "Maidenhead") @@ -375,9 +375,6 @@ if(WITH_CORE) find_package(Protobuf CONFIG) find_package(Protobuf REQUIRED) - if(Protobuf_VERSION GREATER_EQUAL 4.23) - set(Protobuf_LITE_LIBRARY protobuf::libprotobuf-lite) - endif() message(STATUS "Found Protobuf: ${Protobuf_LIBRARIES}") if (NOT Protobuf_PROTOC_EXECUTABLE) @@ -1105,7 +1102,7 @@ if (WITH_CORE AND WITH_BINDINGS) include(SIPMacros) set(SIP_INCLUDES ${PYQT_SIP_DIR} ${CMAKE_SOURCE_DIR}/python) - set(SIP_CONCAT_PARTS 26) + set(SIP_CONCAT_PARTS 25) if (NOT BINDINGS_GLOBAL_INSTALL) set(Python_SITEARCH ${QGIS_DATA_DIR}/python) diff --git a/cmake_templates/Doxyfile.in b/cmake_templates/Doxyfile.in index 1951c58504aa..5f38d1215fcb 100644 --- a/cmake_templates/Doxyfile.in +++ b/cmake_templates/Doxyfile.in @@ -2384,7 +2384,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = +PREDEFINED = "Q_DECLARE_FLAGS(Flags, Enum)=typedef QFlags Flags;" # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The @@ -2437,7 +2437,8 @@ EXPAND_AS_DEFINED = "SIP_ABSTRACT" \ "SIP_WHEN_FEATURE" \ "SIP_MONKEYPATCH_COMPAT_NAME" \ "SIP_MONKEYPATCH_SCOPEENUM" \ - "SIP_MONKEYPATCH_SCOPEENUM_UNNEST" + "SIP_MONKEYPATCH_SCOPEENUM_UNNEST" \ + "SIP_MONKEYPATCH_FLAGS_UNNEST" # If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will # remove all references to function-like macros that are alone on a line, have diff --git a/cmake_templates/qgsconfig.h.in b/cmake_templates/qgsconfig.h.in index 6eaca0c237d5..b8f0949c9850 100644 --- a/cmake_templates/qgsconfig.h.in +++ b/cmake_templates/qgsconfig.h.in @@ -30,8 +30,6 @@ #define QGIS_LIBEXEC_SUBDIR "${QGIS_LIBEXEC_SUBDIR}" #define QGIS_LIB_SUBDIR "${QGIS_LIB_SUBDIR}" #define QGIS_QML_SUBDIR "${QGIS_QML_SUBDIR}" -#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" -#define CMAKE_SOURCE_DIR "${CMAKE_SOURCE_DIR}" #define QGIS_SERVER_MODULE_SUBDIR "${QGIS_SERVER_MODULE_SUBDIR}" @@ -44,8 +42,6 @@ #define QGIS_MACAPP_BUNDLE ${QGIS_MACAPP_BUNDLE} #endif -#define QT_PLUGINS_DIR "${QT_PLUGINS_DIR}" - #define PYTHON_VERSION "${Python_VERSION}" #define PYTHON_VERSION_MAJOR "${Python_VERSION_MAJOR}" #define PYTHON_VERSION_MINOR "${Python_VERSION_MINOR}" diff --git a/debian/changelog b/debian/changelog index 39f66ae7bb4c..3bdd443f6502 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ -qgis (3.36.0) UNRELEASED; urgency=medium +qgis (3.37.0) UNRELEASED; urgency=medium + + * New development version 3.37 after branch of 3.36 + + -- Jürgen E. Fischer Fri, 23 Feb 2024 13:06:57 +0100 + +qgis (3.36.0) unstable; urgency=medium * Release of 3.36.0 - -- Jürgen E. Fischer Fri, 23 Feb 2024 13:04:10 +0100 + -- Jürgen E. Fischer Fri, 23 Feb 2024 13:06:57 +0100 qgis (3.35.0) unstable; urgency=medium 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/doc/index.dox b/doc/index.dox index 98ebe7e05714..8e63558eb5a1 100644 --- a/doc/index.dox +++ b/doc/index.dox @@ -33,6 +33,7 @@ the documentation available. See \ref api_break for information about incompatible changes to API between releases. Earlier versions of the documentation are also available on the QGIS website: +3.36, 3.34 (LTR), 3.32, 3.30, diff --git a/linux/org.qgis.qgis.appdata.xml.in b/linux/org.qgis.qgis.appdata.xml.in index f695aba2194b..1ead089d9604 100644 --- a/linux/org.qgis.qgis.appdata.xml.in +++ b/linux/org.qgis.qgis.appdata.xml.in @@ -6,7 +6,7 @@ QGIShttps://qgis.org/en/_static/images/about-screenshot.png - QGIS Development Team + QGIS Development Team

QGIS is a user friendly Open Source Geographic Information System (GIS) licensed under the GNU General Public License. QGIS is an official project of the Open Source Geospatial Foundation (OSGeo). It runs on Linux, Unix, Mac OSX, Windows and Android and supports numerous vector, raster, and database formats and functionalities.

CC-BY-3.0 diff --git a/python/PyQt/PyQt/QtCore.py.in b/python/PyQt/PyQt/QtCore.py.in index 064f56b6e58a..920a0e488be7 100644 --- a/python/PyQt/PyQt/QtCore.py.in +++ b/python/PyQt/PyQt/QtCore.py.in @@ -132,6 +132,19 @@ if (QT_VERSION >= 0x060000): QVariant.SizePolicy = QMetaType.Type.QSizePolicy QVariant.UserType = QMetaType.Type.User + from enum import Enum + + + def _force_int(v): return int(v.value) if isinstance(v, Enum) else v + + QMetaType.Type.__int__ = _force_int + QMetaType.Type.__eq__ = lambda t1, t2: _force_int(t1) == _force_int(t2) + + # These types aren't IntEnums or IntFlags, so patch that back in + # See discussion at https://www.riverbankcomputing.com/pipermail/pyqt/2024-February/045715.html + Qt.CheckState.__int__ = _force_int + Qt.CheckState.__eq__ = lambda t1, t2: _force_int(t1) == _force_int(t2) + # patch back in Qt flags removed in PyQt QAbstractItemModel.CheckIndexOptions = lambda flags=0: QAbstractItemModel.CheckIndexOption(flags) diff --git a/python/PyQt6/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in b/python/PyQt6/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in index 5500bf617c96..1d1cec6d173e 100644 --- a/python/PyQt6/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in +++ b/python/PyQt6/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in @@ -87,7 +87,7 @@ Sets the ``crs`` of the destination point. QgsPointXY transformedDestinationPoint( const QgsCoordinateReferenceSystem &targetCrs, const QgsCoordinateTransformContext &context ) const; %Docstring -Returns the :py:func:`~QgsGcpPoint.destionationPoint` transformed to the given target CRS. +Returns the :py:func:`~QgsGcpPoint.destinationPoint` transformed to the given target CRS. %End bool isEnabled() const; diff --git a/python/PyQt6/core/__init__.py.in b/python/PyQt6/core/__init__.py.in index b319ab41d840..e51863aee03c 100644 --- a/python/PyQt6/core/__init__.py.in +++ b/python/PyQt6/core/__init__.py.in @@ -407,3 +407,20 @@ def set_display_all(self, enabled): QgsPalLayerSettings.displayAll = property(get_display_all) QgsPalLayerSettings.displayAll = QgsPalLayerSettings.displayAll.setter(set_display_all) + +QgsLocatorResult.userData = property(QgsLocatorResult._userData) +QgsLocatorResult.userData = QgsLocatorResult.userData.setter(QgsLocatorResult.setUserData) + + +def get_pixel_transparency(self): + return (1.0 - self.opacity) * 100 + + +def set_pixel_transparency(self, transparency): + self.opacity = 1.0 - (transparency / 100) + + +QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent = property(get_pixel_transparency) +QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent = QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent.setter(set_pixel_transparency) +QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent = property(get_pixel_transparency) +QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent = QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent.setter(set_pixel_transparency) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 8a985163a435..02cb7d44a228 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -1279,6 +1279,14 @@ Qgis.RasterRendererFlags.baseClass = Qgis RasterRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ = "The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability)" +Qgis.RasterRendererCapability.__doc__ = "Raster renderer capabilities.\n\n.. versionadded:: 3.48\n\n" + '* ``UsesMultipleBands``: ' + Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ +# -- +Qgis.RasterRendererCapability.baseClass = Qgis +Qgis.RasterRendererCapabilities = lambda flags=0: Qgis.RasterRendererCapability(flags) +Qgis.RasterRendererCapabilities.baseClass = Qgis +RasterRendererCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module +# monkey patching scoped based enum Qgis.RasterAttributeTableFieldUsage.Generic.__doc__ = "Field usage Generic" Qgis.RasterAttributeTableFieldUsage.PixelCount.__doc__ = "Field usage PixelCount" Qgis.RasterAttributeTableFieldUsage.Name.__doc__ = "Field usage Name" @@ -1830,7 +1838,9 @@ Qgis.AutoRefreshMode.baseClass = Qgis # monkey patching scoped based enum Qgis.DataProviderFlag.IsBasemapSource.__doc__ = "Associated source should be considered a 'basemap' layer. See Qgis.MapLayerProperty.IsBasemapLayer." -Qgis.DataProviderFlag.__doc__ = "Generic data provider flags.\n\n.. versionadded:: 3.26\n\n" + '* ``IsBasemapSource``: ' + Qgis.DataProviderFlag.IsBasemapSource.__doc__ +Qgis.DataProviderFlag.FastExtent2D.__doc__ = "Provider's 2D extent retrieval via QgsDataProvider.extent() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38." +Qgis.DataProviderFlag.FastExtent3D.__doc__ = "Provider's 3D extent retrieval via QgsDataProvider.extent3D() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38." +Qgis.DataProviderFlag.__doc__ = "Generic data provider flags.\n\n.. versionadded:: 3.26\n\n" + '* ``IsBasemapSource``: ' + Qgis.DataProviderFlag.IsBasemapSource.__doc__ + '\n' + '* ``FastExtent2D``: ' + Qgis.DataProviderFlag.FastExtent2D.__doc__ + '\n' + '* ``FastExtent3D``: ' + Qgis.DataProviderFlag.FastExtent3D.__doc__ # -- Qgis.DataProviderFlags = lambda flags=0: Qgis.DataProviderFlag(flags) Qgis.DataProviderFlag.baseClass = Qgis @@ -2103,7 +2113,10 @@ QgsRasterLayerTemporalProperties.TemporalMode.ModeRedrawLayerOnly = Qgis.RasterTemporalMode.RedrawLayerOnly QgsRasterLayerTemporalProperties.ModeRedrawLayerOnly.is_monkey_patched = True QgsRasterLayerTemporalProperties.ModeRedrawLayerOnly.__doc__ = "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)" -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__ +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__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod @@ -3218,6 +3231,28 @@ # -- Qgis.AltitudeBinding.baseClass = Qgis # monkey patching scoped based enum +Qgis.RangeLimits.IncludeBoth.__doc__ = "Both lower and upper values are included in the range" +Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ = "Lower value is included in the range, upper value is excluded" +Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ = "Lower value is excluded from the range, upper value in inccluded" +Qgis.RangeLimits.ExcludeBoth.__doc__ = "Both lower and upper values are excluded from the range" +Qgis.RangeLimits.__doc__ = "Describes how the limits of a range are handled.\n\n.. versionadded:: 3.38\n\n" + '* ``IncludeBoth``: ' + Qgis.RangeLimits.IncludeBoth.__doc__ + '\n' + '* ``IncludeLowerExcludeUpper``: ' + Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ + '\n' + '* ``ExcludeLowerIncludeUpper``: ' + Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ + '\n' + '* ``ExcludeBoth``: ' + Qgis.RangeLimits.ExcludeBoth.__doc__ +# -- +Qgis.RangeLimits.baseClass = Qgis +# monkey patching scoped based enum +Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" +Qgis.RasterElevationMode.FixedRangePerBand.__doc__ = "Layer has a fixed (manually specified) elevation range per band" +Qgis.RasterElevationMode.DynamicRangePerBand.__doc__ = "Layer has a elevation range per band, calculated dynamically from an expression" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterElevationMode.FixedRangePerBand.__doc__ + '\n' + '* ``DynamicRangePerBand``: ' + Qgis.RasterElevationMode.DynamicRangePerBand.__doc__ +# -- +Qgis.RasterElevationMode.baseClass = Qgis +# monkey patching scoped based enum +Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +# -- +Qgis.MeshElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" @@ -4869,6 +4904,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_additions/qgsdxfexport.py b/python/PyQt6/core/auto_additions/qgsdxfexport.py index 2fb37b4ca32b..349d7a111efb 100644 --- a/python/PyQt6/core/auto_additions/qgsdxfexport.py +++ b/python/PyQt6/core/auto_additions/qgsdxfexport.py @@ -1,5 +1,6 @@ # The following has been generated automatically from src/core/dxf/qgsdxfexport.h QgsDxfExport.FlagNoMText = QgsDxfExport.Flag.FlagNoMText +QgsDxfExport.FlagOnlySelectedFeatures = QgsDxfExport.Flag.FlagOnlySelectedFeatures QgsDxfExport.Flags = lambda flags=0: QgsDxfExport.Flag(flags) # monkey patching scoped based enum QgsDxfExport.ExportResult.Success.__doc__ = "Successful export" diff --git a/python/PyQt6/core/auto_additions/qgsfieldproxymodel.py b/python/PyQt6/core/auto_additions/qgsfieldproxymodel.py index a00b47195f67..cd4fdb2c4688 100644 --- a/python/PyQt6/core/auto_additions/qgsfieldproxymodel.py +++ b/python/PyQt6/core/auto_additions/qgsfieldproxymodel.py @@ -10,6 +10,7 @@ QgsFieldProxyModel.DateTime = QgsFieldProxyModel.Filter.DateTime QgsFieldProxyModel.Binary = QgsFieldProxyModel.Filter.Binary QgsFieldProxyModel.Boolean = QgsFieldProxyModel.Filter.Boolean +QgsFieldProxyModel.OriginProvider = QgsFieldProxyModel.Filter.OriginProvider QgsFieldProxyModel.AllTypes = QgsFieldProxyModel.Filter.AllTypes QgsFieldProxyModel.Filters = lambda flags=0: QgsFieldProxyModel.Filter(flags) QgsFieldProxyModel.Filters.baseClass = QgsFieldProxyModel diff --git a/python/PyQt6/core/auto_additions/qgslayoutobject.py b/python/PyQt6/core/auto_additions/qgslayoutobject.py index a8928e576264..b656dd5b5beb 100644 --- a/python/PyQt6/core/auto_additions/qgslayoutobject.py +++ b/python/PyQt6/core/auto_additions/qgslayoutobject.py @@ -156,6 +156,21 @@ QgsLayoutObject.MapGridFrameDivisionsBottom = QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom QgsLayoutObject.MapGridFrameDivisionsBottom.is_monkey_patched = True QgsLayoutObject.MapGridFrameDivisionsBottom.__doc__ = "Map frame division display bottom" +QgsLayoutObject.MapCrs = QgsLayoutObject.DataDefinedProperty.MapCrs +QgsLayoutObject.MapCrs.is_monkey_patched = True +QgsLayoutObject.MapCrs.__doc__ = "Map CRS" +QgsLayoutObject.StartDateTime = QgsLayoutObject.DataDefinedProperty.StartDateTime +QgsLayoutObject.StartDateTime.is_monkey_patched = True +QgsLayoutObject.StartDateTime.__doc__ = "Temporal range's start DateTime" +QgsLayoutObject.EndDateTime = QgsLayoutObject.DataDefinedProperty.EndDateTime +QgsLayoutObject.EndDateTime.is_monkey_patched = True +QgsLayoutObject.EndDateTime.__doc__ = "Temporal range's end DateTime" +QgsLayoutObject.MapZRangeLower = QgsLayoutObject.DataDefinedProperty.MapZRangeLower +QgsLayoutObject.MapZRangeLower.is_monkey_patched = True +QgsLayoutObject.MapZRangeLower.__doc__ = "Map frame Z-range lower value (since QGIS 3.38)" +QgsLayoutObject.MapZRangeUpper = QgsLayoutObject.DataDefinedProperty.MapZRangeUpper +QgsLayoutObject.MapZRangeUpper.is_monkey_patched = True +QgsLayoutObject.MapZRangeUpper.__doc__ = "Map frame Z-range lower value (since QGIS 3.38)" QgsLayoutObject.PictureSource = QgsLayoutObject.DataDefinedProperty.PictureSource QgsLayoutObject.PictureSource.is_monkey_patched = True QgsLayoutObject.PictureSource.__doc__ = "Picture source url" @@ -216,15 +231,6 @@ QgsLayoutObject.AttributeTableSourceLayer = QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer QgsLayoutObject.AttributeTableSourceLayer.is_monkey_patched = True QgsLayoutObject.AttributeTableSourceLayer.__doc__ = "Attribute table source layer" -QgsLayoutObject.MapCrs = QgsLayoutObject.DataDefinedProperty.MapCrs -QgsLayoutObject.MapCrs.is_monkey_patched = True -QgsLayoutObject.MapCrs.__doc__ = "Map CRS" -QgsLayoutObject.StartDateTime = QgsLayoutObject.DataDefinedProperty.StartDateTime -QgsLayoutObject.StartDateTime.is_monkey_patched = True -QgsLayoutObject.StartDateTime.__doc__ = "Temporal range's start DateTime" -QgsLayoutObject.EndDateTime = QgsLayoutObject.DataDefinedProperty.EndDateTime -QgsLayoutObject.EndDateTime.is_monkey_patched = True -QgsLayoutObject.EndDateTime.__doc__ = "Temporal range's end DateTime" QgsLayoutObject.ElevationProfileTolerance = QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance QgsLayoutObject.ElevationProfileTolerance.is_monkey_patched = True QgsLayoutObject.ElevationProfileTolerance.__doc__ = "Tolerance distance for elevation profiles (since QGIS 3.30)" @@ -258,7 +264,7 @@ QgsLayoutObject.ElevationProfileMaximumElevation = QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation QgsLayoutObject.ElevationProfileMaximumElevation.is_monkey_patched = True QgsLayoutObject.ElevationProfileMaximumElevation.__doc__ = "Maximum elevation value for elevation profile (since QGIS 3.30)" -QgsLayoutObject.DataDefinedProperty.__doc__ = "Data defined properties for different item types\n\n" + '* ``NoProperty``: ' + QgsLayoutObject.DataDefinedProperty.NoProperty.__doc__ + '\n' + '* ``AllProperties``: ' + QgsLayoutObject.DataDefinedProperty.AllProperties.__doc__ + '\n' + '* ``TestProperty``: ' + QgsLayoutObject.DataDefinedProperty.TestProperty.__doc__ + '\n' + '* ``PresetPaperSize``: ' + QgsLayoutObject.DataDefinedProperty.PresetPaperSize.__doc__ + '\n' + '* ``PaperWidth``: ' + QgsLayoutObject.DataDefinedProperty.PaperWidth.__doc__ + '\n' + '* ``PaperHeight``: ' + QgsLayoutObject.DataDefinedProperty.PaperHeight.__doc__ + '\n' + '* ``NumPages``: ' + QgsLayoutObject.DataDefinedProperty.NumPages.__doc__ + '\n' + '* ``PaperOrientation``: ' + QgsLayoutObject.DataDefinedProperty.PaperOrientation.__doc__ + '\n' + '* ``PageNumber``: ' + QgsLayoutObject.DataDefinedProperty.PageNumber.__doc__ + '\n' + '* ``PositionX``: ' + QgsLayoutObject.DataDefinedProperty.PositionX.__doc__ + '\n' + '* ``PositionY``: ' + QgsLayoutObject.DataDefinedProperty.PositionY.__doc__ + '\n' + '* ``ItemWidth``: ' + QgsLayoutObject.DataDefinedProperty.ItemWidth.__doc__ + '\n' + '* ``ItemHeight``: ' + QgsLayoutObject.DataDefinedProperty.ItemHeight.__doc__ + '\n' + '* ``ItemRotation``: ' + QgsLayoutObject.DataDefinedProperty.ItemRotation.__doc__ + '\n' + '* ``Transparency``: ' + QgsLayoutObject.DataDefinedProperty.Transparency.__doc__ + '\n' + '* ``Opacity``: ' + QgsLayoutObject.DataDefinedProperty.Opacity.__doc__ + '\n' + '* ``BlendMode``: ' + QgsLayoutObject.DataDefinedProperty.BlendMode.__doc__ + '\n' + '* ``ExcludeFromExports``: ' + QgsLayoutObject.DataDefinedProperty.ExcludeFromExports.__doc__ + '\n' + '* ``FrameColor``: ' + QgsLayoutObject.DataDefinedProperty.FrameColor.__doc__ + '\n' + '* ``BackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.BackgroundColor.__doc__ + '\n' + '* ``MarginLeft``: ' + QgsLayoutObject.DataDefinedProperty.MarginLeft.__doc__ + '\n' + '* ``MarginTop``: ' + QgsLayoutObject.DataDefinedProperty.MarginTop.__doc__ + '\n' + '* ``MarginRight``: ' + QgsLayoutObject.DataDefinedProperty.MarginRight.__doc__ + '\n' + '* ``MarginBottom``: ' + QgsLayoutObject.DataDefinedProperty.MarginBottom.__doc__ + '\n' + '* ``MapRotation``: ' + QgsLayoutObject.DataDefinedProperty.MapRotation.__doc__ + '\n' + '* ``MapScale``: ' + QgsLayoutObject.DataDefinedProperty.MapScale.__doc__ + '\n' + '* ``MapXMin``: ' + QgsLayoutObject.DataDefinedProperty.MapXMin.__doc__ + '\n' + '* ``MapYMin``: ' + QgsLayoutObject.DataDefinedProperty.MapYMin.__doc__ + '\n' + '* ``MapXMax``: ' + QgsLayoutObject.DataDefinedProperty.MapXMax.__doc__ + '\n' + '* ``MapYMax``: ' + QgsLayoutObject.DataDefinedProperty.MapYMax.__doc__ + '\n' + '* ``MapAtlasMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapAtlasMargin.__doc__ + '\n' + '* ``MapLayers``: ' + QgsLayoutObject.DataDefinedProperty.MapLayers.__doc__ + '\n' + '* ``MapStylePreset``: ' + QgsLayoutObject.DataDefinedProperty.MapStylePreset.__doc__ + '\n' + '* ``MapLabelMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapLabelMargin.__doc__ + '\n' + '* ``MapGridEnabled``: ' + QgsLayoutObject.DataDefinedProperty.MapGridEnabled.__doc__ + '\n' + '* ``MapGridIntervalX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalX.__doc__ + '\n' + '* ``MapGridIntervalY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalY.__doc__ + '\n' + '* ``MapGridOffsetX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetX.__doc__ + '\n' + '* ``MapGridOffsetY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetY.__doc__ + '\n' + '* ``MapGridFrameSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameSize.__doc__ + '\n' + '* ``MapGridFrameMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameMargin.__doc__ + '\n' + '* ``MapGridLabelDistance``: ' + QgsLayoutObject.DataDefinedProperty.MapGridLabelDistance.__doc__ + '\n' + '* ``MapGridCrossSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridCrossSize.__doc__ + '\n' + '* ``MapGridFrameLineThickness``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameLineThickness.__doc__ + '\n' + '* ``MapGridAnnotationDisplayLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayLeft.__doc__ + '\n' + '* ``MapGridAnnotationDisplayRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayRight.__doc__ + '\n' + '* ``MapGridAnnotationDisplayTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayTop.__doc__ + '\n' + '* ``MapGridAnnotationDisplayBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayBottom.__doc__ + '\n' + '* ``MapGridFrameDivisionsLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsLeft.__doc__ + '\n' + '* ``MapGridFrameDivisionsRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsRight.__doc__ + '\n' + '* ``MapGridFrameDivisionsTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsTop.__doc__ + '\n' + '* ``MapGridFrameDivisionsBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom.__doc__ + '\n' + '* ``PictureSource``: ' + QgsLayoutObject.DataDefinedProperty.PictureSource.__doc__ + '\n' + '* ``PictureSvgBackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgBackgroundColor.__doc__ + '\n' + '* ``PictureSvgStrokeColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeColor.__doc__ + '\n' + '* ``PictureSvgStrokeWidth``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeWidth.__doc__ + '\n' + '* ``SourceUrl``: ' + QgsLayoutObject.DataDefinedProperty.SourceUrl.__doc__ + '\n' + '* ``LegendTitle``: ' + QgsLayoutObject.DataDefinedProperty.LegendTitle.__doc__ + '\n' + '* ``LegendColumnCount``: ' + QgsLayoutObject.DataDefinedProperty.LegendColumnCount.__doc__ + '\n' + '* ``ScalebarLeftSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLeftSegments.__doc__ + '\n' + '* ``ScalebarRightSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegments.__doc__ + '\n' + '* ``ScalebarSegmentWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSegmentWidth.__doc__ + '\n' + '* ``ScalebarMinimumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMinimumWidth.__doc__ + '\n' + '* ``ScalebarMaximumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMaximumWidth.__doc__ + '\n' + '* ``ScalebarHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarHeight.__doc__ + '\n' + '* ``ScalebarRightSegmentSubdivisions``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegmentSubdivisions.__doc__ + '\n' + '* ``ScalebarSubdivisionHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSubdivisionHeight.__doc__ + '\n' + '* ``ScalebarFillColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor.__doc__ + '\n' + '* ``ScalebarFillColor2``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor2.__doc__ + '\n' + '* ``ScalebarLineColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineColor.__doc__ + '\n' + '* ``ScalebarLineWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineWidth.__doc__ + '\n' + '* ``AttributeTableSourceLayer``: ' + QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer.__doc__ + '\n' + '* ``MapCrs``: ' + QgsLayoutObject.DataDefinedProperty.MapCrs.__doc__ + '\n' + '* ``StartDateTime``: ' + QgsLayoutObject.DataDefinedProperty.StartDateTime.__doc__ + '\n' + '* ``EndDateTime``: ' + QgsLayoutObject.DataDefinedProperty.EndDateTime.__doc__ + '\n' + '* ``ElevationProfileTolerance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance.__doc__ + '\n' + '* ``ElevationProfileDistanceMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMajorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMinorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceLabelInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMajorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMinorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationLabelInterval.__doc__ + '\n' + '* ``ElevationProfileMinimumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumDistance.__doc__ + '\n' + '* ``ElevationProfileMaximumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumDistance.__doc__ + '\n' + '* ``ElevationProfileMinimumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumElevation.__doc__ + '\n' + '* ``ElevationProfileMaximumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation.__doc__ +QgsLayoutObject.DataDefinedProperty.__doc__ = "Data defined properties for different item types\n\n" + '* ``NoProperty``: ' + QgsLayoutObject.DataDefinedProperty.NoProperty.__doc__ + '\n' + '* ``AllProperties``: ' + QgsLayoutObject.DataDefinedProperty.AllProperties.__doc__ + '\n' + '* ``TestProperty``: ' + QgsLayoutObject.DataDefinedProperty.TestProperty.__doc__ + '\n' + '* ``PresetPaperSize``: ' + QgsLayoutObject.DataDefinedProperty.PresetPaperSize.__doc__ + '\n' + '* ``PaperWidth``: ' + QgsLayoutObject.DataDefinedProperty.PaperWidth.__doc__ + '\n' + '* ``PaperHeight``: ' + QgsLayoutObject.DataDefinedProperty.PaperHeight.__doc__ + '\n' + '* ``NumPages``: ' + QgsLayoutObject.DataDefinedProperty.NumPages.__doc__ + '\n' + '* ``PaperOrientation``: ' + QgsLayoutObject.DataDefinedProperty.PaperOrientation.__doc__ + '\n' + '* ``PageNumber``: ' + QgsLayoutObject.DataDefinedProperty.PageNumber.__doc__ + '\n' + '* ``PositionX``: ' + QgsLayoutObject.DataDefinedProperty.PositionX.__doc__ + '\n' + '* ``PositionY``: ' + QgsLayoutObject.DataDefinedProperty.PositionY.__doc__ + '\n' + '* ``ItemWidth``: ' + QgsLayoutObject.DataDefinedProperty.ItemWidth.__doc__ + '\n' + '* ``ItemHeight``: ' + QgsLayoutObject.DataDefinedProperty.ItemHeight.__doc__ + '\n' + '* ``ItemRotation``: ' + QgsLayoutObject.DataDefinedProperty.ItemRotation.__doc__ + '\n' + '* ``Transparency``: ' + QgsLayoutObject.DataDefinedProperty.Transparency.__doc__ + '\n' + '* ``Opacity``: ' + QgsLayoutObject.DataDefinedProperty.Opacity.__doc__ + '\n' + '* ``BlendMode``: ' + QgsLayoutObject.DataDefinedProperty.BlendMode.__doc__ + '\n' + '* ``ExcludeFromExports``: ' + QgsLayoutObject.DataDefinedProperty.ExcludeFromExports.__doc__ + '\n' + '* ``FrameColor``: ' + QgsLayoutObject.DataDefinedProperty.FrameColor.__doc__ + '\n' + '* ``BackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.BackgroundColor.__doc__ + '\n' + '* ``MarginLeft``: ' + QgsLayoutObject.DataDefinedProperty.MarginLeft.__doc__ + '\n' + '* ``MarginTop``: ' + QgsLayoutObject.DataDefinedProperty.MarginTop.__doc__ + '\n' + '* ``MarginRight``: ' + QgsLayoutObject.DataDefinedProperty.MarginRight.__doc__ + '\n' + '* ``MarginBottom``: ' + QgsLayoutObject.DataDefinedProperty.MarginBottom.__doc__ + '\n' + '* ``MapRotation``: ' + QgsLayoutObject.DataDefinedProperty.MapRotation.__doc__ + '\n' + '* ``MapScale``: ' + QgsLayoutObject.DataDefinedProperty.MapScale.__doc__ + '\n' + '* ``MapXMin``: ' + QgsLayoutObject.DataDefinedProperty.MapXMin.__doc__ + '\n' + '* ``MapYMin``: ' + QgsLayoutObject.DataDefinedProperty.MapYMin.__doc__ + '\n' + '* ``MapXMax``: ' + QgsLayoutObject.DataDefinedProperty.MapXMax.__doc__ + '\n' + '* ``MapYMax``: ' + QgsLayoutObject.DataDefinedProperty.MapYMax.__doc__ + '\n' + '* ``MapAtlasMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapAtlasMargin.__doc__ + '\n' + '* ``MapLayers``: ' + QgsLayoutObject.DataDefinedProperty.MapLayers.__doc__ + '\n' + '* ``MapStylePreset``: ' + QgsLayoutObject.DataDefinedProperty.MapStylePreset.__doc__ + '\n' + '* ``MapLabelMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapLabelMargin.__doc__ + '\n' + '* ``MapGridEnabled``: ' + QgsLayoutObject.DataDefinedProperty.MapGridEnabled.__doc__ + '\n' + '* ``MapGridIntervalX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalX.__doc__ + '\n' + '* ``MapGridIntervalY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalY.__doc__ + '\n' + '* ``MapGridOffsetX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetX.__doc__ + '\n' + '* ``MapGridOffsetY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetY.__doc__ + '\n' + '* ``MapGridFrameSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameSize.__doc__ + '\n' + '* ``MapGridFrameMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameMargin.__doc__ + '\n' + '* ``MapGridLabelDistance``: ' + QgsLayoutObject.DataDefinedProperty.MapGridLabelDistance.__doc__ + '\n' + '* ``MapGridCrossSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridCrossSize.__doc__ + '\n' + '* ``MapGridFrameLineThickness``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameLineThickness.__doc__ + '\n' + '* ``MapGridAnnotationDisplayLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayLeft.__doc__ + '\n' + '* ``MapGridAnnotationDisplayRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayRight.__doc__ + '\n' + '* ``MapGridAnnotationDisplayTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayTop.__doc__ + '\n' + '* ``MapGridAnnotationDisplayBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayBottom.__doc__ + '\n' + '* ``MapGridFrameDivisionsLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsLeft.__doc__ + '\n' + '* ``MapGridFrameDivisionsRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsRight.__doc__ + '\n' + '* ``MapGridFrameDivisionsTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsTop.__doc__ + '\n' + '* ``MapGridFrameDivisionsBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom.__doc__ + '\n' + '* ``MapCrs``: ' + QgsLayoutObject.DataDefinedProperty.MapCrs.__doc__ + '\n' + '* ``StartDateTime``: ' + QgsLayoutObject.DataDefinedProperty.StartDateTime.__doc__ + '\n' + '* ``EndDateTime``: ' + QgsLayoutObject.DataDefinedProperty.EndDateTime.__doc__ + '\n' + '* ``MapZRangeLower``: ' + QgsLayoutObject.DataDefinedProperty.MapZRangeLower.__doc__ + '\n' + '* ``MapZRangeUpper``: ' + QgsLayoutObject.DataDefinedProperty.MapZRangeUpper.__doc__ + '\n' + '* ``PictureSource``: ' + QgsLayoutObject.DataDefinedProperty.PictureSource.__doc__ + '\n' + '* ``PictureSvgBackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgBackgroundColor.__doc__ + '\n' + '* ``PictureSvgStrokeColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeColor.__doc__ + '\n' + '* ``PictureSvgStrokeWidth``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeWidth.__doc__ + '\n' + '* ``SourceUrl``: ' + QgsLayoutObject.DataDefinedProperty.SourceUrl.__doc__ + '\n' + '* ``LegendTitle``: ' + QgsLayoutObject.DataDefinedProperty.LegendTitle.__doc__ + '\n' + '* ``LegendColumnCount``: ' + QgsLayoutObject.DataDefinedProperty.LegendColumnCount.__doc__ + '\n' + '* ``ScalebarLeftSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLeftSegments.__doc__ + '\n' + '* ``ScalebarRightSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegments.__doc__ + '\n' + '* ``ScalebarSegmentWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSegmentWidth.__doc__ + '\n' + '* ``ScalebarMinimumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMinimumWidth.__doc__ + '\n' + '* ``ScalebarMaximumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMaximumWidth.__doc__ + '\n' + '* ``ScalebarHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarHeight.__doc__ + '\n' + '* ``ScalebarRightSegmentSubdivisions``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegmentSubdivisions.__doc__ + '\n' + '* ``ScalebarSubdivisionHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSubdivisionHeight.__doc__ + '\n' + '* ``ScalebarFillColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor.__doc__ + '\n' + '* ``ScalebarFillColor2``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor2.__doc__ + '\n' + '* ``ScalebarLineColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineColor.__doc__ + '\n' + '* ``ScalebarLineWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineWidth.__doc__ + '\n' + '* ``AttributeTableSourceLayer``: ' + QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer.__doc__ + '\n' + '* ``ElevationProfileTolerance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance.__doc__ + '\n' + '* ``ElevationProfileDistanceMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMajorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMinorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceLabelInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMajorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMinorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationLabelInterval.__doc__ + '\n' + '* ``ElevationProfileMinimumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumDistance.__doc__ + '\n' + '* ``ElevationProfileMaximumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumDistance.__doc__ + '\n' + '* ``ElevationProfileMinimumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumElevation.__doc__ + '\n' + '* ``ElevationProfileMaximumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation.__doc__ # -- QgsLayoutObject.EvaluatedValue = QgsLayoutObject.PropertyValueType.EvaluatedValue QgsLayoutObject.OriginalValue = QgsLayoutObject.PropertyValueType.OriginalValue diff --git a/python/PyQt6/core/auto_additions/qgsmaplayerelevationproperties.py b/python/PyQt6/core/auto_additions/qgsmaplayerelevationproperties.py index 050475981e38..14ca544969d4 100644 --- a/python/PyQt6/core/auto_additions/qgsmaplayerelevationproperties.py +++ b/python/PyQt6/core/auto_additions/qgsmaplayerelevationproperties.py @@ -6,7 +6,13 @@ QgsMapLayerElevationProperties.ExtrusionHeight = QgsMapLayerElevationProperties.Property.ExtrusionHeight QgsMapLayerElevationProperties.ExtrusionHeight.is_monkey_patched = True QgsMapLayerElevationProperties.ExtrusionHeight.__doc__ = "Extrusion height" -QgsMapLayerElevationProperties.Property.__doc__ = "Data definable properties.\n\n.. versionadded:: 3.26\n\n" + '* ``ZOffset``: ' + QgsMapLayerElevationProperties.Property.ZOffset.__doc__ + '\n' + '* ``ExtrusionHeight``: ' + QgsMapLayerElevationProperties.Property.ExtrusionHeight.__doc__ +QgsMapLayerElevationProperties.RasterPerBandLowerElevation = QgsMapLayerElevationProperties.Property.RasterPerBandLowerElevation +QgsMapLayerElevationProperties.RasterPerBandLowerElevation.is_monkey_patched = True +QgsMapLayerElevationProperties.RasterPerBandLowerElevation.__doc__ = "Lower elevation for each raster band (since QGIS 3.38)" +QgsMapLayerElevationProperties.RasterPerBandUpperElevation = QgsMapLayerElevationProperties.Property.RasterPerBandUpperElevation +QgsMapLayerElevationProperties.RasterPerBandUpperElevation.is_monkey_patched = True +QgsMapLayerElevationProperties.RasterPerBandUpperElevation.__doc__ = "Upper elevation for each raster band (since QGIS 3.38)" +QgsMapLayerElevationProperties.Property.__doc__ = "Data definable properties.\n\n.. versionadded:: 3.26\n\n" + '* ``ZOffset``: ' + QgsMapLayerElevationProperties.Property.ZOffset.__doc__ + '\n' + '* ``ExtrusionHeight``: ' + QgsMapLayerElevationProperties.Property.ExtrusionHeight.__doc__ + '\n' + '* ``RasterPerBandLowerElevation``: ' + QgsMapLayerElevationProperties.Property.RasterPerBandLowerElevation.__doc__ + '\n' + '* ``RasterPerBandUpperElevation``: ' + QgsMapLayerElevationProperties.Property.RasterPerBandUpperElevation.__doc__ # -- QgsMapLayerElevationProperties.FlagDontInvalidateCachedRendersWhenRangeChanges = QgsMapLayerElevationProperties.Flag.FlagDontInvalidateCachedRendersWhenRangeChanges QgsMapLayerElevationProperties.Flags = lambda flags=0: QgsMapLayerElevationProperties.Flag(flags) diff --git a/python/PyQt6/core/auto_generated/auth/qgsauthmanager.sip.in b/python/PyQt6/core/auto_generated/auth/qgsauthmanager.sip.in index 6d29f7c9e2ca..1f8697ab4342 100644 --- a/python/PyQt6/core/auto_generated/auth/qgsauthmanager.sip.in +++ b/python/PyQt6/core/auto_generated/auth/qgsauthmanager.sip.in @@ -161,7 +161,7 @@ Check whether supplied password is the same as the one already set bool resetMasterPassword( const QString &newpass, const QString &oldpass, bool keepbackup, QString *backuppath /In,Out/ = 0 ); %Docstring Reset the master password to a new one, then re-encrypt all previous -configs in a new database file, optionally backup curren database +configs in a new database file, optionally backup current database :param newpass: New master password to replace existing :param oldpass: Current master password to replace existing diff --git a/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in b/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in index 1de5b8c42365..50ef0d003651 100644 --- a/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in +++ b/python/PyQt6/core/auto_generated/dxf/qgsdxfexport.sip.in @@ -27,7 +27,7 @@ Exports QGIS layers to the DXF format. struct DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1 ); + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = false, int ddBlocksMaxNumberOfClasses = -1 ); QgsVectorLayer *layer() const; %Docstring @@ -49,6 +49,24 @@ will be split into several dxf layers, one per each unique value. .. versionadded:: 3.12 +%End + + bool buildDataDefinedBlocks() const; +%Docstring +Flag if data defined point block symbols should be created. Default is false + +:return: True if data defined point block symbols should be created + +.. versionadded:: 3.38 +%End + + int dataDefinedBlocksMaximumNumberOfClasses() const; +%Docstring +Returns the maximum number of data defined symbol classes for which blocks are created. Returns -1 if there is no such limitation + +:return: + +.. versionadded:: 3.38 %End }; @@ -56,6 +74,7 @@ unique value. enum Flag /BaseType=IntEnum/ { FlagNoMText, + FlagOnlySelectedFeatures, }; typedef QFlags Flags; diff --git a/python/PyQt6/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/PyQt6/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index 6bb225783328..f5d5448599ee 100644 --- a/python/PyQt6/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/PyQt6/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -24,7 +24,7 @@ features on another layer. public: struct ValueRelationItem { - ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString() ); + ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString(), const QVariant group = QVariant() ); %Docstring Constructor for ValueRelationItem %End @@ -37,6 +37,7 @@ Constructor for ValueRelationItem QVariant key; QString value; QString description; + QVariant group; }; typedef QVector < QgsValueRelationFieldFormatter::ValueRelationItem > ValueRelationCache; diff --git a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in index 40ed5109df7d..b42346c5e778 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in @@ -107,7 +107,7 @@ The azimuth always takes the default value. :param pt2_tg3: Second point of the third tangent. :param epsilon: Value used to compare point. :param pos: Point to determine which circle use in case of multi return. - If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangets are parallels. (since QGIS 3.18) + If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangents are parallels. (since QGIS 3.18) .. seealso:: :py:func:`from3TangentsMulti` diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in index 85a93c028670..56bc1221b191 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in @@ -708,6 +708,17 @@ on this geometry (including if this geometry is a Point). It is up to the caller to distinguish between these error conditions. (Or maybe we add another method to this object to help make the distinction?) +%End + + bool addTopologicalPoint( const QgsPoint &point, double snappingTolerance = 1e-8, double segmentSearchEpsilon = 1e-12 ); +%Docstring +Adds a vertex to the segment which intersect ``point`` but don't +already have a vertex there. Closest segment is identified using ``segmentSearchEpsilon``. +If a vertex already exists within ``snappingTolearnceDistance``, no additional vertex is inserted. + +:return: ``True`` if point was added, ``False`` otherwise + +.. versionadded:: 3.38 %End bool moveVertex( double x, double y, int atVertex ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgsrectangle.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsrectangle.sip.in index 2c2538134da9..48c377d0358c 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsrectangle.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsrectangle.sip.in @@ -375,7 +375,11 @@ the specified ``spacing`` between the grid lines. SIP_PYOBJECT __repr__(); %MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); + QString str; + if ( sipCpp->isNull() ) + str = QStringLiteral( "" ); + else + str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End diff --git a/python/PyQt6/core/auto_generated/gps/qgsgpsdetector.sip.in b/python/PyQt6/core/auto_generated/gps/qgsgpsdetector.sip.in index 50011ec27f82..3f4627084ba1 100644 --- a/python/PyQt6/core/auto_generated/gps/qgsgpsdetector.sip.in +++ b/python/PyQt6/core/auto_generated/gps/qgsgpsdetector.sip.in @@ -23,11 +23,41 @@ Class to detect the GPS port #include "qgsgpsdetector.h" %End public: - QgsGpsDetector( const QString &portName ); + + + QgsGpsDetector( const QString &portName = QString(), bool useUnsafeSignals = true ); +%Docstring +Constructor for QgsGpsDetector. + +If ``portName`` is specified, then only devices from the given port will be scanned. Otherwise +all connection types will be attempted (including internal GPS devices). + +Since QGIS 3.38, the ``useUnsafeSignals`` parameter can be set to ``False`` to avoid emitting the +dangerous and fragile :py:func:`~QgsGpsDetector.detected` signal. This is highly recommended, but is opt-in to avoid +breaking stable QGIS 3.x API. If ``useUnsafeSignals`` is set to ``False``, only the safe :py:func:`~QgsGpsDetector.connectionDetected` signal +will be emitted and clients must manually take ownership of the detected connection via a call +to :py:func:`~QgsGpsDetector.takeConnection`. +%End ~QgsGpsDetector(); + QgsGpsConnection *takeConnection() /TransferBack/; +%Docstring +Returns the detected GPS connection, and removes it from the detector. + +The caller takes ownership of the connection. Only the first call to this +method following a :py:func:`~QgsGpsDetector.connectionDetected` signal will be able to retrieve the +detected connection -- subsequent calls will return ``None``. + +.. warning:: + + Do not call this method if the useUnsafeSignals option in the + QgsGpsDetector constructor was set to ``True``. + +.. versionadded:: 3.38 +%End + static QList< QPair > availablePorts(); public slots: @@ -37,14 +67,28 @@ Class to detect the GPS port signals: + void connectionDetected(); +%Docstring +Emitted when a GPS connection is successfully detected. + +Call :py:func:`~QgsGpsDetector.takeConnection` to take ownership of the detected connection. - void detected( QgsGpsConnection *connection ); +.. versionadded:: 3.38 +%End + + void detected( QgsGpsConnection *connection ) /Deprecated/; %Docstring Emitted when the GPS connection has been detected. A single connection must listen for this signal and immediately take ownership of the ``connection`` object. + +.. deprecated:: + This signal is dangerous and extremely unsafe! It is recommended to instead set the ``useUnsafeSignals`` parameter to ``False`` in the QgsGpsDetector constructor and use the safe :py:func:`~QgsGpsDetector.connectionDetected` signal instead. %End void detectionFailed(); +%Docstring +Emitted when the detector could not find a valid GPS connection. +%End }; diff --git a/python/PyQt6/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in b/python/PyQt6/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in index a4d8b172d9cd..64cd94dca688 100644 --- a/python/PyQt6/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in +++ b/python/PyQt6/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in @@ -208,7 +208,7 @@ Returns ``True`` if a continuous gradient legend will be used. void setUseContinuousLegend( bool useContinuousLegend ); %Docstring -Sets the flag to use a continuos gradient legend to ``useContinuousLegend``. +Sets the flag to use a continuous gradient legend to ``useContinuousLegend``. When this flag is set the legend will be rendered using a continuous color ramp with min and max values, when it is not set the legend will be rendered using separate diff --git a/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index 1f198a035bd5..22ce85d34b1c 100644 --- a/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/PyQt6/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -477,6 +477,15 @@ Evaluates and returns the text label of the current node :param label: text to evaluate instead of the layer layertree string .. versionadded:: 3.10 +%End + + QgsExpressionContextScope *createSymbolScope() const /Factory/; +%Docstring +Create an expression context scope containing symbol related variables. + +The caller takes ownership of the returned object. + +.. versionadded:: 3.36 %End SIP_PYOBJECT __repr__(); diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutitemmap.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutitemmap.sip.in index eecc75d8bbc5..eea97e4b484c 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutitemmap.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutitemmap.sip.in @@ -923,6 +923,60 @@ Returns the map's atlas clipping settings. Returns the map's item based clip path settings. .. versionadded:: 3.16 +%End + + void setZRangeEnabled( bool enabled ); +%Docstring +Sets whether the z range is ``enabled`` (i.e. whether the map will be filtered +to content within the :py:func:`~QgsLayoutItemMap.zRange`.) + +.. seealso:: :py:func:`zRangeEnabled` + +.. versionadded:: 3.38 +%End + + bool zRangeEnabled() const; +%Docstring +Returns whether the z range is enabled (i.e. whether the map will be filtered +to content within the :py:func:`~QgsLayoutItemMap.zRange`.) + +.. seealso:: :py:func:`setZRangeEnabled` + +.. seealso:: :py:func:`zRange` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange zRange() const; +%Docstring +Returns the map's z range, which is used to filter the map's content to only +display features within the specified z range. + +.. note:: + + This is only considered when :py:func:`~QgsLayoutItemMap.zRangeEnabled` is ``True``. + +.. seealso:: :py:func:`setZRange` + +.. seealso:: :py:func:`zRangeEnabled` + +.. versionadded:: 3.38 +%End + + void setZRange( const QgsDoubleRange &range ); +%Docstring +Sets the map's z ``range``, which is used to filter the map's content to only +display features within the specified z range. + +.. note:: + + This is only considered when :py:func:`~QgsLayoutItemMap.zRangeEnabled` is ``True``. + +.. seealso:: :py:func:`zRange` + +.. seealso:: :py:func:`setZRangeEnabled` + +.. versionadded:: 3.38 %End virtual double estimatedFrameBleed() const; diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in index 33d7baeedf14..7eed0aadf36b 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in @@ -41,7 +41,7 @@ Add a node in current shape. :param point: is the location of the new node (in scene coordinates) :param checkArea: is a flag to indicate if there's a space constraint. -:param radius: is the space contraint and is used only if checkArea is +:param radius: is the space constraint and is used only if checkArea is ``True``. Typically, if this flag is ``True``, the new node has to be nearer than radius to the shape to be added. %End diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutobject.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutobject.sip.in index b1448a3d0d5a..18a7ddc60a69 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutobject.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutobject.sip.in @@ -167,6 +167,11 @@ A base class for objects which belong to a layout. MapGridFrameDivisionsRight, MapGridFrameDivisionsTop, MapGridFrameDivisionsBottom, + MapCrs, + StartDateTime, + EndDateTime, + MapZRangeLower, + MapZRangeUpper, //composer picture PictureSource, PictureSvgBackgroundColor, @@ -192,9 +197,6 @@ A base class for objects which belong to a layout. ScalebarLineWidth, //table item AttributeTableSourceLayer, - MapCrs, - StartDateTime, - EndDateTime, ElevationProfileTolerance, ElevationProfileDistanceMajorInterval, ElevationProfileDistanceMinorInterval, diff --git a/python/PyQt6/core/auto_generated/locator/qgslocatorfilter.sip.in b/python/PyQt6/core/auto_generated/locator/qgslocatorfilter.sip.in index 8ca16a6d9dd8..55a00448f297 100644 --- a/python/PyQt6/core/auto_generated/locator/qgslocatorfilter.sip.in +++ b/python/PyQt6/core/auto_generated/locator/qgslocatorfilter.sip.in @@ -32,14 +32,14 @@ Constructor for QgsLocatorResult. Constructor for QgsLocatorResult. %End - QVariant getUserData() const; + QVariant userData() const /PyName=_userData/; %Docstring Returns the ``userData``. .. versionadded:: 3.18 %End - void setUserData( QVariant userData ); + void setUserData( const QVariant &userData ); %Docstring Set ``userData`` for the locator result @@ -80,8 +80,6 @@ normally. QList actions; -%Property( name = userData, get = getUserData, set = setUserData ) - }; diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in index 381b9590a684..9af2a8fc1b1f 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in @@ -100,7 +100,7 @@ Returns the relative time in milliseconds of the dataset void clear(); %Docstring -Clears alls stored reference times and dataset times +Clears all stored reference times and dataset times %End qint64 firstTimeStepDuration( int group ) const; diff --git a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index ddb9592ae4e2..f15a152d4cf7 100644 --- a/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -40,12 +40,68 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual QgsMeshLayerElevationProperties *clone() const /Factory/; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + + + Qgis::MeshElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::MeshElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End QgsLineSymbol *profileLineSymbol() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/network/qgsnetworkaccessmanager.sip.in b/python/PyQt6/core/auto_generated/network/qgsnetworkaccessmanager.sip.in index 25b6be19976b..e2c6e0ed6b93 100644 --- a/python/PyQt6/core/auto_generated/network/qgsnetworkaccessmanager.sip.in +++ b/python/PyQt6/core/auto_generated/network/qgsnetworkaccessmanager.sip.in @@ -11,7 +11,6 @@ - class QgsNetworkRequestParameters { %Docstring(signature="appended") @@ -490,11 +489,30 @@ This signal is propagated to the main thread QgsNetworkAccessManager instance, s only to connect to the main thread's signal in order to receive notifications about requests created in any thread. +.. seealso:: :py:func:`requestCreated` + .. seealso:: :py:func:`finished` .. seealso:: :py:func:`requestTimedOut` .. versionadded:: 3.6 +%End + + void requestCreated( const QgsNetworkRequestParameters &request ); +%Docstring +Emitted when a network request has been created. + +This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary +only to connect to the main thread's signal in order to receive notifications about requests +created in any thread. + +.. seealso:: :py:func:`requestAboutToBeCreated` + +.. seealso:: :py:func:`finished` + +.. seealso:: :py:func:`requestTimedOut` + +.. versionadded:: 3.38 %End void finished( QgsNetworkReplyContent reply ); diff --git a/python/PyQt6/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in b/python/PyQt6/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in index e0ebab508443..c3ba0ab60654 100644 --- a/python/PyQt6/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in +++ b/python/PyQt6/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in @@ -246,7 +246,7 @@ in the metadata, and even for sources with statistical metadata only some ``stat QgsPointCloudStatistics metadataStatistics(); %Docstring -Returns the object containings the statistics metadata extracted from the dataset +Returns the object containing the statistics metadata extracted from the dataset .. versionadded:: 3.26 %End diff --git a/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in index 3d2c7331f01a..d95e37a0c5ee 100644 --- a/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in @@ -38,7 +38,7 @@ Constructor for QgsPointCloudLayerElevationProperties, with the specified ``pare virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; diff --git a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index d3f26e89b41e..404571d1f5b8 100644 --- a/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/PyQt6/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -323,6 +323,17 @@ Creates a CRS from a specified QGIS SRS ID. .. seealso:: :py:func:`validSrsIds` %End + static QgsCoordinateReferenceSystem createCompoundCrs( const QgsCoordinateReferenceSystem &horizontalCrs, const QgsCoordinateReferenceSystem &verticalCrs ); +%Docstring +Given a horizontal and vertical CRS, attempts to create a compound CRS +from them. + +Returns an invalid CRS if the inputs are not suitable for a compound CRS, +or the compound CRS could not be created for the combination. + +.. versionadded:: 3.38 +%End + bool createFromId( long id, CrsType type = PostgisCrsId ) /Deprecated/; @@ -975,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 @@ -1074,6 +1093,32 @@ May return an invalid CRS if the geographic CRS could not be determined. This method will always return a longitude, latitude ordered CRS. .. versionadded:: 3.24 +%End + + QgsCoordinateReferenceSystem horizontalCrs() const; +%Docstring +Returns the horizontal CRS associated with this CRS object. + +In the case of a compound CRS, this method will return just the horizontal CRS component. + +An invalid CRS will be returned if the object does not contain a horizontal component. + +.. seealso:: :py:func:`verticalCrs` + +.. versionadded:: 3.38 +%End + + QgsCoordinateReferenceSystem verticalCrs() const; +%Docstring +Returns the vertical CRS associated with this CRS object. + +In the case of a compound CRS, this method will return just the vertical CRS component. + +An invalid CRS will be returned if the object does not contain a vertical component. + +.. seealso:: :py:func:`horizontalCrs` + +.. versionadded:: 3.38 %End QString geographicCrsAuthId() const; diff --git a/python/PyQt6/core/auto_generated/project/qgsprojectelevationproperties.sip.in b/python/PyQt6/core/auto_generated/project/qgsprojectelevationproperties.sip.in index 6f972be1fa4b..76d275d57c3e 100644 --- a/python/PyQt6/core/auto_generated/project/qgsprojectelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/project/qgsprojectelevationproperties.sip.in @@ -67,6 +67,44 @@ Sets the project's terrain ``provider``. Ownership of ``provider`` is transferred to this object. .. seealso:: :py:func:`terrainProvider` +%End + + QgsDoubleRange elevationRange() const; +%Docstring +Returns the project's elevation range, which indicates the upper and lower +elevation limits associated with the project. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`setElevationRange` + +.. seealso:: :py:func:`elevationRangeChanged` + + +.. versionadded:: 3.38 +%End + + public slots: + + void setElevationRange( const QgsDoubleRange &range ); +%Docstring +Sets the project's elevation ``range``, which indicates the upper and lower +elevation limits associated with the project. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`elevationRange` + +.. seealso:: :py:func:`elevationRangeChanged` + + +.. versionadded:: 3.38 %End signals: @@ -74,6 +112,23 @@ Ownership of ``provider`` is transferred to this object. void changed(); %Docstring Emitted when the elevation properties change. +%End + + void elevationRangeChanged( const QgsDoubleRange &range ); +%Docstring +Emitted when the project's elevation ``is`` changed. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`elevationRange` + +.. seealso:: :py:func:`setElevationRange` + + +.. versionadded:: 3.38 %End }; diff --git a/python/PyQt6/core/auto_generated/providers/qgsdataprovider.sip.in b/python/PyQt6/core/auto_generated/providers/qgsdataprovider.sip.in index 10232c3e89b0..42274940d0fa 100644 --- a/python/PyQt6/core/auto_generated/providers/qgsdataprovider.sip.in +++ b/python/PyQt6/core/auto_generated/providers/qgsdataprovider.sip.in @@ -175,16 +175,26 @@ This may be ``None``, depending on the data provider. virtual QgsRectangle extent() const = 0; %Docstring -Returns the extent of the layer +Returns the extent of the layer. -:return: :py:class:`QgsRectangle` containing the extent of the layer +.. warning:: + + This may be expensive to calculate for some data providers, as it may involve + additional network requests or in some cases, iterating through all the features in a layer. + If the provider returns the :py:class:`Qgis`.DataProviderFlag.FastExtent2D flag from the :py:func:`~QgsDataProvider.flags` method + then the call to :py:func:`~QgsDataProvider.extent` is guaranteed to ALWAYS be fast and not involve any additional work. %End virtual QgsBox3D extent3D() const; %Docstring -Returns the 3D extent of the layer +Returns the 3D extent of the layer. + +.. warning:: -:return: :py:class:`QgsBox3D` containing the 3D extent of the layer + This may be expensive to calculate for some data providers, as it may involve + additional network requests or in some cases, iterating through all the features in a layer. + If the provider returns the :py:class:`Qgis`.DataProviderFlag.FastExtent3D flag from the :py:func:`~QgsDataProvider.flags` method + then the call to :py:func:`~QgsDataProvider.extent3D` is guaranteed to ALWAYS be fast and not involve any additional work. .. versionadded:: 3.36 %End 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 62e86d15bc20..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,12 +55,40 @@ 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 ); %Docstring Returns a filter string which restricts results to those matching the specified ``entityType`` and ``wkbType``. +%End + + static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent ); +%Docstring +Returns a filter string which restricts results to those within the specified +``extent``. + +The ``extent`` should always be specified in EPSG:4326. + +.. versionadded:: 3.38 +%End + + static QString combineFilters( const QStringList &filters ); +%Docstring +Combines a set of SensorThings API filter operators. + +See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter + +.. versionadded:: 3.38 %End static QList< Qgis::GeometryType > availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback = 0, const QString &authCfg = QString() ); diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 17a500b6d459..38b07fa1399c 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -691,6 +691,14 @@ The development version + enum class RasterRendererCapability /BaseType=IntFlag/ + { + UsesMultipleBands, + }; + + typedef QFlags RasterRendererCapabilities; + + enum class RasterAttributeTableFieldUsage /BaseType=IntEnum/ { Generic, @@ -1068,6 +1076,8 @@ The development version enum class DataProviderFlag /BaseType=IntFlag/ { IsBasemapSource, + FastExtent2D, + FastExtent3D, }; typedef QFlags DataProviderFlags; @@ -1247,6 +1257,7 @@ The development version FixedTemporalRange, TemporalRangeFromDataProvider, RedrawLayerOnly, + FixedRangePerBand, }; enum class TemporalIntervalMatchMethod /BaseType=IntEnum/ @@ -1833,6 +1844,28 @@ The development version Centroid, }; + enum class RangeLimits /BaseType=IntEnum/ + { + IncludeBoth, + IncludeLowerExcludeUpper, + ExcludeLowerIncludeUpper, + ExcludeBoth, + }; + + enum class RasterElevationMode /BaseType=IntEnum/ + { + FixedElevationRange, + RepresentsElevationSurface, + FixedRangePerBand, + DynamicRangePerBand, + }; + + enum class MeshElevationMode /BaseType=IntEnum/ + { + FixedElevationRange, + FromVertices + }; + enum class BetweenLineConstraint /BaseType=IntEnum/ { NoConstraint, @@ -2718,6 +2751,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; @@ -2842,6 +2876,8 @@ QFlags operator|(Qgis::ProjectReadFlag f1, QFlags operator|(Qgis::RasterRendererFlag f1, QFlags f2); +QFlags operator|(Qgis::RasterRendererCapability f1, QFlags f2); + QFlags operator|(Qgis::RasterTemporalCapabilityFlag f1, QFlags f2); QFlags operator|(Qgis::RelationshipCapability f1, QFlags f2); diff --git a/python/PyQt6/core/auto_generated/qgsdbquerylog.sip.in b/python/PyQt6/core/auto_generated/qgsdbquerylog.sip.in index a8ce8473683f..6fc5626a0a2a 100644 --- a/python/PyQt6/core/auto_generated/qgsdbquerylog.sip.in +++ b/python/PyQt6/core/auto_generated/qgsdbquerylog.sip.in @@ -51,7 +51,6 @@ Constructor for QgsDatabaseQueryLogEntry. }; - class QgsDatabaseQueryLog: QObject { %Docstring(signature="appended") diff --git a/python/PyQt6/core/auto_generated/qgsfieldproxymodel.sip.in b/python/PyQt6/core/auto_generated/qgsfieldproxymodel.sip.in index 7a81450b2cc0..5d58c8040ae1 100644 --- a/python/PyQt6/core/auto_generated/qgsfieldproxymodel.sip.in +++ b/python/PyQt6/core/auto_generated/qgsfieldproxymodel.sip.in @@ -35,6 +35,7 @@ The :py:class:`QgsFieldProxyModel` class provides an easy to use model to displa DateTime, Binary, Boolean, + OriginProvider, AllTypes, }; typedef QFlags Filters; diff --git a/python/PyQt6/core/auto_generated/qgsidentifycontext.sip.in b/python/PyQt6/core/auto_generated/qgsidentifycontext.sip.in index 167b85bf820a..ecfbaa826874 100644 --- a/python/PyQt6/core/auto_generated/qgsidentifycontext.sip.in +++ b/python/PyQt6/core/auto_generated/qgsidentifycontext.sip.in @@ -48,6 +48,27 @@ Returns the datetime range to be used with the identify action. bool isTemporal() const; %Docstring Returns ``True`` if the temporal range setting is enabled. +%End + + QgsDoubleRange zRange() const; +%Docstring +Returns the range of z-values to identify within, or an infinite range if no filtering by +z should be applied. + +.. seealso:: :py:func:`setZRange` + +.. versionadded:: 3.38 +%End + + void setZRange( const QgsDoubleRange &range ); +%Docstring +Sets the ``range`` of z-values to identify within. + +Set to an infinite range if no filtering by z should be applied. + +.. seealso:: :py:func:`zRange` + +.. versionadded:: 3.38 %End }; 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/qgsmaplayer.sip.in b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in index a5ecda2dc966..6bdbf089cae3 100644 --- a/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmaplayer.sip.in @@ -1438,7 +1438,7 @@ Returns ``True`` if auto refresh is enabled for the layer. Qgis::AutoRefreshMode autoRefreshMode() const; %Docstring -Returns the layer's automatical refresh mode. +Returns the layer's automatic refresh mode. .. seealso:: :py:func:`autoRefreshInterval` @@ -1602,7 +1602,7 @@ Returns the layer's elevation properties. This may be ``None``, depending on the %Docstring Returns path to the placeholder image or an empty string if a generated legend is shown -:return: placholder image path +:return: placeholder image path .. versionadded:: 3.22 %End diff --git a/python/PyQt6/core/auto_generated/qgsmaplayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/qgsmaplayerelevationproperties.sip.in index d2fc424f43ca..7c36d58fe714 100644 --- a/python/PyQt6/core/auto_generated/qgsmaplayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmaplayerelevationproperties.sip.in @@ -65,6 +65,8 @@ how an individual :py:class:`QgsMapLayer` behaves with relation to z values or e { ZOffset, ExtrusionHeight, + RasterPerBandLowerElevation, + RasterPerBandUpperElevation, }; enum Flag /BaseType=IntEnum/ @@ -119,9 +121,11 @@ Creates a clone of the properties. .. versionadded:: 3.26 %End - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; %Docstring Returns ``True`` if the layer should be visible and rendered for the specified z ``range``. + +Since QGIS 3.38 the ``layer`` argument can be used to specify the target layer. %End virtual QgsMapLayerElevationProperties::Flags flags() const; diff --git a/python/PyQt6/core/auto_generated/qgsmatrix4x4.sip.in b/python/PyQt6/core/auto_generated/qgsmatrix4x4.sip.in index 5a7f165ee461..0ee0c3cdd82c 100644 --- a/python/PyQt6/core/auto_generated/qgsmatrix4x4.sip.in +++ b/python/PyQt6/core/auto_generated/qgsmatrix4x4.sip.in @@ -60,7 +60,7 @@ Multiplies this matrix by another that translates coordinates by the components QgsVector3D map( const QgsVector3D &vector ) const /HoldGIL/; %Docstring -Matrix-vector multiplication (vector is converted to homogenous coordinates [X,Y,Z,1] and back) +Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,Z,1] and back) %End bool isIdentity() const /HoldGIL/; diff --git a/python/PyQt6/core/auto_generated/qgsrange.sip.in b/python/PyQt6/core/auto_generated/qgsrange.sip.in index b3c43147015f..12f109f64d0b 100644 --- a/python/PyQt6/core/auto_generated/qgsrange.sip.in +++ b/python/PyQt6/core/auto_generated/qgsrange.sip.in @@ -41,6 +41,14 @@ whether ranges overlap or during calculation of range intersections. %Docstring Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, and optionally whether or not these bounds are included in the range. +%End + + QgsRange( T lower, T upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 %End T lower() const; @@ -79,6 +87,13 @@ bound is exclusive. .. seealso:: :py:func:`upper` .. seealso:: :py:func:`includeLower` +%End + + Qgis::RangeLimits rangeLimits() const; +%Docstring +Returns the limit handling of the range. + +.. versionadded:: 3.38 %End bool isEmpty() const; @@ -146,6 +161,14 @@ typedef QgsRange QgsRangedoubleBase; %End public: + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsDoubleRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsDoubleRange( double lower, double upper, @@ -186,6 +209,7 @@ Returns ``True`` if the range consists of all possible values. + typedef QgsRange QgsRangeintBase; class QgsIntRange : QgsRangeintBase @@ -206,6 +230,14 @@ typedef QgsRange QgsRangeintBase; %End public: + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsIntRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsIntRange( int lower, int upper, @@ -241,6 +273,7 @@ Returns ``True`` if the range consists of all possible values. }; + template class QgsTemporalRange { diff --git a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in index cc5d267880c4..503741aa9925 100644 --- a/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -56,21 +56,31 @@ Factory method to create a new renderer virtual QList usesBands() const; + virtual int inputBand() const; + virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.setInputBand` instead %End + virtual bool setInputBand( int band ); + double azimuth() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 40300d544def..fabf4a25c007 100644 --- a/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -122,11 +122,19 @@ Returns optional category label Set category label %End - int band() const; + int band() const /Deprecated/; %Docstring Returns the raster band used for rendering the raster. + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsPalettedRasterRenderer.inputBand` instead %End + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; virtual QList< QPair< QString, QColor > > legendSymbologyItems() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index 76ac4cd2e463..55f8394fed14 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -50,16 +50,10 @@ Creates an instance of the renderer based on definition from XML (used by render virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) /Factory/; + virtual int inputBand() const; + virtual bool setInputBand( int band ); - int inputBand() const; -%Docstring -Returns the number of the input raster band -%End - void setInputBand( int band ); -%Docstring -Sets the number of the input raster band -%End double contourInterval() const; %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 64804ffd4d08..e17a74574779 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -40,12 +40,14 @@ Constructor for QgsRasterLayerElevationProperties, with the specified ``parent`` virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + bool isEnabled() const; %Docstring @@ -59,12 +61,34 @@ Returns ``True`` if the elevation properties are enabled, i.e. the raster layer Sets whether the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. .. seealso:: :py:func:`isEnabled` +%End + + Qgis::RasterElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::RasterElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 %End int bandNumber() const; %Docstring Returns the band number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`setBandNumber` %End @@ -72,7 +96,104 @@ Returns the band number from which the elevation should be taken. %Docstring Sets the ``band`` number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`bandNumber` +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerBand` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange elevationRangeForPixelValue( QgsRasterLayer *layer, int band, double pixelValue ) const; +%Docstring +Returns the elevation range corresponding to a raw pixel value from the specified ``band``. + +Returns an infinite range if the pixel value does not correspond to an elevation value. + +.. versionadded:: 3.38 +%End + + int bandForElevationRange( QgsRasterLayer *layer, const QgsDoubleRange &range ) const; +%Docstring +Returns the band corresponding to the specified ``range``. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand or + :py:class:`Qgis`.RasterElevationMode.DynamicRangePerBand. For other modes it will always return -1. + +.. versionadded:: 3.38 %End QgsLineSymbol *profileLineSymbol() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 8e9e12e9b208..62d35f120e7b 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -10,6 +10,7 @@ + class QgsRasterLayerTemporalProperties : QgsMapLayerTemporalProperties { %Docstring(signature="appended") @@ -80,7 +81,7 @@ a render context intersects the specified ``range``. .. warning:: This setting is only effective when :py:func:`~QgsRasterLayerTemporalProperties.mode` is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange + :py:class:`Qgis`.RasterTemporalMode.FixedTemporalRange .. seealso:: :py:func:`fixedTemporalRange` %End @@ -91,10 +92,54 @@ Returns the fixed temporal range for the layer. .. warning:: - To be used only when :py:func:`~QgsRasterLayerTemporalProperties.mode` is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange + To be used only when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedTemporalRange .. seealso:: :py:func:`setFixedTemporalRange` +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed temporal range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed temporal range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. + +.. seealso:: :py:func:`fixedRangePerBand` + +.. versionadded:: 3.38 +%End + + int bandForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns the band corresponding to the specified ``range``. + +.. note:: + + 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 virtual QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ); 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/qgsrasterrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in index e0b862500f9d..200e1c2e86dd 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,39 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; +%Docstring +Returns the input band for the renderer, or -1 if no input band is available. + +For renderers which utilize multiple input bands -1 will be returned. In these +cases :py:func:`~QgsRasterRenderer.usesBands` will return a list of all utilized bands (including alpha +bands). + +.. seealso:: :py:func:`setInputBand` + +.. seealso:: :py:func:`usesBands` + +.. versionadded:: 3.38 +%End + + virtual bool setInputBand( int band ); +%Docstring +Attempts to set the input ``band`` for the renderer. + +Returns ``True`` if the band was successfully set, or ``False`` if the band could not be set. + +.. note:: + + Not all renderers support setting the input band. + +.. seealso:: :py:func:`inputBand` + +.. seealso:: :py:func:`usesBands` + + +.. versionadded:: 3.38 +%End + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, @@ -156,7 +189,9 @@ Useful when cloning renderers. virtual QList usesBands() const; %Docstring -Returns a list of band numbers used by the renderer +Returns a list of band numbers used by the renderer. + +.. seealso:: :py:func:`setInputBand` %End const QgsRasterMinMaxOrigin &minMaxOrigin() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in new file mode 100644 index 000000000000..eeb73ec78d79 --- /dev/null +++ b/python/PyQt6/core/auto_generated/raster/qgsrasterrendererregistry.sip.in @@ -0,0 +1,72 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterRendererRegistry +{ +%Docstring(signature="appended") +Registry for raster renderers. + +:py:class:`QgsRasterRendererRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +.. note:: + + Exposed to Python bindings in QGIS 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterrendererregistry.h" +%End + public: + + QgsRasterRendererRegistry(); +%Docstring +Constructor for QgsRasterRendererRegistry. + +QgsRasterRendererRegistry is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +The registry is pre-populated with standard raster renderers. +%End + + + + + QStringList renderersList() const; +%Docstring +Returns a list of the names of registered renderers. +%End + + + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; +%Docstring +Returns the capabilities for the renderer with the specified name. + +.. versionadded:: 3.38 +%End + + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const /Factory/; +%Docstring +Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). +Caller takes ownership. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/raster/qgsrastertransparency.sip.in b/python/PyQt6/core/auto_generated/raster/qgsrastertransparency.sip.in index a3e38f3290f4..6b137207897c 100644 --- a/python/PyQt6/core/auto_generated/raster/qgsrastertransparency.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgsrastertransparency.sip.in @@ -27,28 +27,81 @@ Constructor for QgsRasterTransparency. struct TransparentThreeValuePixel { + + TransparentThreeValuePixel( double red = 0, double green = 0, double blue = 0, double opacity = 0 ); +%Docstring +Constructor for TransparentThreeValuePixel. + +:param red: red pixel value +:param green: green pixel value +:param blue: blue pixel value +:param opacity: opacity for pixel, between 0 and 1.0 + +.. versionadded:: 3.38 +%End + double red; + double green; + double blue; - double percentTransparent; + + double opacity; + + bool operator==( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const; + bool operator!=( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->red ).arg( sipCpp->green ).arg( sipCpp->blue ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End }; struct TransparentSingleValuePixel { + + TransparentSingleValuePixel( double minimum = 0, double maximum = 0, double opacity = 0, bool includeMinimum = true, bool includeMaximum = true ); +%Docstring +Constructor for TransparentSingleValuePixel. + +:param minimum: minimum pixel value to include in range +:param maximum: maximum pixel value to include in range +:param opacity: opacity for pixel, between 0 and 1.0 +:param includeMinimum: whether the minimum value should be included in the range +:param includeMaximum: whether the maximum value should be included in the range + +.. versionadded:: 3.38 +%End + double min; + double max; - double percentTransparent; - }; + double opacity; + + bool includeMinimum; + + bool includeMaximum; + + bool operator==( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const; + bool operator!=( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->min ).arg( sipCpp->max ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + }; - QList transparentSingleValuePixelList() const; + QVector transparentSingleValuePixelList() const; %Docstring Returns the transparent single value pixel list. .. seealso:: :py:func:`setTransparentSingleValuePixelList` %End - QList transparentThreeValuePixelList() const; + QVector transparentThreeValuePixelList() const; %Docstring Returns the transparent three value pixel list. @@ -65,21 +118,21 @@ Resets the transparency list to a single ``value``. Resets the transparency list to single red, green, and blue values. %End - void setTransparentSingleValuePixelList( const QList &newList ); + void setTransparentSingleValuePixelList( const QVector &newList ); %Docstring Sets the transparent single value pixel list, replacing the whole existing list. .. seealso:: :py:func:`transparentSingleValuePixelList` %End - void setTransparentThreeValuePixelList( const QList &newList ); + void setTransparentThreeValuePixelList( const QVector &newList ); %Docstring Sets the transparent three value pixel list, replacing the whole existing list. .. seealso:: :py:func:`transparentThreeValuePixelList` %End - int alphaValue( double value, int globalTransparency = 255 ) const; + int alphaValue( double value, int globalTransparency = 255 ) const /Deprecated/; %Docstring Returns the transparency value for a single ``value`` pixel. @@ -88,10 +141,22 @@ by the stored transparency value. :param value: the needle to search for in the transparency hay stack :param globalTransparency: the overall transparency level for the layer + +.. deprecated:: + use :py:func:`~QgsRasterTransparency.opacityForValue` instead. %End + double opacityForValue( double value ) const; +%Docstring +Returns the opacity (as a value from 0 to 1) for a single ``value`` pixel. + +Searches through the transparency list, and if a match is found, returns +the opacity corresponding to the value. Returns 1 if no matches are found. - int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const; +.. versionadded:: 3.38 +%End + + int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const /Deprecated/; %Docstring Returns the transparency value for a RGB pixel. @@ -102,6 +167,21 @@ by the stored transparency value. :param greenValue: the green portion of the needle to search for in the transparency hay stack :param blueValue: the green portion of the needle to search for in the transparency hay stack :param globalTransparency: the overall transparency level for the layer + +.. deprecated:: + use :py:func:`~QgsRasterTransparency.opacityForRgbValues` instead. +%End + + double opacityForRgbValues( double redValue, double greenValue, double blueValue ) const; +%Docstring +Returns the opacity (as a value from 0 to 1) for a set of RGB pixel values. + +Searches through the transparency list, and if a match is found, returns +the opacity corresponding to the values. Returns 1 if no matches are found. + +If any of the red, green or blue values are NaN, 0 will be returned. + +.. versionadded:: 3.38 %End bool isEmpty() const; diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index f4403c460cac..1ac7bc0e53ba 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,10 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index 57fd3ac74289..4a332ab54071 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -43,8 +43,25 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; - int grayBand() const; - void setGrayBand( int band ); + int grayBand() const /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.inputBand` instead +%End + + void setGrayBand( int band ) /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.setInputBand` instead +%End + + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + const QgsContrastEnhancement *contrastEnhancement() const; void setContrastEnhancement( QgsContrastEnhancement *ce /Transfer/ ); %Docstring diff --git a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4e2e1d230461..1c756431eb43 100644 --- a/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -90,18 +90,29 @@ Creates a color ramp shader virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.setInputBand` instead %End + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + double classificationMin() const; double classificationMax() const; void setClassificationMin( double min ); diff --git a/python/PyQt6/core/auto_generated/sensor/qgsiodevicesensor.sip.in b/python/PyQt6/core/auto_generated/sensor/qgsiodevicesensor.sip.in index 8d8fa2087210..e723fe1210f3 100644 --- a/python/PyQt6/core/auto_generated/sensor/qgsiodevicesensor.sip.in +++ b/python/PyQt6/core/auto_generated/sensor/qgsiodevicesensor.sip.in @@ -241,6 +241,25 @@ Sets the baudrate of the serial port the sensor connects to. :param baudRate: the baudrate (e.g. 9600) .. versionadded:: 3.36 +%End + + QByteArray delimiter() const; +%Docstring +Returns the current delimiter used to separate data frames. If empty, +each serial port data update will be considered a data frame. + +.. versionadded:: 3.38 +%End + + void setDelimiter( const QByteArray &delimiter ); +%Docstring +Sets the delimiter used to identify data frames out of the data received +from the serial port. If empty, each serial port data update will be +considered a data frame. + +:param delimiter: Character used to identify data frames + +.. versionadded:: 3.38 %End virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document ) const; @@ -255,6 +274,11 @@ Sets the baudrate of the serial port the sensor connects to. virtual void handleDisconnect(); + protected slots: + + virtual void parseData(); + + }; %End diff --git a/python/PyQt6/core/auto_generated/symbology/qgsfillsymbollayer.sip.in b/python/PyQt6/core/auto_generated/symbology/qgsfillsymbollayer.sip.in index 98ba4885d690..910903ef95b9 100644 --- a/python/PyQt6/core/auto_generated/symbology/qgsfillsymbollayer.sip.in +++ b/python/PyQt6/core/auto_generated/symbology/qgsfillsymbollayer.sip.in @@ -782,7 +782,7 @@ Base class for polygon renderers generating texture images void setStrokeWidthUnit( Qgis::RenderUnit unit ); %Docstring -Sets the ``units`` fo the symbol's stroke width. +Sets the ``units`` for the symbol's stroke width. .. seealso:: :py:func:`strokeWidthUnit` diff --git a/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in b/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in index c485930d6f87..ae2abafdf914 100644 --- a/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in +++ b/python/PyQt6/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in @@ -42,7 +42,7 @@ Constructor for QgsVectorLayerElevationProperties, with the specified ``parent`` virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; @@ -308,7 +308,7 @@ Returns ``True`` if the marker symbol should also be shown in continuous surface void setShowMarkerSymbolInSurfacePlots( bool show ); %Docstring -Sets whehter the marker symbol should also be shown in continuous surface plots. +Sets whether the marker symbol should also be shown in continuous surface plots. .. note:: 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 0f1a068e200a..5ae6df11f734 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 @@ -642,6 +643,7 @@ %Include auto_generated/raster/qgsrasterpyramid.sip %Include auto_generated/raster/qgsrasterrange.sip %Include auto_generated/raster/qgsrasterrenderer.sip +%Include auto_generated/raster/qgsrasterrendererregistry.sip %Include auto_generated/raster/qgsrasterrendererutils.sip %Include auto_generated/raster/qgsrasterresamplefilter.sip %Include auto_generated/raster/qgsrasterresampler.sip diff --git a/python/PyQt6/gui/auto_generated/auth/qgsauthsettingswidget.sip.in b/python/PyQt6/gui/auto_generated/auth/qgsauthsettingswidget.sip.in index 4cbf3b9ed1de..5ee5832aa886 100644 --- a/python/PyQt6/gui/auto_generated/auth/qgsauthsettingswidget.sip.in +++ b/python/PyQt6/gui/auto_generated/auth/qgsauthsettingswidget.sip.in @@ -154,7 +154,7 @@ setStoreUsernameChecked check the "Store" checkbox for the username void setStorePasswordChecked( bool checked ); %Docstring -setStorePasswordCheched check the "Store" checkbox for the password +setStorePasswordChecked check the "Store" checkbox for the password :param checked: diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index f7e6e0c277cc..ade00779a2cb 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -72,7 +72,7 @@ Returns the character before the cursor, or an empty string if cursor is set at QString characterAfterCursor() const; %Docstring -Returns the character after the cursor, or an empty string if the cursot is set at end +Returns the character after the cursor, or an empty string if the cursor is set at end .. versionadded:: 3.30 %End diff --git a/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in new file mode 100644 index 000000000000..2fd2258a8d3f --- /dev/null +++ b/python/PyQt6/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -0,0 +1,136 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/elevation/qgselevationcontrollerwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsElevationControllerSettingsAction: QWidgetAction +{ + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerSettingsAction( QWidget *parent = 0 ); + + QgsDoubleSpinBox *sizeSpin(); + +}; + + + +class QgsElevationControllerWidget : QWidget +{ +%Docstring(signature="appended") +A widget for configuring vertical elevation slicing behavior for maps. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerWidget( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsElevationControllerWidget, with the specified ``parent`` widget. +%End + + virtual void resizeEvent( QResizeEvent *event ); + + + QgsDoubleRange range() const; +%Docstring +Returns the current visible range from the widget. + +.. seealso:: :py:func:`setRange` + +.. seealso:: :py:func:`rangeChanged` +%End + + QgsDoubleRange rangeLimits() const; +%Docstring +Returns the limits of the elevation range which can be selected by the widget. + +.. seealso:: :py:func:`rangeLimits` +%End + + QgsRangeSlider *slider(); +%Docstring +Returns a reference to the slider component of the widget. +%End + + QMenu *menu(); +%Docstring +Returns a reference to the widget's configuration menu, which can be used +to add actions to the menu. +%End + + double fixedRangeSize() const; +%Docstring +Returns the fixed range size, or -1 if no fixed size is set. + +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. + +.. seealso:: :py:func:`setFixedRangeSize` +%End + + public slots: + + void setRange( const QgsDoubleRange &range ); +%Docstring +Sets the current visible ``range`` for the widget. + +.. seealso:: :py:func:`range` + +.. seealso:: :py:func:`rangeChanged` +%End + + void setRangeLimits( const QgsDoubleRange &limits ); +%Docstring +Sets the limits of the elevation range which can be selected by the widget. + +.. seealso:: :py:func:`rangeLimits` +%End + + void setFixedRangeSize( double size ); +%Docstring +Sets the fixed range ``size``. Set to -1 if no fixed size is desired. + +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. + +.. seealso:: :py:func:`fixedRangeSize` +%End + + signals: + + void rangeChanged( const QgsDoubleRange &range ); +%Docstring +Emitted when the visible range from the widget is changed. + +.. seealso:: :py:func:`setRange` + +.. seealso:: :py:func:`range` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/elevation/qgselevationcontrollerwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/gui/auto_generated/layertree/qgslayertreeview.sip.in b/python/PyQt6/gui/auto_generated/layertree/qgslayertreeview.sip.in index 108f1c0397a8..2ee01a006d06 100644 --- a/python/PyQt6/gui/auto_generated/layertree/qgslayertreeview.sip.in +++ b/python/PyQt6/gui/auto_generated/layertree/qgslayertreeview.sip.in @@ -44,6 +44,24 @@ Returns if private layers are shown. void setShowPrivateLayers( bool showPrivate ); %Docstring Determines if private layers are shown. +%End + + bool hideValidLayers() const; +%Docstring +Returns if valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 +%End + + void setHideValidLayers( bool hideValid ); +%Docstring +Sets whether valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 %End protected: @@ -324,6 +342,22 @@ Returns width of contextual menu mark, at right of layer node items. + bool showPrivateLayers() const; +%Docstring +Returns the show private layers status + +.. versionadded:: 3.18 +%End + + bool hideValidLayers() const; +%Docstring +Returns if valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 +%End + public slots: void refreshLayerSymbology( const QString &layerId ); %Docstring @@ -363,11 +397,13 @@ Set the show private layers to ``showPrivate`` .. versionadded:: 3.18 %End - bool showPrivateLayers( ); + void setHideValidLayers( bool hideValid ); %Docstring -Returns the show private layers status +Sets whether valid layers should be hidden (i.e. only invalid layers are shown). -.. versionadded:: 3.18 +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 %End signals: diff --git a/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in index 476a2683c162..6614ea27cec9 100644 --- a/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsexpressionbuilderwidget.sip.in @@ -159,6 +159,45 @@ preview result and to populate the list of available functions and variables. Returns if the expression is valid %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + + void saveToRecent( const QString &collection = "generic" ) /Deprecated/; %Docstring Adds the current expression to the given ``collection``. diff --git a/python/PyQt6/gui/auto_generated/qgsexpressionpreviewwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsexpressionpreviewwidget.sip.in index bddec34b2895..44b5ff4fe4c9 100644 --- a/python/PyQt6/gui/auto_generated/qgsexpressionpreviewwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsexpressionpreviewwidget.sip.in @@ -11,6 +11,7 @@ + class QgsExpressionPreviewWidget : QWidget { %Docstring(signature="appended") @@ -34,6 +35,44 @@ Constructor Sets the layer used in the preview %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + void setExpressionText( const QString &expression ); %Docstring Sets the expression @@ -79,7 +118,14 @@ Returns the root node of the expression QList parserErrors() const; %Docstring -Returns the expression parser erros +Returns the expression parser errors +%End + + QString currentPreviewText() const; +%Docstring +Returns the current expression result preview text. + +.. versionadded:: 3.38 %End signals: diff --git a/python/PyQt6/gui/auto_generated/qgsfieldexpressionwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsfieldexpressionwidget.sip.in index 71c750874ea0..5edd269b3202 100644 --- a/python/PyQt6/gui/auto_generated/qgsfieldexpressionwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsfieldexpressionwidget.sip.in @@ -142,6 +142,44 @@ an expression context for the widget. create an expression context when required. %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + bool allowEvalErrors() const; %Docstring Allow accepting expressions with evaluation errors. This can be useful when we are not able to 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/qgslayerpropertiesdialog.sip.in b/python/PyQt6/gui/auto_generated/qgslayerpropertiesdialog.sip.in index 1865306875e0..64a5e13fbc72 100644 --- a/python/PyQt6/gui/auto_generated/qgslayerpropertiesdialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgslayerpropertiesdialog.sip.in @@ -44,7 +44,7 @@ Constructor for QgsLayerPropertiesDialog. %Docstring Sets the metadata ``widget`` and ``page`` associated with the dialog. -This must be called in order for the standard metadata loading/saving functionality to be avialable. +This must be called in order for the standard metadata loading/saving functionality to be available. %End virtual void addPropertiesPageFactory( const QgsMapLayerConfigWidgetFactory *factory ); 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/qgsmapcanvas.sip.in b/python/PyQt6/gui/auto_generated/qgsmapcanvas.sip.in index d80a28e0ccbf..e275616fc633 100644 --- a/python/PyQt6/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsmapcanvas.sip.in @@ -47,6 +47,20 @@ Constructor ~QgsMapCanvas(); + void addOverlayWidget( QWidget *widget /Transfer/, Qt::Edge edge ); +%Docstring +Adds an overlay ``widget`` to the layout, which will be bound to the specified ``edge``. + +The widget will always float above the map canvas. + +.. note:: + + Widgets on the left and right edges will always be positioned first, with + top and bottom edge widgets expanding to take the remaining horizontal space. + +.. versionadded:: 3.38 +%End + double magnificationFactor() const; %Docstring Returns the magnification factor diff --git a/python/PyQt6/gui/auto_generated/qgsnewvectortabledialog.sip.in b/python/PyQt6/gui/auto_generated/qgsnewvectortabledialog.sip.in index a5aa3fadb2c6..58578b82f5fb 100644 --- a/python/PyQt6/gui/auto_generated/qgsnewvectortabledialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsnewvectortabledialog.sip.in @@ -96,7 +96,7 @@ Sets the fields to ``fields`` bool createSpatialIndex(); %Docstring -Returns ``True`` if spatialindex checkbox is cheched +Returns ``True`` if spatialindex checkbox is checked @return %End diff --git a/python/PyQt6/gui/auto_generated/qgsoverlaywidgetlayout.sip.in b/python/PyQt6/gui/auto_generated/qgsoverlaywidgetlayout.sip.in new file mode 100644 index 000000000000..abfde87b94df --- /dev/null +++ b/python/PyQt6/gui/auto_generated/qgsoverlaywidgetlayout.sip.in @@ -0,0 +1,85 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsoverlaywidgetlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsOverlayWidgetLayout : QLayout +{ +%Docstring(signature="appended") +A custom layout which can be used to overlay child widgets over a parent widget. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsoverlaywidgetlayout.h" +%End + public: + + QgsOverlayWidgetLayout( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsOverlayWidgetLayout, with the specified ``parent`` widget. +%End + ~QgsOverlayWidgetLayout(); + + int count() const final; + void addItem( QLayoutItem *item ) final; + QLayoutItem *itemAt( int index ) const final; + QLayoutItem *takeAt( int index ) final; + QSize sizeHint() const final; + QSize minimumSize() const final; + void setGeometry( const QRect &rect ) final; + + void addWidget( QWidget *widget /Transfer/, Qt::Edge edge ); +%Docstring +Adds a ``widget`` to the layout, which will be bound to the specified ``edge``. + +.. note:: + + Widgets on the left and right edges will always be positioned first, with + top and bottom edge widgets expanding to take the remaining horizontal space. +%End + + void setHorizontalSpacing( int spacing ); +%Docstring +Sets the spacing between widgets that are laid out side by side. + +.. seealso:: :py:func:`horizontalSpacing` +%End + + int horizontalSpacing() const; +%Docstring +Returns the spacing between widgets that are laid out side by side. + +.. seealso:: :py:func:`setHorizontalSpacing` +%End + + void setVerticalSpacing( int spacing ); +%Docstring +Sets the spacing between widgets that are laid out on top of each other. + +.. seealso:: :py:func:`verticalSpacing` +%End + + int verticalSpacing() const; +%Docstring +Returns the spacing between widgets that are laid out on top of each other. + +.. seealso:: :py:func:`setVerticalSpacing` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsoverlaywidgetlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in index 2f4fe6ef4fe4..126ed12e3acf 100644 --- a/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsrangeslider.sip.in @@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`setPageStep` .. seealso:: :py:func:`singleStep` +%End + + int fixedRangeSize() const; +%Docstring +Returns the slider's fixed range size, or -1 if not set. + +If a fixed range size is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed size. + +.. seealso:: :py:func:`setFixedRangeSize` + +.. versionadded:: 3.38 +%End + + void setFixedRangeSize( int size ); +%Docstring +Sets the slider's fixed range ``size``. Set to -1 if no fixed size is desired. + +If a fixed range size is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed size. + +.. seealso:: :py:func:`fixedRangeSize` + +.. versionadded:: 3.38 %End public slots: @@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed. void rangeLimitsChanged( int minimum, int maximum ); %Docstring Emitted when the limits of values allowed in the widget is changed. +%End + + void fixedRangeSizeChanged( int size ); +%Docstring +Emitted when the widget's fixed range size is changed. + +.. seealso:: :py:func:`fixedRangeSize` + +.. seealso:: :py:func:`setFixedRangeSize` + +.. versionadded:: 3.38 %End }; diff --git a/python/PyQt6/gui/auto_generated/qgsscalevisibilitydialog.sip.in b/python/PyQt6/gui/auto_generated/qgsscalevisibilitydialog.sip.in index be819eff41f1..888a719f8afc 100644 --- a/python/PyQt6/gui/auto_generated/qgsscalevisibilitydialog.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsscalevisibilitydialog.sip.in @@ -54,11 +54,23 @@ The scale value indicates the scale denominator, e.g. 1000.0 for a 1:1000 map. public slots: - void setScaleVisiblity( bool hasScaleVisibility ); + void setScaleVisiblity( bool hasScaleVisibility ) /Deprecated/; %Docstring Set whether scale based visibility is enabled. .. seealso:: :py:func:`hasScaleVisibility` + +.. deprecated:: + Use :py:func:`~QgsScaleVisibilityDialog.setScaleVisibility` +%End + + void setScaleVisibility( bool hasScaleVisibility ); +%Docstring +Set whether scale based visibility is enabled. + +.. seealso:: :py:func:`hasScaleVisibility` + +.. versionadded:: 3.38 %End void setMinimumScale( double scale ); 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/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in b/python/PyQt6/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in index 5887ab73e7fb..542a183cc34c 100644 --- a/python/PyQt6/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in @@ -14,7 +14,7 @@ class QgsValidityCheckResultsModel : QAbstractItemModel { %Docstring(signature="appended") -A QAbstractItemModel subclass for displaying the results from a :py:class:`QgsAbtractValidityCheck`. +A QAbstractItemModel subclass for displaying the results from a :py:class:`QgsAbstractValidityCheck`. .. versionadded:: 3.6 %End diff --git a/python/PyQt6/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in b/python/PyQt6/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in index ab9afc734aa7..8c1fb9cdf037 100644 --- a/python/PyQt6/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in +++ b/python/PyQt6/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in @@ -11,6 +11,7 @@ + class QgsRasterLayerTemporalPropertiesWidget : QWidget { %Docstring(signature="appended") diff --git a/python/PyQt6/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in b/python/PyQt6/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in index 68f61b0acc4c..9643a4ec83b4 100644 --- a/python/PyQt6/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in @@ -133,11 +133,21 @@ Defines if the group box to fill parameters is visible .. versionadded:: 3.18 %End - bool allowParamerters() const; + bool allowParamerters() const /Deprecated/; %Docstring Returns if the group box to fill parameters is visible .. versionadded:: 3.18 + +.. deprecated:: + Use :py:func:`~QgsSvgSelectorWidget.allowParameters` +%End + + bool allowParameters() const; +%Docstring +Returns if the group box to fill parameters is visible + +.. versionadded:: 3.38 %End void setBrowserVisible( bool visible ); diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index 67dcea160e1a..c668cc6c9a90 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -160,6 +160,7 @@ %Include auto_generated/qgsoptionsdialoghighlightwidgetsimpl.sip %Include auto_generated/qgsoptionswidgetfactory.sip %Include auto_generated/qgsorderbydialog.sip +%Include auto_generated/qgsoverlaywidgetlayout.sip %Include auto_generated/qgsowssourceselect.sip %Include auto_generated/qgspanelwidget.sip %Include auto_generated/qgspanelwidgetstack.sip @@ -341,6 +342,7 @@ %Include auto_generated/effects/qgseffectstackpropertieswidget.sip %Include auto_generated/effects/qgspainteffectpropertieswidget.sip %Include auto_generated/effects/qgspainteffectwidget.sip +%Include auto_generated/elevation/qgselevationcontrollerwidget.sip %Include auto_generated/elevation/qgselevationprofilecanvas.sip %Include auto_generated/history/qgshistoryentry.sip %Include auto_generated/history/qgshistoryentrymodel.sip 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/PyQt6/server/auto_generated/qgsserverfilter.sip.in b/python/PyQt6/server/auto_generated/qgsserverfilter.sip.in index b3d8edf4dfaf..fd0839ce47dd 100644 --- a/python/PyQt6/server/auto_generated/qgsserverfilter.sip.in +++ b/python/PyQt6/server/auto_generated/qgsserverfilter.sip.in @@ -93,6 +93,16 @@ parameters, just before entering the main switch for core services. :return: true if the call must propagate to the subsequent filters, false otherwise .. versionadded:: 3.24 +%End + + virtual bool onProjectReady(); +%Docstring +Method called when the :py:class:`QgsProject` instance is ready to be used to perform the request, +just before entering the main switch for core services. + +:return: true if the call must propagate to the subsequent filters, false otherwise + +.. versionadded:: 3.36 %End virtual bool onResponseComplete(); diff --git a/python/PyQt6/server/auto_generated/qgsserverogcapi.sip.in b/python/PyQt6/server/auto_generated/qgsserverogcapi.sip.in index 2afacb19cfca..8a09af1c054c 100644 --- a/python/PyQt6/server/auto_generated/qgsserverogcapi.sip.in +++ b/python/PyQt6/server/auto_generated/qgsserverogcapi.sip.in @@ -132,9 +132,19 @@ Returns the string representation of a ``ct`` (Content-Type) attribute. Returns the file extension for a ``ct`` (Content-Type). %End - static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ); + static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ) /Deprecated/; %Docstring Returns the Content-Type value corresponding to ``extension``. + +.. deprecated:: + Use :py:func:`~QgsServerOgcApi.contentTypeFromExtension` +%End + + static QgsServerOgcApi::ContentType contentTypeFromExtension( const std::string &extension ); +%Docstring +Returns the Content-Type value corresponding to ``extension``. + +.. versionadded:: 3.38 %End static std::string mimeType( const QgsServerOgcApi::ContentType &contentType ); diff --git a/python/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in b/python/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in index 5500bf617c96..1d1cec6d173e 100644 --- a/python/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in +++ b/python/analysis/auto_generated/georeferencing/qgsgcppoint.sip.in @@ -87,7 +87,7 @@ Sets the ``crs`` of the destination point. QgsPointXY transformedDestinationPoint( const QgsCoordinateReferenceSystem &targetCrs, const QgsCoordinateTransformContext &context ) const; %Docstring -Returns the :py:func:`~QgsGcpPoint.destionationPoint` transformed to the given target CRS. +Returns the :py:func:`~QgsGcpPoint.destinationPoint` transformed to the given target CRS. %End bool isEnabled() const; diff --git a/python/console/console.py b/python/console/console.py index 704d923d699a..1653e77200c2 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -145,8 +145,6 @@ def __init__(self, parent=None): QWidget.__init__(self, parent) self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console")) - self.settings = QgsSettings() - self.shell = ShellScintilla(self) self.setFocusProxy(self.shell) self.shellOut = ShellOutputScintilla(self) @@ -310,7 +308,7 @@ def __init__(self, parent=None): objList = QCoreApplication.translate("PythonConsole", "Object Inspector…") self.objectListButton = QAction(self) self.objectListButton.setCheckable(True) - self.objectListButton.setEnabled(self.settings.value("pythonConsole/enableObjectInsp", + self.objectListButton.setEnabled(QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool)) self.objectListButton.setIcon(QgsApplication.getThemeIcon("console/iconClassBrowserConsole.svg")) self.objectListButton.setMenuRole(QAction.MenuRole.PreferencesRole) @@ -675,7 +673,8 @@ def openScriptFileExtEditor(self): QDesktopServices.openUrl(QUrl.fromLocalFile(path)) def openScriptFile(self): - lastDirPath = self.settings.value("pythonConsole/lastDirPath", QDir.homePath()) + settings = QgsSettings() + lastDirPath = settings.value("pythonConsole/lastDirPath", QDir.homePath()) openFileTr = QCoreApplication.translate("PythonConsole", "Open File") fileList, selected_filter = QFileDialog.getOpenFileNames( self, openFileTr, lastDirPath, "Script file (*.py)") @@ -691,7 +690,7 @@ def openScriptFile(self): self.tabEditorWidget.newTabEditor(tabName, pyFile) lastDirPath = QFileInfo(pyFile).path() - self.settings.setValue("pythonConsole/lastDirPath", pyFile) + settings.setValue("pythonConsole/lastDirPath", pyFile) self.updateTabListScript(pyFile, action='append') def saveScriptFile(self): @@ -710,7 +709,7 @@ def saveAsScriptFile(self, index=None): index = self.tabEditorWidget.currentIndex() if not tabWidget.path: fileName = self.tabEditorWidget.tabText(index).replace('*', '') + '.py' - folder = self.settings.value("pythonConsole/lastDirPath", QDir.homePath()) + folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath()) pathFileName = os.path.join(folder, fileName) fileNone = True else: @@ -776,22 +775,24 @@ def updateTabListScript(self, script, action=None): self.tabListScript.append(script) else: self.tabListScript = [] - self.settings.setValue("pythonConsole/tabScripts", + QgsSettings().setValue("pythonConsole/tabScripts", self.tabListScript) def saveSettingsConsole(self): - self.settings.setValue("pythonConsole/splitterConsole", self.splitter.saveState()) - self.settings.setValue("pythonConsole/splitterObj", self.splitterObj.saveState()) - self.settings.setValue("pythonConsole/splitterEditor", self.splitterEditor.saveState()) + settings = QgsSettings() + settings.setValue("pythonConsole/splitterConsole", self.splitter.saveState()) + settings.setValue("pythonConsole/splitterObj", self.splitterObj.saveState()) + settings.setValue("pythonConsole/splitterEditor", self.splitterEditor.saveState()) self.shell.writeHistoryFile() def restoreSettingsConsole(self): - storedTabScripts = self.settings.value("pythonConsole/tabScripts", []) + settings = QgsSettings() + storedTabScripts = settings.value("pythonConsole/tabScripts", []) self.tabListScript = storedTabScripts - self.splitter.restoreState(self.settings.value("pythonConsole/splitterConsole", QByteArray())) - self.splitterEditor.restoreState(self.settings.value("pythonConsole/splitterEditor", QByteArray())) - self.splitterObj.restoreState(self.settings.value("pythonConsole/splitterObj", QByteArray())) + self.splitter.restoreState(settings.value("pythonConsole/splitterConsole", QByteArray())) + self.splitterEditor.restoreState(settings.value("pythonConsole/splitterEditor", QByteArray())) + self.splitterObj.restoreState(settings.value("pythonConsole/splitterObj", QByteArray())) if __name__ == '__main__': diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 42bdbe1e4c87..41b712707a0d 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -26,6 +26,7 @@ import re import sys import tempfile +from typing import Optional from functools import partial from operator import itemgetter from pathlib import Path @@ -61,10 +62,9 @@ class Editor(QgsCodeEditorPython): def __init__(self, parent=None): super().__init__(parent) self.parent = parent - self.path = None + self.path: Optional[str] = None # recent modification time self.lastModified = 0 - self.settings = QgsSettings() self.setMinimumHeight(120) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) @@ -237,7 +237,7 @@ def contextMenuEvent(self, e): redoAction.setEnabled(True) if QApplication.clipboard().text(): pasteAction.setEnabled(True) - if self.settings.value("pythonConsole/enableObjectInsp", + if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool): showCodeInspection.setEnabled(True) menu.exec(self.mapToGlobal(e.pos())) @@ -290,7 +290,7 @@ def objectListEditor(self): self.pythonconsole.objectListButton.setChecked(True) def shareOnGist(self, is_public): - ACCESS_TOKEN = self.settings.value("pythonConsole/accessTokenGithub", '', type=QByteArray) + ACCESS_TOKEN = QgsSettings().value("pythonConsole/accessTokenGithub", '', type=QByteArray) if not ACCESS_TOKEN: msg_text = QCoreApplication.translate( 'PythonConsole', 'GitHub personal access token must be generated (see Console Options)') @@ -358,7 +358,7 @@ def createTempFile(self): return name def runScriptCode(self): - autoSave = self.settings.value("pythonConsole/autoSaveScript", False, type=bool) + autoSave = QgsSettings().value("pythonConsole/autoSaveScript", False, type=bool) tabWidget = self.tabwidget.currentWidget() filename = tabWidget.path msgEditorBlank = QCoreApplication.translate('PythonConsole', @@ -455,24 +455,23 @@ def loadFile(self, filename, readOnly=False): self.setModified(False) self.recolor() - def save(self, filename=None): + def save(self, filename: Optional[str] = None): if self.isReadOnly(): return - if self.pythonconsole.settings.value("pythonConsole/formatOnSave", False, type=bool): + if QgsSettings().value("pythonConsole/formatOnSave", False, type=bool): self.reformatCode() - tabwidget = self.tabwidget - index = tabwidget.indexOf(self.parent) + index = self.tabwidget.indexOf(self.parent) if filename: self.path = filename - if self.path is None: + if not self.path: saveTr = QCoreApplication.translate('PythonConsole', 'Python Console: Save file') - folder = self.pythonconsole.settings.value("pythonConsole/lastDirPath", QDir.homePath()) + folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath()) self.path, filter = QFileDialog().getSaveFileName(self, saveTr, - os.path.join(folder, tabwidget.tabText(index).replace('*', '') + '.py'), + os.path.join(folder, self.tabwidget.tabText(index).replace('*', '') + '.py'), "Script file (*.py)") # If the user didn't select a file, abort the save operation if not self.path: @@ -487,15 +486,15 @@ def save(self, filename=None): # Need to use newline='' to avoid adding extra \r characters on Windows with open(self.path, 'w', encoding='utf-8', newline='') as f: f.write(self.text()) - tabwidget.setTabTitle(index, Path(self.path).name) - tabwidget.setTabToolTip(index, self.path) + self.tabwidget.setTabTitle(index, Path(self.path).name) + self.tabwidget.setTabToolTip(index, self.path) self.setModified(False) self.pythonconsole.saveFileButton.setEnabled(False) self.lastModified = QFileInfo(self.path).lastModified() self.pythonconsole.updateTabListScript(self.path, action='append') - tabwidget.listObject(self.parent) - lastDirPath = str(Path(self.path).parent) - self.pythonconsole.settings.setValue("pythonConsole/lastDirPath", lastDirPath) + self.tabwidget.listObject(self.parent) + QgsSettings().setValue("pythonConsole/lastDirPath", + Path(self.path).parent.as_posix()) def event(self, e): """ Used to override the Application shortcuts when the editor has focus """ @@ -604,8 +603,6 @@ def __init__(self, parent): super().__init__(parent=None) self.parent = parent - self.settings = QgsSettings() - self.idx = -1 # Layout for top frame (restore tabs) self.layoutTopFrame = QGridLayout(self) @@ -692,7 +689,7 @@ def __init__(self, parent): self.newTabButton.clicked.connect(self.newTabEditor) def _currentWidgetChanged(self, tab): - if self.settings.value("pythonConsole/enableObjectInsp", + if QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool): self.listObject(tab) self.changeLastDirPath(tab) @@ -839,7 +836,7 @@ def restoreTabsOrAddNew(self): Restore tabs if they are found in the settings. If none are found it will add a new empty tab. """ # Restore scripts from the previous session - tabScripts = self.settings.value("pythonConsole/tabScripts", []) + tabScripts = QgsSettings().value("pythonConsole/tabScripts", []) self.restoreTabList = tabScripts if self.restoreTabList: @@ -962,7 +959,7 @@ def listObject(self, tab): self.parent.listClassMethod.addTopLevelItem(msgItem) def refreshSettingsEditor(self): - objInspectorEnabled = self.settings.value("pythonConsole/enableObjectInsp", + objInspectorEnabled = QgsSettings().value("pythonConsole/enableObjectInsp", False, type=bool) listObj = self.parent.objectListButton if self.parent.listClassMethod.isVisible(): @@ -977,7 +974,8 @@ def refreshSettingsEditor(self): def changeLastDirPath(self, tab): tabWidget = self.widget(tab) if tabWidget and tabWidget.path: - self.settings.setValue("pythonConsole/lastDirPath", tabWidget.path) + QgsSettings().setValue("pythonConsole/lastDirPath", + Path(tabWidget.path).parent.as_posix()) def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""): currWidget = self.currentWidget() diff --git a/python/console/console_output.py b/python/console/console_output.py index b68f9aa03642..1dcbe93676cf 100644 --- a/python/console/console_output.py +++ b/python/console/console_output.py @@ -54,7 +54,7 @@ def write(self, m): if self.style == "_traceback": # Show errors in red - stderrColor = QColor(self.sO.settings.value("pythonConsole/stderrFontColor", QColor(self.ERROR_COLOR))) + stderrColor = QColor(QgsSettings().value("pythonConsole/stderrFontColor", QColor(self.ERROR_COLOR))) self.sO.SendScintilla(QsciScintilla.SCI_STYLESETFORE, 0o01, stderrColor) self.sO.SendScintilla(QsciScintilla.SCI_STYLESETITALIC, 0o01, True) self.sO.SendScintilla(QsciScintilla.SCI_STYLESETBOLD, 0o01, True) @@ -122,8 +122,6 @@ def __init__(self, parent=None): self.parent = parent self.shell = self.parent.shell - self.settings = QgsSettings() - # Creates layout for message bar self.layout = QGridLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) diff --git a/python/console/console_sci.py b/python/console/console_sci.py index d16d31f84a83..da755fef4efc 100644 --- a/python/console/console_sci.py +++ b/python/console/console_sci.py @@ -288,8 +288,6 @@ def __init__(self, parent=None): self.opening = ['(', '{', '[', "'", '"'] self.closing = [')', '}', ']', "'", '"'] - self.settings = QgsSettings() - self.setHistoryFilePath( os.path.join(QgsApplication.qgisSettingsDirPath(), "console_history.txt")) diff --git a/python/core/__init__.py.in b/python/core/__init__.py.in index 5f8789071ef4..d1594b5b37d0 100644 --- a/python/core/__init__.py.in +++ b/python/core/__init__.py.in @@ -413,3 +413,20 @@ def set_display_all(self, enabled): QgsPalLayerSettings.displayAll = property(get_display_all) QgsPalLayerSettings.displayAll = QgsPalLayerSettings.displayAll.setter(set_display_all) + +QgsLocatorResult.userData = property(QgsLocatorResult._userData) +QgsLocatorResult.userData = QgsLocatorResult.userData.setter(QgsLocatorResult.setUserData) + + +def get_pixel_transparency(self): + return (1.0 - self.opacity) * 100 + + +def set_pixel_transparency(self, transparency): + self.opacity = 1.0 - (transparency / 100) + + +QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent = property(get_pixel_transparency) +QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent = QgsRasterTransparency.TransparentThreeValuePixel.percentTransparent.setter(set_pixel_transparency) +QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent = property(get_pixel_transparency) +QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent = QgsRasterTransparency.TransparentSingleValuePixel.percentTransparent.setter(set_pixel_transparency) diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 050f9c947b66..4918dd1f3f26 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -1253,6 +1253,13 @@ Qgis.RasterRendererFlags.baseClass = Qgis RasterRendererFlags = Qgis # dirty hack since SIP seems to introduce the flags in module # monkey patching scoped based enum +Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ = "The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability)" +Qgis.RasterRendererCapability.__doc__ = "Raster renderer capabilities.\n\n.. versionadded:: 3.48\n\n" + '* ``UsesMultipleBands``: ' + Qgis.RasterRendererCapability.UsesMultipleBands.__doc__ +# -- +Qgis.RasterRendererCapability.baseClass = Qgis +Qgis.RasterRendererCapabilities.baseClass = Qgis +RasterRendererCapabilities = Qgis # dirty hack since SIP seems to introduce the flags in module +# monkey patching scoped based enum Qgis.RasterAttributeTableFieldUsage.Generic.__doc__ = "Field usage Generic" Qgis.RasterAttributeTableFieldUsage.PixelCount.__doc__ = "Field usage PixelCount" Qgis.RasterAttributeTableFieldUsage.Name.__doc__ = "Field usage Name" @@ -1795,7 +1802,9 @@ Qgis.AutoRefreshMode.baseClass = Qgis # monkey patching scoped based enum Qgis.DataProviderFlag.IsBasemapSource.__doc__ = "Associated source should be considered a 'basemap' layer. See Qgis.MapLayerProperty.IsBasemapLayer." -Qgis.DataProviderFlag.__doc__ = "Generic data provider flags.\n\n.. versionadded:: 3.26\n\n" + '* ``IsBasemapSource``: ' + Qgis.DataProviderFlag.IsBasemapSource.__doc__ +Qgis.DataProviderFlag.FastExtent2D.__doc__ = "Provider's 2D extent retrieval via QgsDataProvider.extent() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38." +Qgis.DataProviderFlag.FastExtent3D.__doc__ = "Provider's 3D extent retrieval via QgsDataProvider.extent3D() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38." +Qgis.DataProviderFlag.__doc__ = "Generic data provider flags.\n\n.. versionadded:: 3.26\n\n" + '* ``IsBasemapSource``: ' + Qgis.DataProviderFlag.IsBasemapSource.__doc__ + '\n' + '* ``FastExtent2D``: ' + Qgis.DataProviderFlag.FastExtent2D.__doc__ + '\n' + '* ``FastExtent3D``: ' + Qgis.DataProviderFlag.FastExtent3D.__doc__ # -- Qgis.DataProviderFlag.baseClass = Qgis Qgis.DataProviderFlags.baseClass = Qgis @@ -2065,7 +2074,10 @@ QgsRasterLayerTemporalProperties.TemporalMode.ModeRedrawLayerOnly = Qgis.RasterTemporalMode.RedrawLayerOnly QgsRasterLayerTemporalProperties.ModeRedrawLayerOnly.is_monkey_patched = True QgsRasterLayerTemporalProperties.ModeRedrawLayerOnly.__doc__ = "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)" -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__ +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__ # -- Qgis.RasterTemporalMode.baseClass = Qgis QgsRasterDataProviderTemporalCapabilities.IntervalHandlingMethod = Qgis.TemporalIntervalMatchMethod @@ -3164,6 +3176,28 @@ # -- Qgis.AltitudeBinding.baseClass = Qgis # monkey patching scoped based enum +Qgis.RangeLimits.IncludeBoth.__doc__ = "Both lower and upper values are included in the range" +Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ = "Lower value is included in the range, upper value is excluded" +Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ = "Lower value is excluded from the range, upper value in inccluded" +Qgis.RangeLimits.ExcludeBoth.__doc__ = "Both lower and upper values are excluded from the range" +Qgis.RangeLimits.__doc__ = "Describes how the limits of a range are handled.\n\n.. versionadded:: 3.38\n\n" + '* ``IncludeBoth``: ' + Qgis.RangeLimits.IncludeBoth.__doc__ + '\n' + '* ``IncludeLowerExcludeUpper``: ' + Qgis.RangeLimits.IncludeLowerExcludeUpper.__doc__ + '\n' + '* ``ExcludeLowerIncludeUpper``: ' + Qgis.RangeLimits.ExcludeLowerIncludeUpper.__doc__ + '\n' + '* ``ExcludeBoth``: ' + Qgis.RangeLimits.ExcludeBoth.__doc__ +# -- +Qgis.RangeLimits.baseClass = Qgis +# monkey patching scoped based enum +Qgis.RasterElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ = "Pixel values represent an elevation surface" +Qgis.RasterElevationMode.FixedRangePerBand.__doc__ = "Layer has a fixed (manually specified) elevation range per band" +Qgis.RasterElevationMode.DynamicRangePerBand.__doc__ = "Layer has a elevation range per band, calculated dynamically from an expression" +Qgis.RasterElevationMode.__doc__ = "Raster layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.RasterElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``RepresentsElevationSurface``: ' + Qgis.RasterElevationMode.RepresentsElevationSurface.__doc__ + '\n' + '* ``FixedRangePerBand``: ' + Qgis.RasterElevationMode.FixedRangePerBand.__doc__ + '\n' + '* ``DynamicRangePerBand``: ' + Qgis.RasterElevationMode.DynamicRangePerBand.__doc__ +# -- +Qgis.RasterElevationMode.baseClass = Qgis +# monkey patching scoped based enum +Qgis.MeshElevationMode.FixedElevationRange.__doc__ = "Layer has a fixed elevation range" +Qgis.MeshElevationMode.FromVertices.__doc__ = "Elevation should be taken from mesh vertices" +Qgis.MeshElevationMode.__doc__ = "Mesh layer elevation modes.\n\n.. versionadded:: 3.38\n\n" + '* ``FixedElevationRange``: ' + Qgis.MeshElevationMode.FixedElevationRange.__doc__ + '\n' + '* ``FromVertices``: ' + Qgis.MeshElevationMode.FromVertices.__doc__ +# -- +Qgis.MeshElevationMode.baseClass = Qgis +# monkey patching scoped based enum Qgis.NoConstraint = Qgis.BetweenLineConstraint.NoConstraint Qgis.NoConstraint.is_monkey_patched = True Qgis.BetweenLineConstraint.NoConstraint.__doc__ = "No additional constraint" @@ -4792,7 +4826,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_additions/qgslayoutobject.py b/python/core/auto_additions/qgslayoutobject.py index d54fe1508b62..d174e55df208 100644 --- a/python/core/auto_additions/qgslayoutobject.py +++ b/python/core/auto_additions/qgslayoutobject.py @@ -156,6 +156,21 @@ QgsLayoutObject.MapGridFrameDivisionsBottom = QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom QgsLayoutObject.MapGridFrameDivisionsBottom.is_monkey_patched = True QgsLayoutObject.MapGridFrameDivisionsBottom.__doc__ = "Map frame division display bottom" +QgsLayoutObject.MapCrs = QgsLayoutObject.DataDefinedProperty.MapCrs +QgsLayoutObject.MapCrs.is_monkey_patched = True +QgsLayoutObject.MapCrs.__doc__ = "Map CRS" +QgsLayoutObject.StartDateTime = QgsLayoutObject.DataDefinedProperty.StartDateTime +QgsLayoutObject.StartDateTime.is_monkey_patched = True +QgsLayoutObject.StartDateTime.__doc__ = "Temporal range's start DateTime" +QgsLayoutObject.EndDateTime = QgsLayoutObject.DataDefinedProperty.EndDateTime +QgsLayoutObject.EndDateTime.is_monkey_patched = True +QgsLayoutObject.EndDateTime.__doc__ = "Temporal range's end DateTime" +QgsLayoutObject.MapZRangeLower = QgsLayoutObject.DataDefinedProperty.MapZRangeLower +QgsLayoutObject.MapZRangeLower.is_monkey_patched = True +QgsLayoutObject.MapZRangeLower.__doc__ = "Map frame Z-range lower value (since QGIS 3.38)" +QgsLayoutObject.MapZRangeUpper = QgsLayoutObject.DataDefinedProperty.MapZRangeUpper +QgsLayoutObject.MapZRangeUpper.is_monkey_patched = True +QgsLayoutObject.MapZRangeUpper.__doc__ = "Map frame Z-range lower value (since QGIS 3.38)" QgsLayoutObject.PictureSource = QgsLayoutObject.DataDefinedProperty.PictureSource QgsLayoutObject.PictureSource.is_monkey_patched = True QgsLayoutObject.PictureSource.__doc__ = "Picture source url" @@ -216,15 +231,6 @@ QgsLayoutObject.AttributeTableSourceLayer = QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer QgsLayoutObject.AttributeTableSourceLayer.is_monkey_patched = True QgsLayoutObject.AttributeTableSourceLayer.__doc__ = "Attribute table source layer" -QgsLayoutObject.MapCrs = QgsLayoutObject.DataDefinedProperty.MapCrs -QgsLayoutObject.MapCrs.is_monkey_patched = True -QgsLayoutObject.MapCrs.__doc__ = "Map CRS" -QgsLayoutObject.StartDateTime = QgsLayoutObject.DataDefinedProperty.StartDateTime -QgsLayoutObject.StartDateTime.is_monkey_patched = True -QgsLayoutObject.StartDateTime.__doc__ = "Temporal range's start DateTime" -QgsLayoutObject.EndDateTime = QgsLayoutObject.DataDefinedProperty.EndDateTime -QgsLayoutObject.EndDateTime.is_monkey_patched = True -QgsLayoutObject.EndDateTime.__doc__ = "Temporal range's end DateTime" QgsLayoutObject.ElevationProfileTolerance = QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance QgsLayoutObject.ElevationProfileTolerance.is_monkey_patched = True QgsLayoutObject.ElevationProfileTolerance.__doc__ = "Tolerance distance for elevation profiles (since QGIS 3.30)" @@ -258,5 +264,5 @@ QgsLayoutObject.ElevationProfileMaximumElevation = QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation QgsLayoutObject.ElevationProfileMaximumElevation.is_monkey_patched = True QgsLayoutObject.ElevationProfileMaximumElevation.__doc__ = "Maximum elevation value for elevation profile (since QGIS 3.30)" -QgsLayoutObject.DataDefinedProperty.__doc__ = "Data defined properties for different item types\n\n" + '* ``NoProperty``: ' + QgsLayoutObject.DataDefinedProperty.NoProperty.__doc__ + '\n' + '* ``AllProperties``: ' + QgsLayoutObject.DataDefinedProperty.AllProperties.__doc__ + '\n' + '* ``TestProperty``: ' + QgsLayoutObject.DataDefinedProperty.TestProperty.__doc__ + '\n' + '* ``PresetPaperSize``: ' + QgsLayoutObject.DataDefinedProperty.PresetPaperSize.__doc__ + '\n' + '* ``PaperWidth``: ' + QgsLayoutObject.DataDefinedProperty.PaperWidth.__doc__ + '\n' + '* ``PaperHeight``: ' + QgsLayoutObject.DataDefinedProperty.PaperHeight.__doc__ + '\n' + '* ``NumPages``: ' + QgsLayoutObject.DataDefinedProperty.NumPages.__doc__ + '\n' + '* ``PaperOrientation``: ' + QgsLayoutObject.DataDefinedProperty.PaperOrientation.__doc__ + '\n' + '* ``PageNumber``: ' + QgsLayoutObject.DataDefinedProperty.PageNumber.__doc__ + '\n' + '* ``PositionX``: ' + QgsLayoutObject.DataDefinedProperty.PositionX.__doc__ + '\n' + '* ``PositionY``: ' + QgsLayoutObject.DataDefinedProperty.PositionY.__doc__ + '\n' + '* ``ItemWidth``: ' + QgsLayoutObject.DataDefinedProperty.ItemWidth.__doc__ + '\n' + '* ``ItemHeight``: ' + QgsLayoutObject.DataDefinedProperty.ItemHeight.__doc__ + '\n' + '* ``ItemRotation``: ' + QgsLayoutObject.DataDefinedProperty.ItemRotation.__doc__ + '\n' + '* ``Transparency``: ' + QgsLayoutObject.DataDefinedProperty.Transparency.__doc__ + '\n' + '* ``Opacity``: ' + QgsLayoutObject.DataDefinedProperty.Opacity.__doc__ + '\n' + '* ``BlendMode``: ' + QgsLayoutObject.DataDefinedProperty.BlendMode.__doc__ + '\n' + '* ``ExcludeFromExports``: ' + QgsLayoutObject.DataDefinedProperty.ExcludeFromExports.__doc__ + '\n' + '* ``FrameColor``: ' + QgsLayoutObject.DataDefinedProperty.FrameColor.__doc__ + '\n' + '* ``BackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.BackgroundColor.__doc__ + '\n' + '* ``MarginLeft``: ' + QgsLayoutObject.DataDefinedProperty.MarginLeft.__doc__ + '\n' + '* ``MarginTop``: ' + QgsLayoutObject.DataDefinedProperty.MarginTop.__doc__ + '\n' + '* ``MarginRight``: ' + QgsLayoutObject.DataDefinedProperty.MarginRight.__doc__ + '\n' + '* ``MarginBottom``: ' + QgsLayoutObject.DataDefinedProperty.MarginBottom.__doc__ + '\n' + '* ``MapRotation``: ' + QgsLayoutObject.DataDefinedProperty.MapRotation.__doc__ + '\n' + '* ``MapScale``: ' + QgsLayoutObject.DataDefinedProperty.MapScale.__doc__ + '\n' + '* ``MapXMin``: ' + QgsLayoutObject.DataDefinedProperty.MapXMin.__doc__ + '\n' + '* ``MapYMin``: ' + QgsLayoutObject.DataDefinedProperty.MapYMin.__doc__ + '\n' + '* ``MapXMax``: ' + QgsLayoutObject.DataDefinedProperty.MapXMax.__doc__ + '\n' + '* ``MapYMax``: ' + QgsLayoutObject.DataDefinedProperty.MapYMax.__doc__ + '\n' + '* ``MapAtlasMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapAtlasMargin.__doc__ + '\n' + '* ``MapLayers``: ' + QgsLayoutObject.DataDefinedProperty.MapLayers.__doc__ + '\n' + '* ``MapStylePreset``: ' + QgsLayoutObject.DataDefinedProperty.MapStylePreset.__doc__ + '\n' + '* ``MapLabelMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapLabelMargin.__doc__ + '\n' + '* ``MapGridEnabled``: ' + QgsLayoutObject.DataDefinedProperty.MapGridEnabled.__doc__ + '\n' + '* ``MapGridIntervalX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalX.__doc__ + '\n' + '* ``MapGridIntervalY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalY.__doc__ + '\n' + '* ``MapGridOffsetX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetX.__doc__ + '\n' + '* ``MapGridOffsetY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetY.__doc__ + '\n' + '* ``MapGridFrameSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameSize.__doc__ + '\n' + '* ``MapGridFrameMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameMargin.__doc__ + '\n' + '* ``MapGridLabelDistance``: ' + QgsLayoutObject.DataDefinedProperty.MapGridLabelDistance.__doc__ + '\n' + '* ``MapGridCrossSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridCrossSize.__doc__ + '\n' + '* ``MapGridFrameLineThickness``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameLineThickness.__doc__ + '\n' + '* ``MapGridAnnotationDisplayLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayLeft.__doc__ + '\n' + '* ``MapGridAnnotationDisplayRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayRight.__doc__ + '\n' + '* ``MapGridAnnotationDisplayTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayTop.__doc__ + '\n' + '* ``MapGridAnnotationDisplayBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayBottom.__doc__ + '\n' + '* ``MapGridFrameDivisionsLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsLeft.__doc__ + '\n' + '* ``MapGridFrameDivisionsRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsRight.__doc__ + '\n' + '* ``MapGridFrameDivisionsTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsTop.__doc__ + '\n' + '* ``MapGridFrameDivisionsBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom.__doc__ + '\n' + '* ``PictureSource``: ' + QgsLayoutObject.DataDefinedProperty.PictureSource.__doc__ + '\n' + '* ``PictureSvgBackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgBackgroundColor.__doc__ + '\n' + '* ``PictureSvgStrokeColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeColor.__doc__ + '\n' + '* ``PictureSvgStrokeWidth``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeWidth.__doc__ + '\n' + '* ``SourceUrl``: ' + QgsLayoutObject.DataDefinedProperty.SourceUrl.__doc__ + '\n' + '* ``LegendTitle``: ' + QgsLayoutObject.DataDefinedProperty.LegendTitle.__doc__ + '\n' + '* ``LegendColumnCount``: ' + QgsLayoutObject.DataDefinedProperty.LegendColumnCount.__doc__ + '\n' + '* ``ScalebarLeftSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLeftSegments.__doc__ + '\n' + '* ``ScalebarRightSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegments.__doc__ + '\n' + '* ``ScalebarSegmentWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSegmentWidth.__doc__ + '\n' + '* ``ScalebarMinimumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMinimumWidth.__doc__ + '\n' + '* ``ScalebarMaximumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMaximumWidth.__doc__ + '\n' + '* ``ScalebarHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarHeight.__doc__ + '\n' + '* ``ScalebarRightSegmentSubdivisions``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegmentSubdivisions.__doc__ + '\n' + '* ``ScalebarSubdivisionHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSubdivisionHeight.__doc__ + '\n' + '* ``ScalebarFillColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor.__doc__ + '\n' + '* ``ScalebarFillColor2``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor2.__doc__ + '\n' + '* ``ScalebarLineColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineColor.__doc__ + '\n' + '* ``ScalebarLineWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineWidth.__doc__ + '\n' + '* ``AttributeTableSourceLayer``: ' + QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer.__doc__ + '\n' + '* ``MapCrs``: ' + QgsLayoutObject.DataDefinedProperty.MapCrs.__doc__ + '\n' + '* ``StartDateTime``: ' + QgsLayoutObject.DataDefinedProperty.StartDateTime.__doc__ + '\n' + '* ``EndDateTime``: ' + QgsLayoutObject.DataDefinedProperty.EndDateTime.__doc__ + '\n' + '* ``ElevationProfileTolerance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance.__doc__ + '\n' + '* ``ElevationProfileDistanceMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMajorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMinorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceLabelInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMajorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMinorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationLabelInterval.__doc__ + '\n' + '* ``ElevationProfileMinimumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumDistance.__doc__ + '\n' + '* ``ElevationProfileMaximumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumDistance.__doc__ + '\n' + '* ``ElevationProfileMinimumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumElevation.__doc__ + '\n' + '* ``ElevationProfileMaximumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation.__doc__ +QgsLayoutObject.DataDefinedProperty.__doc__ = "Data defined properties for different item types\n\n" + '* ``NoProperty``: ' + QgsLayoutObject.DataDefinedProperty.NoProperty.__doc__ + '\n' + '* ``AllProperties``: ' + QgsLayoutObject.DataDefinedProperty.AllProperties.__doc__ + '\n' + '* ``TestProperty``: ' + QgsLayoutObject.DataDefinedProperty.TestProperty.__doc__ + '\n' + '* ``PresetPaperSize``: ' + QgsLayoutObject.DataDefinedProperty.PresetPaperSize.__doc__ + '\n' + '* ``PaperWidth``: ' + QgsLayoutObject.DataDefinedProperty.PaperWidth.__doc__ + '\n' + '* ``PaperHeight``: ' + QgsLayoutObject.DataDefinedProperty.PaperHeight.__doc__ + '\n' + '* ``NumPages``: ' + QgsLayoutObject.DataDefinedProperty.NumPages.__doc__ + '\n' + '* ``PaperOrientation``: ' + QgsLayoutObject.DataDefinedProperty.PaperOrientation.__doc__ + '\n' + '* ``PageNumber``: ' + QgsLayoutObject.DataDefinedProperty.PageNumber.__doc__ + '\n' + '* ``PositionX``: ' + QgsLayoutObject.DataDefinedProperty.PositionX.__doc__ + '\n' + '* ``PositionY``: ' + QgsLayoutObject.DataDefinedProperty.PositionY.__doc__ + '\n' + '* ``ItemWidth``: ' + QgsLayoutObject.DataDefinedProperty.ItemWidth.__doc__ + '\n' + '* ``ItemHeight``: ' + QgsLayoutObject.DataDefinedProperty.ItemHeight.__doc__ + '\n' + '* ``ItemRotation``: ' + QgsLayoutObject.DataDefinedProperty.ItemRotation.__doc__ + '\n' + '* ``Transparency``: ' + QgsLayoutObject.DataDefinedProperty.Transparency.__doc__ + '\n' + '* ``Opacity``: ' + QgsLayoutObject.DataDefinedProperty.Opacity.__doc__ + '\n' + '* ``BlendMode``: ' + QgsLayoutObject.DataDefinedProperty.BlendMode.__doc__ + '\n' + '* ``ExcludeFromExports``: ' + QgsLayoutObject.DataDefinedProperty.ExcludeFromExports.__doc__ + '\n' + '* ``FrameColor``: ' + QgsLayoutObject.DataDefinedProperty.FrameColor.__doc__ + '\n' + '* ``BackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.BackgroundColor.__doc__ + '\n' + '* ``MarginLeft``: ' + QgsLayoutObject.DataDefinedProperty.MarginLeft.__doc__ + '\n' + '* ``MarginTop``: ' + QgsLayoutObject.DataDefinedProperty.MarginTop.__doc__ + '\n' + '* ``MarginRight``: ' + QgsLayoutObject.DataDefinedProperty.MarginRight.__doc__ + '\n' + '* ``MarginBottom``: ' + QgsLayoutObject.DataDefinedProperty.MarginBottom.__doc__ + '\n' + '* ``MapRotation``: ' + QgsLayoutObject.DataDefinedProperty.MapRotation.__doc__ + '\n' + '* ``MapScale``: ' + QgsLayoutObject.DataDefinedProperty.MapScale.__doc__ + '\n' + '* ``MapXMin``: ' + QgsLayoutObject.DataDefinedProperty.MapXMin.__doc__ + '\n' + '* ``MapYMin``: ' + QgsLayoutObject.DataDefinedProperty.MapYMin.__doc__ + '\n' + '* ``MapXMax``: ' + QgsLayoutObject.DataDefinedProperty.MapXMax.__doc__ + '\n' + '* ``MapYMax``: ' + QgsLayoutObject.DataDefinedProperty.MapYMax.__doc__ + '\n' + '* ``MapAtlasMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapAtlasMargin.__doc__ + '\n' + '* ``MapLayers``: ' + QgsLayoutObject.DataDefinedProperty.MapLayers.__doc__ + '\n' + '* ``MapStylePreset``: ' + QgsLayoutObject.DataDefinedProperty.MapStylePreset.__doc__ + '\n' + '* ``MapLabelMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapLabelMargin.__doc__ + '\n' + '* ``MapGridEnabled``: ' + QgsLayoutObject.DataDefinedProperty.MapGridEnabled.__doc__ + '\n' + '* ``MapGridIntervalX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalX.__doc__ + '\n' + '* ``MapGridIntervalY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridIntervalY.__doc__ + '\n' + '* ``MapGridOffsetX``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetX.__doc__ + '\n' + '* ``MapGridOffsetY``: ' + QgsLayoutObject.DataDefinedProperty.MapGridOffsetY.__doc__ + '\n' + '* ``MapGridFrameSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameSize.__doc__ + '\n' + '* ``MapGridFrameMargin``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameMargin.__doc__ + '\n' + '* ``MapGridLabelDistance``: ' + QgsLayoutObject.DataDefinedProperty.MapGridLabelDistance.__doc__ + '\n' + '* ``MapGridCrossSize``: ' + QgsLayoutObject.DataDefinedProperty.MapGridCrossSize.__doc__ + '\n' + '* ``MapGridFrameLineThickness``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameLineThickness.__doc__ + '\n' + '* ``MapGridAnnotationDisplayLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayLeft.__doc__ + '\n' + '* ``MapGridAnnotationDisplayRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayRight.__doc__ + '\n' + '* ``MapGridAnnotationDisplayTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayTop.__doc__ + '\n' + '* ``MapGridAnnotationDisplayBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridAnnotationDisplayBottom.__doc__ + '\n' + '* ``MapGridFrameDivisionsLeft``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsLeft.__doc__ + '\n' + '* ``MapGridFrameDivisionsRight``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsRight.__doc__ + '\n' + '* ``MapGridFrameDivisionsTop``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsTop.__doc__ + '\n' + '* ``MapGridFrameDivisionsBottom``: ' + QgsLayoutObject.DataDefinedProperty.MapGridFrameDivisionsBottom.__doc__ + '\n' + '* ``MapCrs``: ' + QgsLayoutObject.DataDefinedProperty.MapCrs.__doc__ + '\n' + '* ``StartDateTime``: ' + QgsLayoutObject.DataDefinedProperty.StartDateTime.__doc__ + '\n' + '* ``EndDateTime``: ' + QgsLayoutObject.DataDefinedProperty.EndDateTime.__doc__ + '\n' + '* ``MapZRangeLower``: ' + QgsLayoutObject.DataDefinedProperty.MapZRangeLower.__doc__ + '\n' + '* ``MapZRangeUpper``: ' + QgsLayoutObject.DataDefinedProperty.MapZRangeUpper.__doc__ + '\n' + '* ``PictureSource``: ' + QgsLayoutObject.DataDefinedProperty.PictureSource.__doc__ + '\n' + '* ``PictureSvgBackgroundColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgBackgroundColor.__doc__ + '\n' + '* ``PictureSvgStrokeColor``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeColor.__doc__ + '\n' + '* ``PictureSvgStrokeWidth``: ' + QgsLayoutObject.DataDefinedProperty.PictureSvgStrokeWidth.__doc__ + '\n' + '* ``SourceUrl``: ' + QgsLayoutObject.DataDefinedProperty.SourceUrl.__doc__ + '\n' + '* ``LegendTitle``: ' + QgsLayoutObject.DataDefinedProperty.LegendTitle.__doc__ + '\n' + '* ``LegendColumnCount``: ' + QgsLayoutObject.DataDefinedProperty.LegendColumnCount.__doc__ + '\n' + '* ``ScalebarLeftSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLeftSegments.__doc__ + '\n' + '* ``ScalebarRightSegments``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegments.__doc__ + '\n' + '* ``ScalebarSegmentWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSegmentWidth.__doc__ + '\n' + '* ``ScalebarMinimumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMinimumWidth.__doc__ + '\n' + '* ``ScalebarMaximumWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarMaximumWidth.__doc__ + '\n' + '* ``ScalebarHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarHeight.__doc__ + '\n' + '* ``ScalebarRightSegmentSubdivisions``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarRightSegmentSubdivisions.__doc__ + '\n' + '* ``ScalebarSubdivisionHeight``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarSubdivisionHeight.__doc__ + '\n' + '* ``ScalebarFillColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor.__doc__ + '\n' + '* ``ScalebarFillColor2``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarFillColor2.__doc__ + '\n' + '* ``ScalebarLineColor``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineColor.__doc__ + '\n' + '* ``ScalebarLineWidth``: ' + QgsLayoutObject.DataDefinedProperty.ScalebarLineWidth.__doc__ + '\n' + '* ``AttributeTableSourceLayer``: ' + QgsLayoutObject.DataDefinedProperty.AttributeTableSourceLayer.__doc__ + '\n' + '* ``ElevationProfileTolerance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileTolerance.__doc__ + '\n' + '* ``ElevationProfileDistanceMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMajorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceMinorInterval.__doc__ + '\n' + '* ``ElevationProfileDistanceLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileDistanceLabelInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMajorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMajorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationMinorInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationMinorInterval.__doc__ + '\n' + '* ``ElevationProfileElevationLabelInterval``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileElevationLabelInterval.__doc__ + '\n' + '* ``ElevationProfileMinimumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumDistance.__doc__ + '\n' + '* ``ElevationProfileMaximumDistance``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumDistance.__doc__ + '\n' + '* ``ElevationProfileMinimumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMinimumElevation.__doc__ + '\n' + '* ``ElevationProfileMaximumElevation``: ' + QgsLayoutObject.DataDefinedProperty.ElevationProfileMaximumElevation.__doc__ # -- diff --git a/python/core/auto_additions/qgsmaplayerelevationproperties.py b/python/core/auto_additions/qgsmaplayerelevationproperties.py index 5a1ecce6fdc1..47f06bfb5972 100644 --- a/python/core/auto_additions/qgsmaplayerelevationproperties.py +++ b/python/core/auto_additions/qgsmaplayerelevationproperties.py @@ -6,5 +6,11 @@ QgsMapLayerElevationProperties.ExtrusionHeight = QgsMapLayerElevationProperties.Property.ExtrusionHeight QgsMapLayerElevationProperties.ExtrusionHeight.is_monkey_patched = True QgsMapLayerElevationProperties.ExtrusionHeight.__doc__ = "Extrusion height" -QgsMapLayerElevationProperties.Property.__doc__ = "Data definable properties.\n\n.. versionadded:: 3.26\n\n" + '* ``ZOffset``: ' + QgsMapLayerElevationProperties.Property.ZOffset.__doc__ + '\n' + '* ``ExtrusionHeight``: ' + QgsMapLayerElevationProperties.Property.ExtrusionHeight.__doc__ +QgsMapLayerElevationProperties.RasterPerBandLowerElevation = QgsMapLayerElevationProperties.Property.RasterPerBandLowerElevation +QgsMapLayerElevationProperties.RasterPerBandLowerElevation.is_monkey_patched = True +QgsMapLayerElevationProperties.RasterPerBandLowerElevation.__doc__ = "Lower elevation for each raster band (since QGIS 3.38)" +QgsMapLayerElevationProperties.RasterPerBandUpperElevation = QgsMapLayerElevationProperties.Property.RasterPerBandUpperElevation +QgsMapLayerElevationProperties.RasterPerBandUpperElevation.is_monkey_patched = True +QgsMapLayerElevationProperties.RasterPerBandUpperElevation.__doc__ = "Upper elevation for each raster band (since QGIS 3.38)" +QgsMapLayerElevationProperties.Property.__doc__ = "Data definable properties.\n\n.. versionadded:: 3.26\n\n" + '* ``ZOffset``: ' + QgsMapLayerElevationProperties.Property.ZOffset.__doc__ + '\n' + '* ``ExtrusionHeight``: ' + QgsMapLayerElevationProperties.Property.ExtrusionHeight.__doc__ + '\n' + '* ``RasterPerBandLowerElevation``: ' + QgsMapLayerElevationProperties.Property.RasterPerBandLowerElevation.__doc__ + '\n' + '* ``RasterPerBandUpperElevation``: ' + QgsMapLayerElevationProperties.Property.RasterPerBandUpperElevation.__doc__ # -- diff --git a/python/core/auto_generated/auth/qgsauthmanager.sip.in b/python/core/auto_generated/auth/qgsauthmanager.sip.in index 31db5217844d..77a1e5cc1cbe 100644 --- a/python/core/auto_generated/auth/qgsauthmanager.sip.in +++ b/python/core/auto_generated/auth/qgsauthmanager.sip.in @@ -161,7 +161,7 @@ Check whether supplied password is the same as the one already set bool resetMasterPassword( const QString &newpass, const QString &oldpass, bool keepbackup, QString *backuppath /In,Out/ = 0 ); %Docstring Reset the master password to a new one, then re-encrypt all previous -configs in a new database file, optionally backup curren database +configs in a new database file, optionally backup current database :param newpass: New master password to replace existing :param oldpass: Current master password to replace existing diff --git a/python/core/auto_generated/dxf/qgsdxfexport.sip.in b/python/core/auto_generated/dxf/qgsdxfexport.sip.in index cf9d857819e5..6b66baf13d7e 100644 --- a/python/core/auto_generated/dxf/qgsdxfexport.sip.in +++ b/python/core/auto_generated/dxf/qgsdxfexport.sip.in @@ -27,7 +27,7 @@ Exports QGIS layers to the DXF format. struct DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1 ); + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = false, int ddBlocksMaxNumberOfClasses = -1 ); QgsVectorLayer *layer() const; %Docstring @@ -49,6 +49,24 @@ will be split into several dxf layers, one per each unique value. .. versionadded:: 3.12 +%End + + bool buildDataDefinedBlocks() const; +%Docstring +Flag if data defined point block symbols should be created. Default is false + +:return: True if data defined point block symbols should be created + +.. versionadded:: 3.38 +%End + + int dataDefinedBlocksMaximumNumberOfClasses() const; +%Docstring +Returns the maximum number of data defined symbol classes for which blocks are created. Returns -1 if there is no such limitation + +:return: + +.. versionadded:: 3.38 %End }; @@ -56,6 +74,7 @@ unique value. enum Flag { FlagNoMText, + FlagOnlySelectedFeatures, }; typedef QFlags Flags; diff --git a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index 6bb225783328..f5d5448599ee 100644 --- a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -24,7 +24,7 @@ features on another layer. public: struct ValueRelationItem { - ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString() ); + ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString(), const QVariant group = QVariant() ); %Docstring Constructor for ValueRelationItem %End @@ -37,6 +37,7 @@ Constructor for ValueRelationItem QVariant key; QString value; QString description; + QVariant group; }; typedef QVector < QgsValueRelationFieldFormatter::ValueRelationItem > ValueRelationCache; diff --git a/python/core/auto_generated/geometry/qgscircle.sip.in b/python/core/auto_generated/geometry/qgscircle.sip.in index 40ed5109df7d..b42346c5e778 100644 --- a/python/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/core/auto_generated/geometry/qgscircle.sip.in @@ -107,7 +107,7 @@ The azimuth always takes the default value. :param pt2_tg3: Second point of the third tangent. :param epsilon: Value used to compare point. :param pos: Point to determine which circle use in case of multi return. - If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangets are parallels. (since QGIS 3.18) + If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangents are parallels. (since QGIS 3.18) .. seealso:: :py:func:`from3TangentsMulti` diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index 85a93c028670..56bc1221b191 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -708,6 +708,17 @@ on this geometry (including if this geometry is a Point). It is up to the caller to distinguish between these error conditions. (Or maybe we add another method to this object to help make the distinction?) +%End + + bool addTopologicalPoint( const QgsPoint &point, double snappingTolerance = 1e-8, double segmentSearchEpsilon = 1e-12 ); +%Docstring +Adds a vertex to the segment which intersect ``point`` but don't +already have a vertex there. Closest segment is identified using ``segmentSearchEpsilon``. +If a vertex already exists within ``snappingTolearnceDistance``, no additional vertex is inserted. + +:return: ``True`` if point was added, ``False`` otherwise + +.. versionadded:: 3.38 %End bool moveVertex( double x, double y, int atVertex ); diff --git a/python/core/auto_generated/geometry/qgsrectangle.sip.in b/python/core/auto_generated/geometry/qgsrectangle.sip.in index 2c2538134da9..48c377d0358c 100644 --- a/python/core/auto_generated/geometry/qgsrectangle.sip.in +++ b/python/core/auto_generated/geometry/qgsrectangle.sip.in @@ -375,7 +375,11 @@ the specified ``spacing`` between the grid lines. SIP_PYOBJECT __repr__(); %MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); + QString str; + if ( sipCpp->isNull() ) + str = QStringLiteral( "" ); + else + str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End diff --git a/python/core/auto_generated/gps/qgsgpsdetector.sip.in b/python/core/auto_generated/gps/qgsgpsdetector.sip.in index 50011ec27f82..3f4627084ba1 100644 --- a/python/core/auto_generated/gps/qgsgpsdetector.sip.in +++ b/python/core/auto_generated/gps/qgsgpsdetector.sip.in @@ -23,11 +23,41 @@ Class to detect the GPS port #include "qgsgpsdetector.h" %End public: - QgsGpsDetector( const QString &portName ); + + + QgsGpsDetector( const QString &portName = QString(), bool useUnsafeSignals = true ); +%Docstring +Constructor for QgsGpsDetector. + +If ``portName`` is specified, then only devices from the given port will be scanned. Otherwise +all connection types will be attempted (including internal GPS devices). + +Since QGIS 3.38, the ``useUnsafeSignals`` parameter can be set to ``False`` to avoid emitting the +dangerous and fragile :py:func:`~QgsGpsDetector.detected` signal. This is highly recommended, but is opt-in to avoid +breaking stable QGIS 3.x API. If ``useUnsafeSignals`` is set to ``False``, only the safe :py:func:`~QgsGpsDetector.connectionDetected` signal +will be emitted and clients must manually take ownership of the detected connection via a call +to :py:func:`~QgsGpsDetector.takeConnection`. +%End ~QgsGpsDetector(); + QgsGpsConnection *takeConnection() /TransferBack/; +%Docstring +Returns the detected GPS connection, and removes it from the detector. + +The caller takes ownership of the connection. Only the first call to this +method following a :py:func:`~QgsGpsDetector.connectionDetected` signal will be able to retrieve the +detected connection -- subsequent calls will return ``None``. + +.. warning:: + + Do not call this method if the useUnsafeSignals option in the + QgsGpsDetector constructor was set to ``True``. + +.. versionadded:: 3.38 +%End + static QList< QPair > availablePorts(); public slots: @@ -37,14 +67,28 @@ Class to detect the GPS port signals: + void connectionDetected(); +%Docstring +Emitted when a GPS connection is successfully detected. + +Call :py:func:`~QgsGpsDetector.takeConnection` to take ownership of the detected connection. - void detected( QgsGpsConnection *connection ); +.. versionadded:: 3.38 +%End + + void detected( QgsGpsConnection *connection ) /Deprecated/; %Docstring Emitted when the GPS connection has been detected. A single connection must listen for this signal and immediately take ownership of the ``connection`` object. + +.. deprecated:: + This signal is dangerous and extremely unsafe! It is recommended to instead set the ``useUnsafeSignals`` parameter to ``False`` in the QgsGpsDetector constructor and use the safe :py:func:`~QgsGpsDetector.connectionDetected` signal instead. %End void detectionFailed(); +%Docstring +Emitted when the detector could not find a valid GPS connection. +%End }; diff --git a/python/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in b/python/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in index c1806e2becc6..6d12492ece8b 100644 --- a/python/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in +++ b/python/core/auto_generated/layertree/qgscolorramplegendnodesettings.sip.in @@ -208,7 +208,7 @@ Returns ``True`` if a continuous gradient legend will be used. void setUseContinuousLegend( bool useContinuousLegend ); %Docstring -Sets the flag to use a continuos gradient legend to ``useContinuousLegend``. +Sets the flag to use a continuous gradient legend to ``useContinuousLegend``. When this flag is set the legend will be rendered using a continuous color ramp with min and max values, when it is not set the legend will be rendered using separate diff --git a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index 3359621bfad4..7b09fee9f685 100644 --- a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -477,6 +477,15 @@ Evaluates and returns the text label of the current node :param label: text to evaluate instead of the layer layertree string .. versionadded:: 3.10 +%End + + QgsExpressionContextScope *createSymbolScope() const /Factory/; +%Docstring +Create an expression context scope containing symbol related variables. + +The caller takes ownership of the returned object. + +.. versionadded:: 3.36 %End SIP_PYOBJECT __repr__(); diff --git a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in index 6eb9af3b6f22..ddd64087066d 100644 --- a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in @@ -923,6 +923,60 @@ Returns the map's atlas clipping settings. Returns the map's item based clip path settings. .. versionadded:: 3.16 +%End + + void setZRangeEnabled( bool enabled ); +%Docstring +Sets whether the z range is ``enabled`` (i.e. whether the map will be filtered +to content within the :py:func:`~QgsLayoutItemMap.zRange`.) + +.. seealso:: :py:func:`zRangeEnabled` + +.. versionadded:: 3.38 +%End + + bool zRangeEnabled() const; +%Docstring +Returns whether the z range is enabled (i.e. whether the map will be filtered +to content within the :py:func:`~QgsLayoutItemMap.zRange`.) + +.. seealso:: :py:func:`setZRangeEnabled` + +.. seealso:: :py:func:`zRange` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange zRange() const; +%Docstring +Returns the map's z range, which is used to filter the map's content to only +display features within the specified z range. + +.. note:: + + This is only considered when :py:func:`~QgsLayoutItemMap.zRangeEnabled` is ``True``. + +.. seealso:: :py:func:`setZRange` + +.. seealso:: :py:func:`zRangeEnabled` + +.. versionadded:: 3.38 +%End + + void setZRange( const QgsDoubleRange &range ); +%Docstring +Sets the map's z ``range``, which is used to filter the map's content to only +display features within the specified z range. + +.. note:: + + This is only considered when :py:func:`~QgsLayoutItemMap.zRangeEnabled` is ``True``. + +.. seealso:: :py:func:`zRange` + +.. seealso:: :py:func:`setZRangeEnabled` + +.. versionadded:: 3.38 %End virtual double estimatedFrameBleed() const; diff --git a/python/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in b/python/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in index 33d7baeedf14..7eed0aadf36b 100644 --- a/python/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemnodeitem.sip.in @@ -41,7 +41,7 @@ Add a node in current shape. :param point: is the location of the new node (in scene coordinates) :param checkArea: is a flag to indicate if there's a space constraint. -:param radius: is the space contraint and is used only if checkArea is +:param radius: is the space constraint and is used only if checkArea is ``True``. Typically, if this flag is ``True``, the new node has to be nearer than radius to the shape to be added. %End diff --git a/python/core/auto_generated/layout/qgslayoutobject.sip.in b/python/core/auto_generated/layout/qgslayoutobject.sip.in index 34f57b858121..f331bd3c34eb 100644 --- a/python/core/auto_generated/layout/qgslayoutobject.sip.in +++ b/python/core/auto_generated/layout/qgslayoutobject.sip.in @@ -167,6 +167,11 @@ A base class for objects which belong to a layout. MapGridFrameDivisionsRight, MapGridFrameDivisionsTop, MapGridFrameDivisionsBottom, + MapCrs, + StartDateTime, + EndDateTime, + MapZRangeLower, + MapZRangeUpper, //composer picture PictureSource, PictureSvgBackgroundColor, @@ -192,9 +197,6 @@ A base class for objects which belong to a layout. ScalebarLineWidth, //table item AttributeTableSourceLayer, - MapCrs, - StartDateTime, - EndDateTime, ElevationProfileTolerance, ElevationProfileDistanceMajorInterval, ElevationProfileDistanceMinorInterval, diff --git a/python/core/auto_generated/locator/qgslocatorfilter.sip.in b/python/core/auto_generated/locator/qgslocatorfilter.sip.in index fbd9b19a59b3..3dfd4307d7b1 100644 --- a/python/core/auto_generated/locator/qgslocatorfilter.sip.in +++ b/python/core/auto_generated/locator/qgslocatorfilter.sip.in @@ -32,14 +32,14 @@ Constructor for QgsLocatorResult. Constructor for QgsLocatorResult. %End - QVariant getUserData() const; + QVariant userData() const /PyName=_userData/; %Docstring Returns the ``userData``. .. versionadded:: 3.18 %End - void setUserData( QVariant userData ); + void setUserData( const QVariant &userData ); %Docstring Set ``userData`` for the locator result @@ -80,8 +80,6 @@ normally. QList actions; -%Property( name = userData, get = getUserData, set = setUserData ) - }; diff --git a/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in b/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in index fcb618cdef8c..e5e424e261f1 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in @@ -100,7 +100,7 @@ Returns the relative time in milliseconds of the dataset void clear(); %Docstring -Clears alls stored reference times and dataset times +Clears all stored reference times and dataset times %End qint64 firstTimeStepDuration( int group ) const; diff --git a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in index ddb9592ae4e2..f15a152d4cf7 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayerelevationproperties.sip.in @@ -40,12 +40,68 @@ Constructor for QgsMeshLayerElevationProperties, with the specified ``parent`` o virtual QgsMeshLayerElevationProperties *clone() const /Factory/; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + + + Qgis::MeshElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::MeshElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the mesh. + +.. note:: + + This is only considered when :py:func:`~QgsMeshLayerElevationProperties.mode` is :py:class:`Qgis`.MeshElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsMeshLayerElevationProperties.zOffset` and :py:func:`~QgsMeshLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End QgsLineSymbol *profileLineSymbol() const; %Docstring diff --git a/python/core/auto_generated/network/qgsnetworkaccessmanager.sip.in b/python/core/auto_generated/network/qgsnetworkaccessmanager.sip.in index 6a2e92281786..bd75375320df 100644 --- a/python/core/auto_generated/network/qgsnetworkaccessmanager.sip.in +++ b/python/core/auto_generated/network/qgsnetworkaccessmanager.sip.in @@ -11,7 +11,6 @@ - class QgsNetworkRequestParameters { %Docstring(signature="appended") @@ -490,11 +489,30 @@ This signal is propagated to the main thread QgsNetworkAccessManager instance, s only to connect to the main thread's signal in order to receive notifications about requests created in any thread. +.. seealso:: :py:func:`requestCreated` + .. seealso:: :py:func:`finished` .. seealso:: :py:func:`requestTimedOut` .. versionadded:: 3.6 +%End + + void requestCreated( const QgsNetworkRequestParameters &request ); +%Docstring +Emitted when a network request has been created. + +This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary +only to connect to the main thread's signal in order to receive notifications about requests +created in any thread. + +.. seealso:: :py:func:`requestAboutToBeCreated` + +.. seealso:: :py:func:`finished` + +.. seealso:: :py:func:`requestTimedOut` + +.. versionadded:: 3.38 %End void finished( QgsNetworkReplyContent reply ); diff --git a/python/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in b/python/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in index 07e2c20768a6..fe9f0fb135d8 100644 --- a/python/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in +++ b/python/core/auto_generated/pointcloud/qgspointclouddataprovider.sip.in @@ -246,7 +246,7 @@ in the metadata, and even for sources with statistical metadata only some ``stat QgsPointCloudStatistics metadataStatistics(); %Docstring -Returns the object containings the statistics metadata extracted from the dataset +Returns the object containing the statistics metadata extracted from the dataset .. versionadded:: 3.26 %End diff --git a/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in b/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in index 3d2c7331f01a..d95e37a0c5ee 100644 --- a/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in +++ b/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in @@ -38,7 +38,7 @@ Constructor for QgsPointCloudLayerElevationProperties, with the specified ``pare virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; diff --git a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in index a35a7f51df42..80459e767c59 100644 --- a/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in +++ b/python/core/auto_generated/proj/qgscoordinatereferencesystem.sip.in @@ -323,6 +323,17 @@ Creates a CRS from a specified QGIS SRS ID. .. seealso:: :py:func:`validSrsIds` %End + static QgsCoordinateReferenceSystem createCompoundCrs( const QgsCoordinateReferenceSystem &horizontalCrs, const QgsCoordinateReferenceSystem &verticalCrs ); +%Docstring +Given a horizontal and vertical CRS, attempts to create a compound CRS +from them. + +Returns an invalid CRS if the inputs are not suitable for a compound CRS, +or the compound CRS could not be created for the combination. + +.. versionadded:: 3.38 +%End + bool createFromId( long id, CrsType type = PostgisCrsId ) /Deprecated/; @@ -975,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 @@ -1074,6 +1093,32 @@ May return an invalid CRS if the geographic CRS could not be determined. This method will always return a longitude, latitude ordered CRS. .. versionadded:: 3.24 +%End + + QgsCoordinateReferenceSystem horizontalCrs() const; +%Docstring +Returns the horizontal CRS associated with this CRS object. + +In the case of a compound CRS, this method will return just the horizontal CRS component. + +An invalid CRS will be returned if the object does not contain a horizontal component. + +.. seealso:: :py:func:`verticalCrs` + +.. versionadded:: 3.38 +%End + + QgsCoordinateReferenceSystem verticalCrs() const; +%Docstring +Returns the vertical CRS associated with this CRS object. + +In the case of a compound CRS, this method will return just the vertical CRS component. + +An invalid CRS will be returned if the object does not contain a vertical component. + +.. seealso:: :py:func:`horizontalCrs` + +.. versionadded:: 3.38 %End QString geographicCrsAuthId() const; diff --git a/python/core/auto_generated/project/qgsprojectelevationproperties.sip.in b/python/core/auto_generated/project/qgsprojectelevationproperties.sip.in index 6f972be1fa4b..76d275d57c3e 100644 --- a/python/core/auto_generated/project/qgsprojectelevationproperties.sip.in +++ b/python/core/auto_generated/project/qgsprojectelevationproperties.sip.in @@ -67,6 +67,44 @@ Sets the project's terrain ``provider``. Ownership of ``provider`` is transferred to this object. .. seealso:: :py:func:`terrainProvider` +%End + + QgsDoubleRange elevationRange() const; +%Docstring +Returns the project's elevation range, which indicates the upper and lower +elevation limits associated with the project. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`setElevationRange` + +.. seealso:: :py:func:`elevationRangeChanged` + + +.. versionadded:: 3.38 +%End + + public slots: + + void setElevationRange( const QgsDoubleRange &range ); +%Docstring +Sets the project's elevation ``range``, which indicates the upper and lower +elevation limits associated with the project. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`elevationRange` + +.. seealso:: :py:func:`elevationRangeChanged` + + +.. versionadded:: 3.38 %End signals: @@ -74,6 +112,23 @@ Ownership of ``provider`` is transferred to this object. void changed(); %Docstring Emitted when the elevation properties change. +%End + + void elevationRangeChanged( const QgsDoubleRange &range ); +%Docstring +Emitted when the project's elevation ``is`` changed. + +.. note:: + + This is a manual, use-set property, and does not necessarily + coincide with the elevation ranges for individual layers in the project. + +.. seealso:: :py:func:`elevationRange` + +.. seealso:: :py:func:`setElevationRange` + + +.. versionadded:: 3.38 %End }; diff --git a/python/core/auto_generated/providers/qgsdataprovider.sip.in b/python/core/auto_generated/providers/qgsdataprovider.sip.in index 2871f9c41b12..a8ca352bf04a 100644 --- a/python/core/auto_generated/providers/qgsdataprovider.sip.in +++ b/python/core/auto_generated/providers/qgsdataprovider.sip.in @@ -175,16 +175,26 @@ This may be ``None``, depending on the data provider. virtual QgsRectangle extent() const = 0; %Docstring -Returns the extent of the layer +Returns the extent of the layer. -:return: :py:class:`QgsRectangle` containing the extent of the layer +.. warning:: + + This may be expensive to calculate for some data providers, as it may involve + additional network requests or in some cases, iterating through all the features in a layer. + If the provider returns the :py:class:`Qgis`.DataProviderFlag.FastExtent2D flag from the :py:func:`~QgsDataProvider.flags` method + then the call to :py:func:`~QgsDataProvider.extent` is guaranteed to ALWAYS be fast and not involve any additional work. %End virtual QgsBox3D extent3D() const; %Docstring -Returns the 3D extent of the layer +Returns the 3D extent of the layer. + +.. warning:: -:return: :py:class:`QgsBox3D` containing the 3D extent of the layer + This may be expensive to calculate for some data providers, as it may involve + additional network requests or in some cases, iterating through all the features in a layer. + If the provider returns the :py:class:`Qgis`.DataProviderFlag.FastExtent3D flag from the :py:func:`~QgsDataProvider.flags` method + then the call to :py:func:`~QgsDataProvider.extent3D` is guaranteed to ALWAYS be fast and not involve any additional work. .. versionadded:: 3.36 %End diff --git a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in index 62e86d15bc20..3df39d7b2121 100644 --- a/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in +++ b/python/core/auto_generated/providers/sensorthings/qgssensorthingsutils.sip.in @@ -55,12 +55,40 @@ 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 ); %Docstring Returns a filter string which restricts results to those matching the specified ``entityType`` and ``wkbType``. +%End + + static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent ); +%Docstring +Returns a filter string which restricts results to those within the specified +``extent``. + +The ``extent`` should always be specified in EPSG:4326. + +.. versionadded:: 3.38 +%End + + static QString combineFilters( const QStringList &filters ); +%Docstring +Combines a set of SensorThings API filter operators. + +See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter + +.. versionadded:: 3.38 %End static QList< Qgis::GeometryType > availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback = 0, const QString &authCfg = QString() ); diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 24592b32ae17..965636863fe2 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -691,6 +691,14 @@ The development version + enum class RasterRendererCapability + { + UsesMultipleBands, + }; + + typedef QFlags RasterRendererCapabilities; + + enum class RasterAttributeTableFieldUsage { Generic, @@ -1068,6 +1076,8 @@ The development version enum class DataProviderFlag { IsBasemapSource, + FastExtent2D, + FastExtent3D, }; typedef QFlags DataProviderFlags; @@ -1247,6 +1257,7 @@ The development version FixedTemporalRange, TemporalRangeFromDataProvider, RedrawLayerOnly, + FixedRangePerBand, }; enum class TemporalIntervalMatchMethod @@ -1833,6 +1844,28 @@ The development version Centroid, }; + enum class RangeLimits + { + IncludeBoth, + IncludeLowerExcludeUpper, + ExcludeLowerIncludeUpper, + ExcludeBoth, + }; + + enum class RasterElevationMode + { + FixedElevationRange, + RepresentsElevationSurface, + FixedRangePerBand, + DynamicRangePerBand, + }; + + enum class MeshElevationMode + { + FixedElevationRange, + FromVertices + }; + enum class BetweenLineConstraint { NoConstraint, @@ -2718,6 +2751,7 @@ The development version ObservedProperty, Observation, FeatureOfInterest, + MultiDatastream, }; static const double DEFAULT_SEARCH_RADIUS_MM; @@ -2842,6 +2876,8 @@ QFlags operator|(Qgis::ProjectReadFlag f1, QFlags operator|(Qgis::RasterRendererFlag f1, QFlags f2); +QFlags operator|(Qgis::RasterRendererCapability f1, QFlags f2); + QFlags operator|(Qgis::RasterTemporalCapabilityFlag f1, QFlags f2); QFlags operator|(Qgis::RelationshipCapability f1, QFlags f2); diff --git a/python/core/auto_generated/qgsdbquerylog.sip.in b/python/core/auto_generated/qgsdbquerylog.sip.in index a8ce8473683f..6fc5626a0a2a 100644 --- a/python/core/auto_generated/qgsdbquerylog.sip.in +++ b/python/core/auto_generated/qgsdbquerylog.sip.in @@ -51,7 +51,6 @@ Constructor for QgsDatabaseQueryLogEntry. }; - class QgsDatabaseQueryLog: QObject { %Docstring(signature="appended") diff --git a/python/core/auto_generated/qgsfieldproxymodel.sip.in b/python/core/auto_generated/qgsfieldproxymodel.sip.in index 680fd5c45dfd..93de19505372 100644 --- a/python/core/auto_generated/qgsfieldproxymodel.sip.in +++ b/python/core/auto_generated/qgsfieldproxymodel.sip.in @@ -35,6 +35,7 @@ The :py:class:`QgsFieldProxyModel` class provides an easy to use model to displa DateTime, Binary, Boolean, + OriginProvider, AllTypes, }; typedef QFlags Filters; diff --git a/python/core/auto_generated/qgsidentifycontext.sip.in b/python/core/auto_generated/qgsidentifycontext.sip.in index 167b85bf820a..ecfbaa826874 100644 --- a/python/core/auto_generated/qgsidentifycontext.sip.in +++ b/python/core/auto_generated/qgsidentifycontext.sip.in @@ -48,6 +48,27 @@ Returns the datetime range to be used with the identify action. bool isTemporal() const; %Docstring Returns ``True`` if the temporal range setting is enabled. +%End + + QgsDoubleRange zRange() const; +%Docstring +Returns the range of z-values to identify within, or an infinite range if no filtering by +z should be applied. + +.. seealso:: :py:func:`setZRange` + +.. versionadded:: 3.38 +%End + + void setZRange( const QgsDoubleRange &range ); +%Docstring +Sets the ``range`` of z-values to identify within. + +Set to an infinite range if no filtering by z should be applied. + +.. seealso:: :py:func:`zRange` + +.. versionadded:: 3.38 %End }; 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/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index dfa22fa2749c..6dd1b3602ea5 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -1438,7 +1438,7 @@ Returns ``True`` if auto refresh is enabled for the layer. Qgis::AutoRefreshMode autoRefreshMode() const; %Docstring -Returns the layer's automatical refresh mode. +Returns the layer's automatic refresh mode. .. seealso:: :py:func:`autoRefreshInterval` @@ -1602,7 +1602,7 @@ Returns the layer's elevation properties. This may be ``None``, depending on the %Docstring Returns path to the placeholder image or an empty string if a generated legend is shown -:return: placholder image path +:return: placeholder image path .. versionadded:: 3.22 %End diff --git a/python/core/auto_generated/qgsmaplayerelevationproperties.sip.in b/python/core/auto_generated/qgsmaplayerelevationproperties.sip.in index 2681125855ca..78ecfce005a7 100644 --- a/python/core/auto_generated/qgsmaplayerelevationproperties.sip.in +++ b/python/core/auto_generated/qgsmaplayerelevationproperties.sip.in @@ -65,6 +65,8 @@ how an individual :py:class:`QgsMapLayer` behaves with relation to z values or e { ZOffset, ExtrusionHeight, + RasterPerBandLowerElevation, + RasterPerBandUpperElevation, }; enum Flag @@ -119,9 +121,11 @@ Creates a clone of the properties. .. versionadded:: 3.26 %End - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; %Docstring Returns ``True`` if the layer should be visible and rendered for the specified z ``range``. + +Since QGIS 3.38 the ``layer`` argument can be used to specify the target layer. %End virtual QgsMapLayerElevationProperties::Flags flags() const; diff --git a/python/core/auto_generated/qgsmatrix4x4.sip.in b/python/core/auto_generated/qgsmatrix4x4.sip.in index 5a7f165ee461..0ee0c3cdd82c 100644 --- a/python/core/auto_generated/qgsmatrix4x4.sip.in +++ b/python/core/auto_generated/qgsmatrix4x4.sip.in @@ -60,7 +60,7 @@ Multiplies this matrix by another that translates coordinates by the components QgsVector3D map( const QgsVector3D &vector ) const /HoldGIL/; %Docstring -Matrix-vector multiplication (vector is converted to homogenous coordinates [X,Y,Z,1] and back) +Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,Z,1] and back) %End bool isIdentity() const /HoldGIL/; diff --git a/python/core/auto_generated/qgsrange.sip.in b/python/core/auto_generated/qgsrange.sip.in index b3c43147015f..12f109f64d0b 100644 --- a/python/core/auto_generated/qgsrange.sip.in +++ b/python/core/auto_generated/qgsrange.sip.in @@ -41,6 +41,14 @@ whether ranges overlap or during calculation of range intersections. %Docstring Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, and optionally whether or not these bounds are included in the range. +%End + + QgsRange( T lower, T upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 %End T lower() const; @@ -79,6 +87,13 @@ bound is exclusive. .. seealso:: :py:func:`upper` .. seealso:: :py:func:`includeLower` +%End + + Qgis::RangeLimits rangeLimits() const; +%Docstring +Returns the limit handling of the range. + +.. versionadded:: 3.38 %End bool isEmpty() const; @@ -146,6 +161,14 @@ typedef QgsRange QgsRangedoubleBase; %End public: + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsDoubleRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsDoubleRange( double lower, double upper, @@ -186,6 +209,7 @@ Returns ``True`` if the range consists of all possible values. + typedef QgsRange QgsRangeintBase; class QgsIntRange : QgsRangeintBase @@ -206,6 +230,14 @@ typedef QgsRange QgsRangeintBase; %End public: + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ); +%Docstring +Constructor for QgsIntRange. The ``lower`` and ``upper`` bounds are specified, +and whether or not these bounds are included in the range. + +.. versionadded:: 3.38 +%End + QgsIntRange( int lower, int upper, @@ -241,6 +273,7 @@ Returns ``True`` if the range consists of all possible values. }; + template class QgsTemporalRange { diff --git a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in index cc5d267880c4..503741aa9925 100644 --- a/python/core/auto_generated/raster/qgshillshaderenderer.sip.in +++ b/python/core/auto_generated/raster/qgshillshaderenderer.sip.in @@ -56,21 +56,31 @@ Factory method to create a new renderer virtual QList usesBands() const; + virtual int inputBand() const; + virtual void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsHillshadeRenderer.setInputBand` instead %End + virtual bool setInputBand( int band ); + double azimuth() const; %Docstring diff --git a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in index 40300d544def..fabf4a25c007 100644 --- a/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgspalettedrasterrenderer.sip.in @@ -122,11 +122,19 @@ Returns optional category label Set category label %End - int band() const; + int band() const /Deprecated/; %Docstring Returns the raster band used for rendering the raster. + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsPalettedRasterRenderer.inputBand` instead %End + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; virtual QList< QPair< QString, QColor > > legendSymbologyItems() const; diff --git a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in index 76ac4cd2e463..55f8394fed14 100644 --- a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -50,16 +50,10 @@ Creates an instance of the renderer based on definition from XML (used by render virtual QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) /Factory/; + virtual int inputBand() const; + virtual bool setInputBand( int band ); - int inputBand() const; -%Docstring -Returns the number of the input raster band -%End - void setInputBand( int band ); -%Docstring -Sets the number of the input raster band -%End double contourInterval() const; %Docstring diff --git a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in index 64804ffd4d08..e17a74574779 100644 --- a/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayerelevationproperties.sip.in @@ -40,12 +40,14 @@ Constructor for QgsRasterLayerElevationProperties, with the specified ``parent`` virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; virtual bool showByDefaultInElevationProfilePlots() const; + virtual QgsMapLayerElevationProperties::Flags flags() const; + bool isEnabled() const; %Docstring @@ -59,12 +61,34 @@ Returns ``True`` if the elevation properties are enabled, i.e. the raster layer Sets whether the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. .. seealso:: :py:func:`isEnabled` +%End + + Qgis::RasterElevationMode mode() const; +%Docstring +Returns the elevation mode. + +.. seealso:: :py:func:`setMode` + +.. versionadded:: 3.38 +%End + + void setMode( Qgis::RasterElevationMode mode ); +%Docstring +Sets the elevation ``mode``. + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.38 %End int bandNumber() const; %Docstring Returns the band number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`setBandNumber` %End @@ -72,7 +96,104 @@ Returns the band number from which the elevation should be taken. %Docstring Sets the ``band`` number from which the elevation should be taken. +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.RepresentsElevationSurface. + .. seealso:: :py:func:`bandNumber` +%End + + QgsDoubleRange fixedRange() const; +%Docstring +Returns the fixed elevation range for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRange` + +.. versionadded:: 3.38 +%End + + void setFixedRange( const QgsDoubleRange &range ); +%Docstring +Sets the fixed elevation ``range`` for the raster. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedElevationRange. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRange` + +.. versionadded:: 3.38 +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed elevation range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand. + +.. note:: + + When a fixed range is set any :py:func:`~QgsRasterLayerElevationProperties.zOffset` and :py:func:`~QgsRasterLayerElevationProperties.zScale` is ignored. + + +.. seealso:: :py:func:`fixedRangePerBand` + +.. versionadded:: 3.38 +%End + + QgsDoubleRange elevationRangeForPixelValue( QgsRasterLayer *layer, int band, double pixelValue ) const; +%Docstring +Returns the elevation range corresponding to a raw pixel value from the specified ``band``. + +Returns an infinite range if the pixel value does not correspond to an elevation value. + +.. versionadded:: 3.38 +%End + + int bandForElevationRange( QgsRasterLayer *layer, const QgsDoubleRange &range ) const; +%Docstring +Returns the band corresponding to the specified ``range``. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerElevationProperties.mode` is :py:class:`Qgis`.RasterElevationMode.FixedRangePerBand or + :py:class:`Qgis`.RasterElevationMode.DynamicRangePerBand. For other modes it will always return -1. + +.. versionadded:: 3.38 %End QgsLineSymbol *profileLineSymbol() const; diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index 8e9e12e9b208..62d35f120e7b 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -10,6 +10,7 @@ + class QgsRasterLayerTemporalProperties : QgsMapLayerTemporalProperties { %Docstring(signature="appended") @@ -80,7 +81,7 @@ a render context intersects the specified ``range``. .. warning:: This setting is only effective when :py:func:`~QgsRasterLayerTemporalProperties.mode` is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange + :py:class:`Qgis`.RasterTemporalMode.FixedTemporalRange .. seealso:: :py:func:`fixedTemporalRange` %End @@ -91,10 +92,54 @@ Returns the fixed temporal range for the layer. .. warning:: - To be used only when :py:func:`~QgsRasterLayerTemporalProperties.mode` is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange + To be used only when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedTemporalRange .. seealso:: :py:func:`setFixedTemporalRange` +%End + + QMap fixedRangePerBand() const; +%Docstring +Returns the fixed temporal range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. + +.. seealso:: :py:func:`setFixedRangePerBand` + +.. versionadded:: 3.38 +%End + + void setFixedRangePerBand( const QMap &ranges ); +%Docstring +Sets the fixed temporal range for each band. + +.. note:: + + This is only considered when :py:func:`~QgsRasterLayerTemporalProperties.mode` is :py:class:`Qgis`.RasterTemporalMode.FixedRangePerBand. + +.. seealso:: :py:func:`fixedRangePerBand` + +.. versionadded:: 3.38 +%End + + int bandForTemporalRange( QgsRasterLayer *layer, const QgsDateTimeRange &range ) const; +%Docstring +Returns the band corresponding to the specified ``range``. + +.. note:: + + 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 virtual QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ); 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/qgsrasterrenderer.sip.in b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in index e0b862500f9d..200e1c2e86dd 100644 --- a/python/core/auto_generated/raster/qgsrasterrenderer.sip.in +++ b/python/core/auto_generated/raster/qgsrasterrenderer.sip.in @@ -67,6 +67,39 @@ The default implementation returns ``False``. virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; +%Docstring +Returns the input band for the renderer, or -1 if no input band is available. + +For renderers which utilize multiple input bands -1 will be returned. In these +cases :py:func:`~QgsRasterRenderer.usesBands` will return a list of all utilized bands (including alpha +bands). + +.. seealso:: :py:func:`setInputBand` + +.. seealso:: :py:func:`usesBands` + +.. versionadded:: 3.38 +%End + + virtual bool setInputBand( int band ); +%Docstring +Attempts to set the input ``band`` for the renderer. + +Returns ``True`` if the band was successfully set, or ``False`` if the band could not be set. + +.. note:: + + Not all renderers support setting the input band. + +.. seealso:: :py:func:`inputBand` + +.. seealso:: :py:func:`usesBands` + + +.. versionadded:: 3.38 +%End + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, @@ -156,7 +189,9 @@ Useful when cloning renderers. virtual QList usesBands() const; %Docstring -Returns a list of band numbers used by the renderer +Returns a list of band numbers used by the renderer. + +.. seealso:: :py:func:`setInputBand` %End const QgsRasterMinMaxOrigin &minMaxOrigin() const; diff --git a/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in b/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in new file mode 100644 index 000000000000..eeb73ec78d79 --- /dev/null +++ b/python/core/auto_generated/raster/qgsrasterrendererregistry.sip.in @@ -0,0 +1,72 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsRasterRendererRegistry +{ +%Docstring(signature="appended") +Registry for raster renderers. + +:py:class:`QgsRasterRendererRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +.. note:: + + Exposed to Python bindings in QGIS 3.38 +%End + +%TypeHeaderCode +#include "qgsrasterrendererregistry.h" +%End + public: + + QgsRasterRendererRegistry(); +%Docstring +Constructor for QgsRasterRendererRegistry. + +QgsRasterRendererRegistry is not usually directly created, but rather accessed through +:py:func:`QgsApplication.rasterRendererRegistry()`. + +The registry is pre-populated with standard raster renderers. +%End + + + + + QStringList renderersList() const; +%Docstring +Returns a list of the names of registered renderers. +%End + + + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; +%Docstring +Returns the capabilities for the renderer with the specified name. + +.. versionadded:: 3.38 +%End + + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const /Factory/; +%Docstring +Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). +Caller takes ownership. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrasterrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/raster/qgsrastertransparency.sip.in b/python/core/auto_generated/raster/qgsrastertransparency.sip.in index a3e38f3290f4..6b137207897c 100644 --- a/python/core/auto_generated/raster/qgsrastertransparency.sip.in +++ b/python/core/auto_generated/raster/qgsrastertransparency.sip.in @@ -27,28 +27,81 @@ Constructor for QgsRasterTransparency. struct TransparentThreeValuePixel { + + TransparentThreeValuePixel( double red = 0, double green = 0, double blue = 0, double opacity = 0 ); +%Docstring +Constructor for TransparentThreeValuePixel. + +:param red: red pixel value +:param green: green pixel value +:param blue: blue pixel value +:param opacity: opacity for pixel, between 0 and 1.0 + +.. versionadded:: 3.38 +%End + double red; + double green; + double blue; - double percentTransparent; + + double opacity; + + bool operator==( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const; + bool operator!=( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->red ).arg( sipCpp->green ).arg( sipCpp->blue ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End }; struct TransparentSingleValuePixel { + + TransparentSingleValuePixel( double minimum = 0, double maximum = 0, double opacity = 0, bool includeMinimum = true, bool includeMaximum = true ); +%Docstring +Constructor for TransparentSingleValuePixel. + +:param minimum: minimum pixel value to include in range +:param maximum: maximum pixel value to include in range +:param opacity: opacity for pixel, between 0 and 1.0 +:param includeMinimum: whether the minimum value should be included in the range +:param includeMaximum: whether the maximum value should be included in the range + +.. versionadded:: 3.38 +%End + double min; + double max; - double percentTransparent; - }; + double opacity; + + bool includeMinimum; + + bool includeMaximum; + + bool operator==( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const; + bool operator!=( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const; + + SIP_PYOBJECT __repr__(); +%MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->min ).arg( sipCpp->max ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + }; - QList transparentSingleValuePixelList() const; + QVector transparentSingleValuePixelList() const; %Docstring Returns the transparent single value pixel list. .. seealso:: :py:func:`setTransparentSingleValuePixelList` %End - QList transparentThreeValuePixelList() const; + QVector transparentThreeValuePixelList() const; %Docstring Returns the transparent three value pixel list. @@ -65,21 +118,21 @@ Resets the transparency list to a single ``value``. Resets the transparency list to single red, green, and blue values. %End - void setTransparentSingleValuePixelList( const QList &newList ); + void setTransparentSingleValuePixelList( const QVector &newList ); %Docstring Sets the transparent single value pixel list, replacing the whole existing list. .. seealso:: :py:func:`transparentSingleValuePixelList` %End - void setTransparentThreeValuePixelList( const QList &newList ); + void setTransparentThreeValuePixelList( const QVector &newList ); %Docstring Sets the transparent three value pixel list, replacing the whole existing list. .. seealso:: :py:func:`transparentThreeValuePixelList` %End - int alphaValue( double value, int globalTransparency = 255 ) const; + int alphaValue( double value, int globalTransparency = 255 ) const /Deprecated/; %Docstring Returns the transparency value for a single ``value`` pixel. @@ -88,10 +141,22 @@ by the stored transparency value. :param value: the needle to search for in the transparency hay stack :param globalTransparency: the overall transparency level for the layer + +.. deprecated:: + use :py:func:`~QgsRasterTransparency.opacityForValue` instead. %End + double opacityForValue( double value ) const; +%Docstring +Returns the opacity (as a value from 0 to 1) for a single ``value`` pixel. + +Searches through the transparency list, and if a match is found, returns +the opacity corresponding to the value. Returns 1 if no matches are found. - int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const; +.. versionadded:: 3.38 +%End + + int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const /Deprecated/; %Docstring Returns the transparency value for a RGB pixel. @@ -102,6 +167,21 @@ by the stored transparency value. :param greenValue: the green portion of the needle to search for in the transparency hay stack :param blueValue: the green portion of the needle to search for in the transparency hay stack :param globalTransparency: the overall transparency level for the layer + +.. deprecated:: + use :py:func:`~QgsRasterTransparency.opacityForRgbValues` instead. +%End + + double opacityForRgbValues( double redValue, double greenValue, double blueValue ) const; +%Docstring +Returns the opacity (as a value from 0 to 1) for a set of RGB pixel values. + +Searches through the transparency list, and if a match is found, returns +the opacity corresponding to the values. Returns 1 if no matches are found. + +If any of the red, green or blue values are NaN, 0 will be returned. + +.. versionadded:: 3.38 %End bool isEmpty() const; diff --git a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in index f4403c460cac..1ac7bc0e53ba 100644 --- a/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandcolordatarenderer.sip.in @@ -35,6 +35,10 @@ QgsSingleBandColorDataRenderer cannot be copied. Use :py:func:`~QgsSingleBandCol virtual bool setInput( QgsRasterInterface *input ); + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; diff --git a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in index adbb2fdd92a0..08dd967c5a6f 100644 --- a/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandgrayrenderer.sip.in @@ -43,8 +43,25 @@ QgsSingleBandGrayRenderer cannot be copied. Use :py:func:`~QgsSingleBandGrayRend virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; - int grayBand() const; - void setGrayBand( int band ); + int grayBand() const /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.inputBand` instead +%End + + void setGrayBand( int band ) /Deprecated/; +%Docstring + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandGrayRenderer.setInputBand` instead +%End + + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + const QgsContrastEnhancement *contrastEnhancement() const; void setContrastEnhancement( QgsContrastEnhancement *ce /Transfer/ ); %Docstring diff --git a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in index 4e2e1d230461..1c756431eb43 100644 --- a/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in +++ b/python/core/auto_generated/raster/qgssinglebandpseudocolorrenderer.sip.in @@ -90,18 +90,29 @@ Creates a color ramp shader virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; - int band() const; + int band() const /Deprecated/; %Docstring Returns the band used by the renderer + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.inputBand` instead %End - void setBand( int bandNo ); + void setBand( int bandNo ) /Deprecated/; %Docstring Sets the band used by the renderer. .. seealso:: :py:func:`band` + +.. deprecated:: QGIS 3.38 + use :py:func:`~QgsSingleBandPseudoColorRenderer.setInputBand` instead %End + virtual int inputBand() const; + + virtual bool setInputBand( int band ); + + double classificationMin() const; double classificationMax() const; void setClassificationMin( double min ); diff --git a/python/core/auto_generated/sensor/qgsiodevicesensor.sip.in b/python/core/auto_generated/sensor/qgsiodevicesensor.sip.in index 8d8fa2087210..e723fe1210f3 100644 --- a/python/core/auto_generated/sensor/qgsiodevicesensor.sip.in +++ b/python/core/auto_generated/sensor/qgsiodevicesensor.sip.in @@ -241,6 +241,25 @@ Sets the baudrate of the serial port the sensor connects to. :param baudRate: the baudrate (e.g. 9600) .. versionadded:: 3.36 +%End + + QByteArray delimiter() const; +%Docstring +Returns the current delimiter used to separate data frames. If empty, +each serial port data update will be considered a data frame. + +.. versionadded:: 3.38 +%End + + void setDelimiter( const QByteArray &delimiter ); +%Docstring +Sets the delimiter used to identify data frames out of the data received +from the serial port. If empty, each serial port data update will be +considered a data frame. + +:param delimiter: Character used to identify data frames + +.. versionadded:: 3.38 %End virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document ) const; @@ -255,6 +274,11 @@ Sets the baudrate of the serial port the sensor connects to. virtual void handleDisconnect(); + protected slots: + + virtual void parseData(); + + }; %End diff --git a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in index 98ba4885d690..910903ef95b9 100644 --- a/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in @@ -782,7 +782,7 @@ Base class for polygon renderers generating texture images void setStrokeWidthUnit( Qgis::RenderUnit unit ); %Docstring -Sets the ``units`` fo the symbol's stroke width. +Sets the ``units`` for the symbol's stroke width. .. seealso:: :py:func:`strokeWidthUnit` diff --git a/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in b/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in index c485930d6f87..ae2abafdf914 100644 --- a/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in +++ b/python/core/auto_generated/vector/qgsvectorlayerelevationproperties.sip.in @@ -42,7 +42,7 @@ Constructor for QgsVectorLayerElevationProperties, with the specified ``parent`` virtual QString htmlSummary() const; - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = 0 ) const; virtual QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const; @@ -308,7 +308,7 @@ Returns ``True`` if the marker symbol should also be shown in continuous surface void setShowMarkerSymbolInSurfacePlots( bool show ); %Docstring -Sets whehter the marker symbol should also be shown in continuous surface plots. +Sets whether the marker symbol should also be shown in continuous surface plots. .. note:: 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 0f1a068e200a..5ae6df11f734 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 @@ -642,6 +643,7 @@ %Include auto_generated/raster/qgsrasterpyramid.sip %Include auto_generated/raster/qgsrasterrange.sip %Include auto_generated/raster/qgsrasterrenderer.sip +%Include auto_generated/raster/qgsrasterrendererregistry.sip %Include auto_generated/raster/qgsrasterrendererutils.sip %Include auto_generated/raster/qgsrasterresamplefilter.sip %Include auto_generated/raster/qgsrasterresampler.sip diff --git a/python/gui/auto_generated/auth/qgsauthsettingswidget.sip.in b/python/gui/auto_generated/auth/qgsauthsettingswidget.sip.in index 7e63c447588d..dde9e04507ca 100644 --- a/python/gui/auto_generated/auth/qgsauthsettingswidget.sip.in +++ b/python/gui/auto_generated/auth/qgsauthsettingswidget.sip.in @@ -154,7 +154,7 @@ setStoreUsernameChecked check the "Store" checkbox for the username void setStorePasswordChecked( bool checked ); %Docstring -setStorePasswordCheched check the "Store" checkbox for the password +setStorePasswordChecked check the "Store" checkbox for the password :param checked: diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index f7e6e0c277cc..ade00779a2cb 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -72,7 +72,7 @@ Returns the character before the cursor, or an empty string if cursor is set at QString characterAfterCursor() const; %Docstring -Returns the character after the cursor, or an empty string if the cursot is set at end +Returns the character after the cursor, or an empty string if the cursor is set at end .. versionadded:: 3.30 %End diff --git a/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in new file mode 100644 index 000000000000..2fd2258a8d3f --- /dev/null +++ b/python/gui/auto_generated/elevation/qgselevationcontrollerwidget.sip.in @@ -0,0 +1,136 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/elevation/qgselevationcontrollerwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsElevationControllerSettingsAction: QWidgetAction +{ + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerSettingsAction( QWidget *parent = 0 ); + + QgsDoubleSpinBox *sizeSpin(); + +}; + + + +class QgsElevationControllerWidget : QWidget +{ +%Docstring(signature="appended") +A widget for configuring vertical elevation slicing behavior for maps. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgselevationcontrollerwidget.h" +%End + public: + + QgsElevationControllerWidget( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsElevationControllerWidget, with the specified ``parent`` widget. +%End + + virtual void resizeEvent( QResizeEvent *event ); + + + QgsDoubleRange range() const; +%Docstring +Returns the current visible range from the widget. + +.. seealso:: :py:func:`setRange` + +.. seealso:: :py:func:`rangeChanged` +%End + + QgsDoubleRange rangeLimits() const; +%Docstring +Returns the limits of the elevation range which can be selected by the widget. + +.. seealso:: :py:func:`rangeLimits` +%End + + QgsRangeSlider *slider(); +%Docstring +Returns a reference to the slider component of the widget. +%End + + QMenu *menu(); +%Docstring +Returns a reference to the widget's configuration menu, which can be used +to add actions to the menu. +%End + + double fixedRangeSize() const; +%Docstring +Returns the fixed range size, or -1 if no fixed size is set. + +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. + +.. seealso:: :py:func:`setFixedRangeSize` +%End + + public slots: + + void setRange( const QgsDoubleRange &range ); +%Docstring +Sets the current visible ``range`` for the widget. + +.. seealso:: :py:func:`range` + +.. seealso:: :py:func:`rangeChanged` +%End + + void setRangeLimits( const QgsDoubleRange &limits ); +%Docstring +Sets the limits of the elevation range which can be selected by the widget. + +.. seealso:: :py:func:`rangeLimits` +%End + + void setFixedRangeSize( double size ); +%Docstring +Sets the fixed range ``size``. Set to -1 if no fixed size is desired. + +A fixed size forces the selected elevation range to have a matching difference between +the upper and lower elevation. + +.. seealso:: :py:func:`fixedRangeSize` +%End + + signals: + + void rangeChanged( const QgsDoubleRange &range ); +%Docstring +Emitted when the visible range from the widget is changed. + +.. seealso:: :py:func:`setRange` + +.. seealso:: :py:func:`range` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/elevation/qgselevationcontrollerwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/layertree/qgslayertreeview.sip.in b/python/gui/auto_generated/layertree/qgslayertreeview.sip.in index 108f1c0397a8..2ee01a006d06 100644 --- a/python/gui/auto_generated/layertree/qgslayertreeview.sip.in +++ b/python/gui/auto_generated/layertree/qgslayertreeview.sip.in @@ -44,6 +44,24 @@ Returns if private layers are shown. void setShowPrivateLayers( bool showPrivate ); %Docstring Determines if private layers are shown. +%End + + bool hideValidLayers() const; +%Docstring +Returns if valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 +%End + + void setHideValidLayers( bool hideValid ); +%Docstring +Sets whether valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 %End protected: @@ -324,6 +342,22 @@ Returns width of contextual menu mark, at right of layer node items. + bool showPrivateLayers() const; +%Docstring +Returns the show private layers status + +.. versionadded:: 3.18 +%End + + bool hideValidLayers() const; +%Docstring +Returns if valid layers should be hidden (i.e. only invalid layers are shown). + +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 +%End + public slots: void refreshLayerSymbology( const QString &layerId ); %Docstring @@ -363,11 +397,13 @@ Set the show private layers to ``showPrivate`` .. versionadded:: 3.18 %End - bool showPrivateLayers( ); + void setHideValidLayers( bool hideValid ); %Docstring -Returns the show private layers status +Sets whether valid layers should be hidden (i.e. only invalid layers are shown). -.. versionadded:: 3.18 +.. seealso:: :py:func:`setHideValidLayers` + +.. versionadded:: 3.38 %End signals: diff --git a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in index af8d02a7e59d..b746daf926d1 100644 --- a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in +++ b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in @@ -159,6 +159,45 @@ preview result and to populate the list of available functions and variables. Returns if the expression is valid %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + + void saveToRecent( const QString &collection = "generic" ) /Deprecated/; %Docstring Adds the current expression to the given ``collection``. diff --git a/python/gui/auto_generated/qgsexpressionpreviewwidget.sip.in b/python/gui/auto_generated/qgsexpressionpreviewwidget.sip.in index bddec34b2895..44b5ff4fe4c9 100644 --- a/python/gui/auto_generated/qgsexpressionpreviewwidget.sip.in +++ b/python/gui/auto_generated/qgsexpressionpreviewwidget.sip.in @@ -11,6 +11,7 @@ + class QgsExpressionPreviewWidget : QWidget { %Docstring(signature="appended") @@ -34,6 +35,44 @@ Constructor Sets the layer used in the preview %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + void setExpressionText( const QString &expression ); %Docstring Sets the expression @@ -79,7 +118,14 @@ Returns the root node of the expression QList parserErrors() const; %Docstring -Returns the expression parser erros +Returns the expression parser errors +%End + + QString currentPreviewText() const; +%Docstring +Returns the current expression result preview text. + +.. versionadded:: 3.38 %End signals: diff --git a/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in b/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in index 71c750874ea0..5edd269b3202 100644 --- a/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in +++ b/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in @@ -142,6 +142,44 @@ an expression context for the widget. create an expression context when required. %End + + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); +%Docstring +Sets the widget to run using a custom preview generator. + +In this mode, the widget will call a callback function to generate a new :py:class:`QgsExpressionContext` +as the previewed object changes. This can be used to provide custom preview values for different +objects (i.e. for objects which aren't vector layer features). + +:param label: The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands +:param choices: A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. +:param previewContextGenerator: A function which takes a QVariant representing the object to preview, and returns a :py:class:`QgsExpressionContext` to use for previewing the object. + +.. versionadded:: 3.38 +%End +%MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + bool allowEvalErrors() const; %Docstring Allow accepting expressions with evaluation errors. This can be useful when we are not able to 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/qgslayerpropertiesdialog.sip.in b/python/gui/auto_generated/qgslayerpropertiesdialog.sip.in index 1865306875e0..64a5e13fbc72 100644 --- a/python/gui/auto_generated/qgslayerpropertiesdialog.sip.in +++ b/python/gui/auto_generated/qgslayerpropertiesdialog.sip.in @@ -44,7 +44,7 @@ Constructor for QgsLayerPropertiesDialog. %Docstring Sets the metadata ``widget`` and ``page`` associated with the dialog. -This must be called in order for the standard metadata loading/saving functionality to be avialable. +This must be called in order for the standard metadata loading/saving functionality to be available. %End virtual void addPropertiesPageFactory( const QgsMapLayerConfigWidgetFactory *factory ); 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/qgsmapcanvas.sip.in b/python/gui/auto_generated/qgsmapcanvas.sip.in index d80a28e0ccbf..e275616fc633 100644 --- a/python/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/gui/auto_generated/qgsmapcanvas.sip.in @@ -47,6 +47,20 @@ Constructor ~QgsMapCanvas(); + void addOverlayWidget( QWidget *widget /Transfer/, Qt::Edge edge ); +%Docstring +Adds an overlay ``widget`` to the layout, which will be bound to the specified ``edge``. + +The widget will always float above the map canvas. + +.. note:: + + Widgets on the left and right edges will always be positioned first, with + top and bottom edge widgets expanding to take the remaining horizontal space. + +.. versionadded:: 3.38 +%End + double magnificationFactor() const; %Docstring Returns the magnification factor diff --git a/python/gui/auto_generated/qgsnewvectortabledialog.sip.in b/python/gui/auto_generated/qgsnewvectortabledialog.sip.in index a5aa3fadb2c6..58578b82f5fb 100644 --- a/python/gui/auto_generated/qgsnewvectortabledialog.sip.in +++ b/python/gui/auto_generated/qgsnewvectortabledialog.sip.in @@ -96,7 +96,7 @@ Sets the fields to ``fields`` bool createSpatialIndex(); %Docstring -Returns ``True`` if spatialindex checkbox is cheched +Returns ``True`` if spatialindex checkbox is checked @return %End diff --git a/python/gui/auto_generated/qgsoverlaywidgetlayout.sip.in b/python/gui/auto_generated/qgsoverlaywidgetlayout.sip.in new file mode 100644 index 000000000000..abfde87b94df --- /dev/null +++ b/python/gui/auto_generated/qgsoverlaywidgetlayout.sip.in @@ -0,0 +1,85 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsoverlaywidgetlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsOverlayWidgetLayout : QLayout +{ +%Docstring(signature="appended") +A custom layout which can be used to overlay child widgets over a parent widget. + +.. versionadded:: 3.38 +%End + +%TypeHeaderCode +#include "qgsoverlaywidgetlayout.h" +%End + public: + + QgsOverlayWidgetLayout( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsOverlayWidgetLayout, with the specified ``parent`` widget. +%End + ~QgsOverlayWidgetLayout(); + + int count() const final; + void addItem( QLayoutItem *item ) final; + QLayoutItem *itemAt( int index ) const final; + QLayoutItem *takeAt( int index ) final; + QSize sizeHint() const final; + QSize minimumSize() const final; + void setGeometry( const QRect &rect ) final; + + void addWidget( QWidget *widget /Transfer/, Qt::Edge edge ); +%Docstring +Adds a ``widget`` to the layout, which will be bound to the specified ``edge``. + +.. note:: + + Widgets on the left and right edges will always be positioned first, with + top and bottom edge widgets expanding to take the remaining horizontal space. +%End + + void setHorizontalSpacing( int spacing ); +%Docstring +Sets the spacing between widgets that are laid out side by side. + +.. seealso:: :py:func:`horizontalSpacing` +%End + + int horizontalSpacing() const; +%Docstring +Returns the spacing between widgets that are laid out side by side. + +.. seealso:: :py:func:`setHorizontalSpacing` +%End + + void setVerticalSpacing( int spacing ); +%Docstring +Sets the spacing between widgets that are laid out on top of each other. + +.. seealso:: :py:func:`verticalSpacing` +%End + + int verticalSpacing() const; +%Docstring +Returns the spacing between widgets that are laid out on top of each other. + +.. seealso:: :py:func:`setVerticalSpacing` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsoverlaywidgetlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsrangeslider.sip.in b/python/gui/auto_generated/qgsrangeslider.sip.in index 2f4fe6ef4fe4..126ed12e3acf 100644 --- a/python/gui/auto_generated/qgsrangeslider.sip.in +++ b/python/gui/auto_generated/qgsrangeslider.sip.in @@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres .. seealso:: :py:func:`setPageStep` .. seealso:: :py:func:`singleStep` +%End + + int fixedRangeSize() const; +%Docstring +Returns the slider's fixed range size, or -1 if not set. + +If a fixed range size is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed size. + +.. seealso:: :py:func:`setFixedRangeSize` + +.. versionadded:: 3.38 +%End + + void setFixedRangeSize( int size ); +%Docstring +Sets the slider's fixed range ``size``. Set to -1 if no fixed size is desired. + +If a fixed range size is set then moving either the lower or upper slider will automatically +move the other slider accordingly, in order to keep the selected range at the specified +fixed size. + +.. seealso:: :py:func:`fixedRangeSize` + +.. versionadded:: 3.38 %End public slots: @@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed. void rangeLimitsChanged( int minimum, int maximum ); %Docstring Emitted when the limits of values allowed in the widget is changed. +%End + + void fixedRangeSizeChanged( int size ); +%Docstring +Emitted when the widget's fixed range size is changed. + +.. seealso:: :py:func:`fixedRangeSize` + +.. seealso:: :py:func:`setFixedRangeSize` + +.. versionadded:: 3.38 %End }; diff --git a/python/gui/auto_generated/qgsscalevisibilitydialog.sip.in b/python/gui/auto_generated/qgsscalevisibilitydialog.sip.in index be819eff41f1..888a719f8afc 100644 --- a/python/gui/auto_generated/qgsscalevisibilitydialog.sip.in +++ b/python/gui/auto_generated/qgsscalevisibilitydialog.sip.in @@ -54,11 +54,23 @@ The scale value indicates the scale denominator, e.g. 1000.0 for a 1:1000 map. public slots: - void setScaleVisiblity( bool hasScaleVisibility ); + void setScaleVisiblity( bool hasScaleVisibility ) /Deprecated/; %Docstring Set whether scale based visibility is enabled. .. seealso:: :py:func:`hasScaleVisibility` + +.. deprecated:: + Use :py:func:`~QgsScaleVisibilityDialog.setScaleVisibility` +%End + + void setScaleVisibility( bool hasScaleVisibility ); +%Docstring +Set whether scale based visibility is enabled. + +.. seealso:: :py:func:`hasScaleVisibility` + +.. versionadded:: 3.38 %End void setMinimumScale( double scale ); 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/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in b/python/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in index 95c08e8526c1..0af3e6641e3b 100644 --- a/python/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in +++ b/python/gui/auto_generated/qgsvaliditycheckresultswidget.sip.in @@ -14,7 +14,7 @@ class QgsValidityCheckResultsModel : QAbstractItemModel { %Docstring(signature="appended") -A QAbstractItemModel subclass for displaying the results from a :py:class:`QgsAbtractValidityCheck`. +A QAbstractItemModel subclass for displaying the results from a :py:class:`QgsAbstractValidityCheck`. .. versionadded:: 3.6 %End diff --git a/python/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in b/python/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in index ab9afc734aa7..8c1fb9cdf037 100644 --- a/python/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in +++ b/python/gui/auto_generated/raster/qgsrasterlayertemporalpropertieswidget.sip.in @@ -11,6 +11,7 @@ + class QgsRasterLayerTemporalPropertiesWidget : QWidget { %Docstring(signature="appended") diff --git a/python/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in b/python/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in index 68f61b0acc4c..9643a4ec83b4 100644 --- a/python/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in +++ b/python/gui/auto_generated/symbology/qgssvgselectorwidget.sip.in @@ -133,11 +133,21 @@ Defines if the group box to fill parameters is visible .. versionadded:: 3.18 %End - bool allowParamerters() const; + bool allowParamerters() const /Deprecated/; %Docstring Returns if the group box to fill parameters is visible .. versionadded:: 3.18 + +.. deprecated:: + Use :py:func:`~QgsSvgSelectorWidget.allowParameters` +%End + + bool allowParameters() const; +%Docstring +Returns if the group box to fill parameters is visible + +.. versionadded:: 3.38 %End void setBrowserVisible( bool visible ); diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index 67dcea160e1a..c668cc6c9a90 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -160,6 +160,7 @@ %Include auto_generated/qgsoptionsdialoghighlightwidgetsimpl.sip %Include auto_generated/qgsoptionswidgetfactory.sip %Include auto_generated/qgsorderbydialog.sip +%Include auto_generated/qgsoverlaywidgetlayout.sip %Include auto_generated/qgsowssourceselect.sip %Include auto_generated/qgspanelwidget.sip %Include auto_generated/qgspanelwidgetstack.sip @@ -341,6 +342,7 @@ %Include auto_generated/effects/qgseffectstackpropertieswidget.sip %Include auto_generated/effects/qgspainteffectpropertieswidget.sip %Include auto_generated/effects/qgspainteffectwidget.sip +%Include auto_generated/elevation/qgselevationcontrollerwidget.sip %Include auto_generated/elevation/qgselevationprofilecanvas.sip %Include auto_generated/history/qgshistoryentry.sip %Include auto_generated/history/qgshistoryentrymodel.sip diff --git a/python/plugins/db_manager/CMakeLists.txt b/python/plugins/db_manager/CMakeLists.txt index a910ee7fc05e..88bcb3ba26d0 100644 --- a/python/plugins/db_manager/CMakeLists.txt +++ b/python/plugins/db_manager/CMakeLists.txt @@ -5,8 +5,6 @@ file(GLOB OTHER_FILES LICENSE README TODO) file(GLOB PY_FILES *.py) file(GLOB UI_FILES ui/*.ui) -PYQT_WRAP_UI(PYUI_FILES ${UI_FILES}) -PYQT_ADD_RESOURCES(PYRC_FILES resources.qrc) PLUGIN_INSTALL(db_manager . ${OTHER_FILES} ${PY_FILES} ${PYRC_FILES} metadata.txt) -PLUGIN_INSTALL(db_manager ui ${PYUI_FILES} ui/__init__.py) +PLUGIN_INSTALL(db_manager ui ${UI_FILES}) diff --git a/python/plugins/db_manager/db_manager.py b/python/plugins/db_manager/db_manager.py index 874ba6151494..5562d023b8e0 100644 --- a/python/plugins/db_manager/db_manager.py +++ b/python/plugins/db_manager/db_manager.py @@ -43,6 +43,7 @@ from .db_plugins.plugin import BaseError from .dlg_db_error import DlgDbError +from .gui_utils import GuiUtils class DBManager(QMainWindow): @@ -380,7 +381,7 @@ def toolBarOrientation(self): def setupUi(self): self.setWindowTitle(self.tr("DB Manager")) - self.setWindowIcon(QIcon(":/db_manager/icon")) + self.setWindowIcon(GuiUtils.get_icon("dbmanager")) self.resize(QSize(700, 500).expandedTo(self.minimumSizeHint())) # create central tab widget and add the first 3 tabs: info, table and preview @@ -457,7 +458,8 @@ def setupUi(self): self.actionRefresh.setShortcut(QKeySequence("F5")) self.menuDb.addAction(self.actionRefresh) - self.actionSqlWindow = QAction(QIcon(":/db_manager/actions/sql_window"), self.tr("&SQL Window"), + self.actionSqlWindow = QAction(GuiUtils.get_icon('mActionSQLWindow'), + self.tr("&SQL Window"), self.menuDb) self.actionSqlWindow.triggered.connect(self.runSqlWindow) self.actionSqlWindow.setShortcut(QKeySequence("F2")) @@ -482,10 +484,10 @@ def setupUi(self): sep.setObjectName("DB_Manager_TableMenu_placeholder") sep.setVisible(False) - self.actionImport = self.menuTable.addAction(QIcon(":/db_manager/actions/import"), + self.actionImport = self.menuTable.addAction(GuiUtils.get_icon("mActionDBImport"), QApplication.translate("DBManager", "&Import Layer/File…"), self.importActionSlot) - self.actionExport = self.menuTable.addAction(QIcon(":/db_manager/actions/export"), + self.actionExport = self.menuTable.addAction(GuiUtils.get_icon("mActionDBExport"), QApplication.translate("DBManager", "&Export to File…"), self.exportActionSlot) self.menuTable.addSeparator() diff --git a/python/plugins/db_manager/db_manager_plugin.py b/python/plugins/db_manager/db_manager_plugin.py index 92386c7b4419..cd0ea9318e05 100644 --- a/python/plugins/db_manager/db_manager_plugin.py +++ b/python/plugins/db_manager/db_manager_plugin.py @@ -29,8 +29,6 @@ QgsApplication ) -from . import resources_rc # NOQA - class DBManagerPlugin: diff --git a/python/plugins/db_manager/db_model.py b/python/plugins/db_manager/db_model.py index 85a834e94161..59d25ffa7d77 100644 --- a/python/plugins/db_manager/db_model.py +++ b/python/plugins/db_manager/db_model.py @@ -26,6 +26,7 @@ from .db_plugins import supportedDbTypes, createDbPlugin from .db_plugins.plugin import BaseError, Table, Database from .dlg_db_error import DlgDbError +from .gui_utils import GuiUtils from qgis.core import ( QgsApplication, @@ -41,8 +42,6 @@ from qgis.utils import OverrideCursor -from . import resources_rc # NOQA - try: from qgis.core import QgsVectorLayerExporter # NOQA @@ -156,8 +155,8 @@ def __init__(self, connection, parent=None): # load (shared) icon with first instance of table item if not hasattr(ConnectionItem, 'connectedIcon'): - ConnectionItem.connectedIcon = QIcon(":/db_manager/icons/plugged.png") - ConnectionItem.disconnectedIcon = QIcon(":/db_manager/icons/unplugged.png") + ConnectionItem.connectedIcon = GuiUtils.get_icon("plugged") + ConnectionItem.disconnectedIcon = GuiUtils.get_icon("unplugged") def data(self, column): if column == 0: @@ -214,7 +213,7 @@ def __init__(self, schema, parent): # load (shared) icon with first instance of schema item if not hasattr(SchemaItem, 'schemaIcon'): - SchemaItem.schemaIcon = QIcon(":/db_manager/icons/namespace.png") + SchemaItem.schemaIcon = GuiUtils.get_icon("namespace") def data(self, column): if column == 0: @@ -246,13 +245,13 @@ def __init__(self, table, parent): # load (shared) icon with first instance of table item if not hasattr(TableItem, 'tableIcon'): TableItem.tableIcon = QgsApplication.getThemeIcon("/mIconTableLayer.svg") - TableItem.viewIcon = QIcon(":/db_manager/icons/view.png") - TableItem.viewMaterializedIcon = QIcon(":/db_manager/icons/view_materialized.png") + TableItem.viewIcon = GuiUtils.get_icon("view") + TableItem.viewMaterializedIcon = GuiUtils.get_icon("view_materialized") TableItem.layerPointIcon = QgsApplication.getThemeIcon("/mIconPointLayer.svg") TableItem.layerLineIcon = QgsApplication.getThemeIcon("/mIconLineLayer.svg") TableItem.layerPolygonIcon = QgsApplication.getThemeIcon("/mIconPolygonLayer.svg") TableItem.layerRasterIcon = QgsApplication.getThemeIcon("/mIconRasterLayer.svg") - TableItem.layerUnknownIcon = QIcon(":/db_manager/icons/layer_unknown.png") + TableItem.layerUnknownIcon = GuiUtils.get_icon("layer_unknown") def data(self, column): if column == 0: diff --git a/python/plugins/db_manager/db_plugins/data_model.py b/python/plugins/db_manager/db_plugins/data_model.py index 61ff8db5bd8a..4842a79c37f8 100644 --- a/python/plugins/db_manager/db_plugins/data_model.py +++ b/python/plugins/db_manager/db_plugins/data_model.py @@ -19,7 +19,7 @@ """ from qgis.PyQt.QtCore import (Qt, - QTime, + QElapsedTimer, QRegularExpression, QAbstractTableModel, pyqtSignal, @@ -185,7 +185,7 @@ class SqlResultModel(BaseTableModel): def __init__(self, db, sql, parent=None): self.db = db.connector - t = QTime() + t = QElapsedTimer() t.start() c = self.db._execute(None, sql) diff --git a/python/plugins/db_manager/db_plugins/html_elems.py b/python/plugins/db_manager/db_plugins/html_elems.py index 8e4f1b4d5839..96b47f4c301e 100644 --- a/python/plugins/db_manager/db_plugins/html_elems.py +++ b/python/plugins/db_manager/db_plugins/html_elems.py @@ -153,13 +153,6 @@ def __init__(self, rows, attrs=None): HtmlElem.__init__(self, 'table', rows, attrs) -class HtmlWarning(HtmlContent): - - def __init__(self, data): - data = ['   ', data] - HtmlContent.__init__(self, data) - - class HtmlSection(HtmlContent): def __init__(self, title, content=None): diff --git a/python/plugins/db_manager/db_plugins/info_model.py b/python/plugins/db_manager/db_plugins/info_model.py index 0dac39e3a249..c27d77b521b3 100644 --- a/python/plugins/db_manager/db_plugins/info_model.py +++ b/python/plugins/db_manager/db_plugins/info_model.py @@ -77,7 +77,7 @@ def privilegesDetails(self): def toHtml(self): if self.db is None: - return HtmlSection(QApplication.translate("DBManagerPlugin", 'Not connected')).toHtml() + return HtmlSection(QApplication.translate("DBManagerPlugin", ' Not connected')).toHtml() ret = [] diff --git a/python/plugins/db_manager/db_plugins/oracle/data_model.py b/python/plugins/db_manager/db_plugins/oracle/data_model.py index 9a39d2bc16ce..0fc8f6512932 100644 --- a/python/plugins/db_manager/db_plugins/oracle/data_model.py +++ b/python/plugins/db_manager/db_plugins/oracle/data_model.py @@ -21,7 +21,7 @@ ***************************************************************************/ """ -from qgis.PyQt.QtCore import QTime +from qgis.PyQt.QtCore import QElapsedTimer from qgis.core import QgsMessageLog from ..data_model import (TableDataModel, SqlResultModel, @@ -151,7 +151,7 @@ class ORSqlResultModel(SqlResultModel): def __init__(self, db, sql, parent=None): self.db = db.connector - t = QTime() + t = QElapsedTimer() t.start() c = self.db._execute(None, str(sql)) diff --git a/python/plugins/db_manager/db_plugins/vlayers/data_model.py b/python/plugins/db_manager/db_plugins/vlayers/data_model.py index 50dfef7fef65..22b28d1686ff 100644 --- a/python/plugins/db_manager/db_plugins/vlayers/data_model.py +++ b/python/plugins/db_manager/db_plugins/vlayers/data_model.py @@ -26,7 +26,7 @@ from .plugin import LVectorTable from ..plugin import DbError, BaseError -from qgis.PyQt.QtCore import QTime, QTemporaryFile +from qgis.PyQt.QtCore import QElapsedTimer, QTemporaryFile from qgis.core import (QgsVectorLayer, QgsWkbTypes, QgsVirtualLayerDefinition, @@ -119,7 +119,7 @@ def modelDone(self): class LSqlResultModel(BaseTableModel): def __init__(self, db, sql, parent=None, layer=None, path=None): - t = QTime() + t = QElapsedTimer() t.start() if not layer: diff --git a/python/plugins/db_manager/dlg_add_geometry_column.py b/python/plugins/db_manager/dlg_add_geometry_column.py index 4f45203b7199..559fae0ca76d 100644 --- a/python/plugins/db_manager/dlg_add_geometry_column.py +++ b/python/plugins/db_manager/dlg_add_geometry_column.py @@ -20,14 +20,16 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QApplication from qgis.utils import OverrideCursor from .db_plugins.plugin import DbError from .dlg_db_error import DlgDbError +from .gui_utils import GuiUtils -from .ui.ui_DlgAddGeometryColumn import Ui_DbManagerDlgAddGeometryColumn as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgAddGeometryColumn.ui')) class DlgAddGeometryColumn(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_create_constraint.py b/python/plugins/db_manager/dlg_create_constraint.py index 81cd0e799985..b2c571494fec 100644 --- a/python/plugins/db_manager/dlg_create_constraint.py +++ b/python/plugins/db_manager/dlg_create_constraint.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QDialog, QApplication from qgis.utils import OverrideCursor @@ -27,8 +28,9 @@ from .db_plugins.plugin import DbError from .dlg_db_error import DlgDbError from .db_plugins.plugin import TableConstraint +from .gui_utils import GuiUtils -from .ui.ui_DlgCreateConstraint import Ui_DbManagerDlgCreateConstraint as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgCreateConstraint.ui')) class DlgCreateConstraint(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_create_index.py b/python/plugins/db_manager/dlg_create_index.py index ffdb6a5a7d4a..40e3b4c1611a 100644 --- a/python/plugins/db_manager/dlg_create_index.py +++ b/python/plugins/db_manager/dlg_create_index.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QApplication from qgis.utils import OverrideCursor @@ -27,8 +28,9 @@ from .db_plugins.plugin import DbError from .dlg_db_error import DlgDbError from .db_plugins.plugin import TableIndex +from .gui_utils import GuiUtils -from .ui.ui_DlgCreateIndex import Ui_DbManagerDlgCreateIndex as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgCreateIndex.ui')) class DlgCreateIndex(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_create_table.py b/python/plugins/db_manager/dlg_create_table.py index 02d2dd49878d..c7b36d6e6d4d 100644 --- a/python/plugins/db_manager/dlg_create_table.py +++ b/python/plugins/db_manager/dlg_create_table.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, QModelIndex from qgis.PyQt.QtWidgets import QItemDelegate, QComboBox, QDialog, QPushButton, QDialogButtonBox, QMessageBox, QApplication from qgis.PyQt.QtCore import QItemSelectionModel, pyqtSignal @@ -29,8 +30,9 @@ from .db_plugins.data_model import TableFieldsModel from .db_plugins.plugin import DbError, ConnectionError from .dlg_db_error import DlgDbError +from .gui_utils import GuiUtils -from .ui.ui_DlgCreateTable import Ui_DbManagerDlgCreateTable as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgCreateTable.ui')) class TableFieldsDelegate(QItemDelegate): diff --git a/python/plugins/db_manager/dlg_db_error.py b/python/plugins/db_manager/dlg_db_error.py index c7fc2226e531..3da3daa55cda 100644 --- a/python/plugins/db_manager/dlg_db_error.py +++ b/python/plugins/db_manager/dlg_db_error.py @@ -20,11 +20,14 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtWidgets import QDialog -from .ui.ui_DlgDbError import Ui_DbManagerDlgDbError as Ui_Dialog +from .gui_utils import GuiUtils from .db_plugins.plugin import DbError +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgDbError.ui')) + class DlgDbError(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_export_vector.py b/python/plugins/db_manager/dlg_export_vector.py index a5fa8d75d3fc..232b821e3d0d 100644 --- a/python/plugins/db_manager/dlg_export_vector.py +++ b/python/plugins/db_manager/dlg_export_vector.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, QFileInfo from qgis.PyQt.QtWidgets import QDialog, QFileDialog, QMessageBox, QApplication from qgis.PyQt.QtGui import QCursor @@ -31,7 +32,9 @@ QgsSettings) from qgis.utils import OverrideCursor -from .ui.ui_DlgExportVector import Ui_DbManagerDlgExportVector as Ui_Dialog +from .gui_utils import GuiUtils + +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgExportVector.ui')) class DlgExportVector(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_field_properties.py b/python/plugins/db_manager/dlg_field_properties.py index c751eba5b8eb..c4fb29b5c356 100644 --- a/python/plugins/db_manager/dlg_field_properties.py +++ b/python/plugins/db_manager/dlg_field_properties.py @@ -19,10 +19,13 @@ __date__ = 'April 2012' __copyright__ = '(C) 2012, Giuseppe Sucameli' +from qgis.PyQt import uic from qgis.PyQt.QtWidgets import QDialog, QMessageBox from .db_plugins.plugin import TableField -from .ui.ui_DlgFieldProperties import Ui_DbManagerDlgFieldProperties as Ui_Dialog +from .gui_utils import GuiUtils + +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgFieldProperties.ui')) class DlgFieldProperties(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_import_vector.py b/python/plugins/db_manager/dlg_import_vector.py index 81586c57a70f..ebc2140a468c 100644 --- a/python/plugins/db_manager/dlg_import_vector.py +++ b/python/plugins/db_manager/dlg_import_vector.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, QFileInfo from qgis.PyQt.QtWidgets import QDialog, QFileDialog, QMessageBox @@ -35,7 +36,9 @@ from qgis.gui import QgsMessageViewer from qgis.utils import OverrideCursor, iface -from .ui.ui_DlgImportVector import Ui_DbManagerDlgImportVector as Ui_Dialog +from .gui_utils import GuiUtils + +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgImportVector.ui')) class DlgImportVector(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/dlg_query_builder.py b/python/plugins/db_manager/dlg_query_builder.py index 189fad9bf1c5..33998530d23b 100644 --- a/python/plugins/db_manager/dlg_query_builder.py +++ b/python/plugins/db_manager/dlg_query_builder.py @@ -19,11 +19,14 @@ Query builder dialog, based on the QSpatialite plugin (GPLv2+) by Romain Riviere """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, QObject, QEvent from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QTextEdit -from .ui.ui_DlgQueryBuilder import Ui_DbManagerQueryBuilderDlg as Ui_Dialog from .db_plugins.plugin import VectorTable +from .gui_utils import GuiUtils + +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgQueryBuilder.ui')) class FocusEventFilter(QObject): diff --git a/python/plugins/db_manager/dlg_sql_layer_window.py b/python/plugins/db_manager/dlg_sql_layer_window.py index 254ea9d185d3..25bd49a1b204 100644 --- a/python/plugins/db_manager/dlg_sql_layer_window.py +++ b/python/plugins/db_manager/dlg_sql_layer_window.py @@ -21,6 +21,7 @@ """ from hashlib import md5 +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, pyqtSignal from qgis.PyQt.QtWidgets import (QDialog, QWidget, @@ -52,6 +53,7 @@ from .db_plugins.postgis.plugin import PGDatabase from .dlg_db_error import DlgDbError from .dlg_query_builder import QueryBuilderDlg +from .gui_utils import GuiUtils try: from qgis.gui import QgsCodeEditorSQL # NOQA @@ -61,7 +63,7 @@ gui.QgsCodeEditorSQL = SqlEdit -from .ui.ui_DlgSqlLayerWindow import Ui_DbManagerDlgSqlLayerWindow as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgSqlLayerWindow.ui')) import re @@ -127,8 +129,8 @@ def __init__(self, iface, layer, parent=None): self.presetStore.clicked.connect(self.storePreset) self.presetDelete.clicked.connect(self.deletePreset) - self.presetCombo.activated[str].connect(self.loadPreset) - self.presetCombo.activated[str].connect(self.presetName.setText) + self.presetCombo.textActivated.connect(self.loadPreset) + self.presetCombo.textActivated.connect(self.presetName.setText) self.editSql.textChanged.connect(self.updatePresetButtonsState) self.presetName.textChanged.connect(self.updatePresetButtonsState) @@ -155,7 +157,7 @@ def __init__(self, iface, layer, parent=None): self.getColumnsBtn.clicked.connect(self.fillColumnCombos) self.queryBuilderFirst = True - self.queryBuilderBtn.setIcon(QIcon(":/db_manager/icons/sql.gif")) + self.queryBuilderBtn.setIcon(GuiUtils.get_icon("sql")) self.queryBuilderBtn.clicked.connect(self.displayQueryBuilder) self.presetName.textChanged.connect(self.nameChanged) diff --git a/python/plugins/db_manager/dlg_sql_window.py b/python/plugins/db_manager/dlg_sql_window.py index 104e1688f907..8109fa731ce3 100644 --- a/python/plugins/db_manager/dlg_sql_window.py +++ b/python/plugins/db_manager/dlg_sql_window.py @@ -23,6 +23,7 @@ import os +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, pyqtSignal, QDir, QCoreApplication from qgis.PyQt.QtWidgets import (QDialog, QWidget, @@ -56,6 +57,7 @@ from .db_plugins.postgis.plugin import PGDatabase from .dlg_db_error import DlgDbError from .dlg_query_builder import QueryBuilderDlg +from .gui_utils import GuiUtils try: from qgis.gui import QgsCodeEditorSQL # NOQA @@ -65,7 +67,8 @@ gui.QgsCodeEditorSQL = SqlEdit -from .ui.ui_DlgSqlWindow import Ui_DbManagerDlgSqlWindow as Ui_Dialog +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgSqlWindow.ui')) + import re @@ -171,8 +174,8 @@ def __init__(self, iface, db, parent=None): self.presetSaveAsFile.clicked.connect(self.saveAsFilePreset) self.presetLoadFile.clicked.connect(self.loadFilePreset) self.presetDelete.clicked.connect(self.deletePreset) - self.presetCombo.activated[str].connect(self.loadPreset) - self.presetCombo.activated[str].connect(self.presetName.setText) + self.presetCombo.textActivated.connect(self.loadPreset) + self.presetCombo.textActivated.connect(self.presetName.setText) self.updatePresetsCombobox() @@ -205,7 +208,7 @@ def __init__(self, iface, db, parent=None): self.btnCreateView.clicked.connect(self.createView) self.queryBuilderFirst = True - self.queryBuilderBtn.setIcon(QIcon(":/db_manager/icons/sql.gif")) + self.queryBuilderBtn.setIcon(GuiUtils.get_icon("sql")) self.queryBuilderBtn.clicked.connect(self.displayQueryBuilder) self.presetName.textChanged.connect(self.nameChanged) diff --git a/python/plugins/db_manager/dlg_table_properties.py b/python/plugins/db_manager/dlg_table_properties.py index 09d4dc7d0cac..36b9d7357162 100644 --- a/python/plugins/db_manager/dlg_table_properties.py +++ b/python/plugins/db_manager/dlg_table_properties.py @@ -20,6 +20,7 @@ ***************************************************************************/ """ +from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt, pyqtSignal from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QApplication @@ -33,8 +34,10 @@ from .dlg_add_geometry_column import DlgAddGeometryColumn from .dlg_create_constraint import DlgCreateConstraint from .dlg_create_index import DlgCreateIndex +from .gui_utils import GuiUtils -from .ui.ui_DlgTableProperties import Ui_DbManagerDlgTableProperties as Ui_Dialog + +Ui_Dialog, _ = uic.loadUiType(GuiUtils.get_ui_file_path('DlgTableProperties.ui')) class DlgTableProperties(QDialog, Ui_Dialog): diff --git a/python/plugins/db_manager/gui_utils.py b/python/plugins/db_manager/gui_utils.py new file mode 100644 index 000000000000..2d9994538568 --- /dev/null +++ b/python/plugins/db_manager/gui_utils.py @@ -0,0 +1,117 @@ +# /*************************************************************************** +# * * +# * 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. * +# * * +# ***************************************************************************/ + +""" +GUI Utilities +""" + +import os +from typing import Optional, Dict + +from qgis.PyQt.QtGui import ( + QIcon, + QImage, + QPixmap +) + + +class GuiUtils: + """ + Utilities for GUI plugin components + """ + + ICON_CACHE: Dict[str, QIcon] = {} + + @staticmethod + def get_icon(icon: str) -> QIcon: + """ + Returns a plugin icon + :param icon: icon name (base part of file name) + :return: QIcon + """ + if icon in GuiUtils.ICON_CACHE: + return GuiUtils.ICON_CACHE[icon] + + # prefer SVG files if present + path = GuiUtils.get_icon_svg(icon) + if path: + res = QIcon(path) + GuiUtils.ICON_CACHE[icon] = res + return res + + pixmap = GuiUtils.get_icon_as_pixmap(icon) + if pixmap is not None: + res = QIcon(pixmap) + GuiUtils.ICON_CACHE[icon] = res + return res + + # return an invalid icon + GuiUtils.ICON_CACHE[icon] = QIcon() + return QIcon() + + @staticmethod + def get_icon_svg(icon: str) -> str: + """ + Returns a plugin icon's SVG file path + :param icon: icon name (base part of file name) + :return: icon svg path + """ + path = os.path.join( + os.path.dirname(__file__), + 'icons', + icon + '.svg') + if not os.path.exists(path): + return '' + + return path + + @staticmethod + def get_pixmap_path(icon: str) -> Optional[str]: + """ + Returns the path to a pixmap icon + """ + for suffix in ('.png', '.gif', '.xpm'): + path = os.path.join( + os.path.dirname(__file__), + 'icons', + icon + suffix) + if os.path.exists(path): + return path + + return None + + @staticmethod + def get_icon_as_pixmap(icon: str) -> Optional[QPixmap]: + """ + Returns a plugin icon's PNG file path + :param icon: icon name (png file name) + :return: icon png path + """ + path = GuiUtils.get_pixmap_path(icon) + if path is not None: + im = QImage(path) + return QPixmap.fromImage(im) + + return None + + @staticmethod + def get_ui_file_path(file: str) -> str: + """ + Returns a UI file's path + :param file: file name (uifile name) + :return: ui file path + """ + path = os.path.join( + os.path.dirname(__file__), + 'ui', + file) + if not os.path.exists(path): + return '' + + return path diff --git a/python/plugins/db_manager/icons/CMakeLists.txt b/python/plugins/db_manager/icons/CMakeLists.txt index cd201354620a..e19dfa823966 100644 --- a/python/plugins/db_manager/icons/CMakeLists.txt +++ b/python/plugins/db_manager/icons/CMakeLists.txt @@ -1,3 +1,3 @@ -file(GLOB ICON_FILES *.gif *.png *svg *.xpm toolbar/*.png) +file(GLOB ICON_FILES *.gif *.png *.svg *.xpm) PLUGIN_INSTALL(db_manager icons ${ICON_FILES}) diff --git a/python/plugins/db_manager/icons/toolbar/mActionDBExport.svg b/python/plugins/db_manager/icons/mActionDBExport.svg similarity index 100% rename from python/plugins/db_manager/icons/toolbar/mActionDBExport.svg rename to python/plugins/db_manager/icons/mActionDBExport.svg diff --git a/python/plugins/db_manager/icons/toolbar/mActionDBImport.svg b/python/plugins/db_manager/icons/mActionDBImport.svg similarity index 100% rename from python/plugins/db_manager/icons/toolbar/mActionDBImport.svg rename to python/plugins/db_manager/icons/mActionDBImport.svg diff --git a/python/plugins/db_manager/icons/toolbar/mActionSQLWindow.svg b/python/plugins/db_manager/icons/mActionSQLWindow.svg similarity index 100% rename from python/plugins/db_manager/icons/toolbar/mActionSQLWindow.svg rename to python/plugins/db_manager/icons/mActionSQLWindow.svg diff --git a/python/plugins/db_manager/info_viewer.py b/python/plugins/db_manager/info_viewer.py index 3e259af1ad22..d691c8815a01 100644 --- a/python/plugins/db_manager/info_viewer.py +++ b/python/plugins/db_manager/info_viewer.py @@ -24,6 +24,7 @@ from .db_plugins.plugin import BaseError, DbError, DBPlugin, Schema, Table from .dlg_db_error import DlgDbError +from .gui_utils import GuiUtils class InfoViewer(QTextBrowser): @@ -135,8 +136,9 @@ def _showTableInfo(self, table): return True def setHtml(self, html): - # convert special tags :) - html = str(html).replace('', '   ') + # convert special tags + warning_icon_path = GuiUtils.get_pixmap_path('warning-20px') + html = str(html).replace('', f'   ') # add default style html = """ diff --git a/python/plugins/db_manager/metadata.txt b/python/plugins/db_manager/metadata.txt index 152e4698df3a..d6a6e9907499 100644 --- a/python/plugins/db_manager/metadata.txt +++ b/python/plugins/db_manager/metadata.txt @@ -4,7 +4,7 @@ description=Manage your databases within QGIS category=Database version=0.1.20 qgisMinimumVersion=3.0 -supportsQt6=no +supportsQt6=yes author=Giuseppe Sucameli email=brush.tyler@gmail.com diff --git a/python/plugins/db_manager/resources.qrc b/python/plugins/db_manager/resources.qrc deleted file mode 100644 index 8c101a8eb032..000000000000 --- a/python/plugins/db_manager/resources.qrc +++ /dev/null @@ -1,23 +0,0 @@ - - - icons/sql.gif - icons/layer_unknown.png - icons/namespace.png - icons/namespaces.xpm - icons/tables.xpm - icons/user.xpm - icons/users.xpm - icons/view.png - icons/view_materialized.png - icons/warning-20px.png - icons/plugged.png - icons/unplugged.png - icons/about.png - icons/dbmanager.svg - - - icons/toolbar/mActionDBExport.svg - icons/toolbar/mActionDBImport.svg - icons/toolbar/mActionSQLWindow.svg - - diff --git a/python/plugins/db_manager/ui/__init__.py b/python/plugins/db_manager/ui/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/python/plugins/grassprovider/ext/r_li.py b/python/plugins/grassprovider/ext/r_li.py index 9a49286b2ae7..351414bc7c5d 100644 --- a/python/plugins/grassprovider/ext/r_li.py +++ b/python/plugins/grassprovider/ext/r_li.py @@ -34,11 +34,11 @@ def rliPath(): """Return r.li GRASS user dir""" + grass_version = GrassUtils.installedVersion().split('.')[0] if isWindows(): homeDir = win32api.GetShortPathName(os.path.expanduser('~')) - return os.path.join(homeDir, 'AppData', 'Roaming', 'GRASS7', 'r.li') + return os.path.join(homeDir, 'AppData', 'Roaming', f'GRASS{grass_version}', 'r.li') else: - grass_version = GrassUtils.installedVersion().split('.')[0] return os.path.join(os.path.expanduser("~"), f'.grass{grass_version}', 'r.li') diff --git a/python/plugins/grassprovider/ext/v_net.py b/python/plugins/grassprovider/ext/v_net.py index 463e1c9867a1..35d056a7dd2b 100644 --- a/python/plugins/grassprovider/ext/v_net.py +++ b/python/plugins/grassprovider/ext/v_net.py @@ -47,7 +47,7 @@ def incorporatePoints(alg, parameters, context, feedback, pointLayerName='points lineLayer = alg.exportedLayers[networkLayerName] else: raise QgsProcessingException( - alg.tr('GRASS GIS 7 v.net requires a lines layer!')) + alg.tr('GRASS GIS v.net requires a lines layer!')) threshold = alg.parameterAsDouble(parameters, 'threshold', context) diff --git a/python/plugins/grassprovider/grass_algorithm.py b/python/plugins/grassprovider/grass_algorithm.py index 1641e02b6beb..4504177caf26 100644 --- a/python/plugins/grassprovider/grass_algorithm.py +++ b/python/plugins/grassprovider/grass_algorithm.py @@ -276,7 +276,7 @@ def _define_characteristics_from_parsed_description( except Exception as e: QgsMessageLog.logMessage( QCoreApplication.translate("GrassAlgorithm", - 'Could not open GRASS GIS 7 algorithm: {0}').format( + 'Could not open GRASS GIS algorithm: {0}').format( self._name), QCoreApplication.translate("GrassAlgorithm", 'Processing'), @@ -309,7 +309,7 @@ def _define_characteristics_from_parsed_description( param = QgsProcessingParameterExtent( self.GRASS_REGION_EXTENT_PARAMETER, - self.tr('GRASS GIS 7 region extent'), + self.tr('GRASS GIS region extent'), optional=True ) param.setFlags(param.flags() | QgsProcessingParameterDefinition.Flag.FlagAdvanced) @@ -319,7 +319,7 @@ def _define_characteristics_from_parsed_description( # Add a cellsize parameter param = QgsProcessingParameterNumber( self.GRASS_REGION_CELLSIZE_PARAMETER, - self.tr('GRASS GIS 7 region cellsize (leave 0 for default)'), + self.tr('GRASS GIS region cellsize (leave 0 for default)'), type=QgsProcessingParameterNumber.Type.Double, minValue=0.0, maxValue=sys.float_info.max + 1, defaultValue=0.0 ) @@ -450,8 +450,8 @@ def processAlgorithm(self, original_parameters, context, feedback): path = GrassUtils.grassPath() if path == '': raise QgsProcessingException( - self.tr('GRASS GIS 7 folder is not configured. Please ' - 'configure it before running GRASS GIS 7 algorithms.')) + self.tr('GRASS GIS folder is not configured. Please ' + 'configure it before running GRASS GIS algorithms.')) # make a copy of the original parameters dictionary - it gets modified by grass algorithms parameters = {k: v for k, v in original_parameters.items()} @@ -483,7 +483,7 @@ def processAlgorithm(self, original_parameters, context, feedback): getattr(self, fullName)(parameters, context, feedback) # Run GRASS - loglines = [self.tr('GRASS GIS 7 execution commands')] + loglines = [self.tr('GRASS GIS execution commands')] for line in self.commands: feedback.pushCommandInfo(line) loglines.append(line) @@ -493,7 +493,7 @@ def processAlgorithm(self, original_parameters, context, feedback): GrassUtils.executeGrass(self.commands, feedback, self.outputCommands) # If the session has been created outside of this algorithm, add - # the new GRASS GIS 7 layers to it otherwise finish the session + # the new GRASS GIS layers to it otherwise finish the session if existingSession: GrassUtils.addSessionLayers(self.exportedLayers) else: diff --git a/python/plugins/grassprovider/grass_provider.py b/python/plugins/grassprovider/grass_provider.py index 492a8361ff92..4db424ab26f6 100644 --- a/python/plugins/grassprovider/grass_provider.py +++ b/python/plugins/grassprovider/grass_provider.py @@ -102,10 +102,10 @@ def parse_algorithms(self) -> List[QgsProcessingAlgorithm]: if alg.name().strip() != '': algs.append(alg) else: - QgsMessageLog.logMessage(self.tr('Could not open GRASS GIS 7 algorithm: {0}').format(algorithm_json.get('name')), self.tr('Processing'), Qgis.MessageLevel.Critical) + QgsMessageLog.logMessage(self.tr('Could not open GRASS GIS algorithm: {0}').format(algorithm_json.get('name')), self.tr('Processing'), Qgis.MessageLevel.Critical) except Exception as e: QgsMessageLog.logMessage( - self.tr('Could not open GRASS GIS 7 algorithm: {0}\n{1}').format(algorithm_json.get('name'), e), self.tr('Processing'), Qgis.MessageLevel.Critical) + self.tr('Could not open GRASS GIS algorithm: {0}\n{1}').format(algorithm_json.get('name'), e), self.tr('Processing'), Qgis.MessageLevel.Critical) else: # slow approach - pass txt files one by one for descriptionFile in folder.glob('*.txt'): @@ -115,10 +115,10 @@ def parse_algorithms(self) -> List[QgsProcessingAlgorithm]: if alg.name().strip() != '': algs.append(alg) else: - QgsMessageLog.logMessage(self.tr('Could not open GRASS GIS 7 algorithm: {0}').format(descriptionFile), self.tr('Processing'), Qgis.MessageLevel.Critical) + QgsMessageLog.logMessage(self.tr('Could not open GRASS GIS algorithm: {0}').format(descriptionFile), self.tr('Processing'), Qgis.MessageLevel.Critical) except Exception as e: QgsMessageLog.logMessage( - self.tr('Could not open GRASS GIS 7 algorithm: {0}\n{1}').format(descriptionFile, e), self.tr('Processing'), Qgis.MessageLevel.Critical) + self.tr('Could not open GRASS GIS algorithm: {0}\n{1}').format(descriptionFile, e), self.tr('Processing'), Qgis.MessageLevel.Critical) return algs def loadAlgorithms(self): diff --git a/python/plugins/grassprovider/grass_utils.py b/python/plugins/grassprovider/grass_utils.py index 50e8bf75a6dc..814e3fe13b15 100644 --- a/python/plugins/grassprovider/grass_utils.py +++ b/python/plugins/grassprovider/grass_utils.py @@ -354,7 +354,7 @@ def createTempMapset(): os.path.join(folder, 'PERMANENT', 'DEFAULT_WIND')) with open(os.path.join(folder, 'PERMANENT', 'MYNAME'), 'w') as outfile: outfile.write( - 'QGIS GRASS GIS 7 interface: temporary data processing location.\n') + 'QGIS GRASS GIS interface: temporary data processing location.\n') GrassUtils.writeGrassWindow(os.path.join(folder, 'PERMANENT', 'WIND')) mkdir(os.path.join(folder, 'PERMANENT', 'sqlite')) @@ -412,7 +412,7 @@ def prepareGrassExecution(commands): @staticmethod def executeGrass(commands, feedback, outputCommands=None): - loglines = [GrassUtils.tr('GRASS GIS 7 execution console output')] + loglines = [GrassUtils.tr('GRASS GIS execution console output')] grassOutDone = False command, grassenv = GrassUtils.prepareGrassExecution(commands) # QgsMessageLog.logMessage('exec: {}'.format(command), 'DEBUG', Qgis.Info) @@ -589,14 +589,14 @@ def checkGrassIsInstalled(ignorePreviousState=False): 'running GRASS algorithms.') if GrassUtils.command is None: return GrassUtils.tr( - 'GRASS GIS 7 binary {} can\'t be found on this system from a shell. ' + 'GRASS GIS binary {} can\'t be found on this system from a shell. ' 'Please install it or configure your PATH {} environment variable.'.format( '(grass.bat)' if isWindows() else '(grass.sh)', 'or OSGEO4W_ROOT' if isWindows() else '')) # GNU/Linux else: return GrassUtils.tr( - 'GRASS 7 can\'t be found on this system from a shell. ' + 'GRASS can\'t be found on this system from a shell. ' 'Please install it or configure your PATH environment variable.') @staticmethod diff --git a/python/plugins/processing/algs/qgis/CheckValidity.py b/python/plugins/processing/algs/qgis/CheckValidity.py index e298626fb376..7c9aa6028bc6 100644 --- a/python/plugins/processing/algs/qgis/CheckValidity.py +++ b/python/plugins/processing/algs/qgis/CheckValidity.py @@ -44,7 +44,7 @@ QgsProcessingParameterBoolean) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -settings_method_key = "/qgis/digitizing/validate_geometries" +settings_method_key = "/digitizing/validate-geometries" pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] diff --git a/python/plugins/processing/algs/qgis/RandomPointsAlongLines.py b/python/plugins/processing/algs/qgis/RandomPointsAlongLines.py index 7b1b30fab38e..f55ff528cbab 100644 --- a/python/plugins/processing/algs/qgis/RandomPointsAlongLines.py +++ b/python/plugins/processing/algs/qgis/RandomPointsAlongLines.py @@ -100,7 +100,6 @@ def processAlgorithm(self, parameters, context, feedback): nPoints = 0 nIterations = 0 maxIterations = pointCount * 200 - featureCount = source.featureCount() total = 100.0 / pointCount if pointCount else 1 index = QgsSpatialIndex() @@ -116,14 +115,22 @@ def processAlgorithm(self, parameters, context, feedback): ids = source.allFeatureIds() - while nIterations < maxIterations and nPoints < pointCount: + while ids and nIterations < maxIterations and nPoints < pointCount: if feedback.isCanceled(): break # pick random feature fid = random.choice(ids) - f = next(source.getFeatures(request.setFilterFid(fid).setSubsetOfAttributes([]))) + try: + f = next(source.getFeatures(request.setFilterFid(fid).setSubsetOfAttributes([]))) + except StopIteration: + ids.remove(fid) + continue + fGeom = f.geometry() + if fGeom.isEmpty(): + ids.remove(fid) + continue if fGeom.isMultipart(): lines = fGeom.asMultiPolyline() @@ -134,13 +141,20 @@ def processAlgorithm(self, parameters, context, feedback): vertices = fGeom.asPolyline() # pick random segment - if len(vertices) == 2: + nVertices = len(vertices) + if nVertices < 2: + nIterations += 1 + continue + if nVertices == 2: vid = 0 else: - vid = random.randint(0, len(vertices) - 2) + vid = random.randint(0, nVertices - 2) startPoint = vertices[vid] endPoint = vertices[vid + 1] length = da.measureLine(startPoint, endPoint) + if length == 0: + nIterations += 1 + continue dist = length * random.random() d = dist / (length - dist) diff --git a/python/plugins/processing/tests/AlgorithmsTestBase.py b/python/plugins/processing/tests/AlgorithmsTestBase.py index 5dbdbfd58134..270c4b864e57 100644 --- a/python/plugins/processing/tests/AlgorithmsTestBase.py +++ b/python/plugins/processing/tests/AlgorithmsTestBase.py @@ -167,6 +167,12 @@ def check_algorithm(self, name, defs): # ignore user setting for invalid geometry handling context = QgsProcessingContext() context.setProject(QgsProject.instance()) + if 'ellipsoid' in defs: + # depending on the project settings, we can't always rely + # on QgsProject.ellipsoid() returning the same ellipsoid as was + # specified in the test definition. So just force ensure that the + # context's ellipsoid is the desired one + context.setEllipsoid(defs['ellipsoid']) if 'skipInvalid' in defs and defs['skipInvalid']: context.setInvalidGeometryCheck(QgsFeatureRequest.InvalidGeometryCheck.GeometrySkipInvalid) diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml index 7c45de6d2531..7005e7b50f2c 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml @@ -1823,6 +1823,7 @@ tests: - algorithm: native:shortestpathpointtopoint name: Shortest path (point to point, shortest route) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1853,6 +1854,7 @@ tests: - algorithm: native:shortestpathpointtopoint name: Shortest path (point to point, fastest route) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1884,6 +1886,7 @@ tests: - algorithm: native:shortestpathlayertopoint name: Shortest path layer to point + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1914,6 +1917,7 @@ tests: - algorithm: native:shortestpathpointtolayer name: Shortest path point to layer + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 5.0 @@ -1944,6 +1948,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, nodes) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -1975,6 +1980,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, nodes, bounds) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2001,6 +2007,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (shortest, lines) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2032,6 +2039,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (fastest, old parameter) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2066,6 +2074,7 @@ tests: - algorithm: native:serviceareafrompoint name: Service area from point (fastest, new parameter) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2100,6 +2109,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, nodes) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2135,6 +2145,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, nodes, boundary) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 @@ -2162,6 +2173,7 @@ tests: - algorithm: native:serviceareafromlayer name: Service area from layer (shortest, lines) + ellipsoid: WGS84 params: DEFAULT_DIRECTION: 2 DEFAULT_SPEED: 50.0 diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml index c0ef9a7f3bfc..3310b15a1f83 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml @@ -284,7 +284,7 @@ tests: pk: id - algorithm: native:voronoipolygons - name: Voronoi without source atributes + name: Voronoi without source attributes params: BUFFER: 0.0 COPY_ATTRIBUTES: false diff --git a/python/processing/algfactory.py b/python/processing/algfactory.py index 524a001c96f3..d6a82105059a 100644 --- a/python/processing/algfactory.py +++ b/python/processing/algfactory.py @@ -345,6 +345,8 @@ class ProcessingAlgFactory(): LAYOUT = "LAYOUT" LAYOUT_ITEM = "LAYOUT_ITEM" DATETIME = "DATETIME" + DATE = "DATE" + TIME = "TIME" MAP_THEME = "MAP_THEME" PROVIDER_CONNECTION = "PROVIDER_CONNECTION" DATABASE_SCHEMA = "DATABASE_SCHEMA" @@ -487,7 +489,9 @@ def input(self, type, *args, **kwargs): alg.LAYOUT: QgsProcessingParameterLayout alg.LAYOUT_ITEM: QgsProcessingParameterLayoutItem alg.COLOR: QgsProcessingParameterColor - alg.DATETIME: QgsProcessingParameterDateTime + alg.DATETIME: QgsProcessingParameterDateTime(type=QgsProcessingParameterDateTime.Type.DateTime) + alg.DATE: QgsProcessingParameterDateTime(type=QgsProcessingParameterDateTime.Type.Date) + alg.TIME: QgsProcessingParameterDateTime(type=QgsProcessingParameterDateTime.Type.Time) alg.MAP_THEME: QgsProcessingParameterMapTheme alg.PROVIDER_CONNECTION: QgsProcessingParameterProviderConnection alg.DATABASE_SCHEMA: QgsProcessingParameterDatabaseSchema @@ -548,7 +552,9 @@ def dec(f): ProcessingAlgFactory.LAYOUT: QgsProcessingParameterLayout, ProcessingAlgFactory.LAYOUT_ITEM: QgsProcessingParameterLayoutItem, ProcessingAlgFactory.COLOR: QgsProcessingParameterColor, - ProcessingAlgFactory.DATETIME: QgsProcessingParameterDateTime, + ProcessingAlgFactory.DATETIME: partial(QgsProcessingParameterDateTime, type=QgsProcessingParameterDateTime.Type.DateTime), + ProcessingAlgFactory.DATE: partial(QgsProcessingParameterDateTime, type=QgsProcessingParameterDateTime.Type.Date), + ProcessingAlgFactory.TIME: partial(QgsProcessingParameterDateTime, type=QgsProcessingParameterDateTime.Type.Time), ProcessingAlgFactory.MAP_THEME: QgsProcessingParameterMapTheme, ProcessingAlgFactory.PROVIDER_CONNECTION: QgsProcessingParameterProviderConnection, ProcessingAlgFactory.DATABASE_SCHEMA: QgsProcessingParameterDatabaseSchema, diff --git a/python/pyplugin_installer/installer_data.py b/python/pyplugin_installer/installer_data.py index 615f2cc5e58d..089d10a872a5 100644 --- a/python/pyplugin_installer/installer_data.py +++ b/python/pyplugin_installer/installer_data.py @@ -387,8 +387,8 @@ def xmlDownloaded(self): "plugin_id": plugin_id, "name": pluginNodes.item(i).toElement().attribute("name"), "version_available": version, - "version_available_stable": version if not experimental else "", - "version_available_experimental": version if experimental else "", + "version_available_stable": normalizeVersion(version) if not experimental else "", + "version_available_experimental": normalizeVersion(version) if experimental else "", "description": pluginNodes.item(i).firstChildElement("description").text().strip(), "about": pluginNodes.item(i).firstChildElement("about").text().strip(), "author_name": pluginNodes.item(i).firstChildElement("author_name").text().strip(), @@ -583,7 +583,7 @@ def pluginMetadata(fct): qt_version = int(QT_VERSION_STR.split('.')[0]) supports_qt6 = pluginMetadata("supportsQt6").strip().upper() in ("TRUE", "YES") - if qt_version == 6 and not supports_qt6: + if qt_version == 6 and not supports_qt6 and "QGIS_DISABLE_SUPPORTS_QT6_CHECK" not in os.environ: error = "incompatible" errorDetails = QCoreApplication.translate("QgsPluginInstaller", "Plugin does not support Qt6 versions of QGIS") elif version: 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/python/server/auto_generated/qgsserverfilter.sip.in b/python/server/auto_generated/qgsserverfilter.sip.in index b3d8edf4dfaf..fd0839ce47dd 100644 --- a/python/server/auto_generated/qgsserverfilter.sip.in +++ b/python/server/auto_generated/qgsserverfilter.sip.in @@ -93,6 +93,16 @@ parameters, just before entering the main switch for core services. :return: true if the call must propagate to the subsequent filters, false otherwise .. versionadded:: 3.24 +%End + + virtual bool onProjectReady(); +%Docstring +Method called when the :py:class:`QgsProject` instance is ready to be used to perform the request, +just before entering the main switch for core services. + +:return: true if the call must propagate to the subsequent filters, false otherwise + +.. versionadded:: 3.36 %End virtual bool onResponseComplete(); diff --git a/python/server/auto_generated/qgsserverogcapi.sip.in b/python/server/auto_generated/qgsserverogcapi.sip.in index a7e171c9894c..a72ff2ffa22f 100644 --- a/python/server/auto_generated/qgsserverogcapi.sip.in +++ b/python/server/auto_generated/qgsserverogcapi.sip.in @@ -132,9 +132,19 @@ Returns the string representation of a ``ct`` (Content-Type) attribute. Returns the file extension for a ``ct`` (Content-Type). %End - static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ); + static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ) /Deprecated/; %Docstring Returns the Content-Type value corresponding to ``extension``. + +.. deprecated:: + Use :py:func:`~QgsServerOgcApi.contentTypeFromExtension` +%End + + static QgsServerOgcApi::ContentType contentTypeFromExtension( const std::string &extension ); +%Docstring +Returns the Content-Type value corresponding to ``extension``. + +.. versionadded:: 3.38 %End static std::string mimeType( const QgsServerOgcApi::ContentType &contentType ); diff --git a/resources/server/src/landingpage/yarn.lock b/resources/server/src/landingpage/yarn.lock index b5dc9843d076..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" @@ -4544,9 +4567,9 @@ flow-parser@0.*: integrity sha512-3dipGWKnXmE4LEE5yCPHJrSlMYOPAYU7wMBecfKiWPQSZp1CvkpJ59dfuuUIeM2TSttKGSatep77vGG9cjkeqg== follow-redirects@^1.0.0: - version "1.15.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" - integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-in@^1.0.2: version "1.0.2" @@ -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" @@ -8661,9 +8694,9 @@ webpack-chain@^6.5.1: javascript-stringify "^2.0.1" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" diff --git a/scripts/pyqt5_to_pyqt6/pyqt5_to_pyqt6.py b/scripts/pyqt5_to_pyqt6/pyqt5_to_pyqt6.py index be034d1fd717..1b8a5978bdc7 100755 --- a/scripts/pyqt5_to_pyqt6/pyqt5_to_pyqt6.py +++ b/scripts/pyqt5_to_pyqt6/pyqt5_to_pyqt6.py @@ -319,6 +319,17 @@ def _replace_qvariant_type(start_index: int, tokens): 'has been removed in Qt6. Use QFontMetrics.horizontalAdvance() if plugin can ' 'safely require Qt >= 5.11, or QFontMetrics.boundingRect().width() otherwise.\n') + def visit_subscript(_node: ast.Subscript, _parent): + if isinstance(_node.value, ast.Attribute): + if (_node.value.attr == 'activated' and + isinstance(_node.slice, ast.Name) and + _node.slice.id == 'str'): + sys.stderr.write( + f'{filename}:{_node.lineno}:{_node.col_offset} WARNING: activated[str] ' + 'has been removed in Qt6. Consider using QComboBox.activated instead if the string is not required, ' + 'or QComboBox.textActivated if the plugin can ' + 'safely require Qt >= 5.14. Otherwise conditional Qt version code will need to be introduced.\n') + def visit_import(_node: ast.ImportFrom, _parent): import_offsets[Offset(node.lineno, node.col_offset)] = ( node.module, set(name.name for name in node.names), node.end_lineno, @@ -327,6 +338,11 @@ def visit_import(_node: ast.ImportFrom, _parent): for name in node.names: if name.name in import_warnings: print(f'{filename}: {import_warnings[name.name]}') + if name.name == 'resources_rc': + sys.stderr.write( + f'{filename}:{_node.lineno}:{_node.col_offset} WARNING: support for compiled resources ' + 'is removed in Qt6. Directly load icon resources by file path and load UI fields using ' + 'uic.loadUiType by file path instead.\n') if _node.module == 'qgis.PyQt.Qt': extra_imports['qgis.PyQt.QtCore'].update({'Qt'}) removed_imports['qgis.PyQt.Qt'].update({'Qt'}) @@ -343,9 +359,10 @@ def visit_import(_node: ast.ImportFrom, _parent): if isinstance(node, ast.Call): visit_call(node, parent) - - if isinstance(node, ast.Attribute): + elif isinstance(node, ast.Attribute): visit_attribute(node, parent) + elif isinstance(node, ast.Subscript): + visit_subscript(node, parent) if isinstance(node, ast.FunctionDef) and node.name in rename_function_definitions: function_def_renames[ diff --git a/scripts/spell_check/check_spelling.sh b/scripts/spell_check/check_spelling.sh index e549d645c9c2..cc550d574aa3 100755 --- a/scripts/spell_check/check_spelling.sh +++ b/scripts/spell_check/check_spelling.sh @@ -61,9 +61,21 @@ while getopts ":rdl:" opt; do done shift $((OPTIND - 1)) -if [ $# -ne 0 ]; then +# check pipe or command line +if [ -p /dev/stdin ]; then + # with pipe input + read SCRIPT_INPUT + if [ -z "$SCRIPT_INPUT" ]; then + exit 0 + fi +else + # no pipe input + SCRIPT_INPUT="$@" +fi + +if [ -n "$SCRIPT_INPUT" ]; then EXCLUDE=$(${GP}sed -e 's/\s*#.*$//' -e '/^\s*$/d' $AGIGNORE | tr '\n' '|' | ${GP}sed -e 's/|$//') - INPUTFILES=$(echo "$@" | tr -s '[[:blank:]]' '\n' | ${GP}grep -Eiv "$EXCLUDE" | tr '\n' ' ' ) + INPUTFILES=$(echo "$SCRIPT_INPUT" | tr -s '[[:blank:]]' '\n' | ${GP}grep -Eiv "$EXCLUDE" | tr '\n' ' ' ) if [[ -z $INPUTFILES ]]; then exit 0 fi diff --git a/src/3d/materials/qgsphongmaterialsettings.cpp b/src/3d/materials/qgsphongmaterialsettings.cpp index d7c5842804b0..3784feae3a5b 100644 --- a/src/3d/materials/qgsphongmaterialsettings.cpp +++ b/src/3d/materials/qgsphongmaterialsettings.cpp @@ -37,9 +37,6 @@ typedef Qt3DCore::QGeometry Qt3DQGeometry; #include #include #include -#include -#include -#include #include @@ -301,22 +298,6 @@ Qt3DRender::QMaterial *QgsPhongMaterialSettings::constantColorMaterial( const Qg mSpecular.greenF() * mSpecularCoefficient, mSpecular.blueF() * mSpecularCoefficient ) ) ); - if ( mOpacity < 1.0f ) - { - Qt3DRender::QNoDepthMask *noDepthMask = new Qt3DRender::QNoDepthMask( renderPass ); - - Qt3DRender::QBlendEquationArguments *blendState = new Qt3DRender::QBlendEquationArguments( renderPass ); - blendState->setSourceRgb( Qt3DRender::QBlendEquationArguments::SourceAlpha ); - blendState->setDestinationRgb( Qt3DRender::QBlendEquationArguments::OneMinusSourceAlpha ); - - Qt3DRender::QBlendEquation *blendEquation = new Qt3DRender::QBlendEquation( renderPass ); - blendEquation->setBlendFunction( Qt3DRender::QBlendEquation::Add ); - - renderPass->addRenderState( noDepthMask ); - renderPass->addRenderState( blendState ); - renderPass->addRenderState( blendEquation ); - } - eff->addTechnique( technique ); material->setEffect( eff ); @@ -354,22 +335,6 @@ Qt3DRender::QMaterial *QgsPhongMaterialSettings::dataDefinedMaterial() const eff->addParameter( new Qt3DRender::QParameter( QStringLiteral( "shininess" ), static_cast< float >( mShininess ) ) ); eff->addParameter( new Qt3DRender::QParameter( QStringLiteral( "opacity" ), static_cast< float >( mOpacity ) ) ); - if ( mOpacity < 1.0f ) - { - Qt3DRender::QNoDepthMask *noDepthMask = new Qt3DRender::QNoDepthMask( renderPass ); - - Qt3DRender::QBlendEquationArguments *blendState = new Qt3DRender::QBlendEquationArguments( renderPass ); - blendState->setSourceRgb( Qt3DRender::QBlendEquationArguments::SourceAlpha ); - blendState->setDestinationRgb( Qt3DRender::QBlendEquationArguments::OneMinusSourceAlpha ); - - Qt3DRender::QBlendEquation *blendEquation = new Qt3DRender::QBlendEquation( renderPass ); - blendEquation->setBlendFunction( Qt3DRender::QBlendEquation::Add ); - - renderPass->addRenderState( noDepthMask ); - renderPass->addRenderState( blendState ); - renderPass->addRenderState( blendEquation ); - } - eff->addTechnique( technique ); material->setEffect( eff ); diff --git a/src/3d/mesh/qgsmesh3dgeometry_p.cpp b/src/3d/mesh/qgsmesh3dgeometry_p.cpp index 698b8afa5902..3755e1ddfb9c 100644 --- a/src/3d/mesh/qgsmesh3dgeometry_p.cpp +++ b/src/3d/mesh/qgsmesh3dgeometry_p.cpp @@ -94,7 +94,7 @@ static QByteArray createDatasetVertexData( const QVector scalarMagnitude = QgsMeshLayerUtils::calculateMagnitudeOnVertices( nativeMesh, data.scalarGroupMetadata, data.scalarData, data.activeFaceFlagValues ); - //Calculate normales with Z value equal to verticaleMagnitude + //Calculate normals with Z value equal to verticaleMagnitude const QVector normals = QgsMeshLayerUtils::calculateNormals( mesh, verticalMagnitude, data.isVerticalMagnitudeRelative ); // Populate a buffer with the interleaved per-vertex data with @@ -230,7 +230,7 @@ QgsMeshDataset3DGeometry::QgsMeshDataset3DGeometry( { const int stride = ( 3 /*position*/ + - 3 /*normale*/ + + 3 /*normal*/ + 1 /*magnitude*/ ) * sizeof( float ); prepareVerticesPositionAttribute( mVertexBuffer, stride, 0 ); @@ -327,7 +327,7 @@ QgsMeshTerrain3DGeometry::QgsMeshTerrain3DGeometry( const QgsTriangularMesh &tri { const int stride = ( 3 /*position*/ + - 3 /*normale*/ ) * sizeof( float ); + 3 /*normal*/ ) * sizeof( float ); prepareVerticesPositionAttribute( mVertexBuffer, stride, 0 ); prepareVerticesNormalAttribute( mVertexBuffer, stride, 3 ); diff --git a/src/3d/mesh/qgsmesh3dgeometry_p.h b/src/3d/mesh/qgsmesh3dgeometry_p.h index 2ebc8ada7aa1..2f153bd797b3 100644 --- a/src/3d/mesh/qgsmesh3dgeometry_p.h +++ b/src/3d/mesh/qgsmesh3dgeometry_p.h @@ -173,7 +173,7 @@ class QgsMeshDataset3DGeometryBuilder; * Then the instance launches immediately another thread that constructs 3D vertices, faces and scalar value on vertices * depending on the dataset chosen for vertical magnitude and the one for scalar magnitude (color rendering). * - * When this job is finished, the mesh datset 3D geometry node is updated and can be rendered in the 3D scene. + * When this job is finished, the mesh dataset 3D geometry node is updated and can be rendered in the 3D scene. */ class QgsMeshDataset3DGeometry: public QgsMesh3DGeometry { diff --git a/src/3d/qgs3daxis.h b/src/3d/qgs3daxis.h index f45fa4df0bbe..01f5bc9fe198 100644 --- a/src/3d/qgs3daxis.h +++ b/src/3d/qgs3daxis.h @@ -200,7 +200,7 @@ class Qgs3DWiredMesh : public Qt3DRender::QGeometryRenderer public: /** - * \brief Defaul Qgs3DWiredMesh constructor + * \brief Default Qgs3DWiredMesh constructor */ Qgs3DWiredMesh( Qt3DCore::QNode *parent = nullptr ); ~Qgs3DWiredMesh() override; diff --git a/src/3d/qgs3dmapscene.cpp b/src/3d/qgs3dmapscene.cpp index 9f61f760aa2f..dbe9929cb40a 100644 --- a/src/3d/qgs3dmapscene.cpp +++ b/src/3d/qgs3dmapscene.cpp @@ -1157,24 +1157,22 @@ void Qgs3DMapScene::addCameraRotationCenterEntity( QgsCameraController *controll { mEntityRotationCenter = new Qt3DCore::QEntity; - Qt3DCore::QTransform *trCameraViewCenter = new Qt3DCore::QTransform; - mEntityRotationCenter->addComponent( trCameraViewCenter ); - Qt3DExtras::QPhongMaterial *materialCameraViewCenter = new Qt3DExtras::QPhongMaterial; - materialCameraViewCenter->setAmbient( Qt::blue ); - mEntityRotationCenter->addComponent( materialCameraViewCenter ); - Qt3DExtras::QSphereMesh *rendererCameraViewCenter = new Qt3DExtras::QSphereMesh; - rendererCameraViewCenter->setRadius( 10 ); - mEntityRotationCenter->addComponent( rendererCameraViewCenter ); - mEntityRotationCenter->setEnabled( true ); + Qt3DCore::QTransform *trRotationCenter = new Qt3DCore::QTransform; + mEntityRotationCenter->addComponent( trRotationCenter ); + Qt3DExtras::QPhongMaterial *materialRotationCenter = new Qt3DExtras::QPhongMaterial; + materialRotationCenter->setAmbient( Qt::blue ); + mEntityRotationCenter->addComponent( materialRotationCenter ); + Qt3DExtras::QSphereMesh *rendererRotationCenter = new Qt3DExtras::QSphereMesh; + rendererRotationCenter->setRadius( 10 ); + mEntityRotationCenter->addComponent( rendererRotationCenter ); + mEntityRotationCenter->setEnabled( false ); mEntityRotationCenter->setParent( this ); - connect( controller, &QgsCameraController::cameraRotationCenterChanged, this, [trCameraViewCenter]( QVector3D center ) + connect( controller, &QgsCameraController::cameraRotationCenterChanged, this, [trRotationCenter]( QVector3D center ) { - trCameraViewCenter->setTranslation( center ); + trRotationCenter->setTranslation( center ); } ); - mEntityRotationCenter->setEnabled( mMap.showCameraRotationCenter() ); - connect( &mMap, &Qgs3DMapSettings::showCameraRotationCenterChanged, this, [this] { mEntityRotationCenter->setEnabled( mMap.showCameraRotationCenter() ); diff --git a/src/3d/qgsframegraph.cpp b/src/3d/qgsframegraph.cpp index adc1cff16bd7..b115fdd9fb30 100644 --- a/src/3d/qgsframegraph.cpp +++ b/src/3d/qgsframegraph.cpp @@ -43,6 +43,7 @@ typedef Qt3DCore::QGeometry Qt3DQGeometry; #include #include #include +#include #include #include #include @@ -91,12 +92,13 @@ Qt3DRender::QFrameGraphNode *QgsFrameGraph::constructForwardRenderPass() // | QRenderStateSet | cull back faces | QSortPolicy | back to front // +-----------------+ +-----------------+ // | | - // +-----------------+ +-----------------+ use depth test - // | QFrustumCulling | | QRenderStateSet | don't write depths - // +-----------------+ +-----------------+ no culling - // | use alpha blending - // +-----------------+ - // | QClearBuffers | color and depth + // +-----------------+ +--------------------+--------------------+ + // | QFrustumCulling | | | + // +-----------------+ +-----------------+ use depth tests +-----------------+ use depth tests + // | | QRenderStateSet | don't write depths | QRenderStateSet | write depths + // | +-----------------+ write colors +-----------------+ don't write colors + // +-----------------+ use alpha blending don't use alpha blending + // | QClearBuffers | color and depth no culling no culling // +-----------------+ mMainCameraSelector = new Qt3DRender::QCameraSelector; @@ -138,27 +140,29 @@ Qt3DRender::QFrameGraphNode *QgsFrameGraph::constructForwardRenderPass() mForwardRenderTargetSelector = new Qt3DRender::QRenderTargetSelector( mForwardRenderLayerFilter ); mForwardRenderTargetSelector->setTarget( forwardRenderTarget ); + // first branch: opaque layer filter Qt3DRender::QLayerFilter *opaqueObjectsFilter = new Qt3DRender::QLayerFilter( mForwardRenderTargetSelector ); opaqueObjectsFilter->addLayer( mTransparentObjectsPassLayer ); opaqueObjectsFilter->setFilterMode( Qt3DRender::QLayerFilter::DiscardAnyMatchingLayers ); - Qt3DRender::QRenderStateSet *forwaredRenderStateSet = new Qt3DRender::QRenderStateSet( opaqueObjectsFilter ); + Qt3DRender::QRenderStateSet *forwardedRenderStateSet = new Qt3DRender::QRenderStateSet( opaqueObjectsFilter ); Qt3DRender::QDepthTest *depthTest = new Qt3DRender::QDepthTest; depthTest->setDepthFunction( Qt3DRender::QDepthTest::Less ); - forwaredRenderStateSet->addRenderState( depthTest ); + forwardedRenderStateSet->addRenderState( depthTest ); Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace; cullFace->setMode( Qt3DRender::QCullFace::CullingMode::Back ); - forwaredRenderStateSet->addRenderState( cullFace ); + forwardedRenderStateSet->addRenderState( cullFace ); - mFrustumCulling = new Qt3DRender::QFrustumCulling( forwaredRenderStateSet ); + mFrustumCulling = new Qt3DRender::QFrustumCulling( forwardedRenderStateSet ); mForwardClearBuffers = new Qt3DRender::QClearBuffers( mFrustumCulling ); mForwardClearBuffers->setClearColor( QColor::fromRgbF( 0.0, 0.0, 1.0, 1.0 ) ); mForwardClearBuffers->setBuffers( Qt3DRender::QClearBuffers::ColorDepthBuffer ); mForwardClearBuffers->setClearDepthValue( 1.0f ); + // second branch: transparent layer filter - color Qt3DRender::QLayerFilter *transparentObjectsLayerFilter = new Qt3DRender::QLayerFilter( mForwardRenderTargetSelector ); transparentObjectsLayerFilter->addLayer( mTransparentObjectsPassLayer ); transparentObjectsLayerFilter->setFilterMode( Qt3DRender::QLayerFilter::AcceptAnyMatchingLayers ); @@ -168,35 +172,52 @@ Qt3DRender::QFrameGraphNode *QgsFrameGraph::constructForwardRenderPass() sortTypes.push_back( Qt3DRender::QSortPolicy::BackToFront ); sortPolicy->setSortTypes( sortTypes ); - Qt3DRender::QRenderStateSet *transparentObjectsRenderStateSet = new Qt3DRender::QRenderStateSet( sortPolicy ); + Qt3DRender::QRenderStateSet *transparentObjectsRenderStateSetColor = new Qt3DRender::QRenderStateSet( sortPolicy ); { Qt3DRender::QDepthTest *depthTest = new Qt3DRender::QDepthTest; depthTest->setDepthFunction( Qt3DRender::QDepthTest::Less ); - transparentObjectsRenderStateSet->addRenderState( depthTest ); + transparentObjectsRenderStateSetColor->addRenderState( depthTest ); Qt3DRender::QNoDepthMask *noDepthMask = new Qt3DRender::QNoDepthMask; - transparentObjectsRenderStateSet->addRenderState( noDepthMask ); + transparentObjectsRenderStateSetColor->addRenderState( noDepthMask ); Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace; cullFace->setMode( Qt3DRender::QCullFace::CullingMode::NoCulling ); - transparentObjectsRenderStateSet->addRenderState( cullFace ); + transparentObjectsRenderStateSetColor->addRenderState( cullFace ); Qt3DRender::QBlendEquation *blendEquation = new Qt3DRender::QBlendEquation; blendEquation->setBlendFunction( Qt3DRender::QBlendEquation::Add ); - transparentObjectsRenderStateSet->addRenderState( blendEquation ); - - Qt3DRender::QBlendEquationArguments *blenEquationArgs = new Qt3DRender::QBlendEquationArguments; - blenEquationArgs->setSourceRgb( Qt3DRender::QBlendEquationArguments::Blending::One ); - blenEquationArgs->setDestinationRgb( Qt3DRender::QBlendEquationArguments::Blending::OneMinusSource1Alpha ); - blenEquationArgs->setSourceAlpha( Qt3DRender::QBlendEquationArguments::Blending::One ); - blenEquationArgs->setDestinationAlpha( Qt3DRender::QBlendEquationArguments::Blending::OneMinusSource1Alpha ); - transparentObjectsRenderStateSet->addRenderState( blenEquationArgs ); + transparentObjectsRenderStateSetColor->addRenderState( blendEquation ); + + Qt3DRender::QBlendEquationArguments *blendEquationArgs = new Qt3DRender::QBlendEquationArguments; + blendEquationArgs->setSourceRgb( Qt3DRender::QBlendEquationArguments::Blending::SourceAlpha ); + blendEquationArgs->setDestinationRgb( Qt3DRender::QBlendEquationArguments::Blending::OneMinusSourceAlpha ); + transparentObjectsRenderStateSetColor->addRenderState( blendEquationArgs ); + } + + // third branch: transparent layer filter - depth + Qt3DRender::QRenderStateSet *transparentObjectsRenderStateSetDepth = new Qt3DRender::QRenderStateSet( sortPolicy ); + { + Qt3DRender::QDepthTest *depthTest = new Qt3DRender::QDepthTest; + depthTest->setDepthFunction( Qt3DRender::QDepthTest::Less ); + transparentObjectsRenderStateSetDepth->addRenderState( depthTest ); + + Qt3DRender::QColorMask *noColorMask = new Qt3DRender::QColorMask; + noColorMask->setAlphaMasked( false ); + noColorMask->setRedMasked( false ); + noColorMask->setGreenMasked( false ); + noColorMask->setBlueMasked( false ); + transparentObjectsRenderStateSetDepth->addRenderState( noColorMask ); + + Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace; + cullFace->setMode( Qt3DRender::QCullFace::CullingMode::NoCulling ); + transparentObjectsRenderStateSetDepth->addRenderState( cullFace ); } mDebugOverlay = new Qt3DRender::QDebugOverlay( mForwardClearBuffers ); mDebugOverlay->setEnabled( false ); - // cppcheck wrongly believes transparentObjectsRenderStateSet will leak + // cppcheck wrongly believes transparentObjectsRenderStateSetColor and transparentObjectsRenderStateSetDepth will leak // cppcheck-suppress memleak return mMainCameraSelector; } diff --git a/src/3d/symbols/qgspointcloud3dsymbol_p.h b/src/3d/symbols/qgspointcloud3dsymbol_p.h index 04e60a593460..fd5d9d4b4668 100644 --- a/src/3d/symbols/qgspointcloud3dsymbol_p.h +++ b/src/3d/symbols/qgspointcloud3dsymbol_p.h @@ -90,7 +90,7 @@ class QgsPointCloud3DSymbolHandler // : public QgsFeature3DHandler * Applies a filter on triangles to improve the rendering: * * - keeps only triangles that have a least one point in the bounding box \a bbox - * - if options are selected, skips triangles with horizontale or vertical size greater than a threshold + * - if options are selected, skips triangles with horizontal or vertical size greater than a threshold * * Must be used only in the method triangulate(). */ diff --git a/src/3d/terrain/qgsterraintileloader_p.cpp b/src/3d/terrain/qgsterraintileloader_p.cpp index 064328c525b0..f3b3c803ef4c 100644 --- a/src/3d/terrain/qgsterraintileloader_p.cpp +++ b/src/3d/terrain/qgsterraintileloader_p.cpp @@ -87,9 +87,9 @@ void QgsTerrainTileLoader::createTextureComponent( QgsTerrainTileEntity *entity, // no backface culling on terrain, to allow terrain to be viewed from underground const QVector techniques = material->effect()->techniques(); - for ( Qt3DRender::QTechnique *techique : techniques ) + for ( Qt3DRender::QTechnique *technique : techniques ) { - const QVector passes = techique->renderPasses(); + const QVector passes = technique->renderPasses(); for ( Qt3DRender::QRenderPass *pass : passes ) { Qt3DRender::QCullFace *cullFace = new Qt3DRender::QCullFace; diff --git a/src/analysis/georeferencing/qgsgcppoint.h b/src/analysis/georeferencing/qgsgcppoint.h index 390e73ecd379..c9b072eb3b5e 100644 --- a/src/analysis/georeferencing/qgsgcppoint.h +++ b/src/analysis/georeferencing/qgsgcppoint.h @@ -96,7 +96,7 @@ class ANALYSIS_EXPORT QgsGcpPoint void setDestinationPointCrs( const QgsCoordinateReferenceSystem &crs ); /** - * Returns the destionationPoint() transformed to the given target CRS. + * Returns the destinationPoint() transformed to the given target CRS. */ QgsPointXY transformedDestinationPoint( const QgsCoordinateReferenceSystem &targetCrs, const QgsCoordinateTransformContext &context ) const; diff --git a/src/analysis/interpolation/qgsdualedgetriangulation.cpp b/src/analysis/interpolation/qgsdualedgetriangulation.cpp index 5508b178aaae..bc997b9e5482 100644 --- a/src/analysis/interpolation/qgsdualedgetriangulation.cpp +++ b/src/analysis/interpolation/qgsdualedgetriangulation.cpp @@ -254,7 +254,7 @@ int QgsDualEdgeTriangulation::addPoint( const QgsPoint &p ) return -100; //something gets wrong //add the new colinear point linking it to the extremity of closest edge - const int extremPoint = mHalfEdge[closestEdge]->getPoint(); + const int extremePoint = mHalfEdge[closestEdge]->getPoint(); const int newPoint = mPointVector.count() - 1; //edges that do not change const int edgeFromExtremeToOpposite = mHalfEdge[closestEdge]->getDual(); @@ -264,12 +264,12 @@ int QgsDualEdgeTriangulation::addPoint( const QgsPoint &p ) const int edgeFromExtremeToVirtualSide2 = mHalfEdge[edgeFromVirtualToExtremeSide2]->getDual(); //insert new edge const int edgeFromExtremeToNewPoint = insertEdge( -10, -10, newPoint, false, false ); - const int edgeFromNewPointToExtrem = insertEdge( edgeFromExtremeToNewPoint, edgeFromExtremeToVirtualSide2, extremPoint, false, false ); + const int edgeFromNewPointToExtreme = insertEdge( edgeFromExtremeToNewPoint, edgeFromExtremeToVirtualSide2, extremePoint, false, false ); const int edgeFromNewPointToVirtualSide1 = insertEdge( -10, edgeFromVirtualToExtremeSide1, -1, false, false ); const int edgeFromVirtualToNewPointSide1 = insertEdge( edgeFromNewPointToVirtualSide1, -10, newPoint, false, false ); const int edgeFromNewPointToVirtualSide2 = insertEdge( -10, edgeFromVirtualToNewPointSide1, -1, false, false ); - const int edgeFromVirtualToNewPointSide2 = insertEdge( edgeFromNewPointToVirtualSide2, edgeFromNewPointToExtrem, newPoint, false, false ); - mHalfEdge.at( edgeFromExtremeToNewPoint )->setDual( edgeFromNewPointToExtrem ); + const int edgeFromVirtualToNewPointSide2 = insertEdge( edgeFromNewPointToVirtualSide2, edgeFromNewPointToExtreme, newPoint, false, false ); + mHalfEdge.at( edgeFromExtremeToNewPoint )->setDual( edgeFromNewPointToExtreme ); mHalfEdge.at( edgeFromExtremeToNewPoint )->setNext( edgeFromNewPointToVirtualSide1 ); mHalfEdge.at( edgeFromNewPointToVirtualSide1 )->setDual( edgeFromVirtualToNewPointSide1 ); mHalfEdge.at( edgeFromNewPointToVirtualSide2 )->setDual( edgeFromVirtualToNewPointSide2 ); @@ -288,7 +288,7 @@ int QgsDualEdgeTriangulation::addPoint( const QgsPoint &p ) } mDimension = 2; const int newPoint = mPointVector.count() - 1; - //buil the 2D dimension triangulation + //build the 2D dimension triangulation //First clock wise int cwEdge = mEdgeOutside; while ( mHalfEdge[mHalfEdge[mHalfEdge[mHalfEdge[cwEdge]->getNext()]->getDual()]->getNext()]->getPoint() != -1 ) @@ -1359,7 +1359,7 @@ int QgsDualEdgeTriangulation::insertForcedSegment( int p1, int p2, QgsInterpolat return -100;//return an error code } - //go around p1 and find out, if the segment already exists and if not, which is the first cutted edge + //go around p1 and find out, if the segment already exists and if not, which is the first cut edge int actEdge = mHalfEdge[pointingEdge]->getDual(); const int firstActEdge = actEdge; //number to prevent endless loops diff --git a/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp b/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp index e09bc46f5b58..2a79a7fd66e9 100644 --- a/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp +++ b/src/analysis/processing/pdal/qgspdalalgorithmbase.cpp @@ -163,6 +163,7 @@ class EnableElevationPropertiesPostProcessor : public QgsProcessingLayerPostProc if ( QgsRasterLayer *rl = qobject_cast< QgsRasterLayer * >( layer ) ) { QgsRasterLayerElevationProperties *props = qgis::down_cast< QgsRasterLayerElevationProperties * >( rl->elevationProperties() ); + props->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); props->setEnabled( true ); rl->trigger3DUpdate(); } diff --git a/src/analysis/processing/qgsalgorithmcellstatistics.cpp b/src/analysis/processing/qgsalgorithmcellstatistics.cpp index 1471e8670b70..c4ff71675a14 100644 --- a/src/analysis/processing/qgsalgorithmcellstatistics.cpp +++ b/src/analysis/processing/qgsalgorithmcellstatistics.cpp @@ -426,7 +426,7 @@ bool QgsCellStatisticsPercentileAlgorithm::prepareSpecificAlgorithmParameters( c mPercentile = parameterAsDouble( parameters, QStringLiteral( "PERCENTILE" ), context ); //default percentile output data type to float32 raster if interpolation method is chosen - //otherwise use the most potent data type in the intput raster stack (see prepareAlgorithm() in base class) + //otherwise use the most potent data type in the input raster stack (see prepareAlgorithm() in base class) if ( mMethod != QgsRasterAnalysisUtils::CellValuePercentileMethods::NearestRankPercentile && static_cast< int >( mDataType ) < 6 ) mDataType = Qgis::DataType::Float32; diff --git a/src/analysis/processing/qgsalgorithmdxfexport.cpp b/src/analysis/processing/qgsalgorithmdxfexport.cpp index 3fd3ecb6116c..123781eb9ba2 100644 --- a/src/analysis/processing/qgsalgorithmdxfexport.cpp +++ b/src/analysis/processing/qgsalgorithmdxfexport.cpp @@ -69,6 +69,7 @@ void QgsDxfExportAlgorithm::initAlgorithm( const QVariantMap & ) std::unique_ptr extentParam = std::make_unique( QStringLiteral( "EXTENT" ), QObject::tr( "Extent" ), QVariant(), true ); extentParam->setHelp( QObject::tr( "Limit exported features to those with geometries intersecting the provided extent" ) ); addParameter( extentParam.release() ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SELECTED_FEATURES_ONLY" ), QObject::tr( "Use only selected features" ), false ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_LAYER_TITLE" ), QObject::tr( "Use layer title as name" ), false ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "FORCE_2D" ), QObject::tr( "Force 2D output" ), false ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "MTEXT" ), QObject::tr( "Export labels as MTEXT elements" ), true ) ); @@ -108,6 +109,7 @@ QVariantMap QgsDxfExportAlgorithm::processAlgorithm( const QVariantMap ¶mete const double symbologyScale = parameterAsDouble( parameters, QStringLiteral( "SYMBOLOGY_SCALE" ), context ); const QString encoding = parameterAsEnumString( parameters, QStringLiteral( "ENCODING" ), context ); const QgsCoordinateReferenceSystem crs = parameterAsCrs( parameters, QStringLiteral( "CRS" ), context ); + const bool selectedFeaturesOnly = parameterAsBool( parameters, QStringLiteral( "SELECTED_FEATURES_ONLY" ), context ); const bool useLayerTitle = parameterAsBool( parameters, QStringLiteral( "USE_LAYER_TITLE" ), context ); const bool useMText = parameterAsBool( parameters, QStringLiteral( "MTEXT" ), context ); const bool force2D = parameterAsBool( parameters, QStringLiteral( "FORCE_2D" ), context ); @@ -138,6 +140,8 @@ QVariantMap QgsDxfExportAlgorithm::processAlgorithm( const QVariantMap ¶mete QgsDxfExport::Flags flags = QgsDxfExport::Flags(); if ( !useMText ) flags = flags | QgsDxfExport::FlagNoMText; + if ( selectedFeaturesOnly ) + flags = flags | QgsDxfExport::FlagOnlySelectedFeatures; dxfExport.setFlags( flags ); QFile dxfFile( outputFile ); diff --git a/src/analysis/processing/qgsalgorithmextractspecificvertices.cpp b/src/analysis/processing/qgsalgorithmextractspecificvertices.cpp index 75da549c3418..6c72a8fbd818 100644 --- a/src/analysis/processing/qgsalgorithmextractspecificvertices.cpp +++ b/src/analysis/processing/qgsalgorithmextractspecificvertices.cpp @@ -50,9 +50,9 @@ QString QgsExtractSpecificVerticesAlgorithm::groupId() const QString QgsExtractSpecificVerticesAlgorithm::shortHelpString() const { return QObject::tr( "This algorithm takes a vector layer and generates a point layer with points " - "representing specific vertices in the input lines or polygons. For instance, this algorithm " + "representing specific vertices in the input geometries. For instance, this algorithm " "can be used to extract the first or last vertices in the geometry. The attributes associated " - "to each point are the same ones associated to the line or polygon that the point belongs to." ) + + "to each point are the same ones associated to the feature that the point belongs to." ) + QStringLiteral( "\n\n" ) + QObject::tr( "The vertex indices parameter accepts a comma separated string specifying the indices of the " "vertices to extract. The first vertex corresponds to an index of 0, the second vertex has an " diff --git a/src/analysis/processing/qgsalgorithmextractvertices.cpp b/src/analysis/processing/qgsalgorithmextractvertices.cpp index a8b6cb2d0f37..3b1ebf166f60 100644 --- a/src/analysis/processing/qgsalgorithmextractvertices.cpp +++ b/src/analysis/processing/qgsalgorithmextractvertices.cpp @@ -49,7 +49,7 @@ QString QgsExtractVerticesAlgorithm::groupId() const QString QgsExtractVerticesAlgorithm::shortHelpString() const { - return QObject::tr( "This algorithm takes a line or polygon layer and generates a point layer with points representing the vertices in the input lines or polygons. The attributes associated to each point are the same ones associated to the line or polygon that the point belongs to." ) + + return QObject::tr( "This algorithm takes a vector layer and generates a point layer with points representing the vertices in the input geometries. The attributes associated to each point are the same ones associated to the feature that the point belongs to." ) + QStringLiteral( "\n\n" ) + QObject::tr( "Additional fields are added to the point indicating the vertex index (beginning at 0), the vertex’s part and its index within the part (as well as its ring for polygons), distance along original geometry and bisector angle of vertex for original geometry." ); } diff --git a/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp b/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp index bc956f9d41eb..43dbbdc1b4d7 100644 --- a/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp +++ b/src/analysis/processing/qgsalgorithmnetworkanalysisbase.cpp @@ -133,7 +133,7 @@ void QgsNetworkAnalysisAlgorithmBase::loadCommonParams( const QVariantMap ¶m mDirector->addStrategy( new QgsNetworkDistanceStrategy() ); } - mBuilder = std::make_unique< QgsGraphBuilder >( mNetwork->sourceCrs(), true, tolerance ); + mBuilder = std::make_unique< QgsGraphBuilder >( mNetwork->sourceCrs(), true, tolerance, context.ellipsoid() ); } void QgsNetworkAnalysisAlgorithmBase::loadPoints( QgsFeatureSource *source, QVector< QgsPointXY > &points, QHash< int, QgsAttributes > &attributes, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) diff --git a/src/analysis/processing/qgsalgorithmwritevectortiles.cpp b/src/analysis/processing/qgsalgorithmwritevectortiles.cpp index afee9adf986e..9d3c257133ea 100644 --- a/src/analysis/processing/qgsalgorithmwritevectortiles.cpp +++ b/src/analysis/processing/qgsalgorithmwritevectortiles.cpp @@ -179,7 +179,7 @@ void QgsWriteVectorTilesMbtilesAlgorithm::prepareWriter( QgsVectorTileWriter &wr writer.setDestinationUri( uri ); const QString metaName = parameterAsString( parameters, QStringLiteral( "META_NAME" ), context ); - const QString metaDesciption = parameterAsString( parameters, QStringLiteral( "META_DESCRIPTION" ), context ); + const QString metaDescription = parameterAsString( parameters, QStringLiteral( "META_DESCRIPTION" ), context ); const QString metaAttribution = parameterAsString( parameters, QStringLiteral( "META_ATTRIBUTION" ), context ); const QString metaVersion = parameterAsString( parameters, QStringLiteral( "META_VERSION" ), context ); const QString metaType = parameterAsString( parameters, QStringLiteral( "META_TYPE" ), context ); @@ -188,8 +188,8 @@ void QgsWriteVectorTilesMbtilesAlgorithm::prepareWriter( QgsVectorTileWriter &wr QVariantMap meta; if ( !metaName.isEmpty() ) meta["name"] = metaName; - if ( !metaDesciption.isEmpty() ) - meta["description"] = metaDesciption; + if ( !metaDescription.isEmpty() ) + meta["description"] = metaDescription; if ( !metaAttribution.isEmpty() ) meta["attribution"] = metaAttribution; if ( !metaVersion.isEmpty() ) diff --git a/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.h b/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.h index e7aa9c8d9c80..2843430fb9f7 100644 --- a/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.h +++ b/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.h @@ -53,7 +53,7 @@ class ANALYSIS_EXPORT QgsGeometryOverlapCheckError : public QgsGeometryCheckErro /** * Creates a new overlap check error for \a check and the \a layerFeature combination. - * The \a geometry and \a errorLocation ned to be in map coordinates. + * The \a geometry and \a errorLocation need to be in map coordinates. * The \a value is the area of the overlapping area in map units. * The \a overlappedFeature provides more details about the overlap. */ diff --git a/src/app/3d/qgsphongmaterialwidget.cpp b/src/app/3d/qgsphongmaterialwidget.cpp index 9d66b7832b7c..677f582f9398 100644 --- a/src/app/3d/qgsphongmaterialwidget.cpp +++ b/src/app/3d/qgsphongmaterialwidget.cpp @@ -72,6 +72,7 @@ void QgsPhongMaterialWidget::setTechnique( QgsMaterialSettingsRenderingTechnique { lblDiffuse->setVisible( true ); btnDiffuse->setVisible( true ); + mDiffuseCoefficientWidget->setVisible( true ); mAmbientDataDefinedButton->setVisible( false ); mDiffuseDataDefinedButton->setVisible( false ); mSpecularDataDefinedButton->setVisible( false ); @@ -82,6 +83,7 @@ void QgsPhongMaterialWidget::setTechnique( QgsMaterialSettingsRenderingTechnique { lblDiffuse->setVisible( false ); btnDiffuse->setVisible( false ); + mDiffuseCoefficientWidget->setVisible( false ); mAmbientDataDefinedButton->setVisible( false ); mDiffuseDataDefinedButton->setVisible( false ); mSpecularDataDefinedButton->setVisible( false ); @@ -92,6 +94,7 @@ void QgsPhongMaterialWidget::setTechnique( QgsMaterialSettingsRenderingTechnique { lblDiffuse->setVisible( true ); btnDiffuse->setVisible( true ); + mDiffuseCoefficientWidget->setVisible( true ); mAmbientDataDefinedButton->setVisible( true ); mDiffuseDataDefinedButton->setVisible( true ); mSpecularDataDefinedButton->setVisible( true ); diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 67018c7afb4d..e9847f5041b6 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -110,6 +110,7 @@ set(QGIS_APP_SRCS annotations/qgsannotationitempropertieswidget.cpp annotations/qgsannotationlayerproperties.cpp + canvas/qgsappcanvasfiltering.cpp canvas/qgscanvasrefreshblocker.cpp decorations/qgsdecorationitem.cpp @@ -385,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 @@ -402,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 @@ -515,6 +517,7 @@ else() # require c++17 target_compile_features(${QGIS_APP_NAME} PRIVATE cxx_std_17) + target_compile_definitions(${QGIS_APP_NAME} PRIVATE "QT_PLUGINS_DIR=\"${QT_PLUGINS_DIR}\"") endif() # Putting IMAGE_RCC_SRCS into qgis_app lib is causing problems when the lib is @@ -524,6 +527,8 @@ add_library(qgis_app ${LIBRARY_TYPE} ${QGIS_APP_SRCS}) # require c++17 target_compile_features(qgis_app PRIVATE cxx_std_17) +target_compile_definitions(qgis_app PRIVATE "QT_PLUGINS_DIR=\"${QT_PLUGINS_DIR}\"") + target_include_directories(qgis_app PUBLIC ${CMAKE_SOURCE_DIR}/external/nmea diff --git a/src/app/canvas/qgsappcanvasfiltering.cpp b/src/app/canvas/qgsappcanvasfiltering.cpp new file mode 100644 index 000000000000..433f9a347475 --- /dev/null +++ b/src/app/canvas/qgsappcanvasfiltering.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + qgsappcanvasfiltering.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 "qgsappcanvasfiltering.h" +#include "qgselevationcontrollerwidget.h" +#include "qgsmapcanvas.h" +#include "qgisapp.h" +#include + +QgsAppCanvasFiltering::QgsAppCanvasFiltering( QObject *parent ) + : QObject( parent ) +{ + +} + +void QgsAppCanvasFiltering::setupElevationControllerAction( QAction *action, QgsMapCanvas *canvas ) +{ + action->setCheckable( true ); + connect( action, &QAction::toggled, canvas, [canvas, action, this]( bool checked ) + { + if ( checked ) + { + QgsElevationControllerWidget *controller = new QgsElevationControllerWidget(); + connect( controller, &QgsElevationControllerWidget::rangeChanged, canvas, &QgsMapCanvas::setZRange ); + + QAction *setProjectLimitsAction = new QAction( tr( "Set Elevation Range…" ), controller ); + controller->menu()->addAction( setProjectLimitsAction ); + connect( setProjectLimitsAction, &QAction::triggered, QgisApp::instance(), [] + { + QgisApp::instance()->showProjectProperties( tr( "Elevation" ) ); + } ); + QAction *disableAction = new QAction( tr( "Disable Elevation Filter" ), controller ); + controller->menu()->addAction( disableAction ); + connect( disableAction, &QAction::triggered, action, [action] + { + action->setChecked( false ); + } ); + + canvas->addOverlayWidget( controller, Qt::Edge::LeftEdge ); + mCanvasElevationControllerMap.insert( canvas, controller ); + connect( canvas, &QObject::destroyed, this, [canvas, this] + { + mCanvasElevationControllerMap.remove( canvas ); + } ); + connect( controller, &QObject::destroyed, this, [canvas, this] + { + mCanvasElevationControllerMap.remove( canvas ); + } ); + } + else + { + canvas->setZRange( QgsDoubleRange() ); + if ( QgsElevationControllerWidget *controller = mCanvasElevationControllerMap.value( canvas ) ) + { + controller->deleteLater(); + } + } + } ); +} diff --git a/src/app/canvas/qgsappcanvasfiltering.h b/src/app/canvas/qgsappcanvasfiltering.h new file mode 100644 index 000000000000..d660d6d8732d --- /dev/null +++ b/src/app/canvas/qgsappcanvasfiltering.h @@ -0,0 +1,43 @@ +/*************************************************************************** + qgsappcanvasfiltering.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 QGSAPPCANVASFILTERING_H +#define QGSAPPCANVASFILTERING_H + +#include "qgis.h" +#include +#include +#include + +class QAction; +class QgsMapCanvas; +class QgsElevationControllerWidget; + +class QgsAppCanvasFiltering : public QObject +{ + Q_OBJECT + + public: + + QgsAppCanvasFiltering( QObject *parent ); + + void setupElevationControllerAction( QAction *action, QgsMapCanvas *canvas ); + + private: + + QHash< QgsMapCanvas *, QgsElevationControllerWidget * > mCanvasElevationControllerMap; + +}; + +#endif // QGSAPPCANVASFILTERING_H diff --git a/src/app/devtools/networklogger/qgsnetworklogger.cpp b/src/app/devtools/networklogger/qgsnetworklogger.cpp index 703b921201bd..de0d5d68355e 100644 --- a/src/app/devtools/networklogger/qgsnetworklogger.cpp +++ b/src/app/devtools/networklogger/qgsnetworklogger.cpp @@ -46,6 +46,7 @@ void QgsNetworkLogger::enableLogging( bool enabled ) if ( enabled ) { connect( mNam, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated, Qt::UniqueConnection ); + connect( mNam, qOverload< const QgsNetworkRequestParameters &>( &QgsNetworkAccessManager::requestCreated ), this, &QgsNetworkLogger::requestCreated, Qt::UniqueConnection ); connect( mNam, qOverload< QgsNetworkReplyContent >( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished, Qt::UniqueConnection ); connect( mNam, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut, Qt::UniqueConnection ); connect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress, Qt::UniqueConnection ); @@ -54,6 +55,7 @@ void QgsNetworkLogger::enableLogging( bool enabled ) else { disconnect( mNam, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated ); + disconnect( mNam, qOverload< const QgsNetworkRequestParameters &>( &QgsNetworkAccessManager::requestCreated ), this, &QgsNetworkLogger::requestCreated ); disconnect( mNam, qOverload< QgsNetworkReplyContent >( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished ); disconnect( mNam, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut ); disconnect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress ); @@ -82,6 +84,21 @@ void QgsNetworkLogger::requestAboutToBeCreated( QgsNetworkRequestParameters para endInsertRows(); } +void QgsNetworkLogger::requestCreated( const QgsNetworkRequestParameters ¶meters ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( parameters.requestId() ); + if ( !requestGroup ) + return; + + const QUrl url = parameters.request().url(); + if ( requestGroup->url() != url ) + { + requestGroup->setUrl( url ); + const QModelIndex requestIndex = node2index( requestGroup ); + emit dataChanged( requestIndex, requestIndex ); + } +} + void QgsNetworkLogger::requestFinished( QgsNetworkReplyContent content ) { QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( content.requestId() ); diff --git a/src/app/devtools/networklogger/qgsnetworklogger.h b/src/app/devtools/networklogger/qgsnetworklogger.h index 1aa1845d9ca9..9245e8f6aea1 100644 --- a/src/app/devtools/networklogger/qgsnetworklogger.h +++ b/src/app/devtools/networklogger/qgsnetworklogger.h @@ -101,6 +101,7 @@ class QgsNetworkLogger : public QAbstractItemModel private slots: void requestAboutToBeCreated( QgsNetworkRequestParameters parameters ); + void requestCreated( const QgsNetworkRequestParameters ¶meters ); void requestFinished( QgsNetworkReplyContent content ); void requestTimedOut( QgsNetworkRequestParameters parameters ); void downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal ); diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.cpp b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp index 195c9ccada56..fec6b24770a3 100644 --- a/src/app/devtools/networklogger/qgsnetworkloggernode.cpp +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp @@ -230,6 +230,11 @@ QVariant QgsNetworkLoggerRequestGroup::toVariant() const return res; } +void QgsNetworkLoggerRequestGroup::setUrl( const QUrl &url ) +{ + mUrl = url; +} + void QgsNetworkLoggerRequestGroup::setReply( const QgsNetworkReplyContent &reply ) { switch ( reply.error() ) diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.h b/src/app/devtools/networklogger/qgsnetworkloggernode.h index 9301db62342e..2a13e1219bf3 100644 --- a/src/app/devtools/networklogger/qgsnetworkloggernode.h +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.h @@ -111,6 +111,11 @@ class QgsNetworkLoggerRequestGroup final : public QgsDevToolsModelGroup */ QUrl url() const { return mUrl; } + /** + * Sets the request's URL. + */ + void setUrl( const QUrl &url ); + /** * Returns TRUE if the request was served directly from local cache. */ diff --git a/src/app/georeferencer/qgsgeorefdatapoint.h b/src/app/georeferencer/qgsgeorefdatapoint.h index 7c516e007453..63d2904629c6 100644 --- a/src/app/georeferencer/qgsgeorefdatapoint.h +++ b/src/app/georeferencer/qgsgeorefdatapoint.h @@ -88,7 +88,7 @@ class APP_EXPORT QgsGeorefDataPoint : public QObject void setDestinationPointCrs( const QgsCoordinateReferenceSystem &crs ); /** - * Returns the destionationPoint() transformed to the given target CRS. + * Returns the destinationPoint() transformed to the given target CRS. */ QgsPointXY transformedDestinationPoint( const QgsCoordinateReferenceSystem &targetCrs, const QgsCoordinateTransformContext &context ) const; diff --git a/src/app/gps/qgsappgpsconnection.cpp b/src/app/gps/qgsappgpsconnection.cpp index db5b692f355d..cc94879c574f 100644 --- a/src/app/gps/qgsappgpsconnection.cpp +++ b/src/app/gps/qgsappgpsconnection.cpp @@ -25,7 +25,6 @@ #include "qgssettingsentryimpl.h" #include "qgssettingsentryenumflag.h" - QgsAppGpsConnection::QgsAppGpsConnection( QObject *parent ) : QObject( parent ) { @@ -57,7 +56,7 @@ void QgsAppGpsConnection::setConnection( QgsGpsConnection *connection ) disconnectGps(); } - onConnected( connection ); + setConnectionPrivate( connection ); } QgsPoint QgsAppGpsConnection::lastValidLocation() const @@ -154,10 +153,13 @@ void QgsAppGpsConnection::connectGps() QgisApp::instance()->statusBarIface()->clearMessage(); showStatusBarMessage( tr( "Connecting to GPS device %1…" ).arg( port ) ); - QgsGpsDetector *detector = new QgsGpsDetector( port ); - connect( detector, static_cast < void ( QgsGpsDetector::* )( QgsGpsConnection * ) > ( &QgsGpsDetector::detected ), this, &QgsAppGpsConnection::onConnected ); - connect( detector, &QgsGpsDetector::detectionFailed, this, &QgsAppGpsConnection::onTimeOut ); - detector->advance(); // start the detection process + QgsDebugMsgLevel( QStringLiteral( "Firing up GPS detector" ), 2 ); + + // note -- QgsGpsDetector internally uses deleteLater to clean itself up! + mDetector = new QgsGpsDetector( port, false ); + connect( mDetector, &QgsGpsDetector::connectionDetected, this, &QgsAppGpsConnection::onConnectionDetected ); + connect( mDetector, &QgsGpsDetector::detectionFailed, this, &QgsAppGpsConnection::onTimeOut ); + mDetector->advance(); // start the detection process } void QgsAppGpsConnection::disconnectGps() @@ -179,6 +181,10 @@ void QgsAppGpsConnection::disconnectGps() void QgsAppGpsConnection::onTimeOut() { + if ( sender() != mDetector ) + return; + + QgsDebugMsgLevel( QStringLiteral( "GPS detector reported timeout" ), 2 ); disconnectGps(); emit connectionTimedOut(); @@ -186,9 +192,18 @@ void QgsAppGpsConnection::onTimeOut() showGpsConnectFailureWarning( tr( "TIMEOUT - Failed to connect to GPS device." ) ); } -void QgsAppGpsConnection::onConnected( QgsGpsConnection *conn ) +void QgsAppGpsConnection::onConnectionDetected() +{ + if ( sender() != mDetector ) + return; + + QgsDebugMsgLevel( QStringLiteral( "GPS detector GOT a connection" ), 2 ); + setConnectionPrivate( mDetector->takeConnection() ); +} + +void QgsAppGpsConnection::setConnectionPrivate( QgsGpsConnection *connection ) { - mConnection = conn; + mConnection = connection; connect( mConnection, &QgsGpsConnection::stateChanged, this, &QgsAppGpsConnection::stateChanged ); connect( mConnection, &QgsGpsConnection::nmeaSentenceReceived, this, &QgsAppGpsConnection::nmeaSentenceReceived ); connect( mConnection, &QgsGpsConnection::fixStatusChanged, this, &QgsAppGpsConnection::fixStatusChanged ); diff --git a/src/app/gps/qgsappgpsconnection.h b/src/app/gps/qgsappgpsconnection.h index 9c82af42853f..7b992c8cfeb2 100644 --- a/src/app/gps/qgsappgpsconnection.h +++ b/src/app/gps/qgsappgpsconnection.h @@ -25,6 +25,7 @@ class QgsGpsConnection; class QgsGpsInformation; class QgsPoint; class QgsMessageBarItem; +class QgsGpsDetector; /** * Manages a single "canonical" GPS connection for use in the QGIS app, eg for displaying GPS @@ -140,8 +141,9 @@ class APP_EXPORT QgsAppGpsConnection : public QObject private slots: void onTimeOut(); + void onConnectionDetected(); - void onConnected( QgsGpsConnection *conn ); + void setConnectionPrivate( QgsGpsConnection *connection ); private: @@ -150,6 +152,7 @@ class APP_EXPORT QgsAppGpsConnection : public QObject void showGpsConnectFailureWarning( const QString &message ); void showMessage( Qgis::MessageLevel level, const QString &message ); + QPointer< QgsGpsDetector > mDetector; QgsGpsConnection *mConnection = nullptr; QPointer< QgsMessageBarItem > mConnectionMessageItem; }; diff --git a/src/app/labeling/qgsmaptoollabel.cpp b/src/app/labeling/qgsmaptoollabel.cpp index 58f6db78c69e..f47643b81d2e 100644 --- a/src/app/labeling/qgsmaptoollabel.cpp +++ b/src/app/labeling/qgsmaptoollabel.cpp @@ -366,11 +366,11 @@ QgsMapToolLabel::LabelAlignment QgsMapToolLabel::currentAlignment() { Qgis::LabelQuadrantPosition quadrantOffset = Qgis::LabelQuadrantPosition::AboveRight; - // quadrant offest defined via buttons + // quadrant offset defined via buttons if ( mCurrentLabel.settings.placement == Qgis::LabelPlacement::OverPoint ) quadrantOffset = mCurrentLabel.settings.quadOffset; - // quadrant offest DD defined + // quadrant offset DD defined if ( mCurrentLabel.settings.dataDefinedProperties().isActive( QgsPalLayerSettings::Property::OffsetQuad ) ) { QVariant exprVal = evaluateDataDefinedProperty( QgsPalLayerSettings::Property::OffsetQuad, mCurrentLabel.settings, f, static_cast< int >( quadrantOffset ) ); diff --git a/src/app/layers/qgsapplayerhandling.cpp b/src/app/layers/qgsapplayerhandling.cpp index 4d8730c7d908..17ba4e979e18 100644 --- a/src/app/layers/qgsapplayerhandling.cpp +++ b/src/app/layers/qgsapplayerhandling.cpp @@ -109,6 +109,7 @@ void QgsAppLayerHandling::postProcessAddedLayer( QgsMapLayer *layer ) if ( QgsRasterLayerElevationProperties::layerLooksLikeDem( rasterLayer ) ) { qgis::down_cast< QgsRasterLayerElevationProperties * >( rasterLayer->elevationProperties() )->setEnabled( true ); + qgis::down_cast< QgsRasterLayerElevationProperties * >( rasterLayer->elevationProperties() )->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); } break; diff --git a/src/app/locator/qgsactionlocatorfilter.cpp b/src/app/locator/qgsactionlocatorfilter.cpp index d11d7b8f4a0d..4e27ec0d3818 100644 --- a/src/app/locator/qgsactionlocatorfilter.cpp +++ b/src/app/locator/qgsactionlocatorfilter.cpp @@ -50,7 +50,7 @@ void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocat void QgsActionLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - QAction *action = qobject_cast< QAction * >( qvariant_cast( result.getUserData() ) ); + QAction *action = qobject_cast< QAction * >( qvariant_cast( result.userData() ) ); if ( action ) action->trigger(); } diff --git a/src/app/locator/qgsactivelayerfeatureslocatorfilter.cpp b/src/app/locator/qgsactivelayerfeatureslocatorfilter.cpp index d263eb1a2e9b..7109262844b1 100644 --- a/src/app/locator/qgsactivelayerfeatureslocatorfilter.cpp +++ b/src/app/locator/qgsactivelayerfeatureslocatorfilter.cpp @@ -278,7 +278,7 @@ void QgsActiveLayerFeaturesLocatorFilter::triggerResult( const QgsLocatorResult void QgsActiveLayerFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) { - QVariantMap data = result.getUserData().value(); + QVariantMap data = result.userData().value(); switch ( data.value( QStringLiteral( "type" ) ).value() ) { case ResultType::Feature: diff --git a/src/app/locator/qgsalllayersfeatureslocatorfilter.cpp b/src/app/locator/qgsalllayersfeatureslocatorfilter.cpp index 8eafda83338f..8256121d6593 100644 --- a/src/app/locator/qgsalllayersfeatureslocatorfilter.cpp +++ b/src/app/locator/qgsalllayersfeatureslocatorfilter.cpp @@ -179,7 +179,7 @@ void QgsAllLayersFeaturesLocatorFilter::triggerResult( const QgsLocatorResult &r void QgsAllLayersFeaturesLocatorFilter::triggerResultFromAction( const QgsLocatorResult &result, const int actionId ) { - ResultData data = ResultData::fromVariant( result.getUserData() ); + ResultData data = ResultData::fromVariant( result.userData() ); QgsFeatureId fid = data.id(); QString layerId = data.layerId(); bool layerIsSpatial = data.layerIsSpatial(); diff --git a/src/app/locator/qgsbookmarklocatorfilter.cpp b/src/app/locator/qgsbookmarklocatorfilter.cpp index 32112b6b6066..57e19726e56b 100644 --- a/src/app/locator/qgsbookmarklocatorfilter.cpp +++ b/src/app/locator/qgsbookmarklocatorfilter.cpp @@ -66,6 +66,6 @@ void QgsBookmarkLocatorFilter::fetchResults( const QString &string, const QgsLoc void QgsBookmarkLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - const QModelIndex index = qvariant_cast( result.getUserData() ); + const QModelIndex index = qvariant_cast( result.userData() ); QgisApp::instance()->zoomToBookmarkIndex( index ); } diff --git a/src/app/locator/qgsexpressioncalculatorlocatorfilter.cpp b/src/app/locator/qgsexpressioncalculatorlocatorfilter.cpp index bf3ea9825171..075ae4d1e6c6 100644 --- a/src/app/locator/qgsexpressioncalculatorlocatorfilter.cpp +++ b/src/app/locator/qgsexpressioncalculatorlocatorfilter.cpp @@ -62,5 +62,5 @@ void QgsExpressionCalculatorLocatorFilter::fetchResults( const QString &string, void QgsExpressionCalculatorLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - QApplication::clipboard()->setText( result.getUserData().toString() ); + QApplication::clipboard()->setText( result.userData().toString() ); } diff --git a/src/app/locator/qgsgotolocatorfilter.cpp b/src/app/locator/qgsgotolocatorfilter.cpp index 750b6c1a9ba0..89644a820179 100644 --- a/src/app/locator/qgsgotolocatorfilter.cpp +++ b/src/app/locator/qgsgotolocatorfilter.cpp @@ -316,7 +316,7 @@ void QgsGotoLocatorFilter::triggerResult( const QgsLocatorResult &result ) { QgsMapCanvas *mapCanvas = QgisApp::instance()->mapCanvas(); - QVariantMap data = result.getUserData().toMap(); + QVariantMap data = result.userData().toMap(); const QgsPointXY point = data[QStringLiteral( "point" )].value(); mapCanvas->setCenter( point ); if ( data.contains( QStringLiteral( "scale" ) ) ) diff --git a/src/app/locator/qgslayermetadatalocatorfilter.cpp b/src/app/locator/qgslayermetadatalocatorfilter.cpp index bb0d3cfda91b..2e76d815b8fd 100644 --- a/src/app/locator/qgslayermetadatalocatorfilter.cpp +++ b/src/app/locator/qgslayermetadatalocatorfilter.cpp @@ -55,7 +55,7 @@ void QgsLayerMetadataLocatorFilter::fetchResults( const QString &string, const Q void QgsLayerMetadataLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - QgsLayerMetadataProviderResult metadataResult { result.getUserData().value() }; + QgsLayerMetadataProviderResult metadataResult { result.userData().value() }; switch ( metadataResult.layerType() ) { case Qgis::LayerType::Raster: diff --git a/src/app/locator/qgslayertreelocatorfilter.cpp b/src/app/locator/qgslayertreelocatorfilter.cpp index 7536928f1a4a..b6bc0763bf20 100644 --- a/src/app/locator/qgslayertreelocatorfilter.cpp +++ b/src/app/locator/qgslayertreelocatorfilter.cpp @@ -62,7 +62,7 @@ void QgsLayerTreeLocatorFilter::fetchResults( const QString &string, const QgsLo void QgsLayerTreeLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - const QString layerId = result.getUserData().toString(); + const QString layerId = result.userData().toString(); QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId ); QgisApp::instance()->setActiveLayer( layer ); } diff --git a/src/app/locator/qgslayoutlocatorfilter.cpp b/src/app/locator/qgslayoutlocatorfilter.cpp index 2eb100a42f52..c628c8b0b548 100644 --- a/src/app/locator/qgslayoutlocatorfilter.cpp +++ b/src/app/locator/qgslayoutlocatorfilter.cpp @@ -59,7 +59,7 @@ void QgsLayoutLocatorFilter::fetchResults( const QString &string, const QgsLocat void QgsLayoutLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - const QString layoutName = result.getUserData().toString(); + const QString layoutName = result.userData().toString(); QgsMasterLayoutInterface *layout = QgsProject::instance()->layoutManager()->layoutByName( layoutName ); if ( !layout ) return; diff --git a/src/app/locator/qgssettingslocatorfilter.cpp b/src/app/locator/qgssettingslocatorfilter.cpp index 9378cfc45151..b6429326605b 100644 --- a/src/app/locator/qgssettingslocatorfilter.cpp +++ b/src/app/locator/qgssettingslocatorfilter.cpp @@ -60,7 +60,7 @@ void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLoc QgsLocatorResult result; result.filter = this; result.displayString = title; - result.getUserData().setValue( settingsPage ); + result.userData().setValue( settingsPage ); if ( context.usingPrefix && string.isEmpty() ) { @@ -86,7 +86,7 @@ QMap QgsSettingsLocatorFilter::settingsPage( const QString &ty void QgsSettingsLocatorFilter::triggerResult( const QgsLocatorResult &result ) { - const QMap settingsPage = qvariant_cast>( result.getUserData() ); + const QMap settingsPage = qvariant_cast>( result.userData() ); const QString type = settingsPage.value( QStringLiteral( "type" ) ); const QString page = settingsPage.value( QStringLiteral( "page" ) ); diff --git a/src/app/mesh/qgsmaptooleditmeshframe.cpp b/src/app/mesh/qgsmaptooleditmeshframe.cpp index 736d44f812cf..23a62f23a783 100644 --- a/src/app/mesh/qgsmaptooleditmeshframe.cpp +++ b/src/app/mesh/qgsmaptooleditmeshframe.cpp @@ -963,7 +963,7 @@ void QgsMapToolEditMeshFrame::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) bool acceptPoint = true; if ( ! mNewFaceCandidate.isEmpty() && mNewFaceCandidate.last() == -1 && - !mNewVerticesForNewFaceCandidate.isEmpty() ) //avoid dupplicate new vertex + !mNewVerticesForNewFaceCandidate.isEmpty() ) //avoid duplicate new vertex { acceptPoint = mapPoint.distance( mNewVerticesForNewFaceCandidate.last() ) > tolerance; } @@ -1813,7 +1813,7 @@ void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checke return; const QList faceList = qgis::setToList( mSelectedFaces ); - QgsGeometry faceGeometrie; + QgsGeometry faceGeometry; if ( faceList.count() == 1 ) { const QgsMeshFace &face = mCurrentLayer->nativeMesh()->face( faceList.at( 0 ) ); @@ -1822,11 +1822,11 @@ void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checke for ( int j = 0; j < faceSize; ++j ) faceVertices[j] = mTransformDockWidget->transformedVertex( face.at( j ) ); - faceGeometrie = QgsGeometry::fromPolygonXY( {faceVertices} ); + faceGeometry = QgsGeometry::fromPolygonXY( {faceVertices} ); } else { - std::unique_ptr geomEngine( QgsGeometry::createGeometryEngine( faceGeometrie.constGet() ) ); + std::unique_ptr geomEngine( QgsGeometry::createGeometryEngine( faceGeometry.constGet() ) ); geomEngine->prepareGeometry(); QVector faces( mSelectedFaces.count() ); for ( int i = 0; i < faceList.count(); ++i ) @@ -1840,7 +1840,7 @@ void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checke faces[i] = QgsGeometry::fromPolygonXY( {faceVertices} ); } QString error; - faceGeometrie = QgsGeometry( geomEngine->combine( faces, &error ) ); + faceGeometry = QgsGeometry( geomEngine->combine( faces, &error ) ); } QgsGeometry edgesGeom = QgsGeometry::fromMultiPolylineXY( QgsMultiPolylineXY() ); @@ -1867,7 +1867,7 @@ void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checke try { - faceGeometrie.transform( coordinateTransform ); + faceGeometry.transform( coordinateTransform ); } catch ( QgsCsException & ) {} @@ -1879,7 +1879,7 @@ void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checke catch ( QgsCsException & ) {} - mMovingFacesRubberband->setToGeometry( faceGeometrie ); + mMovingFacesRubberband->setToGeometry( faceGeometry ); mMovingEdgesRubberband->setToGeometry( edgesGeom ); const QList vertexIndexes = mSelectedVertices.keys(); for ( const int vertexIndex : vertexIndexes ) @@ -2174,14 +2174,14 @@ void QgsMapToolEditMeshFrame::prepareSelection() const QList facesList = qgis::setToList( mSelectedFaces ); if ( !facesList.isEmpty() ) { - const QgsGeometry faceGeometrie( new QgsPolygon( new QgsLineString( nativeFaceGeometry( facesList.at( 0 ) ) ) ) ); + const QgsGeometry faceGeometry( new QgsPolygon( new QgsLineString( nativeFaceGeometry( facesList.at( 0 ) ) ) ) ); if ( mSelectedFaces.count() == 1 ) { - mSelectedFacesRubberband->setToGeometry( faceGeometrie ); + mSelectedFacesRubberband->setToGeometry( faceGeometry ); } else { - std::unique_ptr geomEngine( QgsGeometry::createGeometryEngine( faceGeometrie.constGet() ) ); + std::unique_ptr geomEngine( QgsGeometry::createGeometryEngine( faceGeometry.constGet() ) ); geomEngine->prepareGeometry(); QVector otherFaces( mSelectedFaces.count() ); diff --git a/src/app/mesh/qgsmaptooleditmeshframe.h b/src/app/mesh/qgsmaptooleditmeshframe.h index 8d3d57c044d2..850b2a876bb5 100644 --- a/src/app/mesh/qgsmaptooleditmeshframe.h +++ b/src/app/mesh/qgsmaptooleditmeshframe.h @@ -326,7 +326,7 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing //! menbers for refinement face int mRefinableFaceCount = 0; - // assiociated widget + // associated widget QgsZValueWidget *mZValueWidget = nullptr; //own by QgsUserInputWidget instance QgsMeshTransformCoordinatesDockWidget *mTransformDockWidget = nullptr; //own by the application diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp index f794fd672e17..cd5ede8081b1 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.cpp +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.cpp @@ -27,6 +27,17 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer setupUi( this ); setObjectName( QStringLiteral( "mOptsPage_Elevation" ) ); + mModeComboBox->addItem( tr( "From Vertices" ), QVariant::fromValue( Qgis::MeshElevationMode::FromVertices ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::MeshElevationMode::FixedElevationRange ) ); + + mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); + mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower, Include Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ); + + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mSymbologyStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mOffsetZSpinBox->setClearValue( 0 ); mScaleZSpinBox->setClearValue( 1 ); mLineStyleButton->setSymbolType( Qgis::SymbolType::Line ); @@ -36,8 +47,14 @@ QgsMeshElevationPropertiesWidget::QgsMeshElevationPropertiesWidget( QgsMeshLayer mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationFillAbove.svg" ) ), tr( "Fill Above" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::FillAbove ) ); mElevationLimitSpinBox->setClearValue( mElevationLimitSpinBox->minimum(), tr( "Not set" ) ); + mFixedLowerSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedUpperSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedLowerSpinBox->clear(); + mFixedUpperSpinBox->clear(); + syncToLayer( layer ); + connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsMeshElevationPropertiesWidget::modeChanged ); connect( mOffsetZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); connect( mScaleZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); connect( mElevationLimitSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsMeshElevationPropertiesWidget::onChanged ); @@ -70,12 +87,35 @@ void QgsMeshElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mBlockUpdates = true; const QgsMeshLayerElevationProperties *props = qgis::down_cast< const QgsMeshLayerElevationProperties * >( mLayer->elevationProperties() ); + + mModeComboBox->setCurrentIndex( mModeComboBox->findData( QVariant::fromValue( props->mode() ) ) ); + switch ( props->mode() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::MeshElevationMode::FromVertices: + mStackedWidget->setCurrentWidget( mPageFromVertices ); + break; + } + mOffsetZSpinBox->setValue( props->zOffset() ); mScaleZSpinBox->setValue( props->zScale() ); if ( std::isnan( props->elevationLimit() ) ) mElevationLimitSpinBox->clear(); else mElevationLimitSpinBox->setValue( props->elevationLimit() ); + + if ( props->fixedRange().lower() != std::numeric_limits< double >::lowest() ) + mFixedLowerSpinBox->setValue( props->fixedRange().lower() ); + else + mFixedLowerSpinBox->clear(); + if ( props->fixedRange().upper() != std::numeric_limits< double >::max() ) + mFixedUpperSpinBox->setValue( props->fixedRange().upper() ); + else + mFixedUpperSpinBox->clear(); + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); + mLineStyleButton->setSymbol( props->profileLineSymbol()->clone() ); mFillStyleButton->setSymbol( props->profileFillSymbol()->clone() ); @@ -100,18 +140,48 @@ void QgsMeshElevationPropertiesWidget::apply() return; QgsMeshLayerElevationProperties *props = qgis::down_cast< QgsMeshLayerElevationProperties * >( mLayer->elevationProperties() ); + props->setMode( mModeComboBox->currentData().value< Qgis::MeshElevationMode >() ); + props->setZOffset( mOffsetZSpinBox->value() ); props->setZScale( mScaleZSpinBox->value() ); if ( mElevationLimitSpinBox->value() != mElevationLimitSpinBox->clearValue() ) props->setElevationLimit( mElevationLimitSpinBox->value() ); else props->setElevationLimit( std::numeric_limits< double >::quiet_NaN() ); + + double fixedLower = std::numeric_limits< double >::lowest(); + double fixedUpper = std::numeric_limits< double >::max(); + if ( mFixedLowerSpinBox->value() != mFixedLowerSpinBox->clearValue() ) + fixedLower = mFixedLowerSpinBox->value(); + if ( mFixedUpperSpinBox->value() != mFixedUpperSpinBox->clearValue() ) + fixedUpper = mFixedUpperSpinBox->value(); + + props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); + props->setProfileLineSymbol( mLineStyleButton->clonedSymbol< QgsLineSymbol >() ); props->setProfileFillSymbol( mFillStyleButton->clonedSymbol< QgsFillSymbol >() ); props->setProfileSymbology( static_cast< Qgis::ProfileSurfaceSymbology >( mStyleComboBox->currentData().toInt() ) ); mLayer->trigger3DUpdate(); } +void QgsMeshElevationPropertiesWidget::modeChanged() +{ + if ( mModeComboBox->currentData().isValid() ) + { + switch ( mModeComboBox->currentData().value< Qgis::MeshElevationMode >() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::MeshElevationMode::FromVertices: + mStackedWidget->setCurrentWidget( mPageFromVertices ); + break; + } + } + + onChanged(); +} + void QgsMeshElevationPropertiesWidget::onChanged() { if ( !mBlockUpdates ) diff --git a/src/app/mesh/qgsmeshelevationpropertieswidget.h b/src/app/mesh/qgsmeshelevationpropertieswidget.h index 7c99980111a1..c55e406e7d4e 100644 --- a/src/app/mesh/qgsmeshelevationpropertieswidget.h +++ b/src/app/mesh/qgsmeshelevationpropertieswidget.h @@ -36,7 +36,7 @@ class QgsMeshElevationPropertiesWidget : public QgsMapLayerConfigWidget, private void apply() override; private slots: - + void modeChanged(); void onChanged(); private: diff --git a/src/app/pluginmanager/qgspluginmanager.cpp b/src/app/pluginmanager/qgspluginmanager.cpp index da91185fb896..74c9283823ab 100644 --- a/src/app/pluginmanager/qgspluginmanager.cpp +++ b/src/app/pluginmanager/qgspluginmanager.cpp @@ -1045,9 +1045,12 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item ) QString dateUpdatedStr; if ( ! metadata->value( QStringLiteral( "update_date_stable" ) ).isEmpty() ) { - const QDateTime dateUpdated = QDateTime::fromString( metadata->value( QStringLiteral( "update_date_stable" ) ).trimmed(), Qt::ISODate ); - if ( dateUpdated.isValid() ) - dateUpdatedStr += tr( "updated at %1" ).arg( QLocale().toString( dateUpdated, dateTimeFormat ) ); + const QDateTime dateUpdatedUtc = QDateTime::fromString( metadata->value( QStringLiteral( "update_date_stable" ) ).trimmed(), Qt::ISODate ); + if ( dateUpdatedUtc.isValid() ) + { + const QDateTime dateUpdatedLocal = dateUpdatedUtc.toLocalTime(); + dateUpdatedStr += tr( "updated at %1 %2" ).arg( QLocale().toString( dateUpdatedLocal, dateTimeFormat ), dateUpdatedLocal.timeZoneAbbreviation() ); + } } html += QStringLiteral( "%1 %3 %4" diff --git a/src/app/project/qgsprojectelevationsettingswidget.cpp b/src/app/project/qgsprojectelevationsettingswidget.cpp index 6862ffb20d26..8688089a4a89 100644 --- a/src/app/project/qgsprojectelevationsettingswidget.cpp +++ b/src/app/project/qgsprojectelevationsettingswidget.cpp @@ -26,6 +26,9 @@ QgsProjectElevationSettingsWidget::QgsProjectElevationSettingsWidget( QWidget *p { setupUi( this ); + mElevationLowerSpin->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mElevationUpperSpin->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFlatHeightSpinBox->setClearValue( 0.0 ); mDemOffsetSpinBox->setClearValue( 0.0 ); @@ -40,6 +43,8 @@ QgsProjectElevationSettingsWidget::QgsProjectElevationSettingsWidget( QWidget *p mComboTerrainType->addItem( tr( "DEM (Raster Layer)" ), QStringLiteral( "raster" ) ); mComboTerrainType->addItem( tr( "Mesh" ), QStringLiteral( "mesh" ) ); + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mStackedWidget->setCurrentWidget( mPageFlat ); connect( mComboTerrainType, qOverload< int >( &QComboBox::currentIndexChanged ), this, [ = ] { @@ -60,7 +65,8 @@ QgsProjectElevationSettingsWidget::QgsProjectElevationSettingsWidget( QWidget *p } ); // setup with current settings - const QgsAbstractTerrainProvider *provider = QgsProject::instance()->elevationProperties()->terrainProvider(); + QgsProjectElevationProperties *elevationProperties = QgsProject::instance()->elevationProperties(); + const QgsAbstractTerrainProvider *provider = elevationProperties->terrainProvider(); mComboTerrainType->setCurrentIndex( mComboTerrainType->findData( provider->type() ) ); if ( provider->type() == QLatin1String( "flat" ) ) { @@ -85,6 +91,15 @@ QgsProjectElevationSettingsWidget::QgsProjectElevationSettingsWidget( QWidget *p connect( mComboDemLayer, &QgsMapLayerComboBox::layerChanged, this, &QgsProjectElevationSettingsWidget::validate ); connect( mComboMeshLayer, &QgsMapLayerComboBox::layerChanged, this, &QgsProjectElevationSettingsWidget::validate ); + if ( elevationProperties->elevationRange().lower() != std::numeric_limits< double >::lowest() ) + whileBlocking( mElevationLowerSpin )->setValue( elevationProperties->elevationRange().lower() ); + else + whileBlocking( mElevationLowerSpin )->clear(); + if ( elevationProperties->elevationRange().upper() != std::numeric_limits< double >::max() ) + whileBlocking( mElevationUpperSpin )->setValue( elevationProperties->elevationRange().upper() ); + else + whileBlocking( mElevationUpperSpin )->clear(); + validate(); mElevationShadingSettingsWidget = new QgsElevationShadingRendererSettingsWidget( nullptr, nullptr, this ); @@ -109,6 +124,7 @@ void QgsProjectElevationSettingsWidget::apply() QgsRasterLayer *demLayer = qobject_cast< QgsRasterLayer * >( mComboDemLayer->currentLayer() ); // always mark the terrain layer as a "dem" layer -- it seems odd for a user to have to manually set this after picking a terrain raster! qobject_cast< QgsRasterLayerElevationProperties * >( demLayer->elevationProperties() )->setEnabled( true ); + qobject_cast< QgsRasterLayerElevationProperties * >( demLayer->elevationProperties() )->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); qgis::down_cast< QgsRasterDemTerrainProvider * >( provider.get() )->setLayer( demLayer ); } else if ( terrainType == QLatin1String( "mesh" ) ) @@ -121,6 +137,15 @@ void QgsProjectElevationSettingsWidget::apply() QgsProject::instance()->elevationProperties()->setTerrainProvider( provider.release() ); + double zLower = mElevationLowerSpin->value(); + if ( zLower == mElevationLowerSpin->clearValue() ) + zLower = std::numeric_limits< double >::lowest(); + double zUpper = mElevationUpperSpin->value(); + if ( zUpper == mElevationUpperSpin->clearValue() ) + zUpper = std::numeric_limits< double >::max(); + + QgsProject::instance()->elevationProperties()->setElevationRange( QgsDoubleRange( zLower, zUpper ) ); + mElevationShadingSettingsWidget->apply(); } @@ -160,7 +185,7 @@ bool QgsProjectElevationSettingsWidget::isValid() // QgsProjectElevationSettingsWidgetFactory::QgsProjectElevationSettingsWidgetFactory( QObject *parent ) - : QgsOptionsWidgetFactory( tr( "Terrain" ), QgsApplication::getThemeIcon( QStringLiteral( "mLayoutItem3DMap.svg" ) ), QStringLiteral( "terrain" ) ) + : QgsOptionsWidgetFactory( tr( "Elevation" ), QgsApplication::getThemeIcon( QStringLiteral( "propertyicons/elevationscale.svg" ) ), QStringLiteral( "terrain" ) ) { setParent( parent ); } diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 8de2a1a87477..3b36ea2c3a43 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -129,6 +129,7 @@ #include "layers/qgsapplayerhandling.h" #include "qgsmaplayerstylemanager.h" +#include "canvas/qgsappcanvasfiltering.h" #include "canvas/qgscanvasrefreshblocker.h" #include "qgsdockablewidgethelper.h" @@ -1136,7 +1137,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 ); @@ -1967,6 +1968,9 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers mOptionWidgetFactories.emplace_back( QgsScopedOptionsWidgetFactory( std::make_unique< Qgs3DOptionsFactory >() ) ); #endif + mAppCanvasFiltering = new QgsAppCanvasFiltering( this ); + mAppCanvasFiltering->setupElevationControllerAction( mActionElevationController, mMapCanvas ); + connect( QgsApplication::fontManager(), &QgsFontManager::fontDownloaded, this, [ = ]( const QStringList & families, const QString & licenseDetails ) { const QString shortMessage = tr( "Installed font %1" ).arg( families.join( QLatin1String( ", " ) ) ); @@ -4976,6 +4980,11 @@ void QgisApp::initLayerTreeView() connect( mFilterLegendToggleShowPrivateLayersAction, &QAction::toggled, this, [ = ]( bool showPrivateLayers ) { layerTreeView()->setShowPrivateLayers( showPrivateLayers ); } ); filterLegendMenu->addAction( mFilterLegendToggleShowPrivateLayersAction ); + mFilterLegendToggleHideValidLayersAction = new QAction( tr( "Show Broken Layers Only" ), this ); + mFilterLegendToggleHideValidLayersAction->setCheckable( true ); + connect( mFilterLegendToggleHideValidLayersAction, &QAction::toggled, this, [ = ]( bool hideValidLayers ) { layerTreeView()->setHideValidLayers( hideValidLayers ); } ); + filterLegendMenu->addAction( mFilterLegendToggleHideValidLayersAction ); + mLegendExpressionFilterButton = new QgsLegendFilterButton( this ); mLegendExpressionFilterButton->setToolTip( tr( "Filter legend by expression" ) ); connect( mLegendExpressionFilterButton, &QAbstractButton::toggled, this, &QgisApp::toggleFilterLegendByExpression ); @@ -6854,6 +6863,8 @@ void QgisApp::dxfExport() QgsDxfExport::Flags flags = QgsDxfExport::Flags(); if ( !d.useMText() ) flags = flags | QgsDxfExport::FlagNoMText; + if ( d.selectedFeaturesOnly() ) + flags = flags | QgsDxfExport::FlagOnlySelectedFeatures; dxfExport.setFlags( flags ); if ( auto *lMapCanvas = mapCanvas() ) @@ -12095,7 +12106,7 @@ void QgisApp::setLayerScaleVisibility() QgsMapLayer *layer = mLayerTreeView->currentLayer(); if ( layer ) { - dlg->setScaleVisiblity( layer->hasScaleBasedVisibility() ); + dlg->setScaleVisibility( layer->hasScaleBasedVisibility() ); dlg->setMinimumScale( layer->minimumScale() ); dlg->setMaximumScale( layer->maximumScale() ); } @@ -12258,7 +12269,7 @@ void QgisApp::legendLayerZoomNative() } else { - mMapCanvas->zoomByFactor( std::sqrt( layer->rasterUnitsPerPixelX() * layer->rasterUnitsPerPixelX() + layer->rasterUnitsPerPixelY() * layer->rasterUnitsPerPixelY() ) / diagonalSize ); + mMapCanvas->zoomByFactor( std::sqrt( layer->rasterUnitsPerPixelX() * layer->rasterUnitsPerPixelX() + layer->rasterUnitsPerPixelY() * layer->rasterUnitsPerPixelY() ) / ( diagonalSize / mMapCanvas->mapSettings().devicePixelRatio() ) ); } mMapCanvas->refresh(); @@ -15986,9 +15997,11 @@ void QgisApp::renameView() if ( renameDlg.exec() || renameDlg.name().isEmpty() ) { QString newName = renameDlg.name(); - view->setWindowTitle( newName ); + view->dockableWidgetHelper()->setWindowTitle( newName ); view->mapCanvas()->setObjectName( newName ); } + view->raise(); + view->activateWindow(); } QgsRasterLayer *QgisApp::addRasterLayer( QString const &uri, QString const &baseName, QString const &providerKey ) @@ -16210,6 +16223,11 @@ QgsMapLayerActionContext QgisApp::createMapLayerActionContext() return context; } +QgsAppCanvasFiltering *QgisApp::canvasFiltering() +{ + return mAppCanvasFiltering; +} + void QgisApp::takeAppScreenShots( const QString &saveDirectory, const int categories ) { QgsAppScreenShots ass( saveDirectory ); diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 68786bd396b8..745c52536a4f 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -161,6 +161,7 @@ class QgsGpsToolBar; class QgsAppGpsSettingsMenu; class Qgs3DMapScene; class Qgs3DMapCanvas; +class QgsAppCanvasFiltering; #include #include @@ -893,6 +894,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsMapLayerActionContext createMapLayerActionContext(); + QgsAppCanvasFiltering *canvasFiltering(); + /** * Take screenshots for user documentation * @param saveDirectory path were the screenshots will be saved @@ -1981,7 +1984,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! show Python console void showPythonDialog(); - //! add Python cnosole at start up + //! add Python console at start up void initPythonConsoleOptions(); //! Shows a warning when an older/newer project file is read. @@ -2509,7 +2512,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsMapOverviewCanvas *mOverviewCanvas = nullptr; //! Table of contents (legend) for the map QgsLayerTreeView *mLayerTreeView = nullptr; - //! Keep track of whether ongoing dataset(s) is/are being dropped through the table of contens + //! Keep track of whether ongoing dataset(s) is/are being dropped through the table of contents bool mLayerTreeDrop = false; //! Helper class that connects layer tree with map canvas @@ -2643,6 +2646,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QToolButton *mFilterLegendToolButton = nullptr; QAction *mFilterLegendByMapContentAction = nullptr; QAction *mFilterLegendToggleShowPrivateLayersAction = nullptr; + QAction *mFilterLegendToggleHideValidLayersAction = nullptr; QAction *mActionStyleDock = nullptr; QgsLegendFilterButton *mLegendExpressionFilterButton = nullptr; @@ -2730,6 +2734,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QMap< QString, QToolButton * > mAnnotationItemGroupToolButtons; QAction *mAnnotationsItemInsertBefore = nullptr; // Used to insert annotation items at the appropriate location in the annotations toolbar + QgsAppCanvasFiltering *mAppCanvasFiltering = nullptr; + QSet mOpen2DMapViews; #ifdef HAVE_3D diff --git a/src/app/qgsdxfexportdialog.cpp b/src/app/qgsdxfexportdialog.cpp index 124fc9739ba7..ff9f1cc5e5a2 100644 --- a/src/app/qgsdxfexportdialog.cpp +++ b/src/app/qgsdxfexportdialog.cpp @@ -34,6 +34,11 @@ #include #include +const int LAYER_COL = 0; +const int OUTPUT_LAYER_ATTRIBUTE_COL = 1; +const int ALLOW_DD_SYMBOL_BLOCKS_COL = 2; +const int MAXIMUM_DD_SYMBOL_BLOCKS_COL = 3; + FieldSelectorDelegate::FieldSelectorDelegate( QObject *parent ) : QItemDelegate( parent ) { @@ -43,6 +48,17 @@ QWidget *FieldSelectorDelegate::createEditor( QWidget *parent, const QStyleOptio { Q_UNUSED( option ) + if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) + { + return nullptr; + } + else if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + QLineEdit *le = new QLineEdit( parent ); + le->setValidator( new QIntValidator( le ) ); + return le; + } + QgsVectorLayer *vl = indexToLayer( index.model(), index ); if ( !vl ) return nullptr; @@ -55,6 +71,16 @@ QWidget *FieldSelectorDelegate::createEditor( QWidget *parent, const QStyleOptio void FieldSelectorDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const { + if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + QLineEdit *le = qobject_cast( editor ); + if ( le ) + { + le->setText( index.data().toString() ); + } + return; + } + QgsVectorLayer *vl = indexToLayer( index.model(), index ); if ( !vl ) return; @@ -70,6 +96,15 @@ void FieldSelectorDelegate::setEditorData( QWidget *editor, const QModelIndex &i void FieldSelectorDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const { + if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + QLineEdit *le = qobject_cast( editor ); + if ( le ) + { + model->setData( index, le->text().toInt() ); + } + } + QgsVectorLayer *vl = indexToLayer( index.model(), index ); if ( !vl ) return; @@ -106,24 +141,46 @@ int FieldSelectorDelegate::attributeIndex( const QAbstractItemModel *model, cons QgsVectorLayerAndAttributeModel::QgsVectorLayerAndAttributeModel( QgsLayerTree *rootNode, QObject *parent ) : QgsLayerTreeModel( rootNode, parent ) { + //init mCreateDDBlockInfo, mDDBlocksMaxNumberOfClasses + QSet layerIds; + retrieveAllLayers( rootNode, layerIds ); + for ( const auto &id : std::as_const( layerIds ) ) + { + const QgsVectorLayer *vLayer = qobject_cast< const QgsVectorLayer *>( QgsProject::instance()->mapLayer( id ) ); + if ( vLayer ) + { + mCreateDDBlockInfo[vLayer] = QgsDxfExportDialog::settingsDxfEnableDDBlocks->value(); + mDDBlocksMaxNumberOfClasses[vLayer] = -1; + } + } } int QgsVectorLayerAndAttributeModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) - return 2; + return 4; } Qt::ItemFlags QgsVectorLayerAndAttributeModel::flags( const QModelIndex &index ) const { - if ( index.column() == 0 ) - return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; - QgsVectorLayer *vl = vectorLayer( index ); - if ( !vl ) - return Qt::ItemIsEnabled; - else - return Qt::ItemIsEnabled | Qt::ItemIsEditable; + if ( index.column() == LAYER_COL ) + { + return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; + } + else if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) + { + return vl ? Qt::ItemIsEnabled | Qt::ItemIsEditable : Qt::ItemIsEnabled; + } + else if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) + { + return ( vl && vl->geometryType() == Qgis::GeometryType::Point ) ? Qt::ItemIsEnabled | Qt::ItemIsUserCheckable : Qt::ItemIsEnabled ; + } + else if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + return vl && vl->geometryType() == Qgis::GeometryType::Point ? Qt::ItemIsEnabled | Qt::ItemIsEditable : Qt::ItemIsEnabled; + } + return Qt::ItemIsEnabled; } QgsVectorLayer *QgsVectorLayerAndAttributeModel::vectorLayer( const QModelIndex &idx ) const @@ -146,14 +203,22 @@ QVariant QgsVectorLayerAndAttributeModel::headerData( int section, Qt::Orientati { if ( role == Qt::DisplayRole ) { - if ( section == 0 ) + if ( section == LAYER_COL ) return tr( "Layer" ); - else if ( section == 1 ) + else if ( section == OUTPUT_LAYER_ATTRIBUTE_COL ) return tr( "Output Layer Attribute" ); + else if ( section == ALLOW_DD_SYMBOL_BLOCKS_COL ) + { + return tr( "Allow data defined symbol blocks" ); + } + else if ( section == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + return tr( "Maximum number of symbol blocks" ); + } } else if ( role == Qt::ToolTipRole ) { - if ( section == 1 ) + if ( section == OUTPUT_LAYER_ATTRIBUTE_COL ) return tr( "Attribute containing the name of the destination layer in the DXF output." ); } } @@ -162,7 +227,8 @@ QVariant QgsVectorLayerAndAttributeModel::headerData( int section, Qt::Orientati QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role ) const { - if ( idx.column() == 0 ) + QgsVectorLayer *vl = vectorLayer( idx ); + if ( idx.column() == LAYER_COL ) { if ( role == Qt::CheckStateRole ) { @@ -213,9 +279,45 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role else return QgsLayerTreeModel::data( idx, role ); } + else if ( idx.column() == ALLOW_DD_SYMBOL_BLOCKS_COL ) + { + if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) + { + return QVariant(); + } - QgsVectorLayer *vl = vectorLayer( idx ); - if ( vl ) + bool checked = mCreateDDBlockInfo.contains( vl ) ? mCreateDDBlockInfo[vl] : false; + if ( role == Qt::CheckStateRole ) + { + return checked ? Qt::Checked : Qt::Unchecked; + } + else + { + return QgsLayerTreeModel::data( idx, role ); + } + } + else if ( idx.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL ) + { + if ( !vl || vl->geometryType() != Qgis::GeometryType::Point ) + { + return QVariant(); + } + + if ( role == Qt::DisplayRole ) + { + if ( !mDDBlocksMaxNumberOfClasses.contains( vl ) ) + { + return QVariant( -1 ); + } + else + { + return QVariant( mDDBlocksMaxNumberOfClasses[vl] ); + } + } + } + + + if ( idx.column() == OUTPUT_LAYER_ATTRIBUTE_COL && vl ) { int idx = mAttributeIdx.value( vl, -1 ); if ( role == Qt::EditRole ) @@ -240,7 +342,7 @@ QVariant QgsVectorLayerAndAttributeModel::data( const QModelIndex &idx, int role bool QgsVectorLayerAndAttributeModel::setData( const QModelIndex &index, const QVariant &value, int role ) { - if ( index.column() == 0 && role == Qt::CheckStateRole ) + if ( index.column() == LAYER_COL && role == Qt::CheckStateRole ) { int i = 0; for ( i = 0; ; i++ ) @@ -267,12 +369,13 @@ bool QgsVectorLayerAndAttributeModel::setData( const QModelIndex &index, const Q return true; } - if ( index.column() == 1 ) + QgsVectorLayer *vl = vectorLayer( index ); + if ( index.column() == OUTPUT_LAYER_ATTRIBUTE_COL ) { if ( role != Qt::EditRole ) return false; - QgsVectorLayer *vl = vectorLayer( index ); + if ( vl ) { mAttributeIdx[ vl ] = value.toInt(); @@ -280,6 +383,23 @@ bool QgsVectorLayerAndAttributeModel::setData( const QModelIndex &index, const Q } } + if ( index.column() == ALLOW_DD_SYMBOL_BLOCKS_COL && role == Qt::CheckStateRole ) + { + if ( vl ) + { + mCreateDDBlockInfo[ vl ] = value.toBool(); + return true; + } + } + else if ( index.column() == MAXIMUM_DD_SYMBOL_BLOCKS_COL && role == Qt::EditRole ) + { + if ( vl ) + { + mDDBlocksMaxNumberOfClasses[ vl ] = value.toInt(); + return true; + } + } + return QgsLayerTreeModel::setData( index, value, role ); } @@ -302,7 +422,7 @@ QList< QgsDxfExport::DxfLayer > QgsVectorLayerAndAttributeModel::layers() const if ( !layerIdx.contains( vl->id() ) ) { layerIdx.insert( vl->id(), layers.size() ); - layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ) ); + layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ), mCreateDDBlockInfo.value( vl, false ), mDDBlocksMaxNumberOfClasses.value( vl, -1 ) ); } } } @@ -313,7 +433,7 @@ QList< QgsDxfExport::DxfLayer > QgsVectorLayerAndAttributeModel::layers() const if ( !layerIdx.contains( vl->id() ) ) { layerIdx.insert( vl->id(), layers.size() ); - layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ) ); + layers << QgsDxfExport::DxfLayer( vl, mAttributeIdx.value( vl, -1 ), mCreateDDBlockInfo.value( vl, false ), mDDBlocksMaxNumberOfClasses.value( vl, -1 ) ); } } } @@ -417,7 +537,6 @@ void QgsVectorLayerAndAttributeModel::loadLayersOutputAttribute( QgsLayerTreeNod emit dataChanged( idx, idx, QVector() << Qt::EditRole ); } } - continue; } else if ( QgsLayerTree::isGroup( child ) ) { @@ -448,7 +567,6 @@ void QgsVectorLayerAndAttributeModel::saveLayersOutputAttribute( QgsLayerTreeNod vl->removeCustomProperty( QStringLiteral( "lastDxfOutputAttribute" ) ); } } - continue; } else if ( QgsLayerTree::isGroup( child ) ) { @@ -494,6 +612,26 @@ void QgsVectorLayerAndAttributeModel::deSelectAll() emit dataChanged( index( 0, 0 ), index( rowCount() - 1, 0 ) ); } +void QgsVectorLayerAndAttributeModel::selectDataDefinedBlocks() +{ + enableDataDefinedBlocks( true ); +} + +void QgsVectorLayerAndAttributeModel::deselectDataDefinedBlocks() +{ + enableDataDefinedBlocks( false ); +} + +void QgsVectorLayerAndAttributeModel::enableDataDefinedBlocks( bool enabled ) +{ + QHash::const_iterator it = mCreateDDBlockInfo.constBegin(); + for ( ; it != mCreateDDBlockInfo.constEnd(); ++it ) + { + mCreateDDBlockInfo[it.key()] = enabled; + } + emit dataChanged( index( 0, 0 ), index( rowCount() - 1, 0 ) ); +} + QgsDxfExportLayerTreeView::QgsDxfExportLayerTreeView( QWidget *parent ) : QgsLayerTreeView( parent ) { @@ -542,6 +680,8 @@ QgsDxfExportDialog::QgsDxfExportDialog( QWidget *parent, Qt::WindowFlags f ) connect( this, &QDialog::accepted, this, &QgsDxfExportDialog::saveSettings ); connect( mSelectAllButton, &QAbstractButton::clicked, this, &QgsDxfExportDialog::selectAll ); connect( mDeselectAllButton, &QAbstractButton::clicked, this, &QgsDxfExportDialog::deSelectAll ); + connect( mSelectDataDefinedBlocks, &QAbstractButton::clicked, this, &QgsDxfExportDialog::selectDataDefinedBlocks ); + connect( mDeselectDataDefinedBlocks, &QAbstractButton::clicked, this, &QgsDxfExportDialog::deselectDataDefinedBlocks ); connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsDxfExportDialog::showHelp ); connect( mFileName, &QgsFileWidget::fileChanged, this, [ = ]( const QString & filePath ) @@ -567,6 +707,7 @@ QgsDxfExportDialog::QgsDxfExportDialog( QWidget *parent, Qt::WindowFlags f ) mScaleWidget->setScale( 1.0 / oldScale ); mLayerTitleAsName->setChecked( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfLayerTitleAsName" ), settings.value( QStringLiteral( "qgis/lastDxfLayerTitleAsName" ), "false" ).toString() ) != QLatin1String( "false" ) ); mMapExtentCheckBox->setChecked( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfMapRectangle" ), settings.value( QStringLiteral( "qgis/lastDxfMapRectangle" ), "false" ).toString() ) != QLatin1String( "false" ) ); + mSelectedFeaturesOnly->setChecked( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfSelectedFeaturesOnly" ), settings.value( QStringLiteral( "qgis/lastDxfSelectedFeaturesOnly" ), "false" ).toString() ) != QLatin1String( "false" ) ); mMTextCheckBox->setChecked( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfUseMText" ), settings.value( QStringLiteral( "qgis/lastDxfUseMText" ), "true" ).toString() ) != QLatin1String( "false" ) ); mForce2d->setChecked( QgsProject::instance()->readEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfForce2d" ), settings.value( QStringLiteral( "qgis/lastDxfForce2d" ), "false" ).toString() ) != QLatin1String( "false" ) ); @@ -645,6 +786,16 @@ void QgsDxfExportDialog::deSelectAll() mModel->deSelectAll(); } +void QgsDxfExportDialog::selectDataDefinedBlocks() +{ + mModel->selectDataDefinedBlocks(); +} + +void QgsDxfExportDialog::deselectDataDefinedBlocks() +{ + mModel->deselectDataDefinedBlocks(); +} + QList< QgsDxfExport::DxfLayer > QgsDxfExportDialog::layers() const { @@ -697,6 +848,11 @@ bool QgsDxfExportDialog::exportMapExtent() const return mMapExtentCheckBox->isChecked(); } +bool QgsDxfExportDialog::selectedFeaturesOnly() const +{ + return mSelectedFeaturesOnly->isChecked(); +} + bool QgsDxfExportDialog::layerTitleAsName() const { return mLayerTitleAsName->isChecked(); @@ -720,6 +876,7 @@ void QgsDxfExportDialog::saveSettings() settings.setValue( QStringLiteral( "qgis/lastDxfSymbologyMode" ), mSymbologyModeComboBox->currentIndex() ); settings.setValue( QStringLiteral( "qgis/lastSymbologyExportScale" ), mScaleWidget->scale() != 0 ? 1.0 / mScaleWidget->scale() : 0 ); settings.setValue( QStringLiteral( "qgis/lastDxfMapRectangle" ), mMapExtentCheckBox->isChecked() ); + settings.setValue( QStringLiteral( "qgis/lastDxfSelectedFeaturesOnly" ), mSelectedFeaturesOnly->isChecked() ); settings.setValue( QStringLiteral( "qgis/lastDxfLayerTitleAsName" ), mLayerTitleAsName->isChecked() ); settings.setValue( QStringLiteral( "qgis/lastDxfEncoding" ), mEncoding->currentText() ); settings.setValue( QStringLiteral( "qgis/lastDxfCrs" ), QString::number( mCRS.srsid() ) ); @@ -730,6 +887,7 @@ void QgsDxfExportDialog::saveSettings() QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastSymbologyExportScale" ), mScaleWidget->scale() != 0 ? 1.0 / mScaleWidget->scale() : 0 ); QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfLayerTitleAsName" ), mLayerTitleAsName->isChecked() ); QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfMapRectangle" ), mMapExtentCheckBox->isChecked() ); + QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfSelectedFeaturesOnly" ), mSelectedFeaturesOnly->isChecked() ); QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfEncoding" ), mEncoding->currentText() ); QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastVisibilityPreset" ), mVisibilityPresets->currentText() ); QgsProject::instance()->writeEntry( QStringLiteral( "dxf" ), QStringLiteral( "/lastDxfCrs" ), QString::number( mCRS.srsid() ) ); diff --git a/src/app/qgsdxfexportdialog.h b/src/app/qgsdxfexportdialog.h index 7d9e5ec9f1ae..973bea2722c3 100644 --- a/src/app/qgsdxfexportdialog.h +++ b/src/app/qgsdxfexportdialog.h @@ -20,7 +20,10 @@ #include "ui_qgsdxfexportdialogbase.h" #include "qgslayertreemodel.h" +#include "qgslayertreeview.h" #include "qgsdxfexport.h" +#include "qgssettingstree.h" +#include "qgssettingsentryimpl.h" #include #include @@ -68,13 +71,18 @@ class QgsVectorLayerAndAttributeModel : public QgsLayerTreeModel void selectAll(); void deSelectAll(); + void selectDataDefinedBlocks(); + void deselectDataDefinedBlocks(); private: QHash mAttributeIdx; + QHash mCreateDDBlockInfo; + QHash mDDBlocksMaxNumberOfClasses; QSet mCheckedLeafs; void applyVisibility( QSet &visibleLayers, QgsLayerTreeNode *node ); void retrieveAllLayers( QgsLayerTreeNode *node, QSet &layers ); + void enableDataDefinedBlocks( bool enabled ); }; class QgsDxfExportLayerTreeView : public QgsLayerTreeView @@ -91,6 +99,9 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase { Q_OBJECT public: + static inline QgsSettingsTreeNode *sTreeAppDdxf = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "dxf" ) ); + static const inline QgsSettingsEntryBool *settingsDxfEnableDDBlocks = new QgsSettingsEntryBool( QStringLiteral( "enable-datadefined-blocks" ), sTreeAppDdxf, false ); + QgsDxfExportDialog( QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() ); ~QgsDxfExportDialog() override; @@ -100,6 +111,7 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase Qgis::FeatureSymbologyExport symbologyMode() const; QString saveFile() const; bool exportMapExtent() const; + bool selectedFeaturesOnly() const; bool layerTitleAsName() const; bool force2d() const; bool useMText() const; @@ -111,6 +123,8 @@ class QgsDxfExportDialog : public QDialog, private Ui::QgsDxfExportDialogBase //! Change the selection of layers in the list void selectAll(); void deSelectAll(); + void selectDataDefinedBlocks(); + void deselectDataDefinedBlocks(); private slots: void setOkEnabled(); diff --git a/src/app/qgsidentifyresultsdialog.cpp b/src/app/qgsidentifyresultsdialog.cpp index 799fa054dc1b..ce9073bc5e9d 100644 --- a/src/app/qgsidentifyresultsdialog.cpp +++ b/src/app/qgsidentifyresultsdialog.cpp @@ -364,6 +364,8 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge mOpenFormAction->setDisabled( true ); + lstResults->setVerticalScrollMode( QListView::ScrollMode::ScrollPerPixel ); + QgsSettings mySettings; mDock = new QgsDockWidget( tr( "Identify Results" ), QgisApp::instance() ); mDock->setObjectName( QStringLiteral( "IdentifyResultsDock" ) ); @@ -434,9 +436,6 @@ QgsIdentifyResultsDialog::QgsIdentifyResultsDialog( QgsMapCanvas *canvas, QWidge mPlot->setSizePolicy( sizePolicy ); mPlot->updateGeometry(); - connect( lstResults, &QTreeWidget::itemExpanded, - this, &QgsIdentifyResultsDialog::itemExpanded ); - connect( lstResults, &QTreeWidget::currentItemChanged, this, &QgsIdentifyResultsDialog::handleCurrentItemChanged ); @@ -1437,7 +1436,6 @@ void QgsIdentifyResultsDialog::addFeature( QgsTiledSceneLayer *layer, } } - void QgsIdentifyResultsDialog::editingToggled() { QTreeWidgetItem *layItem = layerItem( sender() ); @@ -1982,33 +1980,37 @@ QgsTiledSceneLayer *QgsIdentifyResultsDialog::tiledSceneLayer( QTreeWidgetItem * return qobject_cast( item->data( 0, Qt::UserRole ).value() ); } -QTreeWidgetItem *QgsIdentifyResultsDialog::retrieveAttributes( QTreeWidgetItem *item, QgsAttributeMap &attributes, int &idx ) +QgsAttributeMap QgsIdentifyResultsDialog::retrieveAttributes( QTreeWidgetItem *item ) { QTreeWidgetItem *featItem = featureItem( item ); if ( !featItem ) - return nullptr; + return {}; - idx = -1; - - attributes.clear(); + QgsAttributeMap attributes; for ( int i = 0; i < featItem->childCount(); i++ ) { QTreeWidgetItem *item = featItem->child( i ); if ( item->childCount() > 0 ) continue; - if ( item == lstResults->currentItem() ) - idx = item->data( 0, Qt::UserRole + 1 ).toInt(); - attributes.insert( item->data( 0, Qt::UserRole + 1 ).toInt(), item->data( 1, REPRESENTED_VALUE_ROLE ) ); + + attributes.insert( item->data( 0, Qt::UserRole + 1 ).toInt(), + retrieveAttribute( item ) ); } - return featItem; + return attributes; } -void QgsIdentifyResultsDialog::itemExpanded( QTreeWidgetItem *item ) +QVariant QgsIdentifyResultsDialog::retrieveAttribute( QTreeWidgetItem *item ) { - Q_UNUSED( item ) - // column width is now stored in settings - //expandColumnsToFit(); + if ( !item ) + return QVariant(); + + // prefer represented values, if available. + const QVariant representedValue = item->data( 1, REPRESENTED_VALUE_ROLE ); + if ( !QgsVariantUtils::isNull( representedValue ) ) + return representedValue; + + return item->data( 1, Qt::DisplayRole ); } void QgsIdentifyResultsDialog::handleCurrentItemChanged( QTreeWidgetItem *current, QTreeWidgetItem *previous ) @@ -2378,7 +2380,8 @@ void QgsIdentifyResultsDialog::collapseAll() void QgsIdentifyResultsDialog::copyAttributeValue() { QClipboard *clipboard = QApplication::clipboard(); - const QString text = lstResults->currentItem()->data( 1, REPRESENTED_VALUE_ROLE ).toString(); + const QVariant attributeValue = retrieveAttribute( lstResults->currentItem() ); + const QString text = attributeValue.toString(); QgsDebugMsgLevel( QStringLiteral( "set clipboard: %1" ).arg( text ), 2 ); clipboard->setText( text ); } @@ -2397,12 +2400,8 @@ void QgsIdentifyResultsDialog::copyFeatureAttributes() if ( vlayer ) { - int idx; - QgsAttributeMap attributes; - retrieveAttributes( lstResults->currentItem(), attributes, idx ); - - const QgsFields &fields = vlayer->fields(); - + const QgsAttributeMap attributes = retrieveAttributes( lstResults->currentItem() ); + const QgsFields fields = vlayer->fields(); for ( QgsAttributeMap::const_iterator it = attributes.constBegin(); it != attributes.constEnd(); ++it ) { const int attrIdx = it.key(); diff --git a/src/app/qgsidentifyresultsdialog.h b/src/app/qgsidentifyresultsdialog.h index 7ceb87b50cce..4f3c1619ae08 100644 --- a/src/app/qgsidentifyresultsdialog.h +++ b/src/app/qgsidentifyresultsdialog.h @@ -255,18 +255,13 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti void collapseAll(); void selectFeatureByAttribute(); - /** - * Called when an item is expanded so that we can ensure that the - * column width if expanded to show it. - */ - void itemExpanded( QTreeWidgetItem * ); - //! sends signal if current feature id has changed void handleCurrentItemChanged( QTreeWidgetItem *current, QTreeWidgetItem *previous ); /* Item in tree was clicked */ void itemClicked( QTreeWidgetItem *lvi, int column ); - QTreeWidgetItem *retrieveAttributes( QTreeWidgetItem *item, QgsAttributeMap &attributes, int ¤tIdx ); + QgsAttributeMap retrieveAttributes( QTreeWidgetItem *item ); + QVariant retrieveAttribute( QTreeWidgetItem *item ); void cmbIdentifyMode_currentIndexChanged( int index ); @@ -347,6 +342,8 @@ class APP_EXPORT QgsIdentifyResultsDialog: public QDialog, private Ui::QgsIdenti void initSelectionModes(); QgsIdentifyResultsFeatureItem *createFeatureItem( QgsVectorLayer *vlayer, const QgsFeature &f, const QMap &derivedAttributes, bool includeRelations, QTreeWidgetItem *parentItem ); + + friend class TestQgsMapToolIdentifyAction; }; class QgsIdentifyResultsDialogMapLayerAction : public QAction diff --git a/src/app/qgsmapcanvasdockwidget.cpp b/src/app/qgsmapcanvasdockwidget.cpp index c8f5f7571885..079dfb1a50d1 100644 --- a/src/app/qgsmapcanvasdockwidget.cpp +++ b/src/app/qgsmapcanvasdockwidget.cpp @@ -30,6 +30,7 @@ #include "qgsvectorlayer.h" #include "qgsapplication.h" #include "qgsdockablewidgethelper.h" +#include "canvas/qgsappcanvasfiltering.h" #include #include @@ -109,10 +110,16 @@ QgsMapCanvasDockWidget::QgsMapCanvasDockWidget( const QString &name, QWidget *pa settingsMenu->addAction( mActionShowCursor ); settingsMenu->addAction( mActionShowExtent ); settingsMenu->addAction( mActionShowLabels ); + settingsMenu->addAction( mActionElevationController ); settingsMenu->addSeparator(); settingsMenu->addAction( mActionSetCrs ); settingsMenu->addAction( mActionRename ); + if ( QgisApp *app = QgisApp::instance() ) + { + app->canvasFiltering()->setupElevationControllerAction( mActionElevationController, mMapCanvas ); + } + connect( settingsMenu, &QMenu::aboutToShow, this, &QgsMapCanvasDockWidget::settingsMenuAboutToShow ); connect( mActionRename, &QAction::triggered, this, &QgsMapCanvasDockWidget::renameTriggered ); diff --git a/src/app/qgsmapsavedialog.cpp b/src/app/qgsmapsavedialog.cpp index 4f6999b27d23..047fa2fa5c7d 100644 --- a/src/app/qgsmapsavedialog.cpp +++ b/src/app/qgsmapsavedialog.cpp @@ -367,6 +367,7 @@ void QgsMapSaveDialog::applyMapSettings( QgsMapSettings &mapSettings ) mapSettings.setPathResolver( QgsProject::instance()->pathResolver() ); mapSettings.setTemporalRange( mMapCanvas->mapSettings().temporalRange() ); mapSettings.setIsTemporal( mMapCanvas->mapSettings().isTemporal() ); + mapSettings.setZRange( mMapCanvas->mapSettings().zRange() ); mapSettings.setElevationShadingRenderer( mMapCanvas->mapSettings().elevationShadingRenderer() ); //build the expression context diff --git a/src/app/qgsmaptoolidentifyaction.cpp b/src/app/qgsmaptoolidentifyaction.cpp index c474740343fa..8295ff46a8a0 100644 --- a/src/app/qgsmaptoolidentifyaction.cpp +++ b/src/app/qgsmaptoolidentifyaction.cpp @@ -132,6 +132,7 @@ void QgsMapToolIdentifyAction::identifyFromGeometry() QgsIdentifyContext identifyContext; if ( mCanvas->mapSettings().isTemporal() ) identifyContext.setTemporalRange( mCanvas->temporalRange() ); + identifyContext.setZRange( mCanvas->zRange() ); const QList results = QgsMapToolIdentify::identify( geometry, mode, layerList, AllLayers, identifyContext ); disconnect( this, &QgsMapToolIdentifyAction::identifyMessage, QgisApp::instance(), &QgisApp::showStatusMessage ); diff --git a/src/app/qgspluginregistry.cpp b/src/app/qgspluginregistry.cpp index b35903fbbf2a..94c38b90aa23 100644 --- a/src/app/qgspluginregistry.cpp +++ b/src/app/qgspluginregistry.cpp @@ -564,7 +564,6 @@ void QgsPluginRegistry::restoreSessionPlugins( const QString &pluginDirString ) QgsDebugMsgLevel( QStringLiteral( "Python plugins will be loaded in the following order: " ) + pluginList.join( "," ), 2 ); QStringList corePlugins = QStringList(); - corePlugins << QStringLiteral( "GdalTools" ); corePlugins << QStringLiteral( "db_manager" ); corePlugins << QStringLiteral( "processing" ); corePlugins << QStringLiteral( "MetaSearch" ); @@ -725,7 +724,10 @@ bool QgsPluginRegistry::isPythonPluginCompatible( const QString &packageName ) c const QString supportsQt6 = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "supportsQt6" ) ).trimmed(); if ( supportsQt6.compare( QLatin1String( "YES" ), Qt::CaseInsensitive ) != 0 && supportsQt6.compare( QLatin1String( "TRUE" ), Qt::CaseInsensitive ) != 0 ) { - return false; + if ( !getenv( "QGIS_DISABLE_SUPPORTS_QT6_CHECK" ) ) + { + return false; + } } #endif const QString minVersion = mPythonUtils->getPluginMetadata( packageName, QStringLiteral( "qgisMinimumVersion" ) ); diff --git a/src/app/qgsstatisticalsummarydockwidget.cpp b/src/app/qgsstatisticalsummarydockwidget.cpp index a8a0a0477a50..cbb8f8803127 100644 --- a/src/app/qgsstatisticalsummarydockwidget.cpp +++ b/src/app/qgsstatisticalsummarydockwidget.cpp @@ -22,7 +22,8 @@ #include "qgsstatisticalsummarydockwidget.h" #include "qgsstatisticalsummary.h" #include "qgsvectorlayer.h" -#include "qgsfeedback.h" +#include "qgsstringstatisticalsummary.h" +#include "qgsdatetimestatisticalsummary.h" #include "qgsvectorlayerutils.h" #include "qgsapplication.h" #include "qgsexpressioncontextutils.h" @@ -107,7 +108,8 @@ void QgsStatisticalSummaryDockWidget::fieldChanged() if ( mFieldExpressionWidget->expression() != mExpression ) { mExpression = mFieldExpressionWidget->expression(); - mLastExpression.insert( mLayerComboBox->currentLayer()->id(), mFieldExpressionWidget->currentText() ); + if ( QgsMapLayer *currentLayer = mLayerComboBox->currentLayer() ) + mLastExpression.insert( currentLayer->id(), mFieldExpressionWidget->currentText() ); refreshStatistics(); } } @@ -220,8 +222,53 @@ void QgsStatisticalSummaryDockWidget::refreshStatistics() const QgsFeatureIterator fit = QgsVectorLayerUtils::getValuesIterator( mLayer, sourceFieldExp, ok, selectedOnly ); if ( ok ) { + Qgis::Statistics statsToCalc; + Qgis::StringStatistics stringStatsToCalc; + Qgis::DateTimeStatistics dateTimeStatsToCalc; + + switch ( mFieldType ) + { + + case Numeric: + { + const auto displayStats = *sDisplayStats(); + for ( const Qgis::Statistic stat : displayStats ) + { + if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) + { + statsToCalc |= stat; + } + } + break; + } + case String: + { + const auto displayStringStats = *sDisplayStringStats(); + for ( const Qgis::StringStatistic stat : displayStringStats ) + { + if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) + { + stringStatsToCalc |= stat; + } + } + break; + } + case DateTime: + { + const auto displayDateTimeStats = *sDisplayDateTimeStats(); + for ( const Qgis::DateTimeStatistic stat : displayDateTimeStats ) + { + if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) + { + dateTimeStatsToCalc |= stat; + } + } + break; + } + } + const long featureCount = selectedOnly ? mLayer->selectedFeatureCount() : mLayer->featureCount(); - std::unique_ptr< QgsStatisticsValueGatherer > gatherer = std::make_unique< QgsStatisticsValueGatherer >( mLayer, fit, featureCount, sourceFieldExp ); + std::unique_ptr< QgsStatisticsValueGatherer > gatherer = std::make_unique< QgsStatisticsValueGatherer >( mLayer, fit, featureCount, sourceFieldExp, mFieldType, statsToCalc, stringStatsToCalc, dateTimeStatsToCalc ); switch ( mFieldType ) { case DataType::Numeric: @@ -277,32 +324,13 @@ void QgsStatisticalSummaryDockWidget::updateNumericStatistics() if ( gatherer != mGatherer ) return; - const QList< QVariant > variantValues = mGatherer->values(); - - QList values; - bool convertOk; - int missingValues = 0; - const auto constVariantValues = variantValues; - for ( const QVariant &value : constVariantValues ) - { - const double val = value.toDouble( &convertOk ); - if ( convertOk ) - values << val; - else if ( QgsVariantUtils::isNull( value ) ) - { - missingValues += 1; - } - } - QList< Qgis::Statistic > statsToDisplay; - Qgis::Statistics statsToCalc; const auto displayStats = *sDisplayStats(); for ( const Qgis::Statistic stat : displayStats ) { if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) { statsToDisplay << stat; - statsToCalc |= stat; } } @@ -310,29 +338,25 @@ void QgsStatisticalSummaryDockWidget::updateNumericStatistics() if ( mStatsActions.value( MISSING_VALUES )->isChecked() ) extraRows++; - QgsStatisticalSummary stats; - stats.setStatistics( statsToCalc ); - stats.calculate( values ); - mStatisticsTable->setRowCount( statsToDisplay.count() + extraRows ); mStatisticsTable->setColumnCount( 2 ); int row = 0; - const auto constStatsToDisplay = statsToDisplay; - for ( const Qgis::Statistic stat : constStatsToDisplay ) + const QgsStatisticalSummary *stats = gatherer->statsSummary(); + for ( const Qgis::Statistic stat : std::as_const( statsToDisplay ) ) { - const double val = stats.statistic( stat ); + const double val = stats->statistic( stat ); addRow( row, QgsStatisticalSummary::displayName( stat ), std::isnan( val ) ? QString() : QLocale().toString( val ), - stats.count() != 0 ); + stats->count() != 0 ); row++; } if ( mStatsActions.value( MISSING_VALUES )->isChecked() ) { addRow( row, tr( "Missing (null) values" ), - QLocale().toString( missingValues ), - stats.count() != 0 || missingValues != 0 ); + QLocale().toString( stats->countMissing() ), + stats->count() != 0 || stats->countMissing() != 0 ); row++; } @@ -349,34 +373,26 @@ void QgsStatisticalSummaryDockWidget::updateStringStatistics() if ( gatherer != mGatherer ) return; - const QVariantList values = mGatherer->values(); - QList< Qgis::StringStatistic > statsToDisplay; - Qgis::StringStatistics statsToCalc; const auto displayStringStats = *sDisplayStringStats(); for ( const Qgis::StringStatistic stat : displayStringStats ) { if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) { statsToDisplay << stat; - statsToCalc |= stat; } } - QgsStringStatisticalSummary stats; - stats.setStatistics( statsToCalc ); - stats.calculateFromVariants( values ); - mStatisticsTable->setRowCount( statsToDisplay.count() ); mStatisticsTable->setColumnCount( 2 ); int row = 0; - const auto constStatsToDisplay = statsToDisplay; - for ( const Qgis::StringStatistic stat : constStatsToDisplay ) + const QgsStringStatisticalSummary *stats = gatherer->stringStatsSummary(); + for ( const Qgis::StringStatistic stat : std::as_const( statsToDisplay ) ) { addRow( row, QgsStringStatisticalSummary::displayName( stat ), - stats.statistic( stat ).toString(), - stats.count() != 0 ); + stats->statistic( stat ).toString(), + stats->count() != 0 ); row++; } @@ -458,7 +474,7 @@ void QgsStatisticalSummaryDockWidget::layersRemoved( const QStringList &layers ) disconnect( mLayer, &QgsVectorLayer::selectionChanged, this, &QgsStatisticalSummaryDockWidget::layerSelectionChanged ); mLayer = nullptr; } - for ( QString layerId : layers ) + for ( const QString &layerId : layers ) { mLastExpression.remove( layerId ); } @@ -478,39 +494,30 @@ void QgsStatisticalSummaryDockWidget::updateDateTimeStatistics() if ( gatherer != mGatherer ) return; - const QVariantList values = mGatherer->values(); - QList< Qgis::DateTimeStatistic > statsToDisplay; - Qgis::DateTimeStatistics statsToCalc; const auto displayDateTimeStats = *sDisplayDateTimeStats(); for ( const Qgis::DateTimeStatistic stat : displayDateTimeStats ) { if ( mStatsActions.value( static_cast< int >( stat ) )->isChecked() ) { statsToDisplay << stat; - statsToCalc |= stat; } } - - QgsDateTimeStatisticalSummary stats; - stats.setStatistics( statsToCalc ); - stats.calculate( values ); - mStatisticsTable->setRowCount( statsToDisplay.count() ); mStatisticsTable->setColumnCount( 2 ); int row = 0; - const auto constStatsToDisplay = statsToDisplay; - for ( const Qgis::DateTimeStatistic stat : constStatsToDisplay ) + const QgsDateTimeStatisticalSummary *stats = gatherer->dateTimeStatsSummary(); + for ( const Qgis::DateTimeStatistic stat : std::as_const( statsToDisplay ) ) { const QString value = ( stat == Qgis::DateTimeStatistic::Range - ? tr( "%n second(s)", nullptr, stats.range().seconds() ) - : stats.statistic( stat ).toString() ); + ? tr( "%n second(s)", nullptr, stats->range().seconds() ) + : stats->statistic( stat ).toString() ); addRow( row, QgsDateTimeStatisticalSummary::displayName( stat ), value, - stats.count() != 0 ); + stats->count() != 0 ); row++; } @@ -610,7 +617,7 @@ void QgsStatisticalSummaryDockWidget::refreshStatisticsMenu() mStatisticsMenu->addAction( mSyncAction ); } -QgsStatisticalSummaryDockWidget::DataType QgsStatisticalSummaryDockWidget::fieldType( const QString &fieldName ) +DataType QgsStatisticalSummaryDockWidget::fieldType( const QString &fieldName ) { const QgsField field = mLayer->fields().field( mLayer->fields().lookupField( fieldName ) ); if ( field.isNumeric() ) @@ -632,11 +639,23 @@ QgsStatisticalSummaryDockWidget::DataType QgsStatisticalSummaryDockWidget::field return DataType::Numeric; } -QgsStatisticsValueGatherer::QgsStatisticsValueGatherer( QgsVectorLayer *layer, const QgsFeatureIterator &fit, long featureCount, const QString &sourceFieldExp ) +QgsStatisticsValueGatherer::QgsStatisticsValueGatherer( + QgsVectorLayer *layer, + const QgsFeatureIterator &fit, + long featureCount, + const QString &sourceFieldExp, + DataType fieldType, + Qgis::Statistics statsToCalculate, + Qgis::StringStatistics stringStatsToCalculate, + Qgis::DateTimeStatistics dateTimeStatsToCalculate ) : QgsTask( tr( "Fetching statistic values" ), QgsTask::CanCancel | QgsTask::CancelWithoutPrompt ) , mFeatureIterator( fit ) , mFeatureCount( featureCount ) , mFieldExpression( sourceFieldExp ) + , mFieldType( fieldType ) + , mStatsToCalculate( statsToCalculate ) + , mStringStatsToCalculate( stringStatsToCalculate ) + , mDateTimeStatsToCalculate( dateTimeStatsToCalculate ) { mFieldIndex = layer->fields().lookupField( mFieldExpression ); if ( mFieldIndex == -1 ) @@ -647,21 +666,60 @@ QgsStatisticsValueGatherer::QgsStatisticsValueGatherer( QgsVectorLayer *layer, c } } +QgsStatisticsValueGatherer::~QgsStatisticsValueGatherer() = default; + bool QgsStatisticsValueGatherer::run() { QgsFeature f; int current = 0; + + switch ( mFieldType ) + { + case Numeric: + mStatsSummary = std::make_unique< QgsStatisticalSummary >( mStatsToCalculate ); + break; + case String: + mStringStatsSummary = std::make_unique< QgsStringStatisticalSummary >( mStringStatsToCalculate ); + break; + case DateTime: + mDateTimeStatsSummary = std::make_unique< QgsDateTimeStatisticalSummary >( mDateTimeStatsToCalculate ); + break; + } + while ( mFeatureIterator.nextFeature( f ) ) { if ( mExpression ) { mContext.setFeature( f ); const QVariant v = mExpression->evaluate( &mContext ); - mValues << v; + + switch ( mFieldType ) + { + case Numeric: + mStatsSummary->addVariant( v ); + break; + case String: + mStringStatsSummary->addValue( v ); + break; + case DateTime: + mDateTimeStatsSummary->addValue( v ); + break; + } } else { - mValues << f.attribute( mFieldIndex ); + switch ( mFieldType ) + { + case Numeric: + mStatsSummary->addVariant( f.attribute( mFieldIndex ) ); + break; + case String: + mStringStatsSummary->addValue( f.attribute( mFieldIndex ) ); + break; + case DateTime: + mDateTimeStatsSummary->addValue( f.attribute( mFieldIndex ) ); + break; + } } if ( isCanceled() ) @@ -675,5 +733,34 @@ bool QgsStatisticsValueGatherer::run() setProgress( 100.0 * static_cast< double >( current ) / mFeatureCount ); } } + + switch ( mFieldType ) + { + case Numeric: + mStatsSummary->finalize(); + break; + case String: + mStringStatsSummary->finalize(); + break; + case DateTime: + mDateTimeStatsSummary->finalize(); + break; + } + return true; } + +const QgsStatisticalSummary *QgsStatisticsValueGatherer::statsSummary() +{ + return mStatsSummary.get(); +} + +const QgsStringStatisticalSummary *QgsStatisticsValueGatherer::stringStatsSummary() +{ + return mStringStatsSummary.get(); +} + +const QgsDateTimeStatisticalSummary *QgsStatisticsValueGatherer::dateTimeStatsSummary() +{ + return mDateTimeStatsSummary.get(); +} diff --git a/src/app/qgsstatisticalsummarydockwidget.h b/src/app/qgsstatisticalsummarydockwidget.h index a24a7a01a078..f661d85ce36b 100644 --- a/src/app/qgsstatisticalsummarydockwidget.h +++ b/src/app/qgsstatisticalsummarydockwidget.h @@ -18,9 +18,6 @@ #include #include "ui_qgsstatisticalsummarybase.h" -#include "qgsstatisticalsummary.h" -#include "qgsstringstatisticalsummary.h" -#include "qgsdatetimestatisticalsummary.h" #include "qgsdockwidget.h" #include "qgsfeatureiterator.h" #include "qgstaskmanager.h" @@ -35,6 +32,17 @@ class QgsDockBrowserTreeView; class QgsLayerItem; class QgsDataItem; class QgsVectorLayer; +class QgsStatisticalSummary; +class QgsStringStatisticalSummary; +class QgsDateTimeStatisticalSummary; + +//! Enumeration of supported statistics types +enum DataType +{ + Numeric, //!< Numeric fields: int, double, etc + String, //!< String fields + DateTime //!< Date and DateTime fields +}; /** * \class QgsStatisticsValueGatherer @@ -45,11 +53,23 @@ class QgsStatisticsValueGatherer : public QgsTask Q_OBJECT public: - QgsStatisticsValueGatherer( QgsVectorLayer *layer, const QgsFeatureIterator &fit, long featureCount, const QString &sourceFieldExp ); + QgsStatisticsValueGatherer( + QgsVectorLayer *layer, + const QgsFeatureIterator &fit, + long featureCount, + const QString &sourceFieldExp, + DataType fieldType, + Qgis::Statistics statsToCalculate, + Qgis::StringStatistics stringStatsToCalculate, + Qgis::DateTimeStatistics dateTimeStatsToCalculate + ); + ~QgsStatisticsValueGatherer() override; bool run() override; - QList values() const { return mValues; } + const QgsStatisticalSummary *statsSummary(); + const QgsStringStatisticalSummary *stringStatsSummary(); + const QgsDateTimeStatisticalSummary *dateTimeStatsSummary(); private: @@ -57,10 +77,17 @@ class QgsStatisticsValueGatherer : public QgsTask long mFeatureCount = 0; QString mFieldExpression; int mFieldIndex = -1; - QList mValues; + DataType mFieldType; + Qgis::Statistics mStatsToCalculate; + Qgis::StringStatistics mStringStatsToCalculate; + Qgis::DateTimeStatistics mDateTimeStatsToCalculate; std::unique_ptr mExpression; QgsExpressionContext mContext; + + std::unique_ptr< QgsStatisticalSummary > mStatsSummary; + std::unique_ptr< QgsStringStatisticalSummary > mStringStatsSummary; + std::unique_ptr< QgsDateTimeStatisticalSummary > mDateTimeStatsSummary; }; /** @@ -102,14 +129,6 @@ class APP_EXPORT QgsStatisticalSummaryDockWidget : public QgsDockWidget, private private: - //! Enumeration of supported statistics types - enum DataType - { - Numeric, //!< Numeric fields: int, double, etc - String, //!< String fields - DateTime //!< Date and DateTime fields - }; - QgsVectorLayer *mLayer = nullptr; QMap< int, QAction * > mStatsActions; diff --git a/src/app/qgsundowidget.h b/src/app/qgsundowidget.h index 8c02251c55ca..ae983f1dea62 100644 --- a/src/app/qgsundowidget.h +++ b/src/app/qgsundowidget.h @@ -34,7 +34,7 @@ class QgsMapCanvas; class QgsMapLayer; /** - * Class that handles undo display fo undo commands + * Class that handles undo display for undo commands */ class APP_EXPORT QgsUndoWidget : public QgsPanelWidget { diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.cpp b/src/app/raster/qgsrasterelevationpropertieswidget.cpp index d89f148717b9..6ddee5d0b650 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.cpp +++ b/src/app/raster/qgsrasterelevationpropertieswidget.cpp @@ -20,6 +20,12 @@ #include "qgsrasterlayerelevationproperties.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" +#include "qgsexpressionbuilderdialog.h" +#include "qgsrasterrendererregistry.h" +#include "qgsrasterrenderer.h" +#include "qgsexpressioncontextutils.h" +#include +#include QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRasterLayer *layer, QgsMapCanvas *canvas, QWidget *parent ) : QgsMapLayerConfigWidget( layer, canvas, parent ) @@ -27,9 +33,30 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste setupUi( this ); setObjectName( QStringLiteral( "mOptsPage_Elevation" ) ); + mModeComboBox->addItem( tr( "Disabled" ) ); + mModeComboBox->addItem( tr( "Represents Elevation Surface" ), QVariant::fromValue( Qgis::RasterElevationMode::RepresentsElevationSurface ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range" ), QVariant::fromValue( Qgis::RasterElevationMode::FixedElevationRange ) ); + mModeComboBox->addItem( tr( "Fixed Elevation Range Per Band" ), QVariant::fromValue( Qgis::RasterElevationMode::FixedRangePerBand ) ); + mModeComboBox->addItem( tr( "Dynamic Elevation Range Per Band" ), QVariant::fromValue( Qgis::RasterElevationMode::DynamicRangePerBand ) ); + + mLimitsComboBox->addItem( tr( "Include Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeBoth ) ); + mLimitsComboBox->addItem( tr( "Include Lower, Exclude Upper" ), QVariant::fromValue( Qgis::RangeLimits::IncludeLowerExcludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower, Include Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeLowerIncludeUpper ) ); + mLimitsComboBox->addItem( tr( "Exclude Lower and Upper" ), QVariant::fromValue( Qgis::RangeLimits::ExcludeBoth ) ); + + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mSymbologyStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mOffsetZSpinBox->setClearValue( 0 ); mScaleZSpinBox->setClearValue( 1 ); - mElevationGroupBox->setChecked( false ); + mFixedLowerSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedUpperSpinBox->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mFixedLowerSpinBox->clear(); + mFixedUpperSpinBox->clear(); + + mLowerExpressionWidget->registerExpressionContextGenerator( this ); + mUpperExpressionWidget->registerExpressionContextGenerator( this ); + mLineStyleButton->setSymbolType( Qgis::SymbolType::Line ); mFillStyleButton->setSymbolType( Qgis::SymbolType::Fill ); mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationLine.svg" ) ), tr( "Line" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::Line ) ); @@ -37,12 +64,43 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste mStyleComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "mIconSurfaceElevationFillAbove.svg" ) ), tr( "Fill Above" ), static_cast< int >( Qgis::ProfileSurfaceSymbology::FillAbove ) ); mElevationLimitSpinBox->setClearValue( mElevationLimitSpinBox->minimum(), tr( "Not set" ) ); + // NOTE -- this doesn't work, there's something broken in QgsStackedWidget which breaks the height calculations + mWidgetFixedRangePerBand->setFixedHeight( QFontMetrics( font() ).height() * 15 ); + + mFixedRangePerBandModel = new QgsRasterBandFixedElevationRangeModel( this ); + mBandElevationTable->verticalHeader()->setVisible( false ); + mBandElevationTable->setModel( mFixedRangePerBandModel ); + QgsFixedElevationRangeDelegate *tableDelegate = new QgsFixedElevationRangeDelegate( mBandElevationTable ); + mBandElevationTable->setItemDelegateForColumn( 1, tableDelegate ); + mBandElevationTable->setItemDelegateForColumn( 2, tableDelegate ); + + mDynamicRangePerBandModel = new QgsRasterBandDynamicElevationRangeModel( this ); + mBandDynamicElevationTable->verticalHeader()->setVisible( false ); + mBandDynamicElevationTable->setModel( mDynamicRangePerBandModel ); + + QMenu *calculateFixedRangePerBandMenu = new QMenu( mCalculateFixedRangePerBandButton ); + mCalculateFixedRangePerBandButton->setMenu( calculateFixedRangePerBandMenu ); + mCalculateFixedRangePerBandButton->setPopupMode( QToolButton::InstantPopup ); + QAction *calculateLowerAction = new QAction( "Calculate Lower by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateLowerAction ); + connect( calculateLowerAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( false ); + } ); + QAction *calculateUpperAction = new QAction( "Calculate Upper by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateUpperAction ); + connect( calculateUpperAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( true ); + } ); + syncToLayer( layer ); connect( mOffsetZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mScaleZSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mElevationLimitSpinBox, qOverload( &QDoubleSpinBox::valueChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); - connect( mElevationGroupBox, &QGroupBox::toggled, this, &QgsRasterElevationPropertiesWidget::onChanged ); + connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterElevationPropertiesWidget::modeChanged ); + connect( mLimitsComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mLineStyleButton, &QgsSymbolButton::changed, this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mFillStyleButton, &QgsSymbolButton::changed, this, &QgsRasterElevationPropertiesWidget::onChanged ); connect( mBandComboBox, &QgsRasterBandComboBox::bandChanged, this, &QgsRasterElevationPropertiesWidget::onChanged ); @@ -61,6 +119,16 @@ QgsRasterElevationPropertiesWidget::QgsRasterElevationPropertiesWidget( QgsRaste onChanged(); } ); + connect( mLowerExpressionWidget, qOverload< const QString &, bool >( &QgsFieldExpressionWidget::fieldChanged ), this, [this]( const QString & expression, bool isValid ) + { + if ( isValid ) + mDynamicRangePerBandModel->setLowerExpression( expression ); + } ); + connect( mUpperExpressionWidget, qOverload< const QString &, bool >( &QgsFieldExpressionWidget::fieldChanged ), this, [this]( const QString & expression, bool isValid ) + { + if ( isValid ) + mDynamicRangePerBandModel->setUpperExpression( expression ); + } ); setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#elevation-properties" ) ); } @@ -72,8 +140,35 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) return; mBlockUpdates = true; + const QgsRasterLayerElevationProperties *props = qgis::down_cast< const QgsRasterLayerElevationProperties * >( mLayer->elevationProperties() ); - mElevationGroupBox->setChecked( props->isEnabled() ); + if ( !props->isEnabled() ) + { + mModeComboBox->setCurrentIndex( 0 ); + mStackedWidget->setCurrentWidget( mPageDisabled ); + mProfileChartGroupBox->hide(); + } + else + { + mModeComboBox->setCurrentIndex( mModeComboBox->findData( QVariant::fromValue( props->mode() ) ) ); + switch ( props->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mStackedWidget->setCurrentWidget( mPageSurface ); + break; + case Qgis::RasterElevationMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); + break; + case Qgis::RasterElevationMode::DynamicRangePerBand: + mStackedWidget->setCurrentWidget( mPageDynamicPerBand ); + break; + } + mProfileChartGroupBox->show(); + } + mOffsetZSpinBox->setValue( props->zOffset() ); mScaleZSpinBox->setValue( props->zScale() ); if ( std::isnan( props->elevationLimit() ) ) @@ -84,6 +179,33 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mFillStyleButton->setSymbol( props->profileFillSymbol()->clone() ); mBandComboBox->setLayer( mLayer ); mBandComboBox->setBand( props->bandNumber() ); + + if ( props->fixedRange().lower() != std::numeric_limits< double >::lowest() ) + mFixedLowerSpinBox->setValue( props->fixedRange().lower() ); + else + mFixedLowerSpinBox->clear(); + if ( props->fixedRange().upper() != std::numeric_limits< double >::max() ) + mFixedUpperSpinBox->setValue( props->fixedRange().upper() ); + else + mFixedUpperSpinBox->clear(); + mLimitsComboBox->setCurrentIndex( mLimitsComboBox->findData( QVariant::fromValue( props->fixedRange().rangeLimits() ) ) ); + + mFixedRangePerBandModel->setLayerData( mLayer, props->fixedRangePerBand() ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + mBandElevationTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + + mDynamicRangePerBandModel->setLayer( mLayer ); + mBandDynamicElevationTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch ); + mBandDynamicElevationTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + mBandDynamicElevationTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); + + if ( QgsApplication::rasterRendererRegistry()->rendererCapabilities( mLayer->renderer()->type() ) & Qgis::RasterRendererCapability::UsesMultipleBands ) + { + mWidgetFixedRangePerBand->hide(); + mFixedRangePerBandLabel->setText( tr( "This mode cannot be used with a multi-band renderer." ) ); + } + mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( static_cast ( props->profileSymbology() ) ) ); switch ( props->profileSymbology() ) { @@ -96,16 +218,53 @@ void QgsRasterElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) break; } + mLowerExpressionWidget->setExpression( + props->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandLowerElevation ).asExpression() ); + mUpperExpressionWidget->setExpression( + props->dataDefinedProperties().property( QgsMapLayerElevationProperties::Property::RasterPerBandUpperElevation ).asExpression() ); + + mDynamicRangePerBandModel->setLowerExpression( mLowerExpressionWidget->expression() ); + mDynamicRangePerBandModel->setUpperExpression( mUpperExpressionWidget->expression() ); + + QList > bandChoices; + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandChoices << qMakePair( mLayer->dataProvider()->displayBandName( band ), band ); + } + mLowerExpressionWidget->setCustomPreviewGenerator( tr( "Band" ), bandChoices, [this]( const QVariant & value )-> QgsExpressionContext + { + return createExpressionContextForBand( value.toInt() ); + } ); + mUpperExpressionWidget->setCustomPreviewGenerator( tr( "Band" ), bandChoices, [this]( const QVariant & value )-> QgsExpressionContext + { + return createExpressionContextForBand( value.toInt() ); + } ); + mBlockUpdates = false; } +QgsExpressionContext QgsRasterElevationPropertiesWidget::createExpressionContext() const +{ + return createExpressionContextForBand( 1 ); +} + void QgsRasterElevationPropertiesWidget::apply() { if ( !mLayer ) return; QgsRasterLayerElevationProperties *props = qgis::down_cast< QgsRasterLayerElevationProperties * >( mLayer->elevationProperties() ); - props->setEnabled( mElevationGroupBox->isChecked() ); + + if ( !mModeComboBox->currentData().isValid() ) + { + props->setEnabled( false ); + } + else + { + props->setEnabled( true ); + props->setMode( mModeComboBox->currentData().value< Qgis::RasterElevationMode >() ); + } + props->setZOffset( mOffsetZSpinBox->value() ); props->setZScale( mScaleZSpinBox->value() ); if ( mElevationLimitSpinBox->value() != mElevationLimitSpinBox->clearValue() ) @@ -116,15 +275,119 @@ void QgsRasterElevationPropertiesWidget::apply() props->setProfileFillSymbol( mFillStyleButton->clonedSymbol< QgsFillSymbol >() ); props->setProfileSymbology( static_cast< Qgis::ProfileSurfaceSymbology >( mStyleComboBox->currentData().toInt() ) ); props->setBandNumber( mBandComboBox->currentBand() ); + + double fixedLower = std::numeric_limits< double >::lowest(); + double fixedUpper = std::numeric_limits< double >::max(); + if ( mFixedLowerSpinBox->value() != mFixedLowerSpinBox->clearValue() ) + fixedLower = mFixedLowerSpinBox->value(); + if ( mFixedUpperSpinBox->value() != mFixedUpperSpinBox->clearValue() ) + fixedUpper = mFixedUpperSpinBox->value(); + + props->setFixedRange( QgsDoubleRange( fixedLower, fixedUpper, mLimitsComboBox->currentData().value< Qgis::RangeLimits >() ) ); + + props->setFixedRangePerBand( mFixedRangePerBandModel->rangeData() ); + + QgsPropertyCollection properties; + properties.setProperty( QgsMapLayerElevationProperties::Property::RasterPerBandLowerElevation, QgsProperty::fromExpression( mLowerExpressionWidget->asExpression() ) ); + properties.setProperty( QgsMapLayerElevationProperties::Property::RasterPerBandUpperElevation, QgsProperty::fromExpression( mUpperExpressionWidget->asExpression() ) ); + props->setDataDefinedProperties( properties ); + mLayer->trigger3DUpdate(); } +void QgsRasterElevationPropertiesWidget::modeChanged() +{ + if ( mModeComboBox->currentData().isValid() ) + { + switch ( mModeComboBox->currentData().value< Qgis::RasterElevationMode >() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mStackedWidget->setCurrentWidget( mPageSurface ); + break; + case Qgis::RasterElevationMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); + break; + case Qgis::RasterElevationMode::DynamicRangePerBand: + mStackedWidget->setCurrentWidget( mPageDynamicPerBand ); + break; + } + mProfileChartGroupBox->show(); + } + else + { + mStackedWidget->setCurrentWidget( mPageDisabled ); + mProfileChartGroupBox->hide(); + } + + onChanged(); +} + void QgsRasterElevationPropertiesWidget::onChanged() { if ( !mBlockUpdates ) emit widgetChanged(); } +void QgsRasterElevationPropertiesWidget::calculateRangeByExpression( bool isUpper ) +{ + QgsExpressionContext expressionContext; + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), 1, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( 1 ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( 1 ), true, false, tr( "Band description" ) ) ); + + expressionContext.appendScope( bandScope ); + expressionContext.setHighlightedVariables( { QStringLiteral( "band" ), QStringLiteral( "band_name" ), QStringLiteral( "band_description" )} ); + + QgsExpressionBuilderDialog dlg = QgsExpressionBuilderDialog( nullptr, isUpper ? mFixedRangeUpperExpression : mFixedRangeLowerExpression, this, QStringLiteral( "generic" ), expressionContext ); + + QList > bandChoices; + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandChoices << qMakePair( mLayer->dataProvider()->displayBandName( band ), band ); + } + dlg.expressionBuilder()->setCustomPreviewGenerator( tr( "Band" ), bandChoices, [this]( const QVariant & value )-> QgsExpressionContext + { + return createExpressionContextForBand( value.toInt() ); + } ); + + if ( dlg.exec() ) + { + if ( isUpper ) + mFixedRangeUpperExpression = dlg.expressionText(); + else + mFixedRangeLowerExpression = dlg.expressionText(); + + QgsExpression exp( dlg.expressionText() ); + exp.prepare( &expressionContext ); + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( band ) ); + + const QVariant res = exp.evaluate( &expressionContext ); + mFixedRangePerBandModel->setData( mFixedRangePerBandModel->index( band - 1, isUpper ? 2 : 1 ), res, Qt::EditRole ); + } + } +} + +QgsExpressionContext QgsRasterElevationPropertiesWidget::createExpressionContextForBand( int band ) const +{ + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), band, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->displayBandName( band ) : QString(), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->bandDescription( band ) : QString(), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + context.setHighlightedVariables( { QStringLiteral( "band" ), QStringLiteral( "band_name" ), QStringLiteral( "band_description" )} ); + return context; +} + // // QgsRasterElevationPropertiesWidgetFactory @@ -162,3 +425,393 @@ QString QgsRasterElevationPropertiesWidgetFactory::layerPropertiesPagePositionHi return QStringLiteral( "mOptsPage_Metadata" ); } +// +// QgsRasterBandFixedElevationRangeModel +// + +QgsRasterBandFixedElevationRangeModel::QgsRasterBandFixedElevationRangeModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsRasterBandFixedElevationRangeModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsRasterBandFixedElevationRangeModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mBandCount; +} + +QModelIndex QgsRasterBandFixedElevationRangeModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsRasterBandFixedElevationRangeModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsRasterBandFixedElevationRangeModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case 0: + return Qt::ItemFlag::ItemIsEnabled; + case 1: + case 2: + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsRasterBandFixedElevationRangeModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const int band = index.row() + 1; + const QgsDoubleRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return mBandNames.value( band, QString::number( band ) ); + + case 1: + return range.lower() > std::numeric_limits< double >::lowest() ? range.lower() : QVariant(); + + case 2: + return range.upper() < std::numeric_limits< double >::max() ? range.upper() : QVariant(); + + default: + break; + } + break; + } + + case Qt::TextAlignmentRole: + { + switch ( index.column() ) + { + case 0: + return static_cast( Qt::AlignLeft | Qt::AlignVCenter ); + + case 1: + case 2: + return static_cast( Qt::AlignRight | Qt::AlignVCenter ); + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QVariant QgsRasterBandFixedElevationRangeModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole && orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "Band" ); + case 1: + return tr( "Lower" ); + case 2: + return tr( "Upper" ); + default: + break; + } + } + return QAbstractItemModel::headerData( section, orientation, role ); +} + +bool QgsRasterBandFixedElevationRangeModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + if ( index.row() > mBandCount || index.row() < 0 ) + return false; + + const int band = index.row() + 1; + const QgsDoubleRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::EditRole: + { + bool ok = false; + double newValue = value.toDouble( &ok ); + if ( !ok ) + return false; + + switch ( index.column() ) + { + case 1: + { + mRanges[band] = QgsDoubleRange( newValue, range.upper(), range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + } + + case 2: + mRanges[band] = QgsDoubleRange( range.lower(), newValue, range.includeLower(), range.includeUpper() ); + emit dataChanged( index, index, QVector() << role ); + break; + + default: + break; + } + return true; + } + + default: + break; + } + + return false; +} + +void QgsRasterBandFixedElevationRangeModel::setLayerData( QgsRasterLayer *layer, const QMap &ranges ) +{ + beginResetModel(); + + mBandCount = layer->bandCount(); + mRanges = ranges; + + mBandNames.clear(); + for ( int band = 1; band <= mBandCount; ++band ) + { + mBandNames[band] = layer->dataProvider()->displayBandName( band ); + } + + endResetModel(); +} + + +// +// QgsRasterBandDynamicElevationRangeModel +// + +QgsRasterBandDynamicElevationRangeModel::QgsRasterBandDynamicElevationRangeModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsRasterBandDynamicElevationRangeModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsRasterBandDynamicElevationRangeModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() || !mLayer ) + return 0; + return mLayer->bandCount(); +} + +QModelIndex QgsRasterBandDynamicElevationRangeModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsRasterBandDynamicElevationRangeModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsRasterBandDynamicElevationRangeModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() || !mLayer ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mLayer->bandCount() || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case 0: + case 1: + case 2: + + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable;; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsRasterBandDynamicElevationRangeModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() || !mLayer ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mLayer->bandCount() || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const int band = index.row() + 1; + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return mLayer->dataProvider() ? QVariant( mLayer->dataProvider()->displayBandName( band ) ) : QVariant( band ); + + case 1: + case 2: + { + const QString expressionString = index.column() == 1 ? mLowerExpression : mUpperExpression; + QgsExpression expression( expressionString ); + + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), band, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->displayBandName( band ) : QString(), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->bandDescription( band ) : QString(), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + + return expression.evaluate( &context ); + } + + default: + break; + } + break; + } + + case Qt::TextAlignmentRole: + { + switch ( index.column() ) + { + case 0: + return static_cast( Qt::AlignLeft | Qt::AlignVCenter ); + + case 1: + case 2: + return static_cast( Qt::AlignRight | Qt::AlignVCenter ); + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QVariant QgsRasterBandDynamicElevationRangeModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole && orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "Band" ); + case 1: + return tr( "Lower" ); + case 2: + return tr( "Upper" ); + default: + break; + } + } + return QAbstractItemModel::headerData( section, orientation, role ); +} + +void QgsRasterBandDynamicElevationRangeModel::setLayer( QgsRasterLayer *layer ) +{ + beginResetModel(); + mLayer = layer; + endResetModel(); +} + +void QgsRasterBandDynamicElevationRangeModel::setLowerExpression( const QString &expression ) +{ + mLowerExpression = expression; + emit dataChanged( index( 0, 1 ), index( rowCount() - 1, 1 ) ); +} + +void QgsRasterBandDynamicElevationRangeModel::setUpperExpression( const QString &expression ) +{ + mUpperExpression = expression; + emit dataChanged( index( 0, 2 ), index( rowCount() - 1, 2 ) ); +} + + +// +// QgsFixedElevationRangeDelegate +// + +QgsFixedElevationRangeDelegate::QgsFixedElevationRangeDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +QWidget *QgsFixedElevationRangeDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const +{ + QgsDoubleSpinBox *spin = new QgsDoubleSpinBox( parent ); + spin->setDecimals( 4 ); + spin->setMinimum( -9999999998.0 ); + spin->setMaximum( 9999999999.0 ); + spin->setShowClearButton( false ); + return spin; +} + +void QgsFixedElevationRangeDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + if ( QgsDoubleSpinBox *spin = qobject_cast< QgsDoubleSpinBox * >( editor ) ) + { + model->setData( index, spin->value() ); + } +} diff --git a/src/app/raster/qgsrasterelevationpropertieswidget.h b/src/app/raster/qgsrasterelevationpropertieswidget.h index c3d2b4dfebc9..c632b5ab8498 100644 --- a/src/app/raster/qgsrasterelevationpropertieswidget.h +++ b/src/app/raster/qgsrasterelevationpropertieswidget.h @@ -18,12 +18,79 @@ #include "qgsmaplayerconfigwidget.h" #include "qgsmaplayerconfigwidgetfactory.h" - +#include "qgsexpressioncontextgenerator.h" #include "ui_qgsrasterelevationpropertieswidgetbase.h" +#include +#include + class QgsRasterLayer; -class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, private Ui::QgsRasterElevationPropertiesWidgetBase +class QgsRasterBandFixedElevationRangeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + QgsRasterBandFixedElevationRangeModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + void setLayerData( QgsRasterLayer *layer, const QMap &ranges ); + QMap rangeData() const { return mRanges; } + + private: + + int mBandCount = 0; + QMap mBandNames; + QMap mRanges; +}; + +class QgsRasterBandDynamicElevationRangeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + QgsRasterBandDynamicElevationRangeModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + void setLayer( QgsRasterLayer *layer ); + void setLowerExpression( const QString &expression ); + void setUpperExpression( const QString &expression ); + private: + + QPointer< QgsRasterLayer > mLayer; + QString mLowerExpression; + QString mUpperExpression; +}; + +class QgsFixedElevationRangeDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + QgsFixedElevationRangeDelegate( QObject *parent ); + + protected: + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + +}; + +class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, public QgsExpressionContextGenerator, private Ui::QgsRasterElevationPropertiesWidgetBase { Q_OBJECT public: @@ -31,18 +98,27 @@ class QgsRasterElevationPropertiesWidget : public QgsMapLayerConfigWidget, priva QgsRasterElevationPropertiesWidget( QgsRasterLayer *layer, QgsMapCanvas *canvas, QWidget *parent ); void syncToLayer( QgsMapLayer *layer ) final; + QgsExpressionContext createExpressionContext() const final; public slots: void apply() override; private slots: + void modeChanged(); void onChanged(); + void calculateRangeByExpression( bool isUpper ); private: + QgsExpressionContext createExpressionContextForBand( int band ) const; + QgsRasterLayer *mLayer = nullptr; bool mBlockUpdates = false; + QgsRasterBandFixedElevationRangeModel *mFixedRangePerBandModel = nullptr; + QgsRasterBandDynamicElevationRangeModel *mDynamicRangePerBandModel = nullptr; + QString mFixedRangeLowerExpression = QStringLiteral( "@band" ); + QString mFixedRangeUpperExpression = QStringLiteral( "@band" ); }; 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/auth/identcert/core/qgsauthidentcertmethod.cpp b/src/auth/identcert/core/qgsauthidentcertmethod.cpp index 00d0dd6b225e..8f8d2d3e56ef 100644 --- a/src/auth/identcert/core/qgsauthidentcertmethod.cpp +++ b/src/auth/identcert/core/qgsauthidentcertmethod.cpp @@ -40,7 +40,9 @@ const QString QgsAuthIdentCertMethod::AUTH_METHOD_KEY = QStringLiteral( "Identit const QString QgsAuthIdentCertMethod::AUTH_METHOD_DESCRIPTION = QStringLiteral( "Identity certificate authentication" ); const QString QgsAuthIdentCertMethod::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "Identity certificate authentication" ); +#ifndef QT_NO_SSL QMap QgsAuthIdentCertMethod::sPkiConfigBundleCache = QMap(); +#endif QgsAuthIdentCertMethod::QgsAuthIdentCertMethod() @@ -57,9 +59,11 @@ QgsAuthIdentCertMethod::QgsAuthIdentCertMethod() QgsAuthIdentCertMethod::~QgsAuthIdentCertMethod() { +#ifndef QT_NO_SSL const QMutexLocker locker( &mMutex ); qDeleteAll( sPkiConfigBundleCache ); sPkiConfigBundleCache.clear(); +#endif } QString QgsAuthIdentCertMethod::key() const @@ -80,6 +84,7 @@ QString QgsAuthIdentCertMethod::displayDescription() const bool QgsAuthIdentCertMethod::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -110,11 +115,15 @@ bool QgsAuthIdentCertMethod::updateNetworkRequest( QNetworkRequest &request, con request.setSslConfiguration( sslConfig ); return true; +#else + return false; +#endif } bool QgsAuthIdentCertMethod::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -210,6 +219,9 @@ bool QgsAuthIdentCertMethod::updateDataSourceUriItems( QStringList &connectionIt } return true; +#else + return false; +#endif } void QgsAuthIdentCertMethod::clearCachedConfig( const QString &authcfg ) @@ -232,6 +244,7 @@ void QgsAuthIdentCertMethod::updateMethodConfig( QgsAuthMethodConfig &mconfig ) // TODO: add updates as method version() increases due to config storage changes } +#ifndef QT_NO_SSL QgsPkiConfigBundle *QgsAuthIdentCertMethod::getPkiConfigBundle( const QString &authcfg ) { const QMutexLocker locker( &mMutex ); @@ -303,6 +316,7 @@ void QgsAuthIdentCertMethod::removePkiConfigBundle( const QString &authcfg ) QgsDebugMsgLevel( QStringLiteral( "Removed PKI bundle for authcfg: %1" ).arg( authcfg ), 2 ); } } +#endif #ifdef HAVE_GUI QWidget *QgsAuthIdentCertMethod::editWidget( QWidget *parent ) const diff --git a/src/auth/identcert/core/qgsauthidentcertmethod.h b/src/auth/identcert/core/qgsauthidentcertmethod.h index ba0abebdd243..b61429bb8669 100644 --- a/src/auth/identcert/core/qgsauthidentcertmethod.h +++ b/src/auth/identcert/core/qgsauthidentcertmethod.h @@ -61,6 +61,7 @@ class QgsAuthIdentCertMethod : public QgsAuthMethod private: +#ifndef QT_NO_SSL QgsPkiConfigBundle *getPkiConfigBundle( const QString &authcfg ); void putPkiConfigBundle( const QString &authcfg, QgsPkiConfigBundle *pkibundle ); @@ -68,6 +69,7 @@ class QgsAuthIdentCertMethod : public QgsAuthMethod void removePkiConfigBundle( const QString &authcfg ); static QMap sPkiConfigBundleCache; +#endif }; diff --git a/src/auth/oauth2/CMakeLists.txt b/src/auth/oauth2/CMakeLists.txt index a9e2cfeddc36..15b3fb2588dc 100644 --- a/src/auth/oauth2/CMakeLists.txt +++ b/src/auth/oauth2/CMakeLists.txt @@ -104,6 +104,7 @@ target_include_directories(authmethod_oauth2_a PUBLIC ${CMAKE_SOURCE_DIR}/src/au # require c++17 target_compile_features(authmethod_oauth2_a PRIVATE cxx_std_17) +target_compile_definitions(authmethod_oauth2_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(authmethod_oauth2_a qgis_core) @@ -146,6 +147,7 @@ else() # require c++17 target_compile_features(authmethod_oauth2 PRIVATE cxx_std_17) + target_compile_definitions(authmethod_oauth2 PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(authmethod_oauth2 qgis_core) diff --git a/src/auth/oauth2/core/qgso2.cpp b/src/auth/oauth2/core/qgso2.cpp index d571d7b1b5c6..e467741a687d 100644 --- a/src/auth/oauth2/core/qgso2.cpp +++ b/src/auth/oauth2/core/qgso2.cpp @@ -21,6 +21,7 @@ #include "qgsauthoauth2config.h" #include "qgslogger.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include diff --git a/src/auth/oauth2/gui/qgsauthoauth2edit.cpp b/src/auth/oauth2/gui/qgsauthoauth2edit.cpp index 024d3ee88bc3..f215722c0b44 100644 --- a/src/auth/oauth2/gui/qgsauthoauth2edit.cpp +++ b/src/auth/oauth2/gui/qgsauthoauth2edit.cpp @@ -28,6 +28,7 @@ #include "qgsauthconfigedit.h" #include "qgsmessagelog.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" QgsAuthOAuth2Edit::QgsAuthOAuth2Edit( QWidget *parent ) : QgsAuthMethodEdit( parent ) diff --git a/src/auth/pkipaths/core/qgsauthpkipathsmethod.cpp b/src/auth/pkipaths/core/qgsauthpkipathsmethod.cpp index 063f9b98dec6..1e4bb6e33647 100644 --- a/src/auth/pkipaths/core/qgsauthpkipathsmethod.cpp +++ b/src/auth/pkipaths/core/qgsauthpkipathsmethod.cpp @@ -39,7 +39,9 @@ const QString QgsAuthPkiPathsMethod::AUTH_METHOD_KEY = QStringLiteral( "PKI-Path const QString QgsAuthPkiPathsMethod::AUTH_METHOD_DESCRIPTION = QStringLiteral( "PKI paths authentication" ); const QString QgsAuthPkiPathsMethod::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "PKI paths authentication" ); +#ifndef QT_NO_SSL QMap QgsAuthPkiPathsMethod::sPkiConfigBundleCache = QMap(); +#endif QgsAuthPkiPathsMethod::QgsAuthPkiPathsMethod() @@ -56,9 +58,11 @@ QgsAuthPkiPathsMethod::QgsAuthPkiPathsMethod() QgsAuthPkiPathsMethod::~QgsAuthPkiPathsMethod() { +#ifndef QT_NO_SSL const QMutexLocker locker( &mMutex ); qDeleteAll( sPkiConfigBundleCache ); sPkiConfigBundleCache.clear(); +#endif } QString QgsAuthPkiPathsMethod::key() const @@ -80,6 +84,7 @@ QString QgsAuthPkiPathsMethod::displayDescription() const bool QgsAuthPkiPathsMethod::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -122,12 +127,16 @@ bool QgsAuthPkiPathsMethod::updateNetworkRequest( QNetworkRequest &request, cons request.setSslConfiguration( sslConfig ); return true; +#else + return false; +#endif } bool QgsAuthPkiPathsMethod::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -243,12 +252,17 @@ bool QgsAuthPkiPathsMethod::updateDataSourceUriItems( QStringList &connectionIte } return true; +#else + return false; +#endif } void QgsAuthPkiPathsMethod::clearCachedConfig( const QString &authcfg ) { +#ifndef QT_NO_SSL const QMutexLocker locker( &mMutex ); removePkiConfigBundle( authcfg ); +#endif } void QgsAuthPkiPathsMethod::updateMethodConfig( QgsAuthMethodConfig &mconfig ) @@ -268,6 +282,7 @@ void QgsAuthPkiPathsMethod::updateMethodConfig( QgsAuthMethodConfig &mconfig ) // TODO: add updates as method version() increases due to config storage changes } +#ifndef QT_NO_SSL QgsPkiConfigBundle *QgsAuthPkiPathsMethod::getPkiConfigBundle( const QString &authcfg ) { const QMutexLocker locker( &mMutex ); @@ -337,6 +352,7 @@ void QgsAuthPkiPathsMethod::removePkiConfigBundle( const QString &authcfg ) QgsDebugMsgLevel( QStringLiteral( "Removed PKI bundle for authcfg: %1" ).arg( authcfg ), 2 ); } } +#endif #ifdef HAVE_GUI QWidget *QgsAuthPkiPathsMethod::editWidget( QWidget *parent ) const diff --git a/src/auth/pkipaths/core/qgsauthpkipathsmethod.h b/src/auth/pkipaths/core/qgsauthpkipathsmethod.h index 5c4352aad320..d44fe644f80e 100644 --- a/src/auth/pkipaths/core/qgsauthpkipathsmethod.h +++ b/src/auth/pkipaths/core/qgsauthpkipathsmethod.h @@ -61,6 +61,7 @@ class QgsAuthPkiPathsMethod : public QgsAuthMethod private: +#ifndef QT_NO_SSL QgsPkiConfigBundle *getPkiConfigBundle( const QString &authcfg ); void putPkiConfigBundle( const QString &authcfg, QgsPkiConfigBundle *pkibundle ); @@ -68,6 +69,7 @@ class QgsAuthPkiPathsMethod : public QgsAuthMethod void removePkiConfigBundle( const QString &authcfg ); static QMap sPkiConfigBundleCache; +#endif }; diff --git a/src/auth/pkipkcs12/core/qgsauthpkcs12method.cpp b/src/auth/pkipkcs12/core/qgsauthpkcs12method.cpp index b9b84449937d..a0930f86939d 100644 --- a/src/auth/pkipkcs12/core/qgsauthpkcs12method.cpp +++ b/src/auth/pkipkcs12/core/qgsauthpkcs12method.cpp @@ -39,7 +39,9 @@ const QString QgsAuthPkcs12Method::AUTH_METHOD_KEY = QStringLiteral( "PKI-PKCS#1 const QString QgsAuthPkcs12Method::AUTH_METHOD_DESCRIPTION = QStringLiteral( "PKI PKCS#12 authentication" ); const QString QgsAuthPkcs12Method::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "PKI PKCS#12 authentication" ); +#ifndef QT_NO_SSL QMap QgsAuthPkcs12Method::sPkiConfigBundleCache = QMap(); +#endif QgsAuthPkcs12Method::QgsAuthPkcs12Method() @@ -56,8 +58,10 @@ QgsAuthPkcs12Method::QgsAuthPkcs12Method() QgsAuthPkcs12Method::~QgsAuthPkcs12Method() { +#ifndef QT_NO_SSL qDeleteAll( sPkiConfigBundleCache ); sPkiConfigBundleCache.clear(); +#endif } QString QgsAuthPkcs12Method::key() const @@ -78,6 +82,7 @@ QString QgsAuthPkcs12Method::displayDescription() const bool QgsAuthPkcs12Method::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -121,11 +126,15 @@ bool QgsAuthPkcs12Method::updateNetworkRequest( QNetworkRequest &request, const request.setSslConfiguration( sslConfig ); return true; +#else + return false; +#endif } bool QgsAuthPkcs12Method::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg, const QString &dataprovider ) { +#ifndef QT_NO_SSL Q_UNUSED( dataprovider ) const QMutexLocker locker( &mMutex ); @@ -137,6 +146,7 @@ bool QgsAuthPkcs12Method::updateDataSourceUriItems( QStringList &connectionItems QgsDebugError( QStringLiteral( "Update URI items FAILED: PKI bundle invalid" ) ); return false; } + QgsDebugMsgLevel( QStringLiteral( "Update URI items: PKI bundle valid" ), 2 ); const QString pkiTempFileBase = QStringLiteral( "tmppki_%1.pem" ); @@ -241,12 +251,17 @@ bool QgsAuthPkcs12Method::updateDataSourceUriItems( QStringList &connectionItems } return true; +#else + return false; +#endif } void QgsAuthPkcs12Method::clearCachedConfig( const QString &authcfg ) { +#ifndef QT_NO_SSL const QMutexLocker locker( &mMutex ); removePkiConfigBundle( authcfg ); +#endif } void QgsAuthPkcs12Method::updateMethodConfig( QgsAuthMethodConfig &mconfig ) @@ -265,6 +280,7 @@ void QgsAuthPkcs12Method::updateMethodConfig( QgsAuthMethodConfig &mconfig ) // TODO: add updates as method version() increases due to config storage changes } +#ifndef QT_NO_SSL QgsPkiConfigBundle *QgsAuthPkcs12Method::getPkiConfigBundle( const QString &authcfg ) { QMutexLocker locker( &mMutex ); @@ -357,6 +373,7 @@ void QgsAuthPkcs12Method::removePkiConfigBundle( const QString &authcfg ) QgsDebugMsgLevel( QStringLiteral( "Removed PKI bundle for authcfg: %1" ).arg( authcfg ), 2 ); } } +#endif #ifdef HAVE_GUI QWidget *QgsAuthPkcs12Method::editWidget( QWidget *parent ) const diff --git a/src/auth/pkipkcs12/core/qgsauthpkcs12method.h b/src/auth/pkipkcs12/core/qgsauthpkcs12method.h index 9c0614b12d5e..56aa887566df 100644 --- a/src/auth/pkipkcs12/core/qgsauthpkcs12method.h +++ b/src/auth/pkipkcs12/core/qgsauthpkcs12method.h @@ -62,6 +62,7 @@ class QgsAuthPkcs12Method : public QgsAuthMethod private: +#ifndef QT_NO_SSL QgsPkiConfigBundle *getPkiConfigBundle( const QString &authcfg ); void putPkiConfigBundle( const QString &authcfg, QgsPkiConfigBundle *pkibundle ); @@ -69,6 +70,7 @@ class QgsAuthPkcs12Method : public QgsAuthMethod void removePkiConfigBundle( const QString &authcfg ); static QMap sPkiConfigBundleCache; +#endif }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 289b6d816777..c2a907fd1067 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 @@ -1861,6 +1862,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 @@ -2460,7 +2462,7 @@ target_link_libraries(qgis_core EXPAT::EXPAT ${SQLITE3_LIBRARY} ${LIBZIP_LIBRARY} - ${Protobuf_LITE_LIBRARY} + $ ${ZLIB_LIBRARIES} ${EXIV2_LIBRARY} PROJ::proj @@ -2573,6 +2575,7 @@ if (${QT_VERSION_BASE}Positioning_FOUND) endif() target_compile_definitions(qgis_core PRIVATE "-DQT_NO_FOREACH") +target_compile_definitions(qgis_core PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") # clang-tidy if(CLANG_TIDY_EXE) diff --git a/src/core/auth/qgsauthcertutils.cpp b/src/core/auth/qgsauthcertutils.cpp index 0d36f14da40c..6849f790f2ba 100644 --- a/src/core/auth/qgsauthcertutils.cpp +++ b/src/core/auth/qgsauthcertutils.cpp @@ -1107,7 +1107,7 @@ bool QgsAuthCertUtils::certificateIsSslServer( const QSslCertificate &cert ) bool decipheronly = false; QCA::PublicKey pubkey( qcacert.subjectPublicKey() ); - // key size may be 0 for eliptical curve-based keys, in which case isDH() crashes QCA + // key size may be 0 for elliptical curve-based keys, in which case isDH() crashes QCA if ( pubkey.bitSize() > 0 && pubkey.isDH() ) { keyagree = pubkey.canKeyAgree(); diff --git a/src/core/auth/qgsauthmanager.h b/src/core/auth/qgsauthmanager.h index 4ae2181a744a..41b311448815 100644 --- a/src/core/auth/qgsauthmanager.h +++ b/src/core/auth/qgsauthmanager.h @@ -168,7 +168,7 @@ class CORE_EXPORT QgsAuthManager : public QObject /** * Reset the master password to a new one, then re-encrypt all previous - * configs in a new database file, optionally backup curren database + * configs in a new database file, optionally backup current database * \param newpass New master password to replace existing * \param oldpass Current master password to replace existing * \param keepbackup Whether to keep the generated backup of current database diff --git a/src/core/browser/qgsdirectoryitem.cpp b/src/core/browser/qgsdirectoryitem.cpp index 16e669fe7850..aba67f0fcf4a 100644 --- a/src/core/browser/qgsdirectoryitem.cpp +++ b/src/core/browser/qgsdirectoryitem.cpp @@ -58,6 +58,10 @@ void QgsDirectoryItem::init( const QString &dirName ) QgsSettings settings; + const QFileInfo fi { mDirPath }; + mIsDir = fi.isDir(); + mIsSymLink = fi.isSymLink(); + mMonitoring = monitoringForPath( mDirPath ); switch ( mMonitoring ) { @@ -164,8 +168,7 @@ QIcon QgsDirectoryItem::icon() return QgsDataItem::icon(); // symbolic link? use link icon - const QFileInfo fi( mDirPath ); - if ( fi.isDir() && fi.isSymLink() ) + if ( mIsDir && mIsSymLink ) { return mIconColor.isValid() ? QgsApplication::getThemeIcon( QStringLiteral( "/mIconFolderLinkParams.svg" ), mIconColor, mIconColor.darker() ) @@ -451,7 +454,9 @@ bool QgsDirectoryItem::pathShouldByMonitoredByDefault( const QString &path ) // else if we know that the path is on a slow device, we don't monitor by default // as this can be very expensive and slow down QGIS - if ( QgsFileUtils::pathIsSlowDevice( path ) ) + // Add trailing slash or windows API functions like GetDriveTypeW won't identify + // UNC network drives correctly + if ( QgsFileUtils::pathIsSlowDevice( path.endsWith( '/' ) ? path : path + '/' ) ) return false; // paths are monitored by default if no explicit setting is in place, and the user hasn't @@ -465,7 +470,7 @@ void QgsDirectoryItem::childrenCreated() if ( mRefreshLater ) { - QgsDebugMsgLevel( QStringLiteral( "directory changed during createChidren() -> refresh() again" ), 3 ); + QgsDebugMsgLevel( QStringLiteral( "directory changed during createChildren() -> refresh() again" ), 3 ); mRefreshLater = false; setState( Qgis::BrowserItemState::Populated ); refresh(); diff --git a/src/core/browser/qgsdirectoryitem.h b/src/core/browser/qgsdirectoryitem.h index eda500f2320e..3e226c6ad914 100644 --- a/src/core/browser/qgsdirectoryitem.h +++ b/src/core/browser/qgsdirectoryitem.h @@ -216,6 +216,9 @@ class CORE_EXPORT QgsDirectoryItem : public QgsDataCollectionItem QDateTime mLastScan; QColor mIconColor; + bool mIsDir = false; + bool mIsSymLink = false; + friend class TestQgsDataItem; }; diff --git a/src/core/dxf/qgsdxfexport.cpp b/src/core/dxf/qgsdxfexport.cpp index b27647705bfd..96b920c0d7d4 100644 --- a/src/core/dxf/qgsdxfexport.cpp +++ b/src/core/dxf/qgsdxfexport.cpp @@ -37,6 +37,7 @@ #include "qgsproject.h" #include "qgsrenderer.h" #include "qgssymbollayer.h" +#include "qgssymbollayerutils.h" #include "qgsfeatureiterator.h" #include "qgslinesymbollayer.h" #include "qgsvectorlayer.h" @@ -95,6 +96,10 @@ void QgsDxfExport::addLayers( const QList &layers ) mLayerList << dxfLayer.layer(); if ( dxfLayer.layerOutputAttributeIndex() >= 0 ) mLayerNameAttribute.insert( dxfLayer.layer()->id(), dxfLayer.layerOutputAttributeIndex() ); + if ( dxfLayer.buildDataDefinedBlocks() ) + { + mLayerDDBlockMaxNumberOfClasses.insert( dxfLayer.layer()->id(), dxfLayer.dataDefinedBlocksMaximumNumberOfClasses() ); + } } } @@ -258,7 +263,7 @@ QgsDxfExport::ExportResult QgsDxfExport::writeToFile( QIODevice *d, const QStrin const QgsRectangle extentRect = mMapSettings.mapToLayerCoordinates( vl, mExtent ); request.setFilterRect( extentRect ); } - QgsFeatureIterator featureIt = vl->getFeatures( request ); + QgsFeatureIterator featureIt = ( mFlags & FlagOnlySelectedFeatures ) ? vl->getSelectedFeatures( request ) : vl->getFeatures( request ); QgsFeature feature; if ( featureIt.nextFeature( feature ) ) { @@ -278,6 +283,7 @@ QgsDxfExport::ExportResult QgsDxfExport::writeToFile( QIODevice *d, const QStrin writeHeader( dxfEncoding( encoding ) ); prepareRenderers(); + createDDBlockInfo(); writeTables(); writeBlocks(); writeEntities(); @@ -286,7 +292,7 @@ QgsDxfExport::ExportResult QgsDxfExport::writeToFile( QIODevice *d, const QStrin if ( !skippedLayers.isEmpty() ) { - mFeedbackMessage = QObject::tr( "The following empty layers were skipped: %1" ).arg( skippedLayers.join( QStringLiteral( ", " ) ) ); + mFeedbackMessage = QObject::tr( "The following empty layers were skipped: %1" ).arg( skippedLayers.join( QLatin1String( ", " ) ) ); } return ExportResult::Success; @@ -417,14 +423,25 @@ void QgsDxfExport::writeTables() continue; if ( hasBlockBreakingDataDefinedProperties( ml, symbolLayer.second ) ) + { + //reference to symbology class from mDataDefinedBlockInfo? + if ( !mDataDefinedBlockInfo.contains( ml ) ) + { + continue; + } + + const QHash &symbolClasses = mDataDefinedBlockInfo[ml]; + for ( const auto &blockInfo : symbolClasses ) + { + writeSymbolTableBlockRef( blockInfo.blockName ); + } + + ++i; continue; + } QString name = QStringLiteral( "symbolLayer%1" ).arg( i++ ); - writeGroup( 0, QStringLiteral( "BLOCK_RECORD" ) ); - mBlockHandles.insert( name, writeHandle() ); - writeGroup( 100, QStringLiteral( "AcDbSymbolTableRecord" ) ); - writeGroup( 100, QStringLiteral( "AcDbBlockTableRecord" ) ); - writeGroup( 2, name ); + writeSymbolTableBlockRef( name ); } writeGroup( 0, QStringLiteral( "ENDTAB" ) ); @@ -599,6 +616,15 @@ void QgsDxfExport::writeTables() endSection(); } +void QgsDxfExport::writeSymbolTableBlockRef( const QString &blockName ) +{ + writeGroup( 0, QStringLiteral( "BLOCK_RECORD" ) ); + mBlockHandles.insert( blockName, writeHandle() ); + writeGroup( 100, QStringLiteral( "AcDbSymbolTableRecord" ) ); + writeGroup( 100, QStringLiteral( "AcDbBlockTableRecord" ) ); + writeGroup( 2, blockName ); +} + void QgsDxfExport::writeBlocks() { startSection(); @@ -653,37 +679,26 @@ void QgsDxfExport::writeBlocks() // markers with data defined properties are inserted inline if ( hasBlockBreakingDataDefinedProperties( ml, symbolLayer.second ) ) { + if ( !mDataDefinedBlockInfo.contains( ml ) ) + { + continue; + } + + //Check if there is an entry for the symbol layer in mDataDefinedBlockInfo + const QHash &symbolClasses = mDataDefinedBlockInfo[ml]; + for ( const auto &blockInfo : symbolClasses ) + { + ctx.setFeature( &blockInfo.feature ); + ctx.renderContext().expressionContext().setFeature( blockInfo.feature ); + writeSymbolLayerBlock( blockInfo.blockName, ml, ctx ); + } + + ++mBlockCounter; continue; } QString block( QStringLiteral( "symbolLayer%1" ).arg( mBlockCounter++ ) ); - mBlockHandle = QString::number( mBlockHandles[ block ], 16 ); - - writeGroup( 0, QStringLiteral( "BLOCK" ) ); - writeHandle(); - writeGroup( 330, mBlockHandle ); - writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); - writeGroup( 8, QStringLiteral( "0" ) ); - writeGroup( 100, QStringLiteral( "AcDbBlockBegin" ) ); - writeGroup( 2, block ); - writeGroup( 70, 0 ); - - // x/y/z coordinates of reference point - // todo: consider anchor point - // double size = ml->size(); - // size *= mapUnitScaleFactor( mSymbologyScale, ml->sizeUnit(), mMapUnits ); - writeGroup( 0, QgsPoint( Qgis::WkbType::PointZ, 0.0, 0.0, 0.0 ) ); - writeGroup( 3, block ); - writeGroup( 1, QString() ); - - // maplayer 0 -> block receives layer from INSERT statement - ml->writeDxf( *this, mapUnitScaleFactor( mSymbologyScale, ml->sizeUnit(), mMapUnits, ctx.renderContext().mapToPixel().mapUnitsPerPixel() ), QStringLiteral( "0" ), ctx ); - - writeGroup( 0, QStringLiteral( "ENDBLK" ) ); - writeHandle(); - writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); - writeGroup( 8, QStringLiteral( "0" ) ); - writeGroup( 100, QStringLiteral( "AcDbBlockEnd" ) ); + writeSymbolLayerBlock( block, ml, ctx ); mPointSymbolBlocks.insert( ml, block ); mPointSymbolBlockSizes.insert( ml, ml->dxfSize( *this, ctx ) ); @@ -692,6 +707,36 @@ void QgsDxfExport::writeBlocks() endSection(); } +void QgsDxfExport::writeSymbolLayerBlock( const QString &blockName, const QgsMarkerSymbolLayer *ml, QgsSymbolRenderContext &ctx ) +{ + mBlockHandle = QString::number( mBlockHandles[ blockName ], 16 ); + writeGroup( 0, QStringLiteral( "BLOCK" ) ); + writeHandle(); + writeGroup( 330, mBlockHandle ); + writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); + writeGroup( 8, QStringLiteral( "0" ) ); + writeGroup( 100, QStringLiteral( "AcDbBlockBegin" ) ); + writeGroup( 2, blockName ); + writeGroup( 70, 0 ); + + // x/y/z coordinates of reference point + // todo: consider anchor point + // double size = ml->size(); + // size *= mapUnitScaleFactor( mSymbologyScale, ml->sizeUnit(), mMapUnits ); + writeGroup( 0, QgsPoint( Qgis::WkbType::PointZ, 0.0, 0.0, 0.0 ) ); + writeGroup( 3, blockName ); + + writeGroup( 1, QString() ); + + // maplayer 0 -> block receives layer from INSERT statement + ml->writeDxf( *this, mapUnitScaleFactor( mSymbologyScale, ml->sizeUnit(), mMapUnits, ctx.renderContext().mapToPixel().mapUnitsPerPixel() ), QStringLiteral( "0" ), ctx ); + + writeGroup( 0, QStringLiteral( "ENDBLK" ) ); + writeHandle(); + writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); + writeGroup( 8, QStringLiteral( "0" ) ); + writeGroup( 100, QStringLiteral( "AcDbBlockEnd" ) ); +} void QgsDxfExport::writeEntities() { @@ -720,6 +765,10 @@ void QgsDxfExport::writeEntities() QgsCoordinateTransform extentTransform = ct; extentTransform.setBallparkTransformsAreAppropriate( true ); request.setFilterRect( extentTransform.transformBoundingBox( mMapSettings.extent(), Qgis::TransformDirection::Reverse ) ); + if ( mFlags & FlagOnlySelectedFeatures ) + { + request.setFilterFids( job->selectedFeatureIds ); + } QgsFeatureIterator featureIt = job->featureSource.getFeatures( request ); @@ -882,6 +931,10 @@ void QgsDxfExport::writeEntitiesSymbolLevels( DxfLayerJob *job ) QgsDebugError( QStringLiteral( "QgsDxfExport::writeEntitiesSymbolLevels(): extent reprojection failed" ) ); return; } + if ( mFlags & FlagOnlySelectedFeatures ) + { + req.setFilterFids( job->selectedFeatureIds ); + } QgsFeatureIterator fit = job->featureSource.getFeatures( req ); @@ -986,40 +1039,88 @@ void QgsDxfExport::writePoint( const QgsPoint &pt, const QString &layer, const Q } #endif // 0 - // insert block or write point directly? + //there is a global block for the point layer QHash< const QgsSymbolLayer *, QString >::const_iterator blockIt = mPointSymbolBlocks.constFind( symbolLayer ); - if ( !symbolLayer || blockIt == mPointSymbolBlocks.constEnd() ) + if ( symbolLayer && blockIt != mPointSymbolBlocks.constEnd() ) + { + writePointBlockReference( pt, symbolLayer, ctx, layer, angle, blockIt.value(), mPointSymbolBlockAngles.value( symbolLayer ), mPointSymbolBlockSizes.value( symbolLayer ) ); + return; + } + + //If there is a data defined block for the point layer, check if the feature falls into a data defined category + QHash< const QgsSymbolLayer *, QHash >::const_iterator ddBlockIt = mDataDefinedBlockInfo.constFind( symbolLayer ); + if ( symbolLayer && ctx.feature() && ddBlockIt != mDataDefinedBlockInfo.constEnd() ) { - // write symbol directly here - const QgsMarkerSymbolLayer *msl = dynamic_cast< const QgsMarkerSymbolLayer * >( symbolLayer ); - if ( msl && symbol ) + const QHash &symbolLayerDDBlocks = ddBlockIt.value(); + + QgsPropertyCollection props = symbolLayer->dataDefinedProperties(); + + uint ddSymbolHash = dataDefinedSymbolClassHash( *( ctx.feature() ), props ); + if ( symbolLayerDDBlocks.contains( ddSymbolHash ) ) { - if ( msl->writeDxf( *this, mapUnitScaleFactor( mSymbologyScale, msl->sizeUnit(), mMapUnits, ctx.renderContext().mapToPixel().mapUnitsPerPixel() ), layer, ctx, QPointF( pt.x(), pt.y() ) ) ) - { - return; - } + const DataDefinedBlockInfo &info = symbolLayerDDBlocks[ddSymbolHash]; + writePointBlockReference( pt, symbolLayer, ctx, layer, angle, info.blockName, info.angle, info.size ); + return; } - writePoint( layer, color, pt ); // write default point symbol } - else + + //no block has been created for the symbol. Write it directly here + const QgsMarkerSymbolLayer *msl = dynamic_cast< const QgsMarkerSymbolLayer * >( symbolLayer ); + if ( msl && symbol ) + { + if ( msl->writeDxf( *this, mapUnitScaleFactor( mSymbologyScale, msl->sizeUnit(), mMapUnits, ctx.renderContext().mapToPixel().mapUnitsPerPixel() ), layer, ctx, QPointF( pt.x(), pt.y() ) ) ) + { + return; + } + } + writePoint( layer, color, pt ); // write default point symbol +} + +void QgsDxfExport::writePointBlockReference( const QgsPoint &pt, const QgsSymbolLayer *symbolLayer, QgsSymbolRenderContext &ctx, const QString &layer, double angle, const QString &blockName, double blockAngle, double blockSize ) +{ + const double scale = symbolLayer->dxfSize( *this, ctx ) / blockSize; + + // insert block reference + writeGroup( 0, QStringLiteral( "INSERT" ) ); + writeHandle(); + writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); + writeGroup( 100, QStringLiteral( "AcDbBlockReference" ) ); + writeGroup( 8, layer ); + writeGroup( 2, blockName ); // Block name + writeGroup( 50, blockAngle - angle ); + if ( std::isfinite( scale ) && scale != 1.0 ) { - const double scale = symbolLayer->dxfSize( *this, ctx ) / mPointSymbolBlockSizes.value( symbolLayer ); + writeGroup( 41, scale ); + writeGroup( 42, scale ); + } + writeGroup( 0, pt ); // Insertion point (in OCS) +} - // insert block reference - writeGroup( 0, QStringLiteral( "INSERT" ) ); - writeHandle(); - writeGroup( 100, QStringLiteral( "AcDbEntity" ) ); - writeGroup( 100, QStringLiteral( "AcDbBlockReference" ) ); - writeGroup( 8, layer ); - writeGroup( 2, blockIt.value() ); // Block name - writeGroup( 50, mPointSymbolBlockAngles.value( symbolLayer ) - angle ); - if ( std::isfinite( scale ) && scale != 1.0 ) +uint QgsDxfExport::dataDefinedSymbolClassHash( const QgsFeature &fet, const QgsPropertyCollection &prop ) +{ + uint hashValue = 0; + + QgsPropertyCollection dxfProp = prop; + dxfProp.setProperty( QgsSymbolLayer::Property::Size, QgsProperty() ); + dxfProp.setProperty( QgsSymbolLayer::Property::Angle, QgsProperty() ); + QList< QString > fields = dxfProp.referencedFields().values(); + std::sort( fields.begin(), fields.end() ); + int i = 0; + for ( const auto &field : std::as_const( fields ) ) //convert set to list to have a well defined order + { + QVariant attValue = fet.attribute( field ); + if ( i == 0 ) + { + hashValue = qHash( attValue ); + } + else { - writeGroup( 41, scale ); - writeGroup( 42, scale ); + hashValue = hashValue ^ qHash( attValue ); } - writeGroup( 0, pt ); // Insertion point (in OCS) + ++i; } + + return hashValue; } void QgsDxfExport::writePolyline( const QgsPointSequence &line, const QString &layer, const QString &lineStyleName, const QColor &color, double width ) @@ -1775,7 +1876,6 @@ void QgsDxfExport::addFeature( QgsSymbolRenderContext &ctx, const QgsCoordinateT if ( brushStyle != Qt::NoBrush ) { const QgsAbstractGeometry *sourceGeom = geom.constGet(); - std::unique_ptr< QgsAbstractGeometry > tempGeom; switch ( QgsWkbTypes::flatType( geometryType ) ) { @@ -2473,3 +2573,113 @@ QString QgsDxfExport::DxfLayer::splitLayerAttribute() const return splitLayerFieldName; } + +void QgsDxfExport::createDDBlockInfo() +{ + int symbolLayerNr = 0; + for ( DxfLayerJob *job : std::as_const( mJobs ) ) + { + int ddMaxNumberOfClasses = -1; + bool createDDBlocks = mLayerDDBlockMaxNumberOfClasses.contains( job->featureSource.id() ); + if ( createDDBlocks ) + { + ddMaxNumberOfClasses = mLayerDDBlockMaxNumberOfClasses[job->featureSource.id()]; + } + else + { + continue; + } + + const QgsSymbolList symbols = job->renderer->symbols( mRenderContext ); + + for ( const QgsSymbol *symbol : symbols ) + { + //Create blocks only for marker symbols + if ( symbol->type() != Qgis::SymbolType::Marker ) + { + continue; + } + + int maxSymbolLayers = symbol->symbolLayerCount(); + if ( mSymbologyExport != Qgis::FeatureSymbologyExport::PerSymbolLayer ) + { + maxSymbolLayers = 1; + } + + for ( int i = 0; i < maxSymbolLayers; ++i ) + { + + const QgsSymbolLayer *sl = symbol->symbolLayer( i ); + if ( !sl ) + { + continue; + } + QgsPropertyCollection properties = sl->dataDefinedProperties(); + + if ( !hasBlockBreakingDataDefinedProperties( sl, symbol ) || !createDDBlocks ) + { + ++symbolLayerNr; + continue; + } + + //iterate layer, evaluate value and get symbology hash groups + QgsSymbolRenderContext sctx( mRenderContext, Qgis::RenderUnit::Millimeters, 1.0, false, Qgis::SymbolRenderHints(), nullptr ); + const QgsCoordinateTransform ct( job->crs, mMapSettings.destinationCrs(), mMapSettings.transformContext() ); + QgsFeatureRequest request = QgsFeatureRequest().setSubsetOfAttributes( job->attributes, job->fields ).setFlags( Qgis::FeatureRequestFlag::NoGeometry ).setExpressionContext( job->renderContext.expressionContext() ); + QgsCoordinateTransform extentTransform = ct; + extentTransform.setBallparkTransformsAreAppropriate( true ); + request.setFilterRect( extentTransform.transformBoundingBox( mExtent, Qgis::TransformDirection::Reverse ) ); + QgsFeatureIterator featureIt = job->featureSource.getFeatures( request ); + + QHash > blockSymbolMap; //symbolHash/occurrences/block Text + + QgsFeature fet; + while ( featureIt.nextFeature( fet ) ) + { + uint symbolHash = dataDefinedSymbolClassHash( fet, properties ); + if ( blockSymbolMap.contains( symbolHash ) ) + { + blockSymbolMap[symbolHash].first += 1; + continue; + } + + sctx.setFeature( &fet ); + + DataDefinedBlockInfo blockInfo; + blockInfo.blockName = QStringLiteral( "symbolLayer%1class%2" ).arg( symbolLayerNr ).arg( symbolHash ); + blockInfo.angle = sl->dxfAngle( sctx ); + blockInfo.size = sl->dxfSize( *this, sctx ); + blockInfo.feature = fet; + + blockSymbolMap.insert( symbolHash, qMakePair( 1, blockInfo ) ); + } + ++symbolLayerNr; + + //keep the entries with the most frequent occurrences + QMultiMap occurrences; + QHash >::const_iterator blockSymbolIt = blockSymbolMap.constBegin(); + for ( ; blockSymbolIt != blockSymbolMap.constEnd(); ++blockSymbolIt ) + { + occurrences.insert( blockSymbolIt.value().first, blockSymbolIt.key() ); + } + + QHash applyBlockSymbolMap; + int nInsertedClasses = 0; + QMultiMap::const_iterator occIt = occurrences.constEnd(); + while ( occurrences.size() > 0 && occIt != occurrences.constBegin() ) + { + --occIt; + applyBlockSymbolMap.insert( occIt.value(), blockSymbolMap[occIt.value()].second ); + ++nInsertedClasses; + if ( ddMaxNumberOfClasses != -1 && nInsertedClasses >= ddMaxNumberOfClasses ) + { + break; + } + } + + //add to mDataDefinedBlockInfo + mDataDefinedBlockInfo.insert( sl, applyBlockSymbolMap ); + } + } + } +} diff --git a/src/core/dxf/qgsdxfexport.h b/src/core/dxf/qgsdxfexport.h index a7d27d077cc5..0589e8cf726c 100644 --- a/src/core/dxf/qgsdxfexport.h +++ b/src/core/dxf/qgsdxfexport.h @@ -41,6 +41,7 @@ class QgsCircularString; class QgsCompoundCurve; struct DxfLayerJob; class QgsSymbolRenderContext; +class QgsMarkerSymbolLayer; #define DXF_HANDSEED 100 #define DXF_HANDMAX 9999999 @@ -72,9 +73,11 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink */ struct CORE_EXPORT DxfLayer { - DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1 ) + DxfLayer( QgsVectorLayer *vl, int layerOutputAttributeIndex = -1, bool buildDDBlocks = false, int ddBlocksMaxNumberOfClasses = -1 ) : mLayer( vl ) , mLayerOutputAttributeIndex( layerOutputAttributeIndex ) + , mBuildDDBlocks( buildDDBlocks ) + , mDDBlocksMaxNumberOfClasses( ddBlocksMaxNumberOfClasses ) {} //! Returns the layer @@ -95,15 +98,40 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink */ QString splitLayerAttribute() const; + /** + * \brief Flag if data defined point block symbols should be created. Default is false + * \return True if data defined point block symbols should be created + * \since QGIS 3.38 + */ + bool buildDataDefinedBlocks() const { return mBuildDDBlocks; } + + /** + * \brief Returns the maximum number of data defined symbol classes for which blocks are created. Returns -1 if there is no such limitation + * \return + * \since QGIS 3.38 + */ + int dataDefinedBlocksMaximumNumberOfClasses() const { return mDDBlocksMaxNumberOfClasses; } + private: QgsVectorLayer *mLayer = nullptr; int mLayerOutputAttributeIndex = -1; + + /** + * \brief try to build data defined symbol blocks if necessary + */ + bool mBuildDDBlocks = false; + + /** + * \brief Limit for the number of data defined symbol block classes (keep only the most used ones). -1 means no limit + */ + int mDDBlocksMaxNumberOfClasses = -1; }; //! Export flags enum Flag SIP_ENUM_BASETYPE( IntFlag ) { FlagNoMText = 1 << 1, //!< Export text as TEXT elements. If not set, text will be exported as MTEXT elements. + FlagOnlySelectedFeatures = 1 << 2, //!< Use only selected features for the export. }; Q_DECLARE_FLAGS( Flags, Flag ) @@ -527,6 +555,15 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink QgsDxfExport( const QgsDxfExport &other ); QgsDxfExport &operator=( const QgsDxfExport & ); #endif + + struct DataDefinedBlockInfo + { + QString blockName; + double angle; + double size; + QgsFeature feature; //a feature representing the attribute combination (without geometry) + }; + //! Extent for export, only intersecting features are exported. If the extent is an empty rectangle, all features are exported QgsRectangle mExtent; //! Scale for symbology export (used if symbols units are mm) @@ -545,8 +582,11 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink QHash< const QgsSymbolLayer *, QString > mPointSymbolBlocks; //reference to point symbol blocks QHash< const QgsSymbolLayer *, double > mPointSymbolBlockSizes; //reference to point symbol size used to create its block QHash< const QgsSymbolLayer *, double > mPointSymbolBlockAngles; //reference to point symbol size used to create its block + //! Layers with data defined symbology (other than size and angle) may also have blocks + QHash< const QgsSymbolLayer *, QHash > mDataDefinedBlockInfo; // symbolLayerName -> symbolHash/Feature //AC1009 + void createDDBlockInfo(); void writeHeader( const QString &codepage ); void prepareRenderers(); void writeTables(); @@ -605,6 +645,10 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink QList< QPair< QgsSymbolLayer *, QgsSymbol * > > symbolLayers( QgsRenderContext &context ); static int nLineTypes( const QList< QPair< QgsSymbolLayer *, QgsSymbol *> > &symbolLayers ); static bool hasBlockBreakingDataDefinedProperties( const QgsSymbolLayer *sl, const QgsSymbol *symbol ); + void writeSymbolTableBlockRef( const QString &blockName ); + void writeSymbolLayerBlock( const QString &blockName, const QgsMarkerSymbolLayer *ml, QgsSymbolRenderContext &ctx ); + void writePointBlockReference( const QgsPoint &pt, const QgsSymbolLayer *symbolLayer, QgsSymbolRenderContext &ctx, const QString &layer, double angle, const QString &blockName, double blockAngle, double blockSize ); + static uint dataDefinedSymbolClassHash( const QgsFeature &fet, const QgsPropertyCollection &prop ); double dashSize() const; double dotSize() const; @@ -622,6 +666,7 @@ class CORE_EXPORT QgsDxfExport : public QgsLabelSink QgsMapSettings mMapSettings; QList mLayerList; QHash mLayerNameAttribute; + QHash mLayerDDBlockMaxNumberOfClasses; double mFactor = 1.0; bool mForce2d = false; diff --git a/src/core/dxf/qgsdxfexport_p.h b/src/core/dxf/qgsdxfexport_p.h index 6bfbc27dc5f5..fe4492b717cc 100644 --- a/src/core/dxf/qgsdxfexport_p.h +++ b/src/core/dxf/qgsdxfexport_p.h @@ -47,8 +47,8 @@ struct DxfLayerJob { styleOverride.setOverrideStyle( layerStyleOverride ); } - fields = vl->fields(); + selectedFeatureIds = vl->selectedFeatureIds(); renderer.reset( vl->renderer()->clone() ); renderContext.expressionContext().appendScope( QgsExpressionContextUtils::layerScope( vl ) ); @@ -94,6 +94,7 @@ struct DxfLayerJob QgsRenderContext renderContext; QgsFields fields; + QgsFeatureIds selectedFeatureIds; QgsMapLayerStyleOverride styleOverride; QgsVectorLayerFeatureSource featureSource; std::unique_ptr< QgsFeatureRenderer > renderer; diff --git a/src/core/elevation/qgsabstractprofilesurfacegenerator.cpp b/src/core/elevation/qgsabstractprofilesurfacegenerator.cpp index adf5ddeb1594..9ec3a9f9b0b4 100644 --- a/src/core/elevation/qgsabstractprofilesurfacegenerator.cpp +++ b/src/core/elevation/qgsabstractprofilesurfacegenerator.cpp @@ -297,67 +297,58 @@ void QgsAbstractProfileSurfaceResults::renderResults( QgsProfileRenderContext &c break; } + auto checkLine = [this]( QPolygonF & currentLine, QgsProfileRenderContext & context, double minZ, double maxZ, + double prevDistance, double currentPartStartDistance ) + { + if ( currentLine.length() > 1 ) + { + switch ( symbology ) + { + case Qgis::ProfileSurfaceSymbology::Line: + mLineSymbol->renderPolyline( currentLine, nullptr, context.renderContext() ); + break; + case Qgis::ProfileSurfaceSymbology::FillBelow: + currentLine.append( context.worldTransform().map( QPointF( prevDistance, minZ ) ) ); + currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, minZ ) ) ); + currentLine.append( currentLine.at( 0 ) ); + mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); + break; + case Qgis::ProfileSurfaceSymbology::FillAbove: + currentLine.append( context.worldTransform().map( QPointF( prevDistance, maxZ ) ) ); + currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, maxZ ) ) ); + currentLine.append( currentLine.at( 0 ) ); + mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); + break; + } + } + }; + QPolygonF currentLine; double prevDistance = std::numeric_limits< double >::quiet_NaN(); double currentPartStartDistance = 0; for ( auto pointIt = mDistanceToHeightMap.constBegin(); pointIt != mDistanceToHeightMap.constEnd(); ++pointIt ) { - if ( std::isnan( pointIt.value() ) ) + if ( currentLine.empty() ) // new part { - if ( currentLine.length() > 1 ) - { - switch ( symbology ) - { - case Qgis::ProfileSurfaceSymbology::Line: - mLineSymbol->renderPolyline( currentLine, nullptr, context.renderContext() ); - break; - case Qgis::ProfileSurfaceSymbology::FillBelow: - currentLine.append( context.worldTransform().map( QPointF( prevDistance, minZ ) ) ); - currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, minZ ) ) ); - currentLine.append( currentLine.at( 0 ) ); - mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); - break; - case Qgis::ProfileSurfaceSymbology::FillAbove: - currentLine.append( context.worldTransform().map( QPointF( prevDistance, maxZ ) ) ); - currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, maxZ ) ) ); - currentLine.append( currentLine.at( 0 ) ); - mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); - break; - } - } - prevDistance = pointIt.key(); - currentLine.clear(); - continue; + if ( std::isnan( pointIt.value() ) ) // skip emptiness + continue; + currentPartStartDistance = pointIt.key(); } - if ( currentLine.length() < 1 ) + + if ( std::isnan( pointIt.value() ) ) { - currentPartStartDistance = pointIt.key(); + checkLine( currentLine, context, minZ, maxZ, prevDistance, currentPartStartDistance ); + currentLine.clear(); } - currentLine.append( context.worldTransform().map( QPointF( pointIt.key(), pointIt.value() ) ) ); - prevDistance = pointIt.key(); - } - if ( currentLine.length() > 1 ) - { - switch ( symbology ) + else { - case Qgis::ProfileSurfaceSymbology::Line: - mLineSymbol->renderPolyline( currentLine, nullptr, context.renderContext() ); - break; - case Qgis::ProfileSurfaceSymbology::FillBelow: - currentLine.append( context.worldTransform().map( QPointF( prevDistance, minZ ) ) ); - currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, minZ ) ) ); - currentLine.append( currentLine.at( 0 ) ); - mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); - break; - case Qgis::ProfileSurfaceSymbology::FillAbove: - currentLine.append( context.worldTransform().map( QPointF( prevDistance, maxZ ) ) ); - currentLine.append( context.worldTransform().map( QPointF( currentPartStartDistance, maxZ ) ) ); - currentLine.append( currentLine.at( 0 ) ); - mFillSymbol->renderPolygon( currentLine, nullptr, nullptr, context.renderContext() ); - break; + currentLine.append( context.worldTransform().map( QPointF( pointIt.key(), pointIt.value() ) ) ); + prevDistance = pointIt.key(); } } + checkLine( currentLine, context, minZ, maxZ, prevDistance, currentPartStartDistance ); + switch ( symbology ) { case Qgis::ProfileSurfaceSymbology::Line: diff --git a/src/core/expression/qgsexpressioncontextutils.cpp b/src/core/expression/qgsexpressioncontextutils.cpp index e73711bf27e8..8345dbfc9b5f 100644 --- a/src/core/expression/qgsexpressioncontextutils.cpp +++ b/src/core/expression/qgsexpressioncontextutils.cpp @@ -528,6 +528,13 @@ QgsExpressionContextScope *QgsExpressionContextUtils::mapSettingsScope( const Qg // IMPORTANT: ANY CHANGES HERE ALSO NEED TO BE MADE TO QgsLayoutItemMap::createExpressionContext() // (rationale is described in QgsLayoutItemMap::createExpressionContext() ) + const QgsDoubleRange zRange = mapSettings.zRange(); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_z_range_lower" ), !zRange.isInfinite() ? zRange.lower() : QVariant(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_z_range_upper" ), !zRange.isInfinite() ? zRange.upper() : QVariant(), true ) ); + + // IMPORTANT: ANY CHANGES HERE ALSO NEED TO BE MADE TO QgsLayoutItemMap::createExpressionContext() + // (rationale is described in QgsLayoutItemMap::createExpressionContext() ) + if ( mapSettings.frameRate() >= 0 ) scope->setVariable( QStringLiteral( "frame_rate" ), mapSettings.frameRate(), true ); if ( mapSettings.currentFrame() >= 0 ) diff --git a/src/core/externalstorage/qgshttpexternalstorage.cpp b/src/core/externalstorage/qgshttpexternalstorage.cpp index 2acf18517953..b13dfe4610da 100644 --- a/src/core/externalstorage/qgshttpexternalstorage.cpp +++ b/src/core/externalstorage/qgshttpexternalstorage.cpp @@ -18,6 +18,7 @@ #include "qgsnetworkcontentfetcherregistry.h" #include "qgsblockingnetworkrequest.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsapplication.h" #include "qgsfeedback.h" diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp index 2bbc1b0f20f4..cceb28da00fe 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp @@ -33,12 +33,12 @@ using namespace nlohmann; bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 ) { - return qgsVariantLessThan( p1.key, p2.key ); + return p1.group == p2.group ? qgsVariantLessThan( p1.key, p2.key ) : qgsVariantLessThan( p1.group, p2.group ); } bool orderByValueLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 ) { - return qgsVariantLessThan( p1.value, p2.value ); + return p1.group == p2.group ? qgsVariantLessThan( p1.value, p2.value ) : qgsVariantLessThan( p1.group, p2.group ); } QgsValueRelationFieldFormatter::QgsValueRelationFieldFormatter() @@ -135,13 +135,19 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte return cache; QgsFields fields = layer->fields(); - int ki = fields.indexOf( config.value( QStringLiteral( "Key" ) ).toString() ); - int vi = fields.indexOf( config.value( QStringLiteral( "Value" ) ).toString() ); + const int keyIdx = fields.indexOf( config.value( QStringLiteral( "Key" ) ).toString() ); + const int valueIdx = fields.indexOf( config.value( QStringLiteral( "Value" ) ).toString() ); QgsFeatureRequest request; request.setFlags( Qgis::FeatureRequestFlag::NoGeometry ); - QgsAttributeIds subsetOfAttributes { ki, vi }; + QgsAttributeIds subsetOfAttributes { keyIdx, valueIdx }; + + const int groupIdx = fields.lookupField( config.value( QStringLiteral( "Group" ) ).toString() ); + if ( groupIdx > -1 ) + { + subsetOfAttributes << groupIdx; + } const QString descriptionExpressionString = config.value( "Description" ).toString(); QgsExpression descriptionExpression( descriptionExpressionString ); @@ -180,7 +186,8 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte context.setFeature( f ); description = descriptionExpression.evaluate( &context ).toString(); } - cache.append( ValueRelationItem( f.attribute( ki ), f.attribute( vi ).toString(), description ) ); + const QVariant group = groupIdx > -1 ? f.attribute( groupIdx ) : QVariant(); + cache.append( ValueRelationItem( f.attribute( keyIdx ), f.attribute( valueIdx ).toString(), description, group ) ); } if ( config.value( QStringLiteral( "OrderByValue" ) ).toBool() ) diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h index dbd0e72e869c..e6ccd8f778b4 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h @@ -39,10 +39,11 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter struct ValueRelationItem { //! Constructor for ValueRelationItem - ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString() ) + ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString(), const QVariant group = QVariant() ) : key( key ) , value( value ) , description( description ) + , group( group ) {} //! Constructor for ValueRelationItem @@ -51,6 +52,8 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter QVariant key; QString value; QString description; + //! Value used to regroup items during sorting (since QGIS 3.38) + QVariant group; }; typedef QVector < QgsValueRelationFieldFormatter::ValueRelationItem > ValueRelationCache; diff --git a/src/core/geocoding/qgsabstractgeocoderlocatorfilter.cpp b/src/core/geocoding/qgsabstractgeocoderlocatorfilter.cpp index d1db36f70599..119a994996f9 100644 --- a/src/core/geocoding/qgsabstractgeocoderlocatorfilter.cpp +++ b/src/core/geocoding/qgsabstractgeocoderlocatorfilter.cpp @@ -72,7 +72,7 @@ QgsGeocoderInterface *QgsAbstractGeocoderLocatorFilter::geocoder() const QgsGeocoderResult QgsAbstractGeocoderLocatorFilter::locatorResultToGeocoderResult( const QgsLocatorResult &result ) const { - const QVariantMap attrs = result.getUserData().toMap(); + const QVariantMap attrs = result.userData().toMap(); QgsGeocoderResult geocodeResult( attrs.value( QStringLiteral( "identifier" ) ).toString(), attrs.value( QStringLiteral( "geom" ) ).value< QgsGeometry >(), attrs.value( QStringLiteral( "crs" ) ).value< QgsCoordinateReferenceSystem >() ); diff --git a/src/core/geocoding/qgsgooglemapsgeocoder.cpp b/src/core/geocoding/qgsgooglemapsgeocoder.cpp index 262f871c2b14..caeb5c733a61 100644 --- a/src/core/geocoding/qgsgooglemapsgeocoder.cpp +++ b/src/core/geocoding/qgsgooglemapsgeocoder.cpp @@ -17,6 +17,7 @@ #include "qgsgeocodercontext.h" #include "qgslogger.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsreadwritelocker.h" #include "qgscoordinatetransform.h" diff --git a/src/core/geocoding/qgsnominatimgeocoder.cpp b/src/core/geocoding/qgsnominatimgeocoder.cpp index 0b26c9213dd1..2b93f7aef71b 100644 --- a/src/core/geocoding/qgsnominatimgeocoder.cpp +++ b/src/core/geocoding/qgsnominatimgeocoder.cpp @@ -18,6 +18,7 @@ #include "qgsgeocodercontext.h" #include "qgslogger.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgscoordinatetransform.h" #include #include diff --git a/src/core/geometry/qgscircle.h b/src/core/geometry/qgscircle.h index 919f0737b32d..2e8320ffb506 100644 --- a/src/core/geometry/qgscircle.h +++ b/src/core/geometry/qgscircle.h @@ -111,7 +111,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * \param pt2_tg3 Second point of the third tangent. * \param epsilon Value used to compare point. * \param pos Point to determine which circle use in case of multi return. - * If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangets are parallels. (since QGIS 3.18) + * If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangents are parallels. (since QGIS 3.18) * * \see from3TangentsMulti() * diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 20fc41f4d0f5..b61d36c158de 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -801,6 +801,37 @@ bool QgsGeometry::insertVertex( const QgsPoint &point, int beforeVertex ) return d->geometry->insertVertex( id, point ); } +bool QgsGeometry::addTopologicalPoint( const QgsPoint &point, double snappingTolerance, double segmentSearchEpsilon ) +{ + if ( !d->geometry ) + { + return false; + } + + const double sqrSnappingTolerance = snappingTolerance * snappingTolerance; + int segmentAfterVertex; + QgsPointXY snappedPoint; + const double sqrDistSegmentSnap = closestSegmentWithContext( point, snappedPoint, segmentAfterVertex, nullptr, segmentSearchEpsilon ); + + if ( sqrDistSegmentSnap > sqrSnappingTolerance ) + return false; + + int atVertex, beforeVertex, afterVertex; + double sqrDistVertexSnap; + closestVertex( point, atVertex, beforeVertex, afterVertex, sqrDistVertexSnap ); + + if ( sqrDistVertexSnap < sqrSnappingTolerance ) + return false; // the vertex already exists - do not insert it + + if ( !insertVertex( point, segmentAfterVertex ) ) + { + QgsDebugError( QStringLiteral( "failed to insert topo point" ) ); + return false; + } + + return true; +} + QgsPoint QgsGeometry::vertexAt( int atVertex ) const { if ( !d->geometry ) @@ -1060,10 +1091,18 @@ Qgis::GeometryOperationResult QgsGeometry::splitGeometry( const QgsPointSequence return Qgis::GeometryOperationResult::InvalidBaseGeometry; } + // We're trying adding the split line's vertices to the geometry so that + // snap to segment always produces a valid split (see https://github.com/qgis/QGIS/issues/29270) + QgsGeometry tmpGeom( *this ); + for ( const QgsPoint &v : splitLine ) + { + tmpGeom.addTopologicalPoint( v ); + } + QVector newGeoms; QgsLineString splitLineString( splitLine ); - QgsGeos geos( d->geometry.get() ); + QgsGeos geos( tmpGeom.get() ); mLastError.clear(); QgsGeometryEngine::EngineOperationResult result = geos.splitGeometry( splitLineString, newGeoms, topological, topologyTestPoints, &mLastError, skipIntersectionTest ); diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index a84ce844a83f..fff7878c646c 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -788,6 +788,15 @@ class CORE_EXPORT QgsGeometry */ bool insertVertex( const QgsPoint &point, int beforeVertex ); + /** + * Adds a vertex to the segment which intersect \a point but don't + * already have a vertex there. Closest segment is identified using \a segmentSearchEpsilon. + * If a vertex already exists within \a snappingTolearnceDistance, no additional vertex is inserted. + * \returns TRUE if point was added, FALSE otherwise + * \since QGIS 3.38 + */ + bool addTopologicalPoint( const QgsPoint &point, double snappingTolerance = 1e-8, double segmentSearchEpsilon = 1e-12 ); + /** * Moves the vertex at the given position number * and item (first number is index 0) @@ -991,7 +1000,6 @@ class CORE_EXPORT QgsGeometry * \param topological TRUE if topological editing is enabled * \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset * \param splitFeature Set to TRUE if you want to split a feature, otherwise set to FALSE to split parts - * fix this bug? * \param skipIntersectionTest set to TRUE to skip the potentially expensive initial intersection check. Only set this flag if an intersection * test has already been performed by the caller! Not available in Python bindings. * \returns Qgis::GeometryOperationResult a result code: success or reason of failure diff --git a/src/core/geometry/qgsgeometrycollection.cpp b/src/core/geometry/qgsgeometrycollection.cpp index 1cf949563769..3263d4d22d62 100644 --- a/src/core/geometry/qgsgeometrycollection.cpp +++ b/src/core/geometry/qgsgeometrycollection.cpp @@ -871,7 +871,7 @@ bool QgsGeometryCollection::isValid( QString &error, Qgis::GeometryValidityFlags return error.isEmpty(); } - QgsGeos geos( this ); + QgsGeos geos( this, /* tolerance = */ 0, /* allowInvalidSubGeom = */ false ); bool res = geos.isValid( &error, flags & Qgis::GeometryValidityFlag::AllowSelfTouchingHoles, nullptr ); if ( flags == 0 ) { diff --git a/src/core/geometry/qgsgeos.cpp b/src/core/geometry/qgsgeos.cpp index f05b834fedf7..6285cf0d4718 100644 --- a/src/core/geometry/qgsgeos.cpp +++ b/src/core/geometry/qgsgeos.cpp @@ -139,12 +139,12 @@ void geos::GeosDeleter::operator()( GEOSCoordSequence *sequence ) const ///@endcond -QgsGeos::QgsGeos( const QgsAbstractGeometry *geometry, double precision ) +QgsGeos::QgsGeos( const QgsAbstractGeometry *geometry, double precision, bool allowInvalidSubGeom ) : QgsGeometryEngine( geometry ) , mGeos( nullptr ) , mPrecision( precision ) { - cacheGeos(); + cacheGeos( allowInvalidSubGeom ); } QgsGeometry QgsGeos::geometryFromGeos( GEOSGeometry *geos ) @@ -246,7 +246,7 @@ void QgsGeos::geometryChanged() { mGeos.reset(); mGeosPrepared.reset(); - cacheGeos(); + cacheGeos( false ); } void QgsGeos::prepareGeometry() @@ -262,7 +262,7 @@ void QgsGeos::prepareGeometry() } } -void QgsGeos::cacheGeos() const +void QgsGeos::cacheGeos( bool allowInvalidSubGeom ) const { if ( mGeos ) { @@ -274,7 +274,7 @@ void QgsGeos::cacheGeos() const return; } - mGeos = asGeos( mGeometry, mPrecision ); + mGeos = asGeos( mGeometry, mPrecision, allowInvalidSubGeom ); } QgsAbstractGeometry *QgsGeos::intersection( const QgsAbstractGeometry *geom, QString *errorMsg, const QgsGeometryParameters ¶meters ) const @@ -437,14 +437,14 @@ QgsAbstractGeometry *QgsGeos::combine( const QgsAbstractGeometry *geom, QString QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QString *errorMsg, const QgsGeometryParameters ¶meters ) const { - QVector< GEOSGeometry * > geosGeometries; + std::vector geosGeometries; geosGeometries.reserve( geomList.size() ); for ( const QgsAbstractGeometry *g : geomList ) { if ( !g ) continue; - geosGeometries << asGeos( g, mPrecision ).release(); + geosGeometries.emplace_back( asGeos( g, mPrecision ) ); } geos::unique_ptr geomUnion; @@ -468,14 +468,14 @@ QgsAbstractGeometry *QgsGeos::combine( const QVector &geo QgsAbstractGeometry *QgsGeos::combine( const QVector &geomList, QString *errorMsg, const QgsGeometryParameters ¶meters ) const { - QVector< GEOSGeometry * > geosGeometries; + std::vector geosGeometries; geosGeometries.reserve( geomList.size() ); for ( const QgsGeometry &g : geomList ) { if ( g.isNull() ) continue; - geosGeometries << asGeos( g.constGet(), mPrecision ).release(); + geosGeometries.emplace_back( asGeos( g.constGet(), mPrecision ) ); } geos::unique_ptr geomUnion; @@ -1136,7 +1136,7 @@ geos::unique_ptr QgsGeos::linePointDifference( GEOSGeometry *GEOSsplitPoint ) co return asGeos( &lines, mPrecision ); } -QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const +QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( const GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const { Q_UNUSED( skipIntersectionCheck ) if ( !splitLine ) @@ -1145,49 +1145,49 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitLinearGeometry( GEOSGeome if ( !mGeos ) return InvalidBaseGeometry; - geos::unique_ptr intersectGeom( GEOSIntersection_r( geosinit()->ctxt, splitLine, mGeos.get() ) ); - if ( !intersectGeom || GEOSisEmpty_r( geosinit()->ctxt, intersectGeom.get() ) ) + GEOSContextHandle_t geosctxt = geosinit()->ctxt; + + geos::unique_ptr intersectGeom( GEOSIntersection_r( geosctxt, splitLine, mGeos.get() ) ); + if ( !intersectGeom || GEOSisEmpty_r( geosctxt, intersectGeom.get() ) ) return NothingHappened; //check that split line has no linear intersection - int linearIntersect = GEOSRelatePattern_r( geosinit()->ctxt, mGeos.get(), splitLine, "1********" ); + const int linearIntersect = GEOSRelatePattern_r( geosctxt, mGeos.get(), splitLine, "1********" ); if ( linearIntersect > 0 ) return InvalidInput; - geos::unique_ptr splitGeom; - splitGeom = linePointDifference( intersectGeom.get() ); + geos::unique_ptr splitGeom = linePointDifference( intersectGeom.get() ); if ( !splitGeom ) return InvalidBaseGeometry; - QVector lineGeoms; + std::vector lineGeoms; - int splitType = GEOSGeomTypeId_r( geosinit()->ctxt, splitGeom.get() ); + const int splitType = GEOSGeomTypeId_r( geosctxt, splitGeom.get() ); if ( splitType == GEOS_MULTILINESTRING ) { - int nGeoms = GEOSGetNumGeometries_r( geosinit()->ctxt, splitGeom.get() ); + const int nGeoms = GEOSGetNumGeometries_r( geosctxt, splitGeom.get() ); lineGeoms.reserve( nGeoms ); for ( int i = 0; i < nGeoms; ++i ) - lineGeoms << GEOSGeom_clone_r( geosinit()->ctxt, GEOSGetGeometryN_r( geosinit()->ctxt, splitGeom.get(), i ) ); + lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, GEOSGetGeometryN_r( geosctxt, splitGeom.get(), i ) ) ); } else { - lineGeoms << GEOSGeom_clone_r( geosinit()->ctxt, splitGeom.get() ); + lineGeoms.emplace_back( GEOSGeom_clone_r( geosctxt, splitGeom.get() ) ); } mergeGeometriesMultiTypeSplit( lineGeoms ); - for ( int i = 0; i < lineGeoms.size(); ++i ) + for ( geos::unique_ptr &lineGeom : lineGeoms ) { - newGeometries << QgsGeometry( fromGeos( lineGeoms[i] ) ); - GEOSGeom_destroy_r( geosinit()->ctxt, lineGeoms[i] ); + newGeometries << QgsGeometry( fromGeos( lineGeom.get() ) ); } return Success; } -QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const +QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( const GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const { if ( !splitLine ) return InvalidInput; @@ -1200,8 +1200,10 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( GEOSGeom if ( !mGeosPrepared ) return EngineError; + GEOSContextHandle_t geosctxt = geosinit()->ctxt; + //first test if linestring intersects geometry. If not, return straight away - if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( geosinit()->ctxt, mGeosPrepared.get(), splitLine ) ) + if ( !skipIntersectionCheck && !GEOSPreparedIntersects_r( geosctxt, mGeosPrepared.get(), splitLine ) ) return NothingHappened; //first union all the polygon rings together (to get them noded, see JTS developer guide) @@ -1210,36 +1212,37 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( GEOSGeom return NodedGeometryError; //an error occurred during noding const GEOSGeometry *noded = nodedGeometry.get(); - geos::unique_ptr polygons( GEOSPolygonize_r( geosinit()->ctxt, &noded, 1 ) ); - if ( !polygons || numberOfGeometries( polygons.get() ) == 0 ) + geos::unique_ptr polygons( GEOSPolygonize_r( geosctxt, &noded, 1 ) ); + if ( !polygons ) + { + return InvalidBaseGeometry; + } + const int numberOfGeometriesPolygon = numberOfGeometries( polygons.get() ); + if ( numberOfGeometriesPolygon == 0 ) { return InvalidBaseGeometry; } //test every polygon is contained in original geometry //include in result if yes - QVector testedGeometries; + std::vector testedGeometries; // test whether the polygon parts returned from polygonize algorithm actually // belong to the source polygon geometry (if the source polygon contains some holes, // those would be also returned by polygonize and we need to skip them) - for ( int i = 0; i < numberOfGeometries( polygons.get() ); i++ ) + for ( int i = 0; i < numberOfGeometriesPolygon; i++ ) { - const GEOSGeometry *polygon = GEOSGetGeometryN_r( geosinit()->ctxt, polygons.get(), i ); + const GEOSGeometry *polygon = GEOSGetGeometryN_r( geosctxt, polygons.get(), i ); - geos::unique_ptr pointOnSurface( GEOSPointOnSurface_r( geosinit()->ctxt, polygon ) ); - if ( pointOnSurface && GEOSPreparedIntersects_r( geosinit()->ctxt, mGeosPrepared.get(), pointOnSurface.get() ) ) - testedGeometries << GEOSGeom_clone_r( geosinit()->ctxt, polygon ); + 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 ) ); } - int nGeometriesThis = numberOfGeometries( mGeos.get() ); //original number of geometries + const size_t nGeometriesThis = numberOfGeometries( mGeos.get() ); //original number of geometries if ( testedGeometries.empty() || testedGeometries.size() == nGeometriesThis ) { //no split done, preserve original geometry - for ( int i = 0; i < testedGeometries.size(); ++i ) - { - GEOSGeom_destroy_r( geosinit()->ctxt, testedGeometries[i] ); - } return NothingHappened; } @@ -1249,22 +1252,18 @@ QgsGeometryEngine::EngineOperationResult QgsGeos::splitPolygonGeometry( GEOSGeom // geometry rather than being separated into two single-part geometries. mergeGeometriesMultiTypeSplit( testedGeometries ); - int i; - for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( geosinit()->ctxt, testedGeometries[i] ); ++i ) + size_t i; + for ( i = 0; i < testedGeometries.size() && GEOSisValid_r( geosctxt, testedGeometries[i].get() ); ++i ) ; if ( i < testedGeometries.size() ) { - for ( i = 0; i < testedGeometries.size(); ++i ) - GEOSGeom_destroy_r( geosinit()->ctxt, testedGeometries[i] ); - return InvalidBaseGeometry; } - for ( i = 0; i < testedGeometries.size(); ++i ) + for ( geos::unique_ptr &testedGeometry : testedGeometries ) { - newGeometries << QgsGeometry( fromGeos( testedGeometries[i] ) ); - GEOSGeom_destroy_r( geosinit()->ctxt, testedGeometries[i] ); + newGeometries << QgsGeometry( fromGeos( testedGeometry.get() ) ); } return Success; @@ -1287,7 +1286,7 @@ geos::unique_ptr QgsGeos::nodeGeometries( const GEOSGeometry *splitLine, const G return unionGeometry; } -int QgsGeos::mergeGeometriesMultiTypeSplit( QVector &splitResult ) const +int QgsGeos::mergeGeometriesMultiTypeSplit( std::vector &splitResult ) const { if ( !mGeos ) return 1; @@ -1300,19 +1299,18 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( QVector &splitResult type != GEOS_MULTIPOINT ) return 0; - QVector copyList = splitResult; - splitResult.clear(); - //collect all the geometries that belong to the initial multifeature - QVector unionGeom; + std::vector unionGeom; + + std::vector newSplitResult; - for ( int i = 0; i < copyList.size(); ++i ) + for ( size_t i = 0; i < splitResult.size(); ++i ) { //is this geometry a part of the original multitype? bool isPart = false; for ( int j = 0; j < GEOSGetNumGeometries_r( geosinit()->ctxt, mGeos.get() ); j++ ) { - if ( GEOSEquals_r( geosinit()->ctxt, copyList[i], GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), j ) ) ) + if ( GEOSEquals_r( geosinit()->ctxt, splitResult[i].get(), GEOSGetGeometryN_r( geosinit()->ctxt, mGeos.get(), j ) ) ) { isPart = true; break; @@ -1321,82 +1319,66 @@ int QgsGeos::mergeGeometriesMultiTypeSplit( QVector &splitResult if ( isPart ) { - unionGeom << copyList[i]; + unionGeom.emplace_back( std::move( splitResult[i] ) ); } else { - QVector geomVector; - geomVector << copyList[i]; + std::vector geomVector; + geomVector.emplace_back( std::move( splitResult[i] ) ); if ( type == GEOS_MULTILINESTRING ) - splitResult << createGeosCollection( GEOS_MULTILINESTRING, geomVector ).release(); + newSplitResult.emplace_back( createGeosCollection( GEOS_MULTILINESTRING, geomVector ) ); else if ( type == GEOS_MULTIPOLYGON ) - splitResult << createGeosCollection( GEOS_MULTIPOLYGON, geomVector ).release(); - else - GEOSGeom_destroy_r( geosinit()->ctxt, copyList[i] ); + newSplitResult.emplace_back( createGeosCollection( GEOS_MULTIPOLYGON, geomVector ) ); } } + splitResult = std::move( newSplitResult ); + //make multifeature out of unionGeom - if ( !unionGeom.isEmpty() ) + if ( !unionGeom.empty() ) { if ( type == GEOS_MULTILINESTRING ) - splitResult << createGeosCollection( GEOS_MULTILINESTRING, unionGeom ).release(); + splitResult.emplace_back( createGeosCollection( GEOS_MULTILINESTRING, unionGeom ) ); else if ( type == GEOS_MULTIPOLYGON ) - splitResult << createGeosCollection( GEOS_MULTIPOLYGON, unionGeom ).release(); - } - else - { - unionGeom.clear(); + splitResult.emplace_back( createGeosCollection( GEOS_MULTIPOLYGON, unionGeom ) ); } return 0; } -geos::unique_ptr QgsGeos::createGeosCollection( int typeId, const QVector &geoms ) +geos::unique_ptr QgsGeos::createGeosCollection( int typeId, std::vector &geoms ) { - int nNullGeoms = geoms.count( nullptr ); - int nNotNullGeoms = geoms.size() - nNullGeoms; + std::vector geomarr; + geomarr.reserve( geoms.size() ); - GEOSGeometry **geomarr = new GEOSGeometry*[ nNotNullGeoms ]; - if ( !geomarr ) + for ( geos::unique_ptr &geomUniquePtr : geoms ) { - return nullptr; - } - - int i = 0; - QVector::const_iterator geomIt = geoms.constBegin(); - for ( ; geomIt != geoms.constEnd(); ++geomIt ) - { - if ( *geomIt ) + if ( geomUniquePtr ) { - if ( GEOSisEmpty_r( geosinit()->ctxt, *geomIt ) ) + if ( !GEOSisEmpty_r( geosinit()->ctxt, geomUniquePtr.get() ) ) { // don't add empty parts to a geos collection, it can cause crashes in GEOS - nNullGeoms++; - nNotNullGeoms--; - GEOSGeom_destroy_r( geosinit()->ctxt, *geomIt ); - } - else - { - geomarr[i] = *geomIt; - ++i; + // transfer ownership of the geometries to GEOSGeom_createCollection_r() + geomarr.emplace_back( geomUniquePtr.release() ); } } } - geos::unique_ptr geom; + geos::unique_ptr geomRes; try { - geom.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, typeId, geomarr, nNotNullGeoms ) ); + geomRes.reset( GEOSGeom_createCollection_r( geosinit()->ctxt, typeId, geomarr.data(), geomarr.size() ) ); } catch ( GEOSException & ) { + for ( GEOSGeometry *geom : geomarr ) + { + GEOSGeom_destroy_r( geosinit()->ctxt, geom ); + } } - delete [] geomarr; - - return geom; + return geomRes; } std::unique_ptr QgsGeos::fromGeos( const GEOSGeometry *geos ) @@ -1624,7 +1606,7 @@ QgsPoint QgsGeos::coordSeqPoint( const GEOSCoordSequence *cs, int i, bool hasZ, return QgsPoint( t, x, y, z, m ); } -geos::unique_ptr QgsGeos::asGeos( const QgsAbstractGeometry *geom, double precision ) +geos::unique_ptr QgsGeos::asGeos( const QgsAbstractGeometry *geom, double precision, bool allowInvalidSubGeom ) { if ( !geom ) return nullptr; @@ -1671,10 +1653,16 @@ geos::unique_ptr QgsGeos::asGeos( const QgsAbstractGeometry *geom, double precis if ( !c ) return nullptr; - QVector< GEOSGeometry * > geomVector( c->numGeometries() ); + std::vector geomVector; + geomVector.reserve( c->numGeometries() ); for ( int i = 0; i < c->numGeometries(); ++i ) { - geomVector[i] = asGeos( c->geometryN( i ), precision ).release(); + geos::unique_ptr geosGeom = asGeos( c->geometryN( i ), precision ); + if ( !allowInvalidSubGeom && !geosGeom ) + { + return nullptr; + } + geomVector.emplace_back( std::move( geosGeom ) ); } return createGeosCollection( geosType, geomVector ); } @@ -2134,6 +2122,8 @@ bool QgsGeos::isValid( QString *errorMsg, const bool allowSelfTouchingHoles, Qgs { if ( !mGeos ) { + if ( errorMsg ) + *errorMsg = QObject::tr( "QGIS geometry cannot be converted to a GEOS geometry", "GEOS Error" ); return false; } diff --git a/src/core/geometry/qgsgeos.h b/src/core/geometry/qgsgeos.h index ee5e5e760e52..1e59b29c1180 100644 --- a/src/core/geometry/qgsgeos.h +++ b/src/core/geometry/qgsgeos.h @@ -102,8 +102,9 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine * GEOS geometry engine constructor * \param geometry The geometry * \param precision The precision of the grid to which to snap the geometry vertices. If 0, no snapping is performed. + * \param allowInvalidSubGeom Whether invalid sub-geometries should be skipped without error (since QGIS 3.38) */ - QgsGeos( const QgsAbstractGeometry *geometry, double precision = 0 ); + QgsGeos( const QgsAbstractGeometry *geometry, double precision = 0, bool allowInvalidSubGeom = true ); /** * Creates a new QgsGeometry object, feeding in a geometry in GEOS format. @@ -654,8 +655,9 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine * Returns a geos geometry - caller takes ownership of the object (should be deleted with GEOSGeom_destroy_r) * \param geometry geometry to convert to GEOS representation * \param precision The precision of the grid to which to snap the geometry vertices. If 0, no snapping is performed. + * \param allowInvalidSubGeom Whether invalid sub-geometries should be skipped without error (since QGIS 3.38) */ - static geos::unique_ptr asGeos( const QgsAbstractGeometry *geometry, double precision = 0 ); + 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(); @@ -686,7 +688,7 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine }; //geos util functions - void cacheGeos() const; + void cacheGeos( bool allowInvalidSubGeom ) const; /** * Returns a geometry representing the overlay operation with \a geom. @@ -701,12 +703,12 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine static std::unique_ptr< QgsLineString > sequenceToLinestring( const GEOSGeometry *geos, bool hasZ, bool hasM ); static int numberOfGeometries( GEOSGeometry *g ); static geos::unique_ptr nodeGeometries( const GEOSGeometry *splitLine, const GEOSGeometry *geom ); - int mergeGeometriesMultiTypeSplit( QVector &splitResult ) const; + int mergeGeometriesMultiTypeSplit( std::vector &splitResult ) const; /** * Ownership of geoms is transferred */ - static geos::unique_ptr createGeosCollection( int typeId, const QVector &geoms ); + static geos::unique_ptr createGeosCollection( int typeId, std::vector &geoms ); static geos::unique_ptr createGeosPointXY( double x, double y, bool hasZ, double z, bool hasM, double m, int coordDims, double precision ); static geos::unique_ptr createGeosPoint( const QgsAbstractGeometry *point, int coordDims, double precision ); @@ -716,8 +718,8 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine //utils for geometry split bool topologicalTestPointsSplit( const GEOSGeometry *splitLine, QgsPointSequence &testPoints, QString *errorMsg = nullptr ) const; geos::unique_ptr linePointDifference( GEOSGeometry *GEOSsplitPoint ) const; - EngineOperationResult splitLinearGeometry( GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const; - EngineOperationResult splitPolygonGeometry( GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const; + EngineOperationResult splitLinearGeometry( const GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const; + EngineOperationResult splitPolygonGeometry( const GEOSGeometry *splitLine, QVector &newGeometries, bool skipIntersectionCheck ) const; //utils for reshape static geos::unique_ptr reshapeLine( const GEOSGeometry *line, const GEOSGeometry *reshapeLineGeos, double precision ); diff --git a/src/core/geometry/qgsrectangle.h b/src/core/geometry/qgsrectangle.h index 72432635a16c..5ae5aacc6e28 100644 --- a/src/core/geometry/qgsrectangle.h +++ b/src/core/geometry/qgsrectangle.h @@ -630,7 +630,11 @@ class CORE_EXPORT QgsRectangle #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); + QString str; + if ( sipCpp->isNull() ) + str = QStringLiteral( "" ); + else + str = QStringLiteral( "" ).arg( sipCpp->asWktCoordinates() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); % End #endif diff --git a/src/core/gps/qgsgpsdetector.cpp b/src/core/gps/qgsgpsdetector.cpp index b78de32e686c..3317ff88b8c1 100644 --- a/src/core/gps/qgsgpsdetector.cpp +++ b/src/core/gps/qgsgpsdetector.cpp @@ -21,7 +21,7 @@ #include "qgsgpsdconnection.h" #include "qgssettingstree.h" #include "qgssettingsentryenumflag.h" - +#include "qgslogger.h" #if defined(QT_POSITIONING_LIB) #include "qgsqtlocationconnection.h" @@ -64,7 +64,8 @@ QList< QPair > QgsGpsDetector::availablePorts() return devs; } -QgsGpsDetector::QgsGpsDetector( const QString &portName ) +QgsGpsDetector::QgsGpsDetector( const QString &portName, bool useUnsafeSignals ) + : mUseUnsafeSignals( useUnsafeSignals ) { #if defined( HAVE_QTSERIALPORT ) mBaudList << QSerialPort::Baud4800 << QSerialPort::Baud9600 << QSerialPort::Baud38400 << QSerialPort::Baud57600 << QSerialPort::Baud115200; //add 57600 for SXBlueII GPS unit @@ -72,19 +73,63 @@ QgsGpsDetector::QgsGpsDetector( const QString &portName ) if ( portName.isEmpty() ) { + QgsDebugMsgLevel( QStringLiteral( "Attempting to autodetect GPS connection" ), 2 ); mPortList = availablePorts(); } else { + QgsDebugMsgLevel( QStringLiteral( "Attempting GPS connection for %1" ).arg( portName ), 2 ); mPortList << QPair( portName, portName ); } + + mTimeoutTimer = new QTimer( this ); + mTimeoutTimer->setSingleShot( true ); + connect( mTimeoutTimer, &QTimer::timeout, this, &QgsGpsDetector::connectionTimeout ); +} + +QgsGpsDetector::~QgsGpsDetector() +{ + QgsDebugMsgLevel( QStringLiteral( "Destroying GPS detector" ), 2 ); } -QgsGpsDetector::~QgsGpsDetector() = default; +QgsGpsConnection *QgsGpsDetector::takeConnection() +{ + if ( mUseUnsafeSignals ) + { + QgsDebugError( QStringLiteral( "QgsGpsDetector::takeConnection() incorrectly called when useUnsafeSignals option is in effect" ) ); + return nullptr; + } + + if ( mConn ) + { + // this is NOT the detectors connection anymore, so disconnect all signals from the connection + // to the detector so that there's no unwanted interaction with the detector + mConn->disconnect( this ); + } + +#ifdef QGISDEBUG + if ( mConn ) + { + QgsDebugMsgLevel( QStringLiteral( "Detected GPS connection is being taken by caller" ), 2 ); + } + else + { + QgsDebugError( QStringLiteral( "Something is trying to take the GPS connection, but it doesn't exist!" ) ); + } +#endif + + return mConn.release(); +} void QgsGpsDetector::advance() { - mConn.reset(); + if ( mConn ) + { + QgsDebugMsgLevel( QStringLiteral( "Destroying existing connection to attempt next configuration combination" ), 2 ); + mConn.reset(); + } + + QgsDebugMsgLevel( QStringLiteral( "Trying to find a connection..." ), 2 ); while ( !mConn ) { @@ -97,11 +142,14 @@ void QgsGpsDetector::advance() if ( mPortIndex == mPortList.size() ) { + QgsDebugError( QStringLiteral( "No more devices to try!" ) ); emit detectionFailed(); deleteLater(); return; } + QgsDebugMsgLevel( QStringLiteral( "Attempting connection to device %1 @ %2" ).arg( mPortIndex ).arg( mBaudIndex ), 2 ); + if ( mPortList.at( mPortIndex ).first.contains( ':' ) ) { mBaudIndex = mBaudList.size() - 1; @@ -109,14 +157,17 @@ void QgsGpsDetector::advance() QStringList gpsParams = mPortList.at( mPortIndex ).first.split( ':' ); Q_ASSERT( gpsParams.size() >= 3 ); + QgsDebugMsgLevel( QStringLiteral( "Connecting to GPSD device %1" ).arg( gpsParams.join( ',' ) ), 2 ); mConn = std::make_unique< QgsGpsdConnection >( gpsParams[0], gpsParams[1].toShort(), gpsParams[2] ); } else if ( mPortList.at( mPortIndex ).first.contains( QLatin1String( "internalGPS" ) ) ) { #if defined(QT_POSITIONING_LIB) + QgsDebugMsgLevel( QStringLiteral( "Connecting to QtLocation service device" ), 2 ); mConn = std::make_unique< QgsQtLocationConnection >(); #else + QgsDebugError( QStringLiteral( "QT_POSITIONING_LIB not found and mPortList matches internalGPS, this should never happen" ) ); qWarning( "QT_POSITIONING_LIB not found and mPortList matches internalGPS, this should never happen" ); #endif } @@ -132,45 +183,94 @@ void QgsGpsDetector::advance() serial->setDataBits( QgsGpsDetector::settingsGpsDataBits->value() ); serial->setStopBits( QgsGpsDetector::settingsGpsStopBits->value() ); + QgsDebugMsgLevel( QStringLiteral( "Connecting to serial GPS device %1 (@ %2)" ).arg( mPortList.at( mPortIndex ).first ).arg( mBaudList[ mBaudIndex ] ), 2 ); + if ( serial->open( QIODevice::ReadOnly ) ) { + QgsDebugMsgLevel( QStringLiteral( "Successfully opened, have a port connection ready" ), 2 ); mConn = std::make_unique< QgsNmeaConnection >( serial.release() ); } + else + { + QgsDebugError( QStringLiteral( "Serial port could NOT be opened" ) ); + } #else + QgsDebugError( QStringLiteral( "QT5SERIALPORT not found and mPortList matches serial port, this should never happen" ) ); qWarning( "QT5SERIALPORT not found and mPortList matches serial port, this should never happen" ); #endif } + + if ( !mConn ) + { + QgsDebugError( QStringLiteral( "Got to end of connection handling loop, but have no connection!" ) ); + } } - connect( mConn.get(), &QgsGpsConnection::stateChanged, this, static_cast < void ( QgsGpsDetector::* )( const QgsGpsInformation & ) >( &QgsGpsDetector::detected ) ); - connect( mConn.get(), &QObject::destroyed, this, &QgsGpsDetector::connDestroyed ); + QgsDebugMsgLevel( QStringLiteral( "Have a connection, now listening for messages" ), 2 ); + + connect( mConn.get(), &QgsGpsConnection::stateChanged, this, qOverload< const QgsGpsInformation & >( &QgsGpsDetector::detected ) ); + if ( mUseUnsafeSignals ) + { + connect( mConn.get(), &QObject::destroyed, this, &QgsGpsDetector::connDestroyed ); + } // leave 2s to pickup a valid string - QTimer::singleShot( 2000, this, &QgsGpsDetector::advance ); + mTimeoutTimer->start( 2000 ); } -void QgsGpsDetector::detected( const QgsGpsInformation &info ) +void QgsGpsDetector::detected( const QgsGpsInformation & ) { - Q_UNUSED( info ) + QgsDebugMsgLevel( QStringLiteral( "Detected information" ), 2 ); if ( !mConn ) { + mTimeoutTimer->stop(); + // advance if connection was destroyed + QgsDebugError( QStringLiteral( "Got information, but CONNECTION WAS DESTROYED EXTERNALLY!" ) ); advance(); } else if ( mConn->status() == QgsGpsConnection::GPSDataReceived ) { + mTimeoutTimer->stop(); + // stop listening for state changed signals, we've already validated this connection and don't want subsequent calls + // to QgsGpsDetector::detected being made + disconnect( mConn.get(), &QgsGpsConnection::stateChanged, this, qOverload< const QgsGpsInformation & >( &QgsGpsDetector::detected ) ); + // signal detected + QgsDebugMsgLevel( QStringLiteral( "Connection status IS GPSDataReceived" ), 2 ); - // let's hope there's a single, unique connection to this signal... otherwise... boom - emit detected( mConn.release() ); + if ( mUseUnsafeSignals ) + { + // let's hope there's a single, unique connection to this signal... otherwise... boom! + Q_NOWARN_DEPRECATED_PUSH + emit detected( mConn.release() ); + Q_NOWARN_DEPRECATED_POP + } + else + { + emit connectionDetected(); + } deleteLater(); } + else + { + // don't stop timeout, we keep waiting to see if later we get the desired connection status... + QgsDebugMsgLevel( QStringLiteral( "Connection status is NOT GPSDataReceived. It is %1" ).arg( mConn->status() ), 2 ); + } +} + +void QgsGpsDetector::connectionTimeout() +{ + QgsDebugMsgLevel( QStringLiteral( "No data received within max listening time" ), 2 ); + advance(); } void QgsGpsDetector::connDestroyed( QObject *obj ) { + QgsDebugError( QStringLiteral( "CONNECTION WAS DESTROYED EXTERNALLY!" ) ); + // WTF? This whole class needs re-writing... if ( obj == mConn.get() ) { diff --git a/src/core/gps/qgsgpsdetector.h b/src/core/gps/qgsgpsdetector.h index 90712a301334..59c9971ba6e1 100644 --- a/src/core/gps/qgsgpsdetector.h +++ b/src/core/gps/qgsgpsdetector.h @@ -38,6 +38,7 @@ class QgsSettingsEntryEnumFlag; class QgsGpsConnection; class QgsGpsInformation; +class QTimer; /** * \ingroup core @@ -47,7 +48,22 @@ class CORE_EXPORT QgsGpsDetector : public QObject { Q_OBJECT public: - QgsGpsDetector( const QString &portName ); + + // TODO QGIS 4.0 -- remove useUnsafeSignals option + + /** + * Constructor for QgsGpsDetector. + * + * If \a portName is specified, then only devices from the given port will be scanned. Otherwise + * all connection types will be attempted (including internal GPS devices). + * + * Since QGIS 3.38, the \a useUnsafeSignals parameter can be set to FALSE to avoid emitting the + * dangerous and fragile detected() signal. This is highly recommended, but is opt-in to avoid + * breaking stable QGIS 3.x API. If \a useUnsafeSignals is set to FALSE, only the safe connectionDetected() signal + * will be emitted and clients must manually take ownership of the detected connection via a call + * to takeConnection(). + */ + QgsGpsDetector( const QString &portName = QString(), bool useUnsafeSignals = true ); #if defined( HAVE_QTSERIALPORT ) static const QgsSettingsEntryEnumFlag *settingsGpsStopBits SIP_SKIP; @@ -58,6 +74,20 @@ class CORE_EXPORT QgsGpsDetector : public QObject ~QgsGpsDetector() override; + /** + * Returns the detected GPS connection, and removes it from the detector. + * + * The caller takes ownership of the connection. Only the first call to this + * method following a connectionDetected() signal will be able to retrieve the + * detected connection -- subsequent calls will return NULLPTR. + * + * \warning Do not call this method if the useUnsafeSignals option in the + * QgsGpsDetector constructor was set to TRUE. + * + * \since QGIS 3.38 + */ + QgsGpsConnection *takeConnection() SIP_TRANSFERBACK; + static QList< QPair > availablePorts(); public slots: @@ -67,24 +97,41 @@ class CORE_EXPORT QgsGpsDetector : public QObject signals: - // TODO QGIS 4.0 - this is horrible, fragile, leaky and crash prone API. - // don't transfer ownership with this signal, and add an explicit takeConnection member! + /** + * Emitted when a GPS connection is successfully detected. + * + * Call takeConnection() to take ownership of the detected connection. + * + * \since QGIS 3.38 + */ + void connectionDetected(); /** * Emitted when the GPS connection has been detected. A single connection must listen for this signal and * immediately take ownership of the \a connection object. + * + * \deprecated This signal is dangerous and extremely unsafe! It is recommended to instead set the \a useUnsafeSignals parameter to FALSE in the QgsGpsDetector constructor and use the safe connectionDetected() signal instead. */ - void detected( QgsGpsConnection *connection ); + Q_DECL_DEPRECATED void detected( QgsGpsConnection *connection ) SIP_DEPRECATED; + /** + * Emitted when the detector could not find a valid GPS connection. + */ void detectionFailed(); + private slots: + + void connectionTimeout(); + private: + bool mUseUnsafeSignals = true; int mPortIndex = 0; int mBaudIndex = -1; QList< QPair< QString, QString > > mPortList; QList mBaudList; std::unique_ptr< QgsGpsConnection > mConn; + QTimer *mTimeoutTimer = nullptr; }; #endif // QGSGPSDETECTOR_H diff --git a/src/core/gps/qgsnmeaconnection.cpp b/src/core/gps/qgsnmeaconnection.cpp index 4460f7c684d8..72dc8fc7cb39 100644 --- a/src/core/gps/qgsnmeaconnection.cpp +++ b/src/core/gps/qgsnmeaconnection.cpp @@ -60,15 +60,24 @@ void QgsNmeaConnection::parseData() if ( numBytes >= 6 ) { + QgsDebugMsgLevel( QStringLiteral( "Got %1 NMEA bytes" ).arg( numBytes ), 3 ); + QgsDebugMsgLevel( QStringLiteral( "Current NMEA device status is %1" ).arg( mStatus ), 3 ); if ( mStatus != GPSDataReceived ) { + QgsDebugMsgLevel( QStringLiteral( "Setting device status to DataReceived" ), 3 ); mStatus = DataReceived; } //append new data to the remaining results from last parseData() call mStringBuffer.append( mSource->read( numBytes ) ); processStringBuffer(); - emit stateChanged( mLastGPSInformation ); + QgsDebugMsgLevel( QStringLiteral( "Processed buffer" ), 3 ); + + QgsDebugMsgLevel( QStringLiteral( "New status is %1" ).arg( mStatus ), 3 ); + if ( mStatus == GPSDataReceived ) + { + emit stateChanged( mLastGPSInformation ); + } } } diff --git a/src/core/labeling/qgslabelfeature.h b/src/core/labeling/qgslabelfeature.h index ea60b3379721..55319daaf3ec 100644 --- a/src/core/labeling/qgslabelfeature.h +++ b/src/core/labeling/qgslabelfeature.h @@ -65,7 +65,7 @@ class CORE_EXPORT QgsLabelFeature * used by the labeling engine to generate candidate placements for the label. For * a vector layer feature this will generally be the feature's geometry. * - * The \a size argument dicates the size of the label's content (e.g. text width and height). + * The \a size argument dictates the size of the label's content (e.g. text width and height). */ QgsLabelFeature( QgsFeatureId id, geos::unique_ptr geometry, QSizeF size ); diff --git a/src/core/layertree/qgscolorramplegendnodesettings.h b/src/core/layertree/qgscolorramplegendnodesettings.h index bf3d816b7ad2..86abaf4f1f6c 100644 --- a/src/core/layertree/qgscolorramplegendnodesettings.h +++ b/src/core/layertree/qgscolorramplegendnodesettings.h @@ -210,7 +210,7 @@ class CORE_EXPORT QgsColorRampLegendNodeSettings bool useContinuousLegend() const; /** - * Sets the flag to use a continuos gradient legend to \a useContinuousLegend. + * Sets the flag to use a continuous gradient legend to \a useContinuousLegend. * * When this flag is set the legend will be rendered using a continuous color ramp with * min and max values, when it is not set the legend will be rendered using separate diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index 0aabc46ac2b7..8dddc7d2c024 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -41,7 +41,10 @@ QgsLayerTreeModel::QgsLayerTreeModel( QgsLayerTree *rootNode, QObject *parent ) , mRootNode( rootNode ) , mFlags( ShowLegend | AllowLegendChangeState | DeferredLegendInvalidation ) { - connectToRootNode(); + if ( rootNode ) + { + connectToRootNode(); + } mFontLayer.setBold( true ); @@ -1072,9 +1075,11 @@ void QgsLayerTreeModel::connectToRootNode() void QgsLayerTreeModel::disconnectFromRootNode() { - disconnect( mRootNode, nullptr, this, nullptr ); - - disconnectFromLayers( mRootNode ); + if ( mRootNode ) + { + disconnect( mRootNode, nullptr, this, nullptr ); + disconnectFromLayers( mRootNode ); + } } void QgsLayerTreeModel::recursivelyEmitDataChanged( const QModelIndex &idx ) diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index a490246636ea..87995e801936 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -908,7 +908,10 @@ void QgsSymbolLegendNode::updateLabel() const bool showFeatureCount = mLayerNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toBool(); QgsVectorLayer *vl = qobject_cast( mLayerNode->layer() ); - mLabel = symbolLabel(); + if ( !mLayerNode->labelExpression().isEmpty() ) + mLabel = "[%" + mLayerNode->labelExpression() + "%]"; + else + mLabel = symbolLabel(); if ( showFeatureCount && vl ) { @@ -940,13 +943,11 @@ QString QgsSymbolLegendNode::evaluateLabel( const QgsExpressionContext &context, if ( label.isEmpty() ) { + const QString symLabel = symbolLabel(); if ( ! mLayerNode->labelExpression().isEmpty() ) mLabel = QgsExpression::replaceExpressionText( "[%" + mLayerNode->labelExpression() + "%]", &contextCopy ); - else if ( mLabel.contains( "[%" ) ) - { - const QString symLabel = symbolLabel(); + else if ( symLabel.contains( "[%" ) ) mLabel = QgsExpression::replaceExpressionText( symLabel, &contextCopy ); - } return mLabel; } else diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h index 15adfae44e55..39385684af37 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.h +++ b/src/core/layertree/qgslayertreemodellegendnode.h @@ -542,6 +542,15 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode */ QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() ); + /** + * Create an expression context scope containing symbol related variables. + * + * The caller takes ownership of the returned object. + * + * \since QGIS 3.36 + */ + QgsExpressionContextScope *createSymbolScope() const SIP_FACTORY; + #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode @@ -571,12 +580,6 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode // ident the symbol icon to make it look like a tree structure static const int INDENT_SIZE = 20; - /** - * Create an expressionContextScope containing symbol related variables - * \since QGIS 3.10 - */ - QgsExpressionContextScope *createSymbolScope() const SIP_FACTORY; - }; diff --git a/src/core/layertree/qgslayertreenode.h b/src/core/layertree/qgslayertreenode.h index 84bd123b58ec..0883bebb4103 100644 --- a/src/core/layertree/qgslayertreenode.h +++ b/src/core/layertree/qgslayertreenode.h @@ -124,7 +124,7 @@ class CORE_EXPORT QgsLayerTreeNode : public QObject QList children() const { return mChildren; } SIP_SKIP /** - * Removes the childrens, disconnect all the forwarded and external signals and sets their parent to NULLPTR + * Removes the children, disconnect all the forwarded and external signals and sets their parent to NULLPTR * \return the removed children * \since QGIS 3.16 */ diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 10f44858a7b2..438f431af847 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -120,6 +120,7 @@ bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument & mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) ); mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt(); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, mHideCoverage ); emit toggled( mEnabled ); emit changed(); diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index a9a5f4a8d8bb..6d1a02d3cfda 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -46,7 +46,7 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout ) : QgsLayoutItem( layout ) - , mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot(), this ) ) + , mLegendModel( new QgsLegendModel( nullptr, this ) ) { #if 0 //no longer required? connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded ); @@ -105,6 +105,8 @@ void QgsLayoutItemLegend::paint( QPainter *painter, const QStyleOptionGraphicsIt if ( !painter ) return; + ensureModelIsInitialized(); + if ( mFilterAskedForUpdate ) { mFilterAskedForUpdate = false; @@ -309,11 +311,28 @@ bool QgsLayoutItemLegend::resizeToContents() const void QgsLayoutItemLegend::setCustomLayerTree( QgsLayerTree *rootGroup ) { - mLegendModel->setRootGroup( rootGroup ? rootGroup : ( mLayout ? mLayout->project()->layerTreeRoot() : nullptr ) ); + if ( !mDeferLegendModelInitialization ) + { + mLegendModel->setRootGroup( rootGroup ? rootGroup : ( mLayout ? mLayout->project()->layerTreeRoot() : nullptr ) ); + } mCustomLayerTree.reset( rootGroup ); } +void QgsLayoutItemLegend::ensureModelIsInitialized() +{ + if ( mDeferLegendModelInitialization ) + { + mDeferLegendModelInitialization = false; + setCustomLayerTree( mCustomLayerTree.release() ); + } +} + +QgsLegendModel *QgsLayoutItemLegend::model() +{ + ensureModelIsInitialized(); + return mLegendModel.get(); +} void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate ) { @@ -1020,7 +1039,10 @@ void QgsLayoutItemLegend::clearLegendCachedData() } }; - clearNodeCache( mLegendModel->rootGroup() ); + if ( QgsLayerTree *rootGroup = mLegendModel->rootGroup() ) + { + clearNodeCache( rootGroup ); + } } void QgsLayoutItemLegend::mapLayerStyleOverridesChanged() @@ -1383,36 +1405,6 @@ QVariant QgsLegendModel::data( const QModelIndex &index, int role ) const return name; } } - - const bool evaluate = ( vlayer && !nodeLayer->labelExpression().isEmpty() ) || name.contains( "[%" ); - if ( evaluate ) - { - QgsExpressionContext expressionContext; - if ( vlayer ) - { - connect( vlayer, &QgsVectorLayer::symbolFeatureCountMapChanged, this, &QgsLegendModel::forceRefresh, Qt::UniqueConnection ); - // counting is done here to ensure that a valid vector layer needs to be evaluated, count is used to validate previous count or update the count if invalidated - vlayer->countSymbolFeatures(); - } - - if ( mLayoutLegend ) - expressionContext = mLayoutLegend->createExpressionContext(); - - const QList legendnodes = layerLegendNodes( nodeLayer, false ); - if ( ! legendnodes.isEmpty() ) - { - if ( legendnodes.count() > 1 ) // evaluate all existing legend nodes but leave the name for the legend evaluator - { - for ( QgsLayerTreeModelLegendNode *treenode : legendnodes ) - { - if ( QgsSymbolLegendNode *symnode = qobject_cast( treenode ) ) - symnode->evaluateLabel( expressionContext ); - } - } - else if ( QgsSymbolLegendNode *symnode = qobject_cast( legendnodes.first() ) ) - symnode->evaluateLabel( expressionContext ); - } - } node->setCustomProperty( QStringLiteral( "cached_name" ), name ); return name; } diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index b4995d1201ee..0302c18141a7 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -23,7 +23,7 @@ #include "qgslayoutitem.h" #include "qgslayertreemodel.h" #include "qgslegendsettings.h" -#include "qgslayertreegroup.h" +#include "qgslayertree.h" #include "qgsexpressioncontext.h" class QgsLayerTreeModel; @@ -156,7 +156,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem /** * Returns the legend model. */ - QgsLegendModel *model() { return mLegendModel.get(); } + QgsLegendModel *model(); /** * Sets whether the legend content should auto update to reflect changes in the project's @@ -634,8 +634,10 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem void setModelStyleOverrides( const QMap &overrides ); + void ensureModelIsInitialized(); std::unique_ptr< QgsLegendModel > mLegendModel; - std::unique_ptr< QgsLayerTreeGroup > mCustomLayerTree; + std::unique_ptr< QgsLayerTree > mCustomLayerTree; + bool mDeferLegendModelInitialization = true; QgsLegendSettings mSettings; diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index d6de637f07da..ac8e60108801 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -834,6 +834,12 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum mapElem.setAttribute( QStringLiteral( "temporalRangeEnd" ), temporalRange().end().toString( Qt::ISODate ) ); } + mapElem.setAttribute( QStringLiteral( "enableZRange" ), mZRangeEnabled ? 1 : 0 ); + if ( mZRange.lower() != std::numeric_limits< double >::lowest() ) + mapElem.setAttribute( QStringLiteral( "zRangeLower" ), qgsDoubleToString( mZRange.lower() ) ); + if ( mZRange.upper() != std::numeric_limits< double >::max() ) + mapElem.setAttribute( QStringLiteral( "zRangeUpper" ), qgsDoubleToString( mZRange.upper() ) ); + mAtlasClippingSettings->writeXml( mapElem, doc, context ); mItemClippingSettings->writeXml( mapElem, doc, context ); @@ -1037,6 +1043,20 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c setTemporalRange( QgsDateTimeRange( begin, end, true, begin == end ) ); } + mZRangeEnabled = itemElem.attribute( QStringLiteral( "enableZRange" ) ).toInt(); + bool ok = false; + double zLower = itemElem.attribute( QStringLiteral( "zRangeLower" ) ).toDouble( &ok ); + if ( !ok ) + { + zLower = std::numeric_limits< double >::lowest(); + } + double zUpper = itemElem.attribute( QStringLiteral( "zRangeUpper" ) ).toDouble( &ok ); + if ( !ok ) + { + zUpper = std::numeric_limits< double >::max(); + } + mZRange = QgsDoubleRange( zLower, zUpper ); + mUpdatesEnabled = true; return true; } @@ -1764,6 +1784,11 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF if ( isTemporal() ) jobMapSettings.setTemporalRange( temporalRange() ); + if ( mZRangeEnabled ) + { + jobMapSettings.setZRange( mZRange ); + } + if ( mAtlasClippingSettings->enabled() && mLayout->reportContext().feature().isValid() ) { QgsGeometry clipGeom( mLayout->reportContext().currentGeometry( jobMapSettings.destinationCrs() ) ); @@ -1910,6 +1935,9 @@ QgsExpressionContext QgsLayoutItemMap::createExpressionContext() const scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_end_time" ), isTemporal() ? temporalRange().end() : QVariant(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_interval" ), isTemporal() ? QgsInterval( temporalRange().end() - temporalRange().begin() ) : QVariant(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_z_range_lower" ), mZRangeEnabled ? mZRange.lower() : QVariant(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "map_z_range_upper" ), mZRangeEnabled ? mZRange.upper() : QVariant(), true ) ); + #if 0 // not relevant here! (but left so as to respect all the dangerous warnings in QgsExpressionContextUtils::mapSettingsScope) if ( mapSettings.frameRate() >= 0 ) scope->setVariable( QStringLiteral( "frame_rate" ), mapSettings.frameRate(), true ); @@ -2141,6 +2169,19 @@ void QgsLayoutItemMap::refreshDataDefinedProperty( const QgsLayoutObject::DataDe setTemporalRange( QgsDateTimeRange( begin, end, true, begin == end ) ); } + if ( mZRangeEnabled && ( property == QgsLayoutObject::DataDefinedProperty::MapZRangeLower || property == QgsLayoutObject::DataDefinedProperty::MapZRangeUpper || property == QgsLayoutObject::DataDefinedProperty::AllProperties ) ) + { + double zLower = mZRange.lower(); + double zUpper = mZRange.upper(); + + if ( property == QgsLayoutObject::DataDefinedProperty::MapZRangeLower || property == QgsLayoutObject::DataDefinedProperty::AllProperties ) + zLower = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MapZRangeLower, context, zLower ); + if ( property == QgsLayoutObject::DataDefinedProperty::MapZRangeUpper || property == QgsLayoutObject::DataDefinedProperty::AllProperties ) + zUpper = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MapZRangeUpper, context, zUpper ); + + mZRange = QgsDoubleRange( zLower, zUpper ); + } + //force redraw mCacheInvalidated = true; @@ -2292,6 +2333,26 @@ QTransform QgsLayoutItemMap::layoutToMapCoordsTransform() const return mapTransform; } +void QgsLayoutItemMap::setZRangeEnabled( bool enabled ) +{ + mZRangeEnabled = enabled; +} + +bool QgsLayoutItemMap::zRangeEnabled() const +{ + return mZRangeEnabled; +} + +QgsDoubleRange QgsLayoutItemMap::zRange() const +{ + return mZRange; +} + +void QgsLayoutItemMap::setZRange( const QgsDoubleRange &range ) +{ + mZRange = range; +} + QList QgsLayoutItemMap::createLabelBlockingRegions( const QgsMapSettings & ) const { const QTransform mapTransform = layoutToMapCoordsTransform(); diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index 3828d06b27fe..38c4a8015797 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -863,6 +863,49 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan */ QgsLayoutItemMapItemClipPathSettings *itemClippingSettings() { return mItemClippingSettings; } + /** + * Sets whether the z range is \a enabled (i.e. whether the map will be filtered + * to content within the zRange().) + * + * \see zRangeEnabled() + * \since QGIS 3.38 + */ + void setZRangeEnabled( bool enabled ); + + /** + * Returns whether the z range is enabled (i.e. whether the map will be filtered + * to content within the zRange().) + * + * \see setZRangeEnabled() + * \see zRange() + * \since QGIS 3.38 + */ + bool zRangeEnabled() const; + + /** + * Returns the map's z range, which is used to filter the map's content to only + * display features within the specified z range. + * + * \note This is only considered when zRangeEnabled() is TRUE. + * + * \see setZRange() + * \see zRangeEnabled() + * \since QGIS 3.38 + */ + QgsDoubleRange zRange() const; + + /** + * Sets the map's z \a range, which is used to filter the map's content to only + * display features within the specified z range. + * + * \note This is only considered when zRangeEnabled() is TRUE. + * + * \see zRange() + * \see setZRangeEnabled() + * \since QGIS 3.38 + */ + void setZRange( const QgsDoubleRange &range ); + // Reimplement estimatedFrameBleed to take the grid frame into account double estimatedFrameBleed() const override; @@ -1032,6 +1075,9 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan */ double mEvaluatedMapRotation = 0; + bool mZRangeEnabled = false; + QgsDoubleRange mZRange; + //! Flag if layers to be displayed should be read from qgis canvas (TRUE) or from stored list in mLayerSet (FALSE) bool mKeepLayerSet = false; diff --git a/src/core/layout/qgslayoutitemnodeitem.h b/src/core/layout/qgslayoutitemnodeitem.h index b71c032e28ff..89145c413de8 100644 --- a/src/core/layout/qgslayoutitemnodeitem.h +++ b/src/core/layout/qgslayoutitemnodeitem.h @@ -47,7 +47,7 @@ class CORE_EXPORT QgsLayoutNodesItem: public QgsLayoutItem * Add a node in current shape. * \param point is the location of the new node (in scene coordinates) * \param checkArea is a flag to indicate if there's a space constraint. - * \param radius is the space contraint and is used only if checkArea is + * \param radius is the space constraint and is used only if checkArea is * TRUE. Typically, if this flag is TRUE, the new node has to be nearer * than radius to the shape to be added. */ diff --git a/src/core/layout/qgslayoutobject.cpp b/src/core/layout/qgslayoutobject.cpp index 5daddde447d6..9ae2bc89a341 100644 --- a/src/core/layout/qgslayoutobject.cpp +++ b/src/core/layout/qgslayoutobject.cpp @@ -87,6 +87,11 @@ void QgsLayoutObject::initPropertyDefinitions() { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsRight ), QgsPropertyDefinition( "dataDefinedMapGridFrameDivisionsRight", QgsPropertyDefinition::DataTypeString, QObject::tr( "Map grid frame divisions display right" ), QObject::tr( "string " ) + QLatin1String( "[all|x_only|y_only|disabled]" ) ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsTop ), QgsPropertyDefinition( "dataDefinedMapGridFrameDivisionsTop", QgsPropertyDefinition::DataTypeString, QObject::tr( "Map grid frame divisions display top" ), QObject::tr( "string " ) + QLatin1String( "[all|x_only|y_only|disabled]" ) ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapGridFrameDivisionsBottom ), QgsPropertyDefinition( "dataDefinedMapGridFrameDivisionsBottom", QgsPropertyDefinition::DataTypeString, QObject::tr( "Map grid frame divisions display bottom" ), QObject::tr( "string " ) + QLatin1String( "[all|x_only|y_only|disabled]" ) ) }, + { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapCrs ), QgsPropertyDefinition( "dataDefinedCrs", QgsPropertyDefinition::DataTypeString, QObject::tr( "Map CRS" ), QObject::tr( "string representing a CRS, either an authority/id pair (e.g. 'EPSG:4326'), a proj string prefixes by \"PROJ:\" (e.g. 'PROJ: +proj=...') or a WKT string prefixed by \"WKT:\" (e.g. 'WKT:GEOGCRS[\"WGS 84\"...]')" ) ) }, + { static_cast< int >( QgsLayoutObject::DataDefinedProperty::StartDateTime ), QgsPropertyDefinition( "dataDefinedStartDateTime", QObject::tr( "Temporal range start date / time" ), QgsPropertyDefinition::DateTime ) }, + { static_cast< int >( QgsLayoutObject::DataDefinedProperty::EndDateTime ), QgsPropertyDefinition( "dataDefinedEndDateTime", QObject::tr( "Temporal range end date / time" ), QgsPropertyDefinition::DateTime ) }, + { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapZRangeLower ), QgsPropertyDefinition( "dataDefinedZRangeLower", QObject::tr( "Z range lower limit" ), QgsPropertyDefinition::Double ) }, + { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapZRangeUpper ), QgsPropertyDefinition( "dataDefinedZRangeUpper", QObject::tr( "Z range upper limit" ), QgsPropertyDefinition::Double ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::PictureSource ), QgsPropertyDefinition( "dataDefinedSource", QObject::tr( "Picture source (URL)" ), QgsPropertyDefinition::String ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::SourceUrl ), QgsPropertyDefinition( "dataDefinedSourceUrl", QObject::tr( "Source URL" ), QgsPropertyDefinition::String ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::PictureSvgBackgroundColor ), QgsPropertyDefinition( "dataDefinedSvgBackgroundColor", QObject::tr( "SVG background color" ), QgsPropertyDefinition::ColorWithAlpha ) }, @@ -99,9 +104,6 @@ void QgsLayoutObject::initPropertyDefinitions() { static_cast< int >( QgsLayoutObject::DataDefinedProperty::ScalebarLineColor ), QgsPropertyDefinition( "dataDefinedScalebarLineColor", QObject::tr( "Line color" ), QgsPropertyDefinition::ColorWithAlpha ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::ScalebarLineWidth ), QgsPropertyDefinition( "dataDefinedScalebarLineWidth", QObject::tr( "Line width" ), QgsPropertyDefinition::StrokeWidth ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::AttributeTableSourceLayer ), QgsPropertyDefinition( "dataDefinedAttributeTableSourceLayer", QObject::tr( "Table source layer" ), QgsPropertyDefinition::String ) }, - { static_cast< int >( QgsLayoutObject::DataDefinedProperty::MapCrs ), QgsPropertyDefinition( "dataDefinedCrs", QgsPropertyDefinition::DataTypeString, QObject::tr( "Map CRS" ), QObject::tr( "string representing a CRS, either an authority/id pair (e.g. 'EPSG:4326'), a proj string prefixes by \"PROJ:\" (e.g. 'PROJ: +proj=...') or a WKT string prefixed by \"WKT:\" (e.g. 'WKT:GEOGCRS[\"WGS 84\"...]')" ) ) }, - { static_cast< int >( QgsLayoutObject::DataDefinedProperty::StartDateTime ), QgsPropertyDefinition( "dataDefinedStartDateTime", QObject::tr( "Temporal range start date / time" ), QgsPropertyDefinition::DateTime ) }, - { static_cast< int >( QgsLayoutObject::DataDefinedProperty::EndDateTime ), QgsPropertyDefinition( "dataDefinedEndDateTime", QObject::tr( "Temporal range end date / time" ), QgsPropertyDefinition::DateTime ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::ScalebarLeftSegments ), QgsPropertyDefinition( "dataDefinedScaleBarLeftSegments", QObject::tr( "Segments to the left of 0" ), QgsPropertyDefinition::IntegerPositive )}, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::ScalebarRightSegments ), QgsPropertyDefinition( "dataDefinedScaleBarRightSegments", QObject::tr( "Segments to the right of 0" ), QgsPropertyDefinition::IntegerPositive ) }, { static_cast< int >( QgsLayoutObject::DataDefinedProperty::ScalebarSegmentWidth ), QgsPropertyDefinition( "dataDefinedScalebarSegmentWidth", QObject::tr( "Length of a segment in map units" ), QgsPropertyDefinition::DoublePositive ) }, @@ -207,6 +209,8 @@ bool QgsLayoutObject::propertyAssociatesWithParentMultiframe( QgsLayoutObject::D case QgsLayoutObject::DataDefinedProperty::MapCrs: case QgsLayoutObject::DataDefinedProperty::StartDateTime: case QgsLayoutObject::DataDefinedProperty::EndDateTime: + case QgsLayoutObject::DataDefinedProperty::MapZRangeLower: + case QgsLayoutObject::DataDefinedProperty::MapZRangeUpper: case QgsLayoutObject::DataDefinedProperty::ElevationProfileTolerance: case QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMajorInterval: case QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMinorInterval: diff --git a/src/core/layout/qgslayoutobject.h b/src/core/layout/qgslayoutobject.h index cfb951520ff0..beaadf87b07f 100644 --- a/src/core/layout/qgslayoutobject.h +++ b/src/core/layout/qgslayoutobject.h @@ -197,6 +197,11 @@ class CORE_EXPORT QgsLayoutObject: public QObject, public QgsExpressionContextGe MapGridFrameDivisionsRight, //!< Map frame division display right MapGridFrameDivisionsTop, //!< Map frame division display top MapGridFrameDivisionsBottom, //!< Map frame division display bottom + MapCrs, //!< Map CRS + StartDateTime, //!< Temporal range's start DateTime + EndDateTime, //!< Temporal range's end DateTime + MapZRangeLower, //!< Map frame Z-range lower value (since QGIS 3.38) + MapZRangeUpper, //!< Map frame Z-range lower value (since QGIS 3.38) //composer picture PictureSource, //!< Picture source url PictureSvgBackgroundColor, //!< SVG background color @@ -222,9 +227,6 @@ class CORE_EXPORT QgsLayoutObject: public QObject, public QgsExpressionContextGe ScalebarLineWidth, //!< Scalebar line width (deprecated, use data defined properties on scalebar line symbol instead) //table item AttributeTableSourceLayer, //!< Attribute table source layer - MapCrs, //!< Map CRS - StartDateTime, //!< Temporal range's start DateTime - EndDateTime, //!< Temporal range's end DateTime ElevationProfileTolerance, //!< Tolerance distance for elevation profiles (since QGIS 3.30) ElevationProfileDistanceMajorInterval, //!< Major grid line interval for elevation profile distance axis (since QGIS 3.30) ElevationProfileDistanceMinorInterval, //!< Minor grid line interval for elevation profile distance axis (since QGIS 3.30) diff --git a/src/core/locator/qgslocatorfilter.cpp b/src/core/locator/qgslocatorfilter.cpp index 98bf96b84d86..9f9d1a031ec0 100644 --- a/src/core/locator/qgslocatorfilter.cpp +++ b/src/core/locator/qgslocatorfilter.cpp @@ -101,12 +101,12 @@ void QgsLocatorFilter::logMessage( const QString &message, Qgis::MessageLevel le } -QVariant QgsLocatorResult::getUserData() const +QVariant QgsLocatorResult::userData() const { return mUserData; } -void QgsLocatorResult::setUserData( QVariant userData ) +void QgsLocatorResult::setUserData( const QVariant &userData ) { mUserData = userData; } diff --git a/src/core/locator/qgslocatorfilter.h b/src/core/locator/qgslocatorfilter.h index 2b30efd2b915..cf78c3e3bee4 100644 --- a/src/core/locator/qgslocatorfilter.h +++ b/src/core/locator/qgslocatorfilter.h @@ -57,14 +57,14 @@ class CORE_EXPORT QgsLocatorResult * * \since QGIS 3.18 */ - QVariant getUserData() const; + QVariant userData() const SIP_PYNAME( _userData ); /** * Set \a userData for the locator result * * \since QGIS 3.34 */ - void setUserData( QVariant userData ); + void setUserData( const QVariant &userData ); /** * Filter from which the result was obtained. This is automatically set. @@ -136,10 +136,6 @@ class CORE_EXPORT QgsLocatorResult */ QList actions; -#ifdef SIP_RUN - SIP_PROPERTY( name = userData, get = getUserData, set = setUserData ) -#endif - private: /** diff --git a/src/core/maprenderer/qgsmaprendererjob.cpp b/src/core/maprenderer/qgsmaprendererjob.cpp index e9057357bf0b..3d10b8e7f337 100644 --- a/src/core/maprenderer/qgsmaprendererjob.cpp +++ b/src/core/maprenderer/qgsmaprendererjob.cpp @@ -512,7 +512,7 @@ std::vector QgsMapRendererJob::prepareJobs( QPainter *painter, Q continue; } - if ( !mSettings.zRange().isInfinite() && ml->elevationProperties() && !ml->elevationProperties()->isVisibleInZRange( mSettings.zRange() ) ) + if ( !mSettings.zRange().isInfinite() && ml->elevationProperties() && !ml->elevationProperties()->isVisibleInZRange( mSettings.zRange(), ml ) ) { QgsDebugMsgLevel( QStringLiteral( "Layer not rendered because it is not visible within the map's z range" ), 3 ); continue; diff --git a/src/core/mesh/qgsmeshadvancedediting.cpp b/src/core/mesh/qgsmeshadvancedediting.cpp index 58eee89c44e2..3f72f9cc22bf 100644 --- a/src/core/mesh/qgsmeshadvancedediting.cpp +++ b/src/core/mesh/qgsmeshadvancedediting.cpp @@ -475,7 +475,7 @@ bool QgsMeshEditRefineFaces::createNewBorderFaces( QgsMeshEditor *meshEditor, QgsTopologicalMesh::TopologicalFaces topologicalFaces = QgsTopologicalMesh::createNewTopologicalFaces( faces, false, error ); QVector neighborhood = topologicalFaces.facesNeighborhood(); - // reindex internal neighborhod + // reindex internal neighborhood for ( int i = 0; i < neighborhood.count(); ++i ) { QgsTopologicalMesh::FaceNeighbors &neighbors = neighborhood[i]; diff --git a/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h index 01316cf32891..157ee66ed845 100644 --- a/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h +++ b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h @@ -131,7 +131,7 @@ class CORE_EXPORT QgsMeshDataProviderTemporalCapabilities: public QgsDataProvide qint64 datasetTime( const QgsMeshDatasetIndex &index ) const; /** - * Clears alls stored reference times and dataset times + * Clears all stored reference times and dataset times */ void clear(); diff --git a/src/core/mesh/qgsmeshdataset.cpp b/src/core/mesh/qgsmeshdataset.cpp index 26109668e08e..56d5dc6830f5 100644 --- a/src/core/mesh/qgsmeshdataset.cpp +++ b/src/core/mesh/qgsmeshdataset.cpp @@ -489,12 +489,12 @@ QgsMeshDatasetGroupTreeItem::QgsMeshDatasetGroupTreeItem( const QDomElement &ite dependOnElement = dependOnElement.nextSiblingElement( QStringLiteral( "dependent-on-item" ) ); } - QDomElement dependencieElement = itemElement.firstChildElement( QStringLiteral( "dependency-item" ) ); - while ( !dependencieElement.isNull() ) + QDomElement dependencyElement = itemElement.firstChildElement( QStringLiteral( "dependency-item" ) ); + while ( !dependencyElement.isNull() ) { - if ( dependencieElement.hasAttribute( QStringLiteral( "dataset-index" ) ) ) - mDatasetGroupDependencies.append( dependencieElement.attribute( QStringLiteral( "dataset-index" ) ).toInt() ); - dependencieElement = dependencieElement.nextSiblingElement( QStringLiteral( "dependency-item" ) ); + if ( dependencyElement.hasAttribute( QStringLiteral( "dataset-index" ) ) ) + mDatasetGroupDependencies.append( dependencyElement.attribute( QStringLiteral( "dataset-index" ) ).toInt() ); + dependencyElement = dependencyElement.nextSiblingElement( QStringLiteral( "dependency-item" ) ); } QDomElement childElement = itemElement.firstChildElement( QStringLiteral( "mesh-dataset-group-tree-item" ) ); @@ -710,9 +710,9 @@ QDomElement QgsMeshDatasetGroupTreeItem::writeXml( QDomDocument &doc, const QgsR for ( const int index : mDatasetGroupDependencies ) { - QDomElement dependencieElement = doc.createElement( QStringLiteral( "dependency-item" ) ); - dependencieElement.setAttribute( QStringLiteral( "dataset-index" ), index ); - itemElement.appendChild( dependencieElement ); + QDomElement dependencyElement = doc.createElement( QStringLiteral( "dependency-item" ) ); + dependencyElement.setAttribute( QStringLiteral( "dataset-index" ), index ); + itemElement.appendChild( dependencyElement ); } for ( int i = 0; i < mChildren.count(); ++i ) @@ -975,7 +975,7 @@ QDomElement QgsMeshMemoryDatasetGroup::writeXml( QDomDocument &doc, const QgsRea void QgsMeshDatasetGroup::calculateStatistic() const { - updateStatictic(); + updateStatistic(); } void QgsMeshDatasetGroup::setStatisticObsolete() const @@ -998,7 +998,7 @@ void QgsMeshDatasetGroup::setReferenceTime( const QDateTime &referenceTime ) mReferenceTime = referenceTime; } -void QgsMeshDatasetGroup::updateStatictic() const +void QgsMeshDatasetGroup::updateStatistic() const { if ( !mIsStatisticObsolete ) return; @@ -1036,13 +1036,13 @@ QgsMeshDatasetGroup::QgsMeshDatasetGroup( const QString &name ): mName( name ) { double QgsMeshDatasetGroup::minimum() const { - updateStatictic(); + updateStatistic(); return mMinimum; } double QgsMeshDatasetGroup::maximum() const { - updateStatictic(); + updateStatistic(); return mMaximum; } diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h index 951ba83e73b7..ee0d34c9df93 100644 --- a/src/core/mesh/qgsmeshdataset.h +++ b/src/core/mesh/qgsmeshdataset.h @@ -688,7 +688,7 @@ class CORE_EXPORT QgsMeshDatasetGroup mutable double mMaximum = std::numeric_limits::quiet_NaN(); mutable bool mIsStatisticObsolete = true; - void updateStatictic() const; + void updateStatistic() const; QDateTime mReferenceTime; }; diff --git a/src/core/mesh/qgsmeshdatasetgroupstore.cpp b/src/core/mesh/qgsmeshdatasetgroupstore.cpp index f0bdeb226787..de83be7d25d1 100644 --- a/src/core/mesh/qgsmeshdatasetgroupstore.cpp +++ b/src/core/mesh/qgsmeshdatasetgroupstore.cpp @@ -24,7 +24,7 @@ QList QgsMeshDatasetGroupStore::datasetGroupIndexes() const { - return mRegistery.keys(); + return mRegistry.keys(); } QList QgsMeshDatasetGroupStore::enabledDatasetGroupIndexes() const @@ -34,7 +34,7 @@ QList QgsMeshDatasetGroupStore::enabledDatasetGroupIndexes() const int QgsMeshDatasetGroupStore::datasetGroupCount() const { - return mRegistery.count(); + return mRegistry.count(); } int QgsMeshDatasetGroupStore::extraDatasetGroupCount() const @@ -74,7 +74,7 @@ void QgsMeshDatasetGroupStore::setPersistentProvider( QgsMeshDataProvider *provi QgsMeshDatasetGroupStore::DatasetGroup QgsMeshDatasetGroupStore::datasetGroup( int index ) const { - return mRegistery.value( index, DatasetGroup{nullptr, -1} ); + return mRegistry.value( index, DatasetGroup{nullptr, -1} ); } bool QgsMeshDatasetGroupStore::addPersistentDatasets( const QString &path ) @@ -285,8 +285,8 @@ QDomElement QgsMeshDatasetGroupStore::writeXml( QDomDocument &doc, const QgsRead QDomElement storeElement = doc.createElement( QStringLiteral( "mesh-dataset-groups-store" ) ); storeElement.appendChild( mDatasetGroupTreeRootItem->writeXml( doc, context ) ); - QMap < int, DatasetGroup>::const_iterator it = mRegistery.constBegin(); - while ( it != mRegistery.constEnd() ) + QMap < int, DatasetGroup>::const_iterator it = mRegistry.constBegin(); + while ( it != mRegistry.constEnd() ) { QDomElement elemDataset; if ( it.value().first == mPersistentProvider ) @@ -327,7 +327,7 @@ QDomElement QgsMeshDatasetGroupStore::writeXml( QDomDocument &doc, const QgsRead void QgsMeshDatasetGroupStore::readXml( const QDomElement &storeElem, const QgsReadWriteContext &context ) { Q_UNUSED( context ); - mRegistery.clear(); + mRegistry.clear(); QDomElement datasetElem = storeElem.firstChildElement( "mesh-dataset" ); QMap extraDatasetGroups; while ( !datasetElem.isNull() ) @@ -351,7 +351,7 @@ void QgsMeshDatasetGroupStore::readXml( const QDomElement &storeElem, const QgsR extraDatasetGroups[globalIndex] = dsg; int sourceIndex = mExtraDatasets.addDatasetGroup( dsg ); - mRegistery[globalIndex] = DatasetGroup{source, sourceIndex}; + mRegistry[globalIndex] = DatasetGroup{source, sourceIndex}; } else { @@ -383,7 +383,7 @@ void QgsMeshDatasetGroupStore::readXml( const QDomElement &storeElem, const QgsR int QgsMeshDatasetGroupStore::globalDatasetGroupIndexInSource( QgsMeshDatasetSourceInterface *source, int nativeGroupIndex ) const { - for ( QMap::const_iterator it = mRegistery.cbegin(); it != mRegistery.cend(); ++it ) + for ( QMap::const_iterator it = mRegistry.cbegin(); it != mRegistry.cend(); ++it ) { if ( it.value().first == source && it.value().second == nativeGroupIndex ) return it.key(); @@ -415,7 +415,7 @@ bool QgsMeshDatasetGroupStore::saveDatasetGroup( QString filePath, int groupInde eraseDatasetGroup( group ); group.first = mPersistentProvider; group.second = mPersistentProvider->datasetGroupCount() - 1; - mRegistery[groupIndex] = group; + mRegistry[groupIndex] = group; //update the item type if ( mDatasetGroupTreeRootItem ) { @@ -441,7 +441,7 @@ void QgsMeshDatasetGroupStore::onPersistentDatasetAdded( int count ) if ( mGroupNameToGlobalIndex.empty() && i < mPersistentExtraDatasetGroupIndexes.count() ) { // This happens with QGIS project saved with version < 3.28 - mRegistery[mPersistentExtraDatasetGroupIndexes.at( i )] = DatasetGroup( mPersistentProvider, i ); + mRegistry[mPersistentExtraDatasetGroupIndexes.at( i )] = DatasetGroup( mPersistentProvider, i ); } else if ( mGroupNameToGlobalIndex.contains( groupName ) ) { @@ -475,11 +475,11 @@ void QgsMeshDatasetGroupStore::removePersistentProvider() disconnect( mPersistentProvider, &QgsMeshDataProvider::datasetGroupsAdded, this, &QgsMeshDatasetGroupStore::onPersistentDatasetAdded ); - QMap < int, DatasetGroup>::iterator it = mRegistery.begin(); - while ( it != mRegistery.end() ) + QMap < int, DatasetGroup>::iterator it = mRegistry.begin(); + while ( it != mRegistry.end() ) { if ( it.value().first == mPersistentProvider ) - it = mRegistery.erase( it ); + it = mRegistry.erase( it ); else ++it; } @@ -489,7 +489,7 @@ void QgsMeshDatasetGroupStore::removePersistentProvider() int QgsMeshDatasetGroupStore::newIndex() { - QSet usedIndex = qgis::listToSet( mRegistery.keys() ); + QSet usedIndex = qgis::listToSet( mRegistry.keys() ); usedIndex.unite( qgis::listToSet( mGroupNameToGlobalIndex.values() ) ); int index = 0; @@ -509,10 +509,10 @@ int QgsMeshDatasetGroupStore::registerDatasetGroup( const QgsMeshDatasetGroupSto { groupIndex = it.value(); - if ( mRegistery.contains( groupIndex ) ) + if ( mRegistry.contains( groupIndex ) ) { - QgsDebugError( QStringLiteral( "Dupplicate group name for %1." ).arg( name ) ); - return -1; // The registery has already a group with this index, we can't have two groups with the same name + QgsDebugError( QStringLiteral( "Duplicate group name for %1." ).arg( name ) ); + return -1; // The registry has already a group with this index, we can't have two groups with the same name } } else @@ -521,7 +521,7 @@ int QgsMeshDatasetGroupStore::registerDatasetGroup( const QgsMeshDatasetGroupSto mGroupNameToGlobalIndex.insert( name, groupIndex ); } - mRegistery[groupIndex] = group; + mRegistry[groupIndex] = group; return groupIndex; } @@ -538,8 +538,8 @@ void QgsMeshDatasetGroupStore::eraseExtraDataset( int indexInExtraStore ) mExtraDatasets.removeDatasetGroup( indexInExtraStore ); //search dataset with index greater than indexInExtraStore and decrement it - QMap < int, DatasetGroup>::iterator it = mRegistery.begin(); - while ( it != mRegistery.end() ) + QMap < int, DatasetGroup>::iterator it = mRegistry.begin(); + while ( it != mRegistry.end() ) { int localIndex = it.value().second; if ( it.value().first == &mExtraDatasets && localIndex > indexInExtraStore ) @@ -565,10 +565,10 @@ void QgsMeshDatasetGroupStore::checkDatasetConsistency( QgsMeshDatasetSourceInte if ( !indexes.isEmpty() ) createDatasetGroupTreeItems( indexes ); - const QList globalIndexes = mRegistery.keys(); + const QList globalIndexes = mRegistry.keys(); for ( int globalIndex : globalIndexes ) { - if ( mRegistery.value( globalIndex ).first == source ) + if ( mRegistry.value( globalIndex ).first == source ) syncItemToDatasetGroup( globalIndex ); } } @@ -584,7 +584,7 @@ void QgsMeshDatasetGroupStore::removeUnregisteredItemFromTree() { QgsMeshDatasetGroupTreeItem *item = itemsToCheck.takeFirst(); int globalIndex = item->datasetGroupIndex(); - if ( !mRegistery.contains( globalIndex ) ) + if ( !mRegistry.contains( globalIndex ) ) indexItemToRemove.append( globalIndex ); for ( int i = 0; i < item->childCount(); ++i ) itemsToCheck.append( item->child( i ) ); @@ -602,19 +602,19 @@ void QgsMeshDatasetGroupStore::unregisterGroupNotPresentInTree() { if ( !mDatasetGroupTreeRootItem ) { - mRegistery.clear(); + mRegistry.clear(); return; } - QMap < int, DatasetGroup>::iterator it = mRegistery.begin(); - while ( it != mRegistery.end() ) + QMap < int, DatasetGroup>::iterator it = mRegistry.begin(); + while ( it != mRegistry.end() ) { DatasetGroup datasetGroup = it.value(); int globalIndex = it.key(); if ( ! mDatasetGroupTreeRootItem->childFromDatasetGroupIndex( globalIndex ) // Not in the tree item && datasetGroup.first != mPersistentProvider ) // and not persistent { - it = mRegistery.erase( it ); //remove from registery + it = mRegistry.erase( it ); //remove from registry eraseDatasetGroup( datasetGroup ); //remove from where the dataset group is stored } else diff --git a/src/core/mesh/qgsmeshdatasetgroupstore.h b/src/core/mesh/qgsmeshdatasetgroupstore.h index 42fb45ac69bf..da4bc2cbb197 100644 --- a/src/core/mesh/qgsmeshdatasetgroupstore.h +++ b/src/core/mesh/qgsmeshdatasetgroupstore.h @@ -120,7 +120,7 @@ class QgsMeshDatasetGroupStore: public QObject { Q_OBJECT - //! Contains a pointer to the dataset source inerface and the index on this dataset groups container + //! Contains a pointer to the dataset source interface and the index on this dataset groups container typedef QPair DatasetGroup; public: @@ -252,7 +252,7 @@ class QgsMeshDatasetGroupStore: public QObject QgsMeshLayer *mLayer = nullptr; QgsMeshDataProvider *mPersistentProvider = nullptr; QgsMeshExtraDatasetStore mExtraDatasets; - QMap < int, DatasetGroup> mRegistery; + QMap < int, DatasetGroup> mRegistry; QList mPersistentExtraDatasetGroupIndexes; QMap mGroupNameToGlobalIndex; std::unique_ptr mDatasetGroupTreeRootItem; diff --git a/src/core/mesh/qgsmesheditor.h b/src/core/mesh/qgsmesheditor.h index d773b7020c3e..ea0f127101db 100644 --- a/src/core/mesh/qgsmesheditor.h +++ b/src/core/mesh/qgsmesheditor.h @@ -72,7 +72,7 @@ class CORE_EXPORT QgsMeshEditor : public QObject //! Constructor with a specified layer \a meshLayer QgsMeshEditor( QgsMeshLayer *meshLayer ); - //! Constructor with a specific mesh \a nativeMesh and an associatd triangular mesh \a triangularMesh + //! Constructor with a specific mesh \a nativeMesh and an associated triangular mesh \a triangularMesh QgsMeshEditor( QgsMesh *nativeMesh, QgsTriangularMesh *triangularMesh, QObject *parent = nullptr ); SIP_SKIP ~QgsMeshEditor(); diff --git a/src/core/mesh/qgsmeshforcebypolylines.cpp b/src/core/mesh/qgsmeshforcebypolylines.cpp index 8b4336cefc9a..46edfb588a4f 100644 --- a/src/core/mesh/qgsmeshforcebypolylines.cpp +++ b/src/core/mesh/qgsmeshforcebypolylines.cpp @@ -360,7 +360,7 @@ bool QgsMeshEditForceByLine::buildForcedElements() QPair currentEdge{-1, -1}; int currentAddedVertex = -1; // Last added point - int nextCutFace = -1; //face that has to be cutted from an intersected edge (not snap on existing vertex) + int nextCutFace = -1; //face that has to be cut from an intersected edge (not snap on existing vertex) int leftFace = -1; //the face that has been just cut in a edge while ( true ) diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.cpp b/src/core/mesh/qgsmeshlayerelevationproperties.cpp index 214a0c489386..2ee3838f55ec 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.cpp +++ b/src/core/mesh/qgsmeshlayerelevationproperties.cpp @@ -43,12 +43,26 @@ bool QgsMeshLayerElevationProperties::hasElevation() const QDomElement QgsMeshLayerElevationProperties::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) { QDomElement element = document.createElement( QStringLiteral( "elevation" ) ); + element.setAttribute( QStringLiteral( "mode" ), qgsEnumValueToKey( mMode ) ); element.setAttribute( QStringLiteral( "symbology" ), qgsEnumValueToKey( mSymbology ) ); if ( !std::isnan( mElevationLimit ) ) element.setAttribute( QStringLiteral( "elevationLimit" ), qgsDoubleToString( mElevationLimit ) ); writeCommonProperties( element, document, context ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + element.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( mFixedRange.lower() ) ); + element.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( mFixedRange.upper() ) ); + element.setAttribute( QStringLiteral( "includeLower" ), mFixedRange.includeLower() ? "1" : "0" ); + element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); + break; + + case Qgis::MeshElevationMode::FromVertices: + break; + } + QDomElement profileLineSymbolElement = document.createElement( QStringLiteral( "profileLineSymbol" ) ); profileLineSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mProfileLineSymbol.get(), document, context ) ); element.appendChild( profileLineSymbolElement ); @@ -64,6 +78,7 @@ QDomElement QgsMeshLayerElevationProperties::writeXml( QDomElement &parentElemen bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { const QDomElement elevationElement = element.firstChildElement( QStringLiteral( "elevation" ) ).toElement(); + mMode = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "mode" ) ), Qgis::MeshElevationMode::FromVertices ); mSymbology = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "symbology" ) ), Qgis::ProfileSurfaceSymbology::Line ); if ( elevationElement.hasAttribute( QStringLiteral( "elevationLimit" ) ) ) mElevationLimit = elevationElement.attribute( QStringLiteral( "elevationLimit" ) ).toDouble(); @@ -72,6 +87,21 @@ bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const readCommonProperties( elevationElement, context ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + { + const double lower = elevationElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = elevationElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = elevationElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = elevationElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); + break; + } + case Qgis::MeshElevationMode::FromVertices: + break; + } + const QColor defaultColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); const QDomElement profileLineSymbolElement = elevationElement.firstChildElement( QStringLiteral( "profileLineSymbol" ) ).firstChildElement( QStringLiteral( "symbol" ) ); @@ -90,32 +120,59 @@ bool QgsMeshLayerElevationProperties::readXml( const QDomElement &element, const QString QgsMeshLayerElevationProperties::htmlSummary() const { QStringList properties; - properties << tr( "Scale: %1" ).arg( mZScale ); - properties << tr( "Offset: %1" ).arg( mZOffset ); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); + break; + + case Qgis::MeshElevationMode::FromVertices: + properties << tr( "Scale: %1" ).arg( mZScale ); + properties << tr( "Offset: %1" ).arg( mZOffset ); + break; + } return QStringLiteral( "
  • %1
  • " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } QgsMeshLayerElevationProperties *QgsMeshLayerElevationProperties::clone() const { std::unique_ptr< QgsMeshLayerElevationProperties > res = std::make_unique< QgsMeshLayerElevationProperties >( nullptr ); + res->setMode( mMode ); res->setProfileLineSymbol( mProfileLineSymbol->clone() ); res->setProfileFillSymbol( mProfileFillSymbol->clone() ); res->setProfileSymbology( mSymbology ); res->setElevationLimit( mElevationLimit ); + res->setFixedRange( mFixedRange ); res->copyCommonProperties( this ); return res.release(); } -bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsMeshLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer * ) const { - // TODO -- test actual raster z range - return true; + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return mFixedRange.overlaps( range ); + + case Qgis::MeshElevationMode::FromVertices: + // TODO -- test actual mesh z range + return true; + } + BUILTIN_UNREACHABLE } QgsDoubleRange QgsMeshLayerElevationProperties::calculateZRange( QgsMapLayer * ) const { - // TODO -- determine actual z range from raster statistics - return QgsDoubleRange(); + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return mFixedRange; + + case Qgis::MeshElevationMode::FromVertices: + // TODO -- determine actual z range from mesh statistics + return QgsDoubleRange(); + } + BUILTIN_UNREACHABLE } bool QgsMeshLayerElevationProperties::showByDefaultInElevationProfilePlots() const @@ -123,6 +180,47 @@ bool QgsMeshLayerElevationProperties::showByDefaultInElevationProfilePlots() con return true; } +QgsMapLayerElevationProperties::Flags QgsMeshLayerElevationProperties::flags() const +{ + switch ( mMode ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + + case Qgis::MeshElevationMode::FromVertices: + break; + } + return QgsMapLayerElevationProperties::Flags(); +} + +Qgis::MeshElevationMode QgsMeshLayerElevationProperties::mode() const +{ + return mMode; +} + +void QgsMeshLayerElevationProperties::setMode( Qgis::MeshElevationMode mode ) +{ + if ( mMode == mode ) + return; + + mMode = mode; + emit changed(); +} + +QgsDoubleRange QgsMeshLayerElevationProperties::fixedRange() const +{ + return mFixedRange; +} + +void QgsMeshLayerElevationProperties::setFixedRange( const QgsDoubleRange &range ) +{ + if ( range == mFixedRange ) + return; + + mFixedRange = range; + emit changed(); +} + QgsLineSymbol *QgsMeshLayerElevationProperties::profileLineSymbol() const { return mProfileLineSymbol.get(); diff --git a/src/core/mesh/qgsmeshlayerelevationproperties.h b/src/core/mesh/qgsmeshlayerelevationproperties.h index d3a8f3e298d1..88be2fb7c6eb 100644 --- a/src/core/mesh/qgsmeshlayerelevationproperties.h +++ b/src/core/mesh/qgsmeshlayerelevationproperties.h @@ -52,9 +52,50 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; QString htmlSummary() const override; QgsMeshLayerElevationProperties *clone() const override SIP_FACTORY; - bool isVisibleInZRange( const QgsDoubleRange &range ) const override; + bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = nullptr ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; + QgsMapLayerElevationProperties::Flags flags() const override; + + /** + * Returns the elevation mode. + * + * \see setMode() + * \since QGIS 3.38 + */ + Qgis::MeshElevationMode mode() const; + + /** + * Sets the elevation \a mode. + * + * \see mode() + * \since QGIS 3.38 + */ + void setMode( Qgis::MeshElevationMode mode ); + + /** + * Returns the fixed elevation range for the mesh. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRange() + * \since QGIS 3.38 + */ + QgsDoubleRange fixedRange() const; + + /** + * Sets the fixed elevation \a range for the mesh. + * + * \note This is only considered when mode() is Qgis::MeshElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRange() + * \since QGIS 3.38 + */ + void setFixedRange( const QgsDoubleRange &range ); /** * Returns the line symbol used to render the mesh profile in elevation profile plots. @@ -131,10 +172,14 @@ class CORE_EXPORT QgsMeshLayerElevationProperties : public QgsMapLayerElevationP void setDefaultProfileLineSymbol( const QColor &color ); void setDefaultProfileFillSymbol( const QColor &color ); + Qgis::MeshElevationMode mMode = Qgis::MeshElevationMode::FromVertices; + std::unique_ptr< QgsLineSymbol > mProfileLineSymbol; std::unique_ptr< QgsFillSymbol > mProfileFillSymbol; Qgis::ProfileSurfaceSymbology mSymbology = Qgis::ProfileSurfaceSymbology::Line; double mElevationLimit = std::numeric_limits< double >::quiet_NaN(); + + QgsDoubleRange mFixedRange; }; #endif // QGSMESHLAYERELEVATIONPROPERTIES_H diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index 5c4200a64370..58d327272c77 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -42,6 +42,7 @@ #include "qgsapplication.h" #include "qgsruntimeprofiler.h" #include "qgsexpressioncontextutils.h" +#include "qgsmeshlayerelevationproperties.h" QgsMeshLayerRenderer::QgsMeshLayerRenderer( QgsMeshLayer *layer, @@ -89,9 +90,28 @@ QgsMeshLayerRenderer::QgsMeshLayerRenderer( if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) { + QgsMeshLayerElevationProperties *elevProp = qobject_cast( layer->elevationProperties() ); + mRenderElevationMap = true; - mElevationScale = layer->elevationProperties()->zScale(); - mElevationOffset = layer->elevationProperties()->zOffset(); + mElevationScale = elevProp->zScale(); + mElevationOffset = elevProp->zOffset(); + + if ( !context.zRange().isInfinite() ) + { + switch ( elevProp->mode() ) + { + case Qgis::MeshElevationMode::FixedElevationRange: + // don't need to handle anything here -- the layer renderer will never be created if the + // render context range doesn't match the layer's fixed elevation range + break; + + case Qgis::MeshElevationMode::FromVertices: + { + // TODO -- filtering by mesh z values is not currently implemented + break; + } + } + } } mPreparationTime = timer.elapsed(); @@ -537,14 +557,14 @@ void QgsMeshLayerRenderer::renderScalarDatasetOnEdges( const QgsMeshRendererScal QgsRenderContext &context = *renderContext(); const QVector edges = mTriangularMesh.edges(); const QVector vertices = mTriangularMesh.vertices(); - const QList egdesInExtent = mTriangularMesh.edgeIndexesForRectangle( context.mapExtent() ); + const QList edgesInExtent = mTriangularMesh.edgeIndexesForRectangle( context.mapExtent() ); QgsInterpolatedLineRenderer edgePlotter; edgePlotter.setInterpolatedColor( QgsInterpolatedLineColor( scalarSettings.colorRampShader() ) ); edgePlotter.setInterpolatedWidth( QgsInterpolatedLineWidth( scalarSettings.edgeStrokeWidth() ) ); edgePlotter.setWidthUnit( scalarSettings.edgeStrokeWidthUnit() ); - for ( const int i : egdesInExtent ) + for ( const int i : edgesInExtent ) { if ( context.renderingStopped() ) break; diff --git a/src/core/mesh/qgsmeshlayerutils.h b/src/core/mesh/qgsmeshlayerutils.h index e215e5efcb15..2d25837692b9 100644 --- a/src/core/mesh/qgsmeshlayerutils.h +++ b/src/core/mesh/qgsmeshlayerutils.h @@ -362,7 +362,7 @@ class CORE_EXPORT QgsMeshLayerUtils * \param triangularMesh the triangular mesh * \param verticalMagnitude the vertical magnitude values used instead Z value of vertices * \param isRelative TRUE if the vertical magnitude is relative to the Z value of vertices - * \returns normales (3D vector) on all the vertices + * \returns normals (3D vector) on all the vertices * \since QGIS 3.14 */ static QVector calculateNormals( diff --git a/src/core/mesh/qgsmeshvectorrenderer.cpp b/src/core/mesh/qgsmeshvectorrenderer.cpp index 702f453330fc..34533115f29f 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.cpp +++ b/src/core/mesh/qgsmeshvectorrenderer.cpp @@ -307,9 +307,9 @@ void QgsMeshVectorArrowRenderer::drawVectorDataOnFaces( ) void QgsMeshVectorArrowRenderer::drawVectorDataOnEdges() { - const QList egdesInExtent = mTriangularMesh.edgeIndexesForRectangle( mBufferedExtent ); + const QList edgesInExtent = mTriangularMesh.edgeIndexesForRectangle( mBufferedExtent ); const QVector ¢roids = mTriangularMesh.edgeCentroids(); - const QSet nativeEdgesInExtent = QgsMeshUtils::nativeEdgesFromEdges( egdesInExtent, + const QSet nativeEdgesInExtent = QgsMeshUtils::nativeEdgesFromEdges( edgesInExtent, mTriangularMesh.edgesToNativeEdges() ); drawVectorDataOnPoints( nativeEdgesInExtent, centroids ); } diff --git a/src/core/mesh/qgstopologicalmesh.cpp b/src/core/mesh/qgstopologicalmesh.cpp index a363aac92e3e..b789f224f99d 100644 --- a/src/core/mesh/qgstopologicalmesh.cpp +++ b/src/core/mesh/qgstopologicalmesh.cpp @@ -403,11 +403,11 @@ void QgsTopologicalMesh::applyChanges( const QgsTopologicalMesh::Changes &change mFacesNeighborhood[changes.addedFaceIndexInMesh( i )] = changes.mFacesNeighborhoodToAdd.at( i ); } - for ( const std::array neigborChange : std::as_const( changes.mNeighborhoodChanges ) ) + for ( const std::array neighborChange : std::as_const( changes.mNeighborhoodChanges ) ) { - const int faceIndex = neigborChange.at( 0 ); - const int positionInFace = neigborChange.at( 1 ); - const int valueToApply = neigborChange.at( 3 ); + const int faceIndex = neighborChange.at( 0 ); + const int positionInFace = neighborChange.at( 1 ); + const int valueToApply = neighborChange.at( 3 ); mFacesNeighborhood[faceIndex][positionInFace] = valueToApply; } @@ -441,11 +441,11 @@ void QgsTopologicalMesh::applyChanges( const QgsTopologicalMesh::Changes &change void QgsTopologicalMesh::reverseChanges( const QgsTopologicalMesh::Changes &changes ) { - for ( const std::array neigborChange : std::as_const( changes.mNeighborhoodChanges ) ) + for ( const std::array neighborChange : std::as_const( changes.mNeighborhoodChanges ) ) { - const int faceIndex = neigborChange.at( 0 ); - const int positionInFace = neigborChange.at( 1 ); - const int valueToApply = neigborChange.at( 2 ); + const int faceIndex = neighborChange.at( 0 ); + const int positionInFace = neighborChange.at( 1 ); + const int valueToApply = neighborChange.at( 2 ); mFacesNeighborhood[faceIndex][positionInFace] = valueToApply; } @@ -838,7 +838,7 @@ bool QgsTopologicalMesh::renumberVertices( QVector &oldToNewIndex ) const circ.goBoundaryCounterClockwise(); neighbors.append( circ.oppositeVertexCounterClockwise() ); - int firsrFace = circ.currentFaceIndex(); + int firstFace = circ.currentFaceIndex(); do { int neighborIndex = circ.oppositeVertexClockwise(); @@ -856,7 +856,7 @@ bool QgsTopologicalMesh::renumberVertices( QVector &oldToNewIndex ) const if ( it == neighbors.end() ) neighbors.append( neighborIndex ); } - while ( circ.turnClockwise() != firsrFace && circ.currentFaceIndex() != -1 ); + while ( circ.turnClockwise() != firstFace && circ.currentFaceIndex() != -1 ); }; int newIndex = 0; diff --git a/src/core/mesh/qgstopologicalmesh.h b/src/core/mesh/qgstopologicalmesh.h index fbceaf517622..6b91e48b2267 100644 --- a/src/core/mesh/qgstopologicalmesh.h +++ b/src/core/mesh/qgstopologicalmesh.h @@ -137,7 +137,7 @@ class CORE_EXPORT QgsTopologicalMesh QVector mFacesNeighborhoodToAdd; QVector mFacesToRemove; QVector mFacesNeighborhoodToRemove; - QList> mNeighborhoodChanges; // {index of concerned face, neigbor position, previous value, changed value} + QList> mNeighborhoodChanges; // {index of concerned face, neighbor position, previous value, changed value} QVector mVerticesToAdd; QVector mVertexToFaceToAdd; @@ -267,7 +267,7 @@ class CORE_EXPORT QgsTopologicalMesh Changes insertVertexInFacesEdge( int faceIndex, int position, const QgsMeshVertex &vertex ); /** - * Adds a free \a vertex in the face, that is a vertex tha tis not included or linked with any faces. + * Adds a free \a vertex in the face, that is a vertex that is not included or linked with any faces. * The method returns a instance of the class QgsTopologicalMesh::Change that can be used to reverse or reapply the operation. */ Changes addFreeVertex( const QgsMeshVertex &vertex ); diff --git a/src/core/mesh/qgstriangularmesh.cpp b/src/core/mesh/qgstriangularmesh.cpp index 12015e23ddd4..f73c754aa880 100644 --- a/src/core/mesh/qgstriangularmesh.cpp +++ b/src/core/mesh/qgstriangularmesh.cpp @@ -474,7 +474,7 @@ QList QgsTriangularMesh::edgeIndexesForRectangle( const QgsRectangle &recta QVector QgsTriangularMesh::vertexNormals( float vertScale ) const { - QVector normales( vertices().count(), QVector3D( 0, 0, 0 ) ); + QVector normals( vertices().count(), QVector3D( 0, 0, 0 ) ); for ( const auto &face : triangles() ) { @@ -494,10 +494,10 @@ QVector QgsTriangularMesh::vertexNormals( float vertScale ) const QVector3D v1( float( otherVert1.x() - vert.x() ), float( otherVert1.y() - vert.y() ), vertScale * float( otherVert1.z() - vert.z() ) ); QVector3D v2( float( otherVert2.x() - vert.x() ), float( otherVert2.y() - vert.y() ), vertScale * float( otherVert2.z() - vert.z() ) ); - normales[index1] += QVector3D::crossProduct( v1, v2 ); + normals[index1] += QVector3D::crossProduct( v1, v2 ); } } - return normales; + return normals; } QVector QgsTriangularMesh::simplifyMesh( double reductionFactor, int minimumTrianglesCount ) const diff --git a/src/core/mesh/qgstriangularmesh.h b/src/core/mesh/qgstriangularmesh.h index ec447bf7d290..663bbdc10af3 100644 --- a/src/core/mesh/qgstriangularmesh.h +++ b/src/core/mesh/qgstriangularmesh.h @@ -190,9 +190,9 @@ class CORE_EXPORT QgsTriangularMesh // TODO rename to QgsRendererMesh in QGIS 4 QList edgeIndexesForRectangle( const QgsRectangle &rectangle ) const ; /** - * Calculates and returns normale vector on each vertex that is part of any face + * Calculates and returns normal vector on each vertex that is part of any face * - * \returns all normales at vertices + * \returns all normals at vertices * * \since QGIS 3.12 */ diff --git a/src/core/network/qgsblockingnetworkrequest.cpp b/src/core/network/qgsblockingnetworkrequest.cpp index 02957153b3d1..19a8f6228c87 100644 --- a/src/core/network/qgsblockingnetworkrequest.cpp +++ b/src/core/network/qgsblockingnetworkrequest.cpp @@ -197,6 +197,9 @@ QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBl connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection ); connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection ); + if ( request.hasRawHeader( "Range" ) ) + connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection ); + auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]() { // when this method is called we have "produced" a single authentication request -- so the buffer is now full @@ -354,6 +357,10 @@ void QgsBlockingNetworkRequest::replyFinished() request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache ); request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true ); + // if that was a range request, use the same range for the redirected request + if ( mReply->request().hasRawHeader( "Range" ) ) + request.setRawHeader( "Range", mReply->request().rawHeader( "Range" ) ); + mReply->deleteLater(); mReply = nullptr; @@ -380,6 +387,10 @@ void QgsBlockingNetworkRequest::replyFinished() connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection ); connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection ); connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection ); + + if ( request.hasRawHeader( "Range" ) ) + connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection ); + return; } } @@ -460,3 +471,15 @@ QString QgsBlockingNetworkRequest::errorMessageFailedAuth() { return tr( "network request update failed for authentication config" ); } + +void QgsBlockingNetworkRequest::abortIfNotPartialContentReturned() +{ + if ( mReply && mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 ) + { + // We're expecting a 206 - Partial Content but the server returned 200 + // It seems it does not support range requests and is returning the whole file! + mReply->abort(); + mErrorMessage = tr( "The server does not support range requests" ); + mErrorCode = ServerExceptionError; + } +} diff --git a/src/core/network/qgsblockingnetworkrequest.h b/src/core/network/qgsblockingnetworkrequest.h index 16dfff9e5b79..0a7ff712d3db 100644 --- a/src/core/network/qgsblockingnetworkrequest.h +++ b/src/core/network/qgsblockingnetworkrequest.h @@ -291,6 +291,8 @@ class CORE_EXPORT QgsBlockingNetworkRequest : public QObject QString errorMessageFailedAuth(); void sendRequestToNetworkAccessManager( const QNetworkRequest &request ); + + void abortIfNotPartialContentReturned(); }; ///@cond PRIVATE diff --git a/src/core/network/qgsfiledownloader.cpp b/src/core/network/qgsfiledownloader.cpp index 817622684ce4..431dfd44ef83 100644 --- a/src/core/network/qgsfiledownloader.cpp +++ b/src/core/network/qgsfiledownloader.cpp @@ -15,6 +15,7 @@ #include "qgsfiledownloader.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsapplication.h" #include "qgsauthmanager.h" #include "qgsvariantutils.h" diff --git a/src/core/network/qgsnetworkaccessmanager.cpp b/src/core/network/qgsnetworkaccessmanager.cpp index 0833680c2e32..9f976e49f93c 100644 --- a/src/core/network/qgsnetworkaccessmanager.cpp +++ b/src/core/network/qgsnetworkaccessmanager.cpp @@ -216,6 +216,8 @@ QgsNetworkAccessManager::QgsNetworkAccessManager( QObject *parent ) { setProxyFactory( new QgsNetworkProxyFactory() ); setCookieJar( new QgsNetworkCookieJar( this ) ); + enableStrictTransportSecurityStore( true ); + setStrictTransportSecurityEnabled( true ); } void QgsNetworkAccessManager::setSslErrorHandler( std::unique_ptr handler ) @@ -356,6 +358,7 @@ QNetworkReply *QgsNetworkAccessManager::createRequest( QNetworkAccessManager::Op QNetworkReply *reply = QNetworkAccessManager::createRequest( op, req, outgoingData ); reply->setProperty( "requestId", requestId ); + emit requestCreated( QgsNetworkRequestParameters( op, reply->request(), requestId, content ) ); Q_NOWARN_DEPRECATED_PUSH emit requestCreated( reply ); Q_NOWARN_DEPRECATED_POP @@ -631,6 +634,9 @@ void QgsNetworkAccessManager::setupDefaultProxyAndCache( Qt::ConnectionType conn connect( this, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestAboutToBeCreated ), sMainNAM, qOverload< QgsNetworkRequestParameters >( &QgsNetworkAccessManager::requestAboutToBeCreated ) ); + connect( this, qOverload< const QgsNetworkRequestParameters & >( &QgsNetworkAccessManager::requestCreated ), + sMainNAM, qOverload< const QgsNetworkRequestParameters & >( &QgsNetworkAccessManager::requestCreated ) ); + connect( this, qOverload< QgsNetworkReplyContent >( &QgsNetworkAccessManager::finished ), sMainNAM, qOverload< QgsNetworkReplyContent >( &QgsNetworkAccessManager::finished ) ); diff --git a/src/core/network/qgsnetworkaccessmanager.h b/src/core/network/qgsnetworkaccessmanager.h index d200cc7c20ca..9010c4ccc572 100644 --- a/src/core/network/qgsnetworkaccessmanager.h +++ b/src/core/network/qgsnetworkaccessmanager.h @@ -38,14 +38,6 @@ class QgsFeedback; class QgsSettingsEntryInteger; -#ifndef SIP_RUN -#include "qgsconfig.h" -constexpr int sFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); - -#define QgsSetRequestInitiatorClass(request, _class) request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorClass ), _class ); request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString(QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")") ); -#define QgsSetRequestInitiatorId(request, str) request.setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString(QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + "): " + str) ); -#endif - /** * \class QgsNetworkRequestParameters * \ingroup core @@ -688,12 +680,27 @@ class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager * only to connect to the main thread's signal in order to receive notifications about requests * created in any thread. * + * \see requestCreated( const QgsNetworkRequestParameters & ) * \see finished( QgsNetworkReplyContent ) * \see requestTimedOut( QgsNetworkRequestParameters ) * \since QGIS 3.6 */ void requestAboutToBeCreated( QgsNetworkRequestParameters request ); + /** + * Emitted when a network request has been created. + * + * This signal is propagated to the main thread QgsNetworkAccessManager instance, so it is necessary + * only to connect to the main thread's signal in order to receive notifications about requests + * created in any thread. + * + * \see requestAboutToBeCreated( QgsNetworkRequestParameters ) + * \see finished( QgsNetworkReplyContent ) + * \see requestTimedOut( QgsNetworkRequestParameters ) + * \since QGIS 3.38 + */ + void requestCreated( const QgsNetworkRequestParameters &request ); + /** * Emitted whenever a pending network reply is finished. * diff --git a/src/core/network/qgsnetworkcontentfetcher.cpp b/src/core/network/qgsnetworkcontentfetcher.cpp index 479d5b097b71..f192efc7a8fd 100644 --- a/src/core/network/qgsnetworkcontentfetcher.cpp +++ b/src/core/network/qgsnetworkcontentfetcher.cpp @@ -18,6 +18,7 @@ #include "qgsnetworkcontentfetcher.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsmessagelog.h" #include "qgsapplication.h" #include "qgsauthmanager.h" diff --git a/src/core/network/qgsnewsfeedparser.cpp b/src/core/network/qgsnewsfeedparser.cpp index 4511d633066c..480655e038ce 100644 --- a/src/core/network/qgsnewsfeedparser.cpp +++ b/src/core/network/qgsnewsfeedparser.cpp @@ -17,6 +17,7 @@ #include "qgsnetworkcontentfetchertask.h" #include "qgsnetworkcontentfetcher.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsjsonutils.h" #include "qgsmessagelog.h" #include "qgsapplication.h" diff --git a/src/core/network/qgssetrequestinitiator_p.h b/src/core/network/qgssetrequestinitiator_p.h new file mode 100644 index 000000000000..0e8d93ac4b3d --- /dev/null +++ b/src/core/network/qgssetrequestinitiator_p.h @@ -0,0 +1,29 @@ +/*************************************************************************** + qgssetrequestinitiator.h - description + ------------------- + begin : 2024-02-10 + copyright : (C) 2024 by Matthias Kuhn + email : matthias@opengis.ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSETREQUESTINITIATOR_P_H +#define QGSSETREQUESTINITIATOR_P_H + +#include "qgsnetworkaccessmanager.h" + +constexpr int sFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); + +#define QgsSetRequestInitiatorClass(request, _class) ( request ).setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorClass ), _class ); ( request ).setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString(QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + ( __FUNCTION__ ) + ")") ); +#define QgsSetRequestInitiatorId(request, str) ( request ).setAttribute( static_cast< QNetworkRequest::Attribute >( QgsNetworkRequestParameters::AttributeInitiatorRequestId ), QString(QString( __FILE__ ).mid( sFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + ( __FUNCTION__ ) + "): " + ( str ) ) ); + +#define QgsSetCPLHTTPFetchOverriderInitiatorClass(overrider, _class) QgsSetRequestInitiatorClass((overrider), _class) +#endif // QGSSETREQUESTINITIATOR_P_H diff --git a/src/core/pal/costcalculator.cpp b/src/core/pal/costcalculator.cpp index 13072d1b3ae3..789e06beba7d 100644 --- a/src/core/pal/costcalculator.cpp +++ b/src/core/pal/costcalculator.cpp @@ -94,7 +94,7 @@ void CostCalculator::addObstacleCostPenalty( LabelPosition *lp, FeaturePart *obs const double priority = 2 * ( 1 - lp->feature->calculatePriority() ); const double obstaclePriority = obstacle->obstacleSettings().factor(); - // if feature priority is < obstaclePriorty, there's a hard conflict... + // if feature priority is < obstaclePriority, there's a hard conflict... if ( n > 0 && ( priority < obstaclePriority && !qgsDoubleNear( priority, obstaclePriority, 0.001 ) ) ) { lp->setHasHardObstacleConflict( true ); diff --git a/src/core/pal/feature.cpp b/src/core/pal/feature.cpp index a9479c0feffe..6fa3f04ab220 100644 --- a/src/core/pal/feature.cpp +++ b/src/core/pal/feature.cpp @@ -1438,7 +1438,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq bool negativeShapeHasNegativeDistance = false; if ( hasAboveBelowLinePlacement && !qgsDoubleNear( offsetDistance, 0 ) ) { - // create offseted map shapes to be used for above and below line placements + // create offsetted map shapes to be used for above and below line placements if ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) || ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) ) mapShapeOffsetPositive = mapShape->clone(); if ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) || ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) ) diff --git a/src/core/pal/feature.h b/src/core/pal/feature.h index 5892f32974a8..cafc5ad211ae 100644 --- a/src/core/pal/feature.h +++ b/src/core/pal/feature.h @@ -76,7 +76,7 @@ namespace pal /** * Creates a new generic feature. - * \param lf a pointer for a feature which contains the spatial entites + * \param lf a pointer for a feature which contains the spatial entities * \param geom a pointer to a GEOS geometry */ FeaturePart( QgsLabelFeature *lf, const GEOSGeometry *geom ); diff --git a/src/core/pal/geomfunction.cpp b/src/core/pal/geomfunction.cpp index 0021799a34d9..550151f24ec8 100644 --- a/src/core/pal/geomfunction.cpp +++ b/src/core/pal/geomfunction.cpp @@ -232,7 +232,7 @@ std::vector< int > GeomFunction::convexHullId( std::vector< int > &id, const std stack[top] = convexHull[i]; } } - else if ( result > 0 ) //convexe + else if ( result > 0 ) //convex { second++; top++; diff --git a/src/core/pal/labelposition.cpp b/src/core/pal/labelposition.cpp index 5bc0819b7b38..724eaaafaba7 100644 --- a/src/core/pal/labelposition.cpp +++ b/src/core/pal/labelposition.cpp @@ -233,7 +233,7 @@ bool LabelPosition::isInConflict( const LabelPosition *lp ) const // this method considers the label's outer bounds if ( this->probFeat == lp->probFeat ) // bugfix #1 - return false; // always overlaping itself ! + return false; // always overlapping itself ! // if either this label doesn't cause collisions, or the other one doesn't, then we don't conflict! if ( this->feature->feature()->overlapHandling() == Qgis::LabelOverlapHandling::AllowOverlapAtNoCost || diff --git a/src/core/pal/pointset.h b/src/core/pal/pointset.h index e33a1bd9bba8..90f9d41c248b 100644 --- a/src/core/pal/pointset.h +++ b/src/core/pal/pointset.h @@ -147,8 +147,8 @@ namespace pal * Optionally, the nearest point is stored in (rx,ry). * \param px x coordinate of the point * \param py y coordinate of the points - * \param rx pointer to x coorinates of the nearest point (can be NULL) - * \param ry pointer to y coorinates of the nearest point (can be NULL) + * \param rx pointer to x coordinates of the nearest point (can be NULL) + * \param ry pointer to y coordinates of the nearest point (can be NULL) * \returns minimum distance */ double minDistanceToPoint( double px, double py, double *rx = nullptr, double *ry = nullptr ) const; diff --git a/src/core/pointcloud/qgscopcpointcloudblockrequest.cpp b/src/core/pointcloud/qgscopcpointcloudblockrequest.cpp index c2568c175a94..3e97dd859e5f 100644 --- a/src/core/pointcloud/qgscopcpointcloudblockrequest.cpp +++ b/src/core/pointcloud/qgscopcpointcloudblockrequest.cpp @@ -21,6 +21,7 @@ #include "qgslazdecoder.h" #include "qgsapplication.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" // // QgsCopcPointCloudBlockRequest diff --git a/src/core/pointcloud/qgscopcpointcloudindex.cpp b/src/core/pointcloud/qgscopcpointcloudindex.cpp index 3db580339442..b9c6cd673989 100644 --- a/src/core/pointcloud/qgscopcpointcloudindex.cpp +++ b/src/core/pointcloud/qgscopcpointcloudindex.cpp @@ -221,7 +221,7 @@ bool QgsCopcPointCloudIndex::writeStatistics( QgsPointCloudStatistics &stats ) QByteArray statsJson = stats.toStatisticsJson(); statsEvlrHeader.data_length = statsJson.size(); - // Save the EVLRs to the end of the original file (while erasing the exisitng EVLRs in the file) + // Save the EVLRs to the end of the original file (while erasing the existing EVLRs in the file) mCopcFile.close(); std::fstream copcFile; copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out ); diff --git a/src/core/pointcloud/qgseptpointcloudblockrequest.cpp b/src/core/pointcloud/qgseptpointcloudblockrequest.cpp index d92bafc47b78..9bcd7b339b5e 100644 --- a/src/core/pointcloud/qgseptpointcloudblockrequest.cpp +++ b/src/core/pointcloud/qgseptpointcloudblockrequest.cpp @@ -22,6 +22,7 @@ #include "qgslazdecoder.h" #include "qgsapplication.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" // // QgsEptPointCloudBlockRequest diff --git a/src/core/pointcloud/qgslazinfo.cpp b/src/core/pointcloud/qgslazinfo.cpp index 31a1d7225c86..419a2042e250 100644 --- a/src/core/pointcloud/qgslazinfo.cpp +++ b/src/core/pointcloud/qgslazinfo.cpp @@ -20,6 +20,7 @@ #include "qgslogger.h" #include "qgsblockingnetworkrequest.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "lazperf/readers.hpp" @@ -297,12 +298,6 @@ QgsLazInfo QgsLazInfo::fromUrl( QUrl &url ) { QgsLazInfo lazInfo; - if ( !supportsRangeQueries( url ) ) - { - lazInfo.mError = QStringLiteral( "The server of submitted URL doesn't support range queries" ); - return lazInfo; - } - // Fetch header data { QNetworkRequest nr( url ); @@ -315,11 +310,20 @@ QgsLazInfo QgsLazInfo::fromUrl( QUrl &url ) if ( errCode != QgsBlockingNetworkRequest::NoError ) { QgsDebugError( QStringLiteral( "Request failed: " ) + url.toString() ); - lazInfo.mError = QStringLiteral( "Range query 0-374 to \"%1\" failed: \"%2\"" ).arg( url.toString() ).arg( req.errorMessage() ); + + if ( req.reply().attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 ) + { + lazInfo.mError = req.errorMessage(); + } + else + { + lazInfo.mError = QStringLiteral( "Range query 0-374 to \"%1\" failed: \"%2\"" ).arg( url.toString() ).arg( req.errorMessage() ); + } return lazInfo; } const QgsNetworkReplyContent reply = req.reply(); + QByteArray lazHeaderData = reply.content(); lazInfo.parseRawHeader( lazHeaderData.data(), lazHeaderData.size() ); @@ -351,19 +355,3 @@ QgsLazInfo QgsLazInfo::fromUrl( QUrl &url ) return lazInfo; } - -bool QgsLazInfo::supportsRangeQueries( QUrl &url ) -{ - QNetworkRequest nr( url ); - QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsLazInfo" ) ); - nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork ); - nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false ); - nr.setRawHeader( "Range", "bytes=0-0" ); - QgsBlockingNetworkRequest req; - // ignore the reply's status, we only care if accept-ranges is in the headers - req.head( nr ); - QgsNetworkReplyContent reply = req.reply(); - - const QString acceptRangesHeader = reply.rawHeader( QStringLiteral( "Accept-Ranges" ).toLocal8Bit() ); - return acceptRangesHeader.compare( QStringLiteral( "bytes" ), Qt::CaseSensitivity::CaseInsensitive ) == 0; -} diff --git a/src/core/pointcloud/qgslazinfo.h b/src/core/pointcloud/qgslazinfo.h index 74f948d71f03..60139c24dafc 100644 --- a/src/core/pointcloud/qgslazinfo.h +++ b/src/core/pointcloud/qgslazinfo.h @@ -135,9 +135,6 @@ class CORE_EXPORT QgsLazInfo //! Static function to create a QgsLazInfo class from a file over network static QgsLazInfo fromUrl( QUrl &url ); - //! Static function to check whether the server of URL \a url supports range queries - static bool supportsRangeQueries( QUrl &url ); - private: void parseHeader( lazperf::header14 &header ); void parseCrs(); diff --git a/src/core/pointcloud/qgspointclouddataprovider.h b/src/core/pointcloud/qgspointclouddataprovider.h index b0c52b0492cd..27d8ab8ec47d 100644 --- a/src/core/pointcloud/qgspointclouddataprovider.h +++ b/src/core/pointcloud/qgspointclouddataprovider.h @@ -336,7 +336,7 @@ class CORE_EXPORT QgsPointCloudDataProvider: public QgsDataProvider /** - * Returns the object containings the statistics metadata extracted from the dataset + * Returns the object containing the statistics metadata extracted from the dataset * \since QGIS 3.26 */ QgsPointCloudStatistics metadataStatistics(); diff --git a/src/core/pointcloud/qgspointcloudindex.h b/src/core/pointcloud/qgspointcloudindex.h index 8b4c04671ac3..9e21240b0c97 100644 --- a/src/core/pointcloud/qgspointcloudindex.h +++ b/src/core/pointcloud/qgspointcloudindex.h @@ -264,7 +264,7 @@ class CORE_EXPORT QgsPointCloudIndex: public QObject virtual QVariantMap originalMetadata() const = 0; /** - * Returns the object containings the statistics metadata extracted from the dataset + * Returns the object containing the statistics metadata extracted from the dataset * \since QGIS 3.26 */ virtual QgsPointCloudStatistics metadataStatistics() const; diff --git a/src/core/pointcloud/qgspointcloudlayer.cpp b/src/core/pointcloud/qgspointcloudlayer.cpp index 139254432f15..079558dc0335 100644 --- a/src/core/pointcloud/qgspointcloudlayer.cpp +++ b/src/core/pointcloud/qgspointcloudlayer.cpp @@ -455,7 +455,7 @@ void QgsPointCloudLayer::setDataSourcePrivate( const QString &dataSource, const calculateStatistics(); } - // Note: we load the statistics from the data provider regardless of it being an existing metadata (do not check fot hasStatisticsMetadata) + // Note: we load the statistics from the data provider regardless of it being an existing metadata (do not check for hasStatisticsMetadata) // since the X, Y & Z coordinates will be in the header of the dataset if ( mDataProvider && mDataProvider->isValid() && mStatistics.sampledPointsCount() == 0 && mDataProvider->indexingState() == QgsPointCloudDataProvider::Indexed ) { diff --git a/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp b/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp index da894c426fa0..e5c0e8284afa 100644 --- a/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp +++ b/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp @@ -114,7 +114,7 @@ QString QgsPointCloudLayerElevationProperties::htmlSummary() const return QStringLiteral( "
    • %1
    " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } -bool QgsPointCloudLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsPointCloudLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &, QgsMapLayer * ) const { // TODO -- test actual point cloud z range return true; diff --git a/src/core/pointcloud/qgspointcloudlayerelevationproperties.h b/src/core/pointcloud/qgspointcloudlayerelevationproperties.h index 9ac2c746c98d..51363e956d03 100644 --- a/src/core/pointcloud/qgspointcloudlayerelevationproperties.h +++ b/src/core/pointcloud/qgspointcloudlayerelevationproperties.h @@ -48,7 +48,7 @@ class CORE_EXPORT QgsPointCloudLayerElevationProperties : public QgsMapLayerElev bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; QgsPointCloudLayerElevationProperties *clone() const override SIP_FACTORY; QString htmlSummary() const override; - bool isVisibleInZRange( const QgsDoubleRange &range ) const override; + bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = nullptr ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; diff --git a/src/core/pointcloud/qgsremotecopcpointcloudindex.cpp b/src/core/pointcloud/qgsremotecopcpointcloudindex.cpp index 8bbe8da9db03..3f4df11b97a9 100644 --- a/src/core/pointcloud/qgsremotecopcpointcloudindex.cpp +++ b/src/core/pointcloud/qgsremotecopcpointcloudindex.cpp @@ -37,6 +37,7 @@ #include "qgscachedpointcloudblockrequest.h" #include "qgspointcloudexpression.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" ///@cond PRIVATE diff --git a/src/core/pointcloud/qgsremoteeptpointcloudindex.cpp b/src/core/pointcloud/qgsremoteeptpointcloudindex.cpp index 59a427ff9af0..702061c72983 100644 --- a/src/core/pointcloud/qgsremoteeptpointcloudindex.cpp +++ b/src/core/pointcloud/qgsremoteeptpointcloudindex.cpp @@ -39,6 +39,7 @@ #include "qgscachedpointcloudblockrequest.h" #include "qgspointcloudexpression.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" ///@cond PRIVATE @@ -97,6 +98,7 @@ void QgsRemoteEptPointCloudIndex::load( const QString &uri ) { QgsDebugError( QStringLiteral( "Request failed: " ) + uri ); mIsValid = false; + mError = req.errorMessage(); return; } diff --git a/src/core/processing/qgsprocessingparameterdxflayers.cpp b/src/core/processing/qgsprocessingparameterdxflayers.cpp index 030b236cac9e..816e989363fe 100644 --- a/src/core/processing/qgsprocessingparameterdxflayers.cpp +++ b/src/core/processing/qgsprocessingparameterdxflayers.cpp @@ -37,9 +37,11 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !input.isValid() ) return mFlags & Qgis::ProcessingParameterFlag::Optional; - if ( qobject_cast< QgsVectorLayer * >( qvariant_cast( input ) ) ) + QgsMapLayer *mapLayer = nullptr; + QgsVectorLayer *vectorLayer = input.value(); + if ( vectorLayer ) { - return true; + return vectorLayer->isSpatial(); } if ( input.type() == QVariant::String ) @@ -50,8 +52,8 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( input.toString(), *context ); - return mapLayer && ( mapLayer->type() == Qgis::LayerType::Vector ); + mapLayer = QgsProcessingUtils::mapLayerFromString( input.toString(), *context ); + return mapLayer && ( mapLayer->type() == Qgis::LayerType::Vector && mapLayer->isSpatial() ); } else if ( input.type() == QVariant::List ) { @@ -61,16 +63,22 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in const QVariantList layerList = input.toList(); for ( const QVariant &variantLayer : layerList ) { - if ( qobject_cast< QgsVectorLayer * >( qvariant_cast( variantLayer ) ) ) - continue; + vectorLayer = input.value(); + if ( vectorLayer ) + { + if ( vectorLayer->isSpatial() ) + continue; + else + return false; + } if ( variantLayer.type() == QVariant::String ) { if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( variantLayer.toString(), *context ); - if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector ) + mapLayer = QgsProcessingUtils::mapLayerFromString( variantLayer.toString(), *context ); + if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector || !mapLayer->isSpatial() ) return false; } else if ( variantLayer.type() == QVariant::Map ) @@ -83,11 +91,11 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in if ( !context ) return true; - QgsMapLayer *mapLayer = QgsProcessingUtils::mapLayerFromString( layerMap.value( QStringLiteral( "layer" ) ).toString(), *context ); - if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector ) + mapLayer = QgsProcessingUtils::mapLayerFromString( layerMap.value( QStringLiteral( "layer" ) ).toString(), *context ); + if ( !mapLayer || mapLayer->type() != Qgis::LayerType::Vector || !mapLayer->isSpatial() ) return false; - QgsVectorLayer *vectorLayer = static_cast( mapLayer ); + vectorLayer = static_cast( mapLayer ); if ( !vectorLayer ) return false; @@ -113,7 +121,13 @@ bool QgsProcessingParameterDxfLayers::checkValueIsAcceptable( const QVariant &in for ( const QString &v : constToStringList ) { - if ( !QgsProcessingUtils::mapLayerFromString( v, *context ) ) + mapLayer = QgsProcessingUtils::mapLayerFromString( v, *context ); + if ( !mapLayer ) + return false; + + if ( mapLayer->type() == Qgis::LayerType::Vector && mapLayer->isSpatial() ) + continue; + else return false; } return true; diff --git a/src/core/processing/qgsprocessingparameters.h b/src/core/processing/qgsprocessingparameters.h index 071df9414913..f9131c869f57 100644 --- a/src/core/processing/qgsprocessingparameters.h +++ b/src/core/processing/qgsprocessingparameters.h @@ -142,10 +142,9 @@ class CORE_EXPORT QgsProcessingFeatureSourceDefinition /** * Geometry check method to apply to this source. This setting is only - * utilized if the QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature is + * utilized if the Qgis::ProcessingFeatureSourceDefinitionFlag::OverrideDefaultGeometryCheck is * set in QgsProcessingFeatureSourceDefinition::flags. * - * \see overrideDefaultGeometryCheck * \since QGIS 3.14 */ Qgis::InvalidGeometryCheck geometryCheck = Qgis::InvalidGeometryCheck::AbortOnInvalid; diff --git a/src/core/processing/qgsprocessingutils.cpp b/src/core/processing/qgsprocessingutils.cpp index 639623087823..1ba0b218c09d 100644 --- a/src/core/processing/qgsprocessingutils.cpp +++ b/src/core/processing/qgsprocessingutils.cpp @@ -452,6 +452,17 @@ QgsMapLayer *QgsProcessingUtils::loadMapLayerFromString( const QString &string, { pointCloudLayer = std::make_unique< QgsPointCloudLayer >( uri, name, preferredProviders.at( 0 ).metadata()->key(), pointCloudOptions ); } + else + { + // pdal provider can read ascii files but it is not exposed by the provider to + // prevent automatic loading of tabular ascii files. + // Try to open the file with pdal provider. + QgsProviderMetadata *pdalProvider = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "pdal" ) ); + if ( pdalProvider ) + { + pointCloudLayer = std::make_unique< QgsPointCloudLayer >( uri, name, QStringLiteral( "pdal" ), pointCloudOptions ); + } + } } if ( pointCloudLayer && pointCloudLayer->isValid() ) { @@ -1418,7 +1429,7 @@ QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool select while ( it.nextFeature( f ) ) { - if ( feedback->isCanceled() ) + if ( feedback && feedback->isCanceled() ) return QString(); writer->addFeature( f, QgsFeatureSink::FastInsert ); } diff --git a/src/core/proj/qgscoordinatereferencesystem.cpp b/src/core/proj/qgscoordinatereferencesystem.cpp index 967b48e70ef1..ba05991fbac8 100644 --- a/src/core/proj/qgscoordinatereferencesystem.cpp +++ b/src/core/proj/qgscoordinatereferencesystem.cpp @@ -241,6 +241,19 @@ QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::fromSrsId( long srsId return crs; } +QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::createCompoundCrs( const QgsCoordinateReferenceSystem &horizontalCrs, const QgsCoordinateReferenceSystem &verticalCrs ) +{ + PJ *horizontalObj = horizontalCrs.projObject(); + PJ *verticalObj = verticalCrs.projObject(); + if ( horizontalObj && verticalObj ) + { + QgsProjUtils::proj_pj_unique_ptr compoundCrs = QgsProjUtils::createCompoundCrs( horizontalObj, verticalObj ); + if ( compoundCrs ) + return QgsCoordinateReferenceSystem::fromProjObject( compoundCrs.get() ); + } + return QgsCoordinateReferenceSystem(); +} + QgsCoordinateReferenceSystem::~QgsCoordinateReferenceSystem() //NOLINT { } @@ -1600,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 ) @@ -1656,9 +1693,9 @@ bool QgsCoordinateReferenceSystem::setWktString( const QString &wkt ) d->mWktPreferred.clear(); PROJ_STRING_LIST warnings = nullptr; - PROJ_STRING_LIST grammerErrors = nullptr; + PROJ_STRING_LIST grammarErrors = nullptr; { - d->setPj( QgsProjUtils::proj_pj_unique_ptr( proj_create_from_wkt( QgsProjContext::get(), wkt.toLatin1().constData(), nullptr, &warnings, &grammerErrors ) ) ); + d->setPj( QgsProjUtils::proj_pj_unique_ptr( proj_create_from_wkt( QgsProjContext::get(), wkt.toLatin1().constData(), nullptr, &warnings, &grammarErrors ) ) ); } res = d->hasPj(); @@ -1669,12 +1706,12 @@ bool QgsCoordinateReferenceSystem::setWktString( const QString &wkt ) QgsDebugMsgLevel( "INPUT: " + wkt, 2 ); for ( auto iter = warnings; iter && *iter; ++iter ) QgsDebugMsgLevel( *iter, 2 ); - for ( auto iter = grammerErrors; iter && *iter; ++iter ) + for ( auto iter = grammarErrors; iter && *iter; ++iter ) QgsDebugMsgLevel( *iter, 2 ); QgsDebugMsgLevel( QStringLiteral( "---------------------------------------------------------------\n" ), 2 ); } proj_string_list_destroy( warnings ); - proj_string_list_destroy( grammerErrors ); + proj_string_list_destroy( grammarErrors ); QgsReadWriteLocker locker( *sProj4CacheLock(), QgsReadWriteLocker::Unlocked ); if ( !res ) @@ -2317,7 +2354,13 @@ bool testIsGeographic( PJ *crs ) { PJ_CONTEXT *pjContext = QgsProjContext::get(); bool isGeographic = false; - QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( pjContext, crs ) ); + + // check horizontal CRS units + QgsProjUtils::proj_pj_unique_ptr horizontalCrs( QgsProjUtils::crsToHorizontalCrs( crs ) ); + if ( !horizontalCrs ) + return false; + + QgsProjUtils::proj_pj_unique_ptr coordinateSystem( proj_crs_get_coordinate_system( pjContext, horizontalCrs.get() ) ); if ( coordinateSystem ) { const int axisCount = proj_cs_get_axis_count( pjContext, coordinateSystem.get() ); @@ -2711,7 +2754,7 @@ int QgsCoordinateReferenceSystem::syncDatabase() proj4 = ""; } - // there's a not-null contraint on these columns, so we must use empty strings instead + // there's a not-null constraint on these columns, so we must use empty strings instead QString operation = ""; QString ellps = ""; getOperationAndEllipsoidFromProjString( proj4, operation, ellps ); @@ -2961,6 +3004,72 @@ QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::toGeographicCrs() con } } +QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::horizontalCrs() const +{ + switch ( type() ) + { + case Qgis::CrsType::Unknown: + case Qgis::CrsType::Geodetic: + case Qgis::CrsType::Geocentric: + case Qgis::CrsType::Geographic2d: + case Qgis::CrsType::Projected: + case Qgis::CrsType::Temporal: + case Qgis::CrsType::Engineering: + case Qgis::CrsType::Bound: + case Qgis::CrsType::Other: + case Qgis::CrsType::DerivedProjected: + case Qgis::CrsType::Geographic3d: + return *this; + + case Qgis::CrsType::Vertical: + return QgsCoordinateReferenceSystem(); + + case Qgis::CrsType::Compound: + break; + } + + if ( PJ *obj = d->threadLocalProjObject() ) + { + QgsProjUtils::proj_pj_unique_ptr hozCrs = QgsProjUtils::crsToHorizontalCrs( obj ); + if ( hozCrs ) + return QgsCoordinateReferenceSystem::fromProjObject( hozCrs.get() ); + } + return QgsCoordinateReferenceSystem(); +} + +QgsCoordinateReferenceSystem QgsCoordinateReferenceSystem::verticalCrs() const +{ + switch ( type() ) + { + case Qgis::CrsType::Unknown: + case Qgis::CrsType::Geodetic: + case Qgis::CrsType::Geocentric: + case Qgis::CrsType::Geographic2d: + case Qgis::CrsType::Projected: + case Qgis::CrsType::Temporal: + case Qgis::CrsType::Engineering: + case Qgis::CrsType::Bound: + case Qgis::CrsType::Other: + case Qgis::CrsType::DerivedProjected: + case Qgis::CrsType::Geographic3d: + return QgsCoordinateReferenceSystem(); + + case Qgis::CrsType::Vertical: + return *this; + + case Qgis::CrsType::Compound: + break; + } + + if ( PJ *obj = d->threadLocalProjObject() ) + { + QgsProjUtils::proj_pj_unique_ptr vertCrs = QgsProjUtils::crsToVerticalCrs( obj ); + if ( vertCrs ) + return QgsCoordinateReferenceSystem::fromProjObject( vertCrs.get() ); + } + return QgsCoordinateReferenceSystem(); +} + QString QgsCoordinateReferenceSystem::geographicCrsAuthId() const { if ( isGeographic() ) diff --git a/src/core/proj/qgscoordinatereferencesystem.h b/src/core/proj/qgscoordinatereferencesystem.h index 194474841f04..e4b97b0af0a5 100644 --- a/src/core/proj/qgscoordinatereferencesystem.h +++ b/src/core/proj/qgscoordinatereferencesystem.h @@ -337,6 +337,17 @@ class CORE_EXPORT QgsCoordinateReferenceSystem */ static QgsCoordinateReferenceSystem fromSrsId( long srsId ); + /** + * Given a horizontal and vertical CRS, attempts to create a compound CRS + * from them. + * + * Returns an invalid CRS if the inputs are not suitable for a compound CRS, + * or the compound CRS could not be created for the combination. + * + * \since QGIS 3.38 + */ + static QgsCoordinateReferenceSystem createCompoundCrs( const QgsCoordinateReferenceSystem &horizontalCrs, const QgsCoordinateReferenceSystem &verticalCrs ); + // Misc helper functions ----------------------- // TODO QGIS 4: remove type and always use EPSG code, rename to createFromEpsg @@ -895,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 ----------------------------------- /** @@ -981,6 +1000,30 @@ class CORE_EXPORT QgsCoordinateReferenceSystem */ QgsCoordinateReferenceSystem toGeographicCrs() const; + /** + * Returns the horizontal CRS associated with this CRS object. + * + * In the case of a compound CRS, this method will return just the horizontal CRS component. + * + * An invalid CRS will be returned if the object does not contain a horizontal component. + * + * \see verticalCrs() + * \since QGIS 3.38 + */ + QgsCoordinateReferenceSystem horizontalCrs() const; + + /** + * Returns the vertical CRS associated with this CRS object. + * + * In the case of a compound CRS, this method will return just the vertical CRS component. + * + * An invalid CRS will be returned if the object does not contain a vertical component. + * + * \see horizontalCrs() + * \since QGIS 3.38 + */ + QgsCoordinateReferenceSystem verticalCrs() const; + //! Returns auth id of related geographic CRS QString geographicCrsAuthId() const; diff --git a/src/core/proj/qgscoordinatereferencesystemutils.cpp b/src/core/proj/qgscoordinatereferencesystemutils.cpp index a9528e5fba1b..79a5ebd4d93e 100644 --- a/src/core/proj/qgscoordinatereferencesystemutils.cpp +++ b/src/core/proj/qgscoordinatereferencesystemutils.cpp @@ -19,7 +19,12 @@ Qgis::CoordinateOrder QgsCoordinateReferenceSystemUtils::defaultCoordinateOrderForCrs( const QgsCoordinateReferenceSystem &crs ) { - const QList< Qgis::CrsAxisDirection > axisList = crs.axisOrdering(); + // crs may be a compound crs, so get just the horizontal component first + const QgsCoordinateReferenceSystem horizontalCrs = crs.horizontalCrs(); + if ( !horizontalCrs.isValid() ) + return Qgis::CoordinateOrder::XY; + + const QList< Qgis::CrsAxisDirection > axisList = horizontalCrs.axisOrdering(); if ( axisList.size() < 2 ) return Qgis::CoordinateOrder::XY; @@ -348,6 +353,8 @@ QString QgsCoordinateReferenceSystemUtils::translateProjection( const QString &p return QObject::tr( "Miller Oblated Stereographic" ); if ( projection == QLatin1String( "mill" ) ) return QObject::tr( "Miller Cylindrical" ); + if ( projection == QLatin1String( "mod_krovak" ) ) + return QObject::tr( "Modified Krovak" ); if ( projection == QLatin1String( "moll" ) ) return QObject::tr( "Mollweide" ); if ( projection == QLatin1String( "murd1" ) ) diff --git a/src/core/proj/qgscoordinatetransform.cpp b/src/core/proj/qgscoordinatetransform.cpp index fe965bd516e3..09618c4109e7 100644 --- a/src/core/proj/qgscoordinatetransform.cpp +++ b/src/core/proj/qgscoordinatetransform.cpp @@ -787,7 +787,7 @@ void QgsCoordinateTransform::transformCoords( int numPoints, double *x, double * y, sizeof( double ), numPoints, z, sizeof( double ), numPoints, useTime ? t.data() : nullptr, sizeof( double ), useTime ? numPoints : 0 ); - // Try to - approximatively - emulate the behavior of pj_transform()... + // Try to - approximately - emulate the behavior of pj_transform()... // In the case of a single point transform, and a transformation error occurs, // pj_transform() would return the errno. In cases of multiple point transform, // it would continue (for non-transient errors, that is pipeline definition @@ -832,7 +832,7 @@ void QgsCoordinateTransform::transformCoords( int numPoints, double *x, double * yprev.data(), sizeof( double ), numPoints, zprev.data(), sizeof( double ), numPoints, useTime ? t.data() : nullptr, sizeof( double ), useTime ? numPoints : 0 ); - // Try to - approximatively - emulate the behavior of pj_transform()... + // Try to - approximately - emulate the behavior of pj_transform()... // In the case of a single point transform, and a transformation error occurs, // pj_transform() would return the errno. In cases of multiple point transform, // it would continue (for non-transient errors, that is pipeline definition diff --git a/src/core/proj/qgsprojutils.cpp b/src/core/proj/qgsprojutils.cpp index 437d8bada109..d42fa5511614 100644 --- a/src/core/proj/qgsprojutils.cpp +++ b/src/core/proj/qgsprojutils.cpp @@ -24,6 +24,7 @@ #include #include +#include #if defined(USE_THREAD_LOCAL) && !defined(Q_OS_WIN) thread_local QgsProjContext QgsProjContext::sProjContext; @@ -217,6 +218,38 @@ QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::crsToHorizontalCrs( const PJ *crs #endif } +QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::crsToVerticalCrs( const PJ *crs ) +{ + if ( !crs ) + return nullptr; + + PJ_CONTEXT *context = QgsProjContext::get(); + switch ( proj_get_type( crs ) ) + { + case PJ_TYPE_COMPOUND_CRS: + { + int i = 0; + QgsProjUtils::proj_pj_unique_ptr res( proj_crs_get_sub_crs( context, crs, i ) ); + while ( res && ( proj_get_type( res.get() ) != PJ_TYPE_VERTICAL_CRS ) ) + { + i++; + res.reset( proj_crs_get_sub_crs( context, crs, i ) ); + } + return res; + } + + case PJ_TYPE_VERTICAL_CRS: + return QgsProjUtils::proj_pj_unique_ptr( proj_clone( context, crs ) ); + + // maybe other types to handle?? + + default: + return nullptr; + } + + BUILTIN_UNREACHABLE +} + QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::unboundCrs( const PJ *crs ) { if ( !crs ) @@ -259,6 +292,18 @@ QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::crsToDatumEnsemble( const PJ *crs #endif } +QgsProjUtils::proj_pj_unique_ptr QgsProjUtils::createCompoundCrs( const PJ *horizontalCrs, const PJ *verticalCrs ) +{ + if ( !horizontalCrs || !verticalCrs ) + return nullptr; + + // const cast here is for compatibility with proj < 9.5 + return QgsProjUtils::proj_pj_unique_ptr( proj_create_compound_crs( QgsProjContext::get(), + nullptr, + const_cast< PJ *>( horizontalCrs ), + const_cast< PJ * >( verticalCrs ) ) ); +} + bool QgsProjUtils::identifyCrs( const PJ *crs, QString &authName, QString &authCode, IdentifyFlags flags ) { authName.clear(); diff --git a/src/core/proj/qgsprojutils.h b/src/core/proj/qgsprojutils.h index 3efe159469d0..383dbb49feaf 100644 --- a/src/core/proj/qgsprojutils.h +++ b/src/core/proj/qgsprojutils.h @@ -169,9 +169,23 @@ class CORE_EXPORT QgsProjUtils * from it. * * If \a crs does not contain a horizontal CRS (i.e. it is a vertical CRS) NULLPTR will be returned. + * + * \see crsToVerticalCrs() */ static proj_pj_unique_ptr crsToHorizontalCrs( const PJ *crs ); + /** + * Given a PROJ crs (which may be a compound crs, or some other type), extract the vertical crs + * from it. + * + * If \a crs does not contain a vertical CRS (i.e. it is a horizontal CRS) NULLPTR will be returned. + * + * \see crsToHorizontalCrs() + * + * \since QGIS 3.38 + */ + static proj_pj_unique_ptr crsToVerticalCrs( const PJ *crs ); + /** * Given a PROJ crs (which may be a compound or bound crs, or some other type), ensure that it is not * a bound CRS object. @@ -193,6 +207,13 @@ class CORE_EXPORT QgsProjUtils */ static proj_pj_unique_ptr crsToDatumEnsemble( const PJ *crs ); + /** + * Given a PROJ horizontal and vertical CRS, attempt to create a compound CRS from them. + * + * \since QGIS 3.38 + */ + static proj_pj_unique_ptr createCompoundCrs( const PJ *horizontalCrs, const PJ *verticalCrs ); + /** * Attempts to identify a \a crs, matching it to a known authority and code within * an acceptable level of tolerance. diff --git a/src/core/project/qgsprojectelevationproperties.cpp b/src/core/project/qgsprojectelevationproperties.cpp index 6bfc862861ab..15badb2340c1 100644 --- a/src/core/project/qgsprojectelevationproperties.cpp +++ b/src/core/project/qgsprojectelevationproperties.cpp @@ -17,7 +17,6 @@ #include "qgsprojectelevationproperties.h" #include "qgis.h" #include "qgsterrainprovider.h" -#include "qgsrasterbandstats.h" #include @@ -33,7 +32,9 @@ QgsProjectElevationProperties::~QgsProjectElevationProperties() = default; void QgsProjectElevationProperties::reset() { mTerrainProvider = std::make_unique< QgsFlatTerrainProvider >(); + mElevationRange = QgsDoubleRange(); emit changed(); + emit elevationRangeChanged( mElevationRange ); } void QgsProjectElevationProperties::resolveReferences( const QgsProject *project ) @@ -64,7 +65,19 @@ bool QgsProjectElevationProperties::readXml( const QDomElement &element, const Q mTerrainProvider = std::make_unique< QgsFlatTerrainProvider >(); } + bool ok = false; + double rangeLower = std::numeric_limits< double >::lowest(); + const double storedRangeLower = element.attribute( QStringLiteral( "RangeLower" ) ).toDouble( &ok ); + if ( ok ) + rangeLower = storedRangeLower; + double rangeUpper = std::numeric_limits< double >::max(); + const double storedRangeUpper = element.attribute( QStringLiteral( "RangeUpper" ) ).toDouble( &ok ); + if ( ok ) + rangeUpper = storedRangeUpper; + mElevationRange = QgsDoubleRange( rangeLower, rangeUpper ); + emit changed(); + emit elevationRangeChanged( mElevationRange ); return true; } @@ -79,6 +92,12 @@ QDomElement QgsProjectElevationProperties::writeXml( QDomDocument &document, con providerElement.appendChild( mTerrainProvider->writeXml( document, context ) ); element.appendChild( providerElement ); } + + if ( mElevationRange.lower() != std::numeric_limits< double >::lowest() ) + element.setAttribute( QStringLiteral( "RangeLower" ), qgsDoubleToString( mElevationRange.lower() ) ); + if ( mElevationRange.upper() != std::numeric_limits< double >::max() ) + element.setAttribute( QStringLiteral( "RangeUpper" ), qgsDoubleToString( mElevationRange.upper() ) ); + return element; } @@ -95,3 +114,13 @@ void QgsProjectElevationProperties::setTerrainProvider( QgsAbstractTerrainProvid mTerrainProvider.reset( provider ); emit changed(); } + +void QgsProjectElevationProperties::setElevationRange( const QgsDoubleRange &range ) +{ + if ( mElevationRange == range ) + return; + + mElevationRange = range; + emit changed(); + emit elevationRangeChanged( mElevationRange ); +} diff --git a/src/core/project/qgsprojectelevationproperties.h b/src/core/project/qgsprojectelevationproperties.h index 365a48337756..30f992686305 100644 --- a/src/core/project/qgsprojectelevationproperties.h +++ b/src/core/project/qgsprojectelevationproperties.h @@ -86,6 +86,36 @@ class CORE_EXPORT QgsProjectElevationProperties : public QObject */ void setTerrainProvider( QgsAbstractTerrainProvider *provider SIP_TRANSFER ); + /** + * Returns the project's elevation range, which indicates the upper and lower + * elevation limits associated with the project. + * + * \note This is a manual, use-set property, and does not necessarily + * coincide with the elevation ranges for individual layers in the project. + * + * \see setElevationRange() + * \see elevationRangeChanged() + * + * \since QGIS 3.38 + */ + QgsDoubleRange elevationRange() const { return mElevationRange; } + + public slots: + + /** + * Sets the project's elevation \a range, which indicates the upper and lower + * elevation limits associated with the project. + * + * \note This is a manual, use-set property, and does not necessarily + * coincide with the elevation ranges for individual layers in the project. + * + * \see elevationRange() + * \see elevationRangeChanged() + * + * \since QGIS 3.38 + */ + void setElevationRange( const QgsDoubleRange &range ); + signals: /** @@ -93,9 +123,23 @@ class CORE_EXPORT QgsProjectElevationProperties : public QObject */ void changed(); + /** + * Emitted when the project's elevation \a is changed. + * + * \note This is a manual, use-set property, and does not necessarily + * coincide with the elevation ranges for individual layers in the project. + * + * \see elevationRange() + * \see setElevationRange() + * + * \since QGIS 3.38 + */ + void elevationRangeChanged( const QgsDoubleRange &range ); + private: std::unique_ptr< QgsAbstractTerrainProvider > mTerrainProvider; + QgsDoubleRange mElevationRange; }; diff --git a/src/core/providers/arcgis/qgsarcgisrestquery.cpp b/src/core/providers/arcgis/qgsarcgisrestquery.cpp index 2e45a58e42a2..4eda6e1fec37 100644 --- a/src/core/providers/arcgis/qgsarcgisrestquery.cpp +++ b/src/core/providers/arcgis/qgsarcgisrestquery.cpp @@ -17,6 +17,7 @@ #include "qgsarcgisrestutils.h" #include "qgsblockingnetworkrequest.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgslogger.h" #include "qgsapplication.h" #include "qgsmessagelog.h" diff --git a/src/core/providers/arcgis/qgsarcgisrestutils.cpp b/src/core/providers/arcgis/qgsarcgisrestutils.cpp index 0c3887a86a77..8c740ff511d7 100644 --- a/src/core/providers/arcgis/qgsarcgisrestutils.cpp +++ b/src/core/providers/arcgis/qgsarcgisrestutils.cpp @@ -1614,7 +1614,10 @@ QVariant QgsArcGisRestUtils::variantToAttributeValue( const QVariant &variant, Q switch ( expectedType ) { case QVariant::String: - return QString( QUrl::toPercentEncoding( variant.toString() ) ); + { + const QString escaped = variant.toString().replace( '\\', QLatin1String( "\\\\" ) ).replace( '"', QLatin1String( "\\\"" ) ); + return QString( QUrl::toPercentEncoding( escaped, "'" ) ); + } case QVariant::DateTime: case QVariant::Date: diff --git a/src/core/providers/copc/qgscopcprovider.cpp b/src/core/providers/copc/qgscopcprovider.cpp index 8ee568ad9435..b2e7271b4537 100644 --- a/src/core/providers/copc/qgscopcprovider.cpp +++ b/src/core/providers/copc/qgscopcprovider.cpp @@ -64,6 +64,11 @@ QgsCoordinateReferenceSystem QgsCopcProvider::crs() const return mIndex->crs(); } +Qgis::DataProviderFlags QgsCopcProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsRectangle QgsCopcProvider::extent() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS diff --git a/src/core/providers/copc/qgscopcprovider.h b/src/core/providers/copc/qgscopcprovider.h index f898553c904f..be39f8f182a9 100644 --- a/src/core/providers/copc/qgscopcprovider.h +++ b/src/core/providers/copc/qgscopcprovider.h @@ -40,7 +40,7 @@ class QgsCopcProvider: public QgsPointCloudDataProvider ~QgsCopcProvider(); QgsCoordinateReferenceSystem crs() const override; - + Qgis::DataProviderFlags flags() const override; QgsRectangle extent() const override; QgsPointCloudAttributeCollection attributes() const override; bool isValid() const override; diff --git a/src/core/providers/ept/qgseptprovider.cpp b/src/core/providers/ept/qgseptprovider.cpp index 1ba13f3efe4a..54c12d0f9628 100644 --- a/src/core/providers/ept/qgseptprovider.cpp +++ b/src/core/providers/ept/qgseptprovider.cpp @@ -49,6 +49,15 @@ QgsEptProvider::QgsEptProvider( profile = std::make_unique< QgsScopedRuntimeProfile >( tr( "Open data source" ), QStringLiteral( "projectload" ) ); loadIndex( ); + if ( mIndex && !mIndex->isValid() ) + { + appendError( mIndex->error() ); + } +} + +Qgis::DataProviderFlags QgsEptProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; } QgsEptProvider::~QgsEptProvider() = default; diff --git a/src/core/providers/ept/qgseptprovider.h b/src/core/providers/ept/qgseptprovider.h index f66be100997a..da0fff21d621 100644 --- a/src/core/providers/ept/qgseptprovider.h +++ b/src/core/providers/ept/qgseptprovider.h @@ -39,8 +39,8 @@ class QgsEptProvider: public QgsPointCloudDataProvider ~QgsEptProvider(); + Qgis::DataProviderFlags flags() const override; QgsCoordinateReferenceSystem crs() const override; - QgsRectangle extent() const override; QgsPointCloudAttributeCollection attributes() const override; bool isValid() const override; diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index b5e828f0b4da..7f9d7a0b9a45 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -1780,6 +1780,11 @@ QString QgsGdalProvider::description() const return PROVIDER_DESCRIPTION; } +Qgis::DataProviderFlags QgsGdalProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsRasterDataProvider::ProviderCapabilities QgsGdalProvider::providerCapabilities() const { return ProviderCapability::ProviderHintBenefitsFromResampling | @@ -2395,7 +2400,7 @@ QList QgsGdalProvider::buildPyramidList() } } mPyramidList.append( myRasterPyramid ); - //sqare the divisor each step + //square the divisor each step myDivisor = ( myDivisor * 2 ); } @@ -2424,7 +2429,7 @@ QList QgsGdalProvider::buildPyramidList( const QList &lis while ( ( myWidth / myDivisor > 32 ) && ( ( myHeight / myDivisor ) > 32 ) ) { overviewList.append( myDivisor ); - //sqare the divisor each step + //square the divisor each step myDivisor = ( myDivisor * 2 ); } } @@ -2746,7 +2751,7 @@ void buildSupportedRasterFileFilterAndExtensions( QString &fileFiltersString, QS // Grind through all the drivers and their respective metadata. // We'll add a file filter for those drivers that have a file // extension defined for them; the others, well, even though - // theoreticaly we can open those files because there exists a + // theoretically we can open those files because there exists a // driver for them, the user will have to use the "All Files" to // open datasets with no explicitly defined file name extension. diff --git a/src/core/providers/gdal/qgsgdalprovider.h b/src/core/providers/gdal/qgsgdalprovider.h index 0cb9b8285197..52a743b42ea5 100644 --- a/src/core/providers/gdal/qgsgdalprovider.h +++ b/src/core/providers/gdal/qgsgdalprovider.h @@ -130,6 +130,7 @@ class QgsGdalProvider final: public QgsRasterDataProvider, QgsGdalProviderBase static QString expandAuthConfig( const QString &dsName ); QString description() const override; + Qgis::DataProviderFlags flags() const override; QgsRasterDataProvider::ProviderCapabilities providerCapabilities() const override; QgsCoordinateReferenceSystem crs() const override; QgsRectangle extent() const override; diff --git a/src/core/providers/ogr/qgsogrdbconnection.h b/src/core/providers/ogr/qgsogrdbconnection.h index ccd9774c5f58..17217cdf7add 100644 --- a/src/core/providers/ogr/qgsogrdbconnection.h +++ b/src/core/providers/ogr/qgsogrdbconnection.h @@ -57,7 +57,7 @@ class CORE_EXPORT QgsOgrDbConnection : public QObject QString path( ) const { return mPath; } //! Returns the connection name QString name() const { return mConnName; } - //! Sets the \a path fo the connection + //! Sets the \a path for the connection void setPath( const QString &path ); //! Store the connection data in the settings void save(); diff --git a/src/core/providers/ogr/qgsogrfeatureiterator.cpp b/src/core/providers/ogr/qgsogrfeatureiterator.cpp index 1b429d2767f0..86e57024df17 100644 --- a/src/core/providers/ogr/qgsogrfeatureiterator.cpp +++ b/src/core/providers/ogr/qgsogrfeatureiterator.cpp @@ -27,6 +27,8 @@ #include "qgssymbol.h" #include "qgsgeometryengine.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" +#include "qgssetrequestinitiator_p.h" #include diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 761232758595..e7bfd5380e44 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -38,6 +38,8 @@ email : sherman at mrcc.com #include "qgsproviderregistry.h" #include "qgsvariantutils.h" #include "qgsjsonutils.h" +#include "qgssetrequestinitiator_p.h" + #include #define CPL_SUPRESS_CPLUSPLUS //#spellok @@ -1029,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 ) @@ -1411,8 +1413,8 @@ QVariant QgsOgrProvider::defaultValue( int fieldId ) const } } - ( void )mAttributeFields.at( fieldId ).convertCompatible( resultVar ); - return resultVar; + const bool compatible = mAttributeFields.at( fieldId ).convertCompatible( resultVar ); + return compatible && !QgsVariantUtils::isNull( resultVar ) ? resultVar : QVariant(); } QString QgsOgrProvider::defaultValueClause( int fieldIndex ) const @@ -2342,6 +2344,8 @@ bool QgsOgrProvider::_setSubsetString( const QString &theSQL, bool updateFeature if ( theSQL == mSubsetString && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) ) return true; + const QString oldSubsetString { mSubsetString }; + const bool subsetStringHasChanged { theSQL != mSubsetString }; const QString cleanSql = QgsOgrProviderUtils::cleanSubsetString( theSQL ); @@ -2365,6 +2369,12 @@ bool QgsOgrProvider::_setSubsetString( const QString &theSQL, bool updateFeature mOgrSqlLayer = QgsOgrProviderUtils::getSqlLayer( mOgrOrigLayer.get(), subsetLayerH, cleanSql ); Q_ASSERT( mOgrSqlLayer.get() ); mOgrLayer = mOgrSqlLayer.get(); + + const QStringList tableNames {QgsOgrProviderUtils::tableNamesFromSelectSQL( cleanSql ) }; + if ( ! tableNames.isEmpty() ) + { + mLayerName.clear(); + } } else { @@ -2382,6 +2392,18 @@ bool QgsOgrProvider::_setSubsetString( const QString &theSQL, bool updateFeature QMutexLocker locker( mutex ); OGR_L_SetAttributeFilter( layer, nullptr ); } + + // Try to guess the table name from the old subset string or we might + // end with a layer URI without layername + if ( !oldSubsetString.isEmpty() ) + { + const QStringList tableNames { QgsOgrProviderUtils::tableNamesFromSelectSQL( oldSubsetString ) }; + if ( tableNames.size() > 0 ) + { + mLayerName = tableNames.at( 0 ); + } + } + } mSubsetString = theSQL; diff --git a/src/core/providers/ogr/qgsogrproviderconnection.cpp b/src/core/providers/ogr/qgsogrproviderconnection.cpp index 48ae01e98f99..667191df92ba 100644 --- a/src/core/providers/ogr/qgsogrproviderconnection.cpp +++ b/src/core/providers/ogr/qgsogrproviderconnection.cpp @@ -25,6 +25,7 @@ #include "qgsfielddomain.h" #include "qgsogrproviderutils.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsprovidersublayerdetails.h" #include "qgsweakrelation.h" #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,4,0) diff --git a/src/core/providers/ogr/qgsogrproviderutils.cpp b/src/core/providers/ogr/qgsogrproviderutils.cpp index 306333f22609..842d5738f0d8 100644 --- a/src/core/providers/ogr/qgsogrproviderutils.cpp +++ b/src/core/providers/ogr/qgsogrproviderutils.cpp @@ -28,6 +28,7 @@ email : nyall dot dawson at gmail dot com #include "qgsfileutils.h" #include "qgsvariantutils.h" #include "qgssettings.h" +#include "qgssqlstatement.h" #include #include @@ -216,7 +217,7 @@ QString createFilters( const QString &type ) // Grind through all the drivers and their respective metadata. // We'll add a file filter for those drivers that have a file // extension defined for them; the others, welll, even though - // theoreticaly we can open those files because there exists a + // theoretically we can open those files because there exists a // driver for them, the user will have to use the "All Files" to // open datasets with no explicitly defined file name extension. QgsDebugMsgLevel( QStringLiteral( "Driver count: %1" ).arg( OGRGetDriverCount() ), 3 ); @@ -719,6 +720,23 @@ QStringList QgsOgrProviderUtils::wildcards() return createFilters( QStringLiteral( "wildcards" ) ).split( '|' ); } +QStringList QgsOgrProviderUtils::tableNamesFromSelectSQL( const QString &sql ) +{ + QStringList tableNames; + const QgsSQLStatement statement { sql }; + const QgsSQLStatement::NodeSelect *nodeSelect { dynamic_cast( statement.rootNode() ) }; + if ( nodeSelect ) + { + const QList tables { nodeSelect->tables() }; + for ( auto table : std::as_const( tables ) ) + { + tableNames.push_back( table->name() ); + } + } + + return tableNames; +} + bool QgsOgrProviderUtils::createEmptyDataSource( const QString &uri, const QString &format, const QString &encoding, diff --git a/src/core/providers/ogr/qgsogrproviderutils.h b/src/core/providers/ogr/qgsogrproviderutils.h index 624eeecce152..d59ffe0757de 100644 --- a/src/core/providers/ogr/qgsogrproviderutils.h +++ b/src/core/providers/ogr/qgsogrproviderutils.h @@ -123,6 +123,7 @@ class CORE_EXPORT QgsOgrProviderUtils static QStringList fileExtensions(); static QStringList directoryExtensions(); static QStringList wildcards(); + static QStringList tableNamesFromSelectSQL( const QString &sql ); //! Whether the file is a local file. static bool IsLocalFile( const QString &path ); @@ -301,6 +302,7 @@ class CORE_EXPORT QgsOgrProviderUtils }; }; + /** * \class QgsOgrDataset * \brief Wrap a GDALDatasetH object in a thread-safe way diff --git a/src/core/providers/qgsabstractdatabaseproviderconnection.h b/src/core/providers/qgsabstractdatabaseproviderconnection.h index a02dd4d3d0ec..5ff31d58c160 100644 --- a/src/core/providers/qgsabstractdatabaseproviderconnection.h +++ b/src/core/providers/qgsabstractdatabaseproviderconnection.h @@ -520,7 +520,7 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv Q_FLAG( Capabilities ) /** - * The GeometryColumnCapability enum represents the geomery column features supported by the connection. + * The GeometryColumnCapability enum represents the geometry column features supported by the connection. * * \since QGIS 3.16 */ diff --git a/src/core/providers/qgsdataprovider.h b/src/core/providers/qgsdataprovider.h index 465faab014fc..1e6aa66e7f7f 100644 --- a/src/core/providers/qgsdataprovider.h +++ b/src/core/providers/qgsdataprovider.h @@ -252,14 +252,23 @@ class CORE_EXPORT QgsDataProvider : public QObject virtual const QgsDataProviderElevationProperties *elevationProperties() const SIP_SKIP; /** - * Returns the extent of the layer - * \returns QgsRectangle containing the extent of the layer + * Returns the extent of the layer. + * + * \warning This may be expensive to calculate for some data providers, as it may involve + * additional network requests or in some cases, iterating through all the features in a layer. + * If the provider returns the Qgis::DataProviderFlag::FastExtent2D flag from the flags() method + * then the call to extent() is guaranteed to ALWAYS be fast and not involve any additional work. */ virtual QgsRectangle extent() const = 0; /** - * Returns the 3D extent of the layer - * \returns QgsBox3D containing the 3D extent of the layer + * Returns the 3D extent of the layer. + * + * \warning This may be expensive to calculate for some data providers, as it may involve + * additional network requests or in some cases, iterating through all the features in a layer. + * If the provider returns the Qgis::DataProviderFlag::FastExtent3D flag from the flags() method + * then the call to extent3D() is guaranteed to ALWAYS be fast and not involve any additional work. + * * \since QGIS 3.36 */ virtual QgsBox3D extent3D() const diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp b/src/core/providers/sensorthings/qgssensorthingsdataitems.cpp index 807bc9f83d19..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; @@ -96,18 +97,16 @@ QVector QgsSensorThingsConnectionItem::createChildren() children.append( new QgsSensorThingsEntityContainerItem( this, QgsSensorThingsUtils::displayString( entity, true ), mPath + '/' + qgsEnumValueToKey( entity ), - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, entityUriParts ) ) ); + entityUriParts, entity, mConnName ) ); } else { children.append( new QgsSensorThingsLayerEntityItem( this, QgsSensorThingsUtils::displayString( entity, true ), mPath + '/' + qgsEnumValueToKey( entity ), - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, entityUriParts ), + entityUriParts, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, - Qgis::BrowserLayerType::TableLayer ) ); + Qgis::BrowserLayerType::TableLayer, entity, mConnName ) ); } } @@ -119,9 +118,11 @@ QVector QgsSensorThingsConnectionItem::createChildren() // QgsSensorThingsEntityContainerItem // -QgsSensorThingsEntityContainerItem::QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &entityUri ) +QgsSensorThingsEntityContainerItem::QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QVariantMap &entityUriParts, Qgis::SensorThingsEntity entityType, const QString &connectionName ) : QgsDataCollectionItem( parent, name, path, QStringLiteral( "sensorthings" ) ) - , mEntityUri( entityUri ) + , mEntityUriParts( entityUriParts ) + , mEntityType( entityType ) + , mConnectionName( connectionName ) { mCapabilities |= Qgis::BrowserItemCapability::Collapse | Qgis::BrowserItemCapability::Fast; populate(); @@ -137,19 +138,31 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() { QVector children; - const QVariantMap entityUriParts = QgsProviderRegistry::instance()->decodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, mEntityUri ); - 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 ) ) { - QVariantMap geometryUriParts = entityUriParts; + 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; Qgis::BrowserLayerType layerType = Qgis::BrowserLayerType::TableLayer; switch ( wkbType ) @@ -174,16 +187,20 @@ 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; } children.append( new QgsSensorThingsLayerEntityItem( this, name, mPath + '/' + name, - QgsProviderRegistry::instance()->encodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, geometryUriParts ), + geometryUriParts, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, - layerType ) ); + layerType, mEntityType, mConnectionName ) ); children.last()->setSortKey( sortKey++ ); } @@ -194,12 +211,60 @@ QVector QgsSensorThingsEntityContainerItem::createChildren() // QgsSensorThingsLayerEntityItem // -QgsSensorThingsLayerEntityItem::QgsSensorThingsLayerEntityItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri, const QString &provider, Qgis::BrowserLayerType type ) - : QgsLayerItem( parent, name, path, encodedUri, type, provider ) +QgsSensorThingsLayerEntityItem::QgsSensorThingsLayerEntityItem( QgsDataItem *parent, const QString &name, const QString &path, + const QVariantMap &uriParts, const QString &provider, Qgis::BrowserLayerType type, Qgis::SensorThingsEntity entityType, const QString &connectionName ) + : QgsLayerItem( parent, name, path, + QgsProviderRegistry::instance()->encodeUri( QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, uriParts ), + type, provider ) + , mUriParts( uriParts ) + , mEntityType( entityType ) + , mConnectionName( connectionName ) { setState( Qgis::BrowserItemState::Populated ); } +QString QgsSensorThingsLayerEntityItem::layerName() const +{ + QString baseName; + if ( QgsSensorThingsUtils::entityTypeHasGeometry( mEntityType ) ) + { + const QString geometryType = mUriParts.value( QStringLiteral( "geometryType" ) ).toString(); + QString geometryNamePart; + if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 || + geometryType.compare( QLatin1String( "multipoint" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Points" ); + } + else if ( geometryType.compare( QLatin1String( "line" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Lines" ); + } + else if ( geometryType.compare( QLatin1String( "polygon" ), Qt::CaseInsensitive ) == 0 ) + { + geometryNamePart = tr( "Polygons" ); + } + + if ( !geometryNamePart.isEmpty() ) + { + baseName = QStringLiteral( "%1 - %2 (%3)" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ), + geometryNamePart ); + } + else + { + baseName = QStringLiteral( "%1 - %2" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ) ); + } + } + else + { + baseName = QStringLiteral( "%1 - %2" ).arg( mConnectionName, + QgsSensorThingsUtils::displayString( mEntityType, true ) ); + } + + return baseName; +} + // // QgsSensorThingsDataItemProvider // diff --git a/src/core/providers/sensorthings/qgssensorthingsdataitems.h b/src/core/providers/sensorthings/qgssensorthingsdataitems.h index 62dfdb34f7a8..fd4db697d751 100644 --- a/src/core/providers/sensorthings/qgssensorthingsdataitems.h +++ b/src/core/providers/sensorthings/qgssensorthingsdataitems.h @@ -52,11 +52,14 @@ class CORE_EXPORT QgsSensorThingsEntityContainerItem : public QgsDataCollectionI { Q_OBJECT public: - QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &entityUri ); + QgsSensorThingsEntityContainerItem( QgsDataItem *parent, const QString &name, const QString &path, const QVariantMap &entityUriParts, + Qgis::SensorThingsEntity entityType, const QString &connectionName ); bool equal( const QgsDataItem *other ) override; QVector createChildren() override; private: - QString mEntityUri; + QVariantMap mEntityUriParts; + Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; + QString mConnectionName; }; @@ -64,8 +67,14 @@ class CORE_EXPORT QgsSensorThingsLayerEntityItem : public QgsLayerItem { Q_OBJECT public: - QgsSensorThingsLayerEntityItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri, const QString &provider, Qgis::BrowserLayerType type ); - + QgsSensorThingsLayerEntityItem( QgsDataItem *parent, const QString &name, const QString &path, + const QVariantMap &uriParts, const QString &provider, Qgis::BrowserLayerType type, + Qgis::SensorThingsEntity entityType, const QString &connectionName ); + QString layerName() const final; + private: + QVariantMap mUriParts; + Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; + QString mConnectionName; }; //! Provider for sensor things root data item diff --git a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp index 1beaf2a24626..7442a50a57a4 100644 --- a/src/core/providers/sensorthings/qgssensorthingsprovider.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsprovider.cpp @@ -18,13 +18,14 @@ #include "qgssensorthingsprovider.h" #include "qgssensorthingsutils.h" #include "qgsapplication.h" -#include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsthreadingutils.h" #include "qgsreadwritelocker.h" #include "qgssensorthingsfeatureiterator.h" #include "qgssensorthingsdataitems.h" #include "qgssensorthingsconnection.h" +#include "qgsmessagelog.h" #include #include @@ -98,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; } } @@ -183,6 +205,13 @@ QString QgsSensorThingsProvider::htmlMetadata() const return metadata; } +Qgis::DataProviderFlags QgsSensorThingsProvider::flags() const +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsVectorDataProvider::Capabilities QgsSensorThingsProvider::capabilities() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS @@ -194,8 +223,47 @@ QgsVectorDataProvider::Capabilities QgsSensorThingsProvider::capabilities() cons return c; } +bool QgsSensorThingsProvider::supportsSubsetString() const +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + return true; +} + +QString QgsSensorThingsProvider::subsetString() const +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + return mSharedData->subsetString(); +} + +bool QgsSensorThingsProvider::setSubsetString( const QString &subset, bool ) +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + + const QString trimmedSubset = subset.trimmed(); + if ( trimmedSubset == mSharedData->subsetString() ) + return true; + + // store this and restore it after the data source is changed, + // to avoid an unwanted network request to retrieve this again + const QString baseUri = mSharedData->mEntityBaseUri; + + QgsDataSourceUri uri = dataSourceUri(); + uri.setSql( trimmedSubset ); + setDataSourceUri( uri.uri( false ) ); + + mSharedData->mEntityBaseUri = baseUri; + + clearMinMaxCache(); + + emit dataChanged(); + + return true; +} + void QgsSensorThingsProvider::setDataSourceUri( const QString &uri ) { + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + mSharedData = std::make_shared< QgsSensorThingsSharedData >( uri ); QgsDataProvider::setDataSourceUri( uri ); } @@ -210,12 +278,7 @@ QgsCoordinateReferenceSystem QgsSensorThingsProvider::crs() const QgsRectangle QgsSensorThingsProvider::extent() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS - -#if 0 return mSharedData->extent(); -#endif - - return QgsRectangle(); } QString QgsSensorThingsProvider::name() const @@ -240,6 +303,12 @@ QString QgsSensorThingsProvider::description() const return SENSORTHINGS_PROVIDER_DESCRIPTION; } +bool QgsSensorThingsProvider::renderInPreview( const PreviewContext & ) +{ + // be nice to the endpoint and don't make any requests we don't have to! + return false; +} + void QgsSensorThingsProvider::reloadProviderData() { #if 0 @@ -258,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 @@ -316,6 +384,13 @@ QVariantMap QgsSensorThingsProviderMetadata::decodeUri( const QString &uri ) con components.insert( QStringLiteral( "pageSize" ), maxPageSizeParam ); } + ok = false; + const int featureLimitParam = dsUri.param( QStringLiteral( "featureLimit" ) ).toInt( &ok ); + if ( ok ) + { + components.insert( QStringLiteral( "featureLimit" ), featureLimitParam ); + } + switch ( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) { case Qgis::GeometryType::Point: @@ -336,6 +411,25 @@ QVariantMap QgsSensorThingsProviderMetadata::decodeUri( const QString &uri ) con break; } + const QStringList bbox = dsUri.param( QStringLiteral( "bbox" ) ).split( ',' ); + if ( bbox.size() == 4 ) + { + QgsRectangle r; + bool xminOk = false; + bool yminOk = false; + bool xmaxOk = false; + bool ymaxOk = false; + r.setXMinimum( bbox[0].toDouble( &xminOk ) ); + r.setYMinimum( bbox[1].toDouble( &yminOk ) ); + r.setXMaximum( bbox[2].toDouble( &xmaxOk ) ); + r.setYMaximum( bbox[3].toDouble( &ymaxOk ) ); + if ( xminOk && yminOk && xmaxOk && ymaxOk ) + components.insert( QStringLiteral( "bounds" ), r ); + } + + if ( !dsUri.sql().isEmpty() ) + components.insert( QStringLiteral( "sql" ), dsUri.sql() ); + return components; } @@ -379,6 +473,13 @@ QString QgsSensorThingsProviderMetadata::encodeUri( const QVariantMap &parts ) c dsUri.setParam( QStringLiteral( "pageSize" ), QString::number( maxPageSizeParam ) ); } + ok = false; + const int featureLimitParam = parts.value( QStringLiteral( "featureLimit" ) ).toInt( &ok ); + if ( ok ) + { + dsUri.setParam( QStringLiteral( "featureLimit" ), QString::number( featureLimitParam ) ); + } + const QString geometryType = parts.value( QStringLiteral( "geometryType" ) ).toString(); if ( geometryType.compare( QLatin1String( "point" ), Qt::CaseInsensitive ) == 0 ) { @@ -397,6 +498,15 @@ QString QgsSensorThingsProviderMetadata::encodeUri( const QVariantMap &parts ) c dsUri.setWkbType( Qgis::WkbType::MultiPolygonZ ); } + if ( parts.contains( QStringLiteral( "bounds" ) ) && parts.value( QStringLiteral( "bounds" ) ).userType() == QMetaType::type( "QgsRectangle" ) ) + { + const QgsRectangle bBox = parts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >(); + dsUri.setParam( QStringLiteral( "bbox" ), QStringLiteral( "%1,%2,%3,%4" ).arg( bBox.xMinimum() ).arg( bBox.yMinimum() ).arg( bBox.xMaximum() ).arg( bBox.yMaximum() ) ); + } + + if ( !parts.value( QStringLiteral( "sql" ) ).toString().isEmpty() ) + dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() ); + return dsUri.uri( false ); } diff --git a/src/core/providers/sensorthings/qgssensorthingsprovider.h b/src/core/providers/sensorthings/qgssensorthingsprovider.h index 8b05f9208b78..7b0089f648b5 100644 --- a/src/core/providers/sensorthings/qgssensorthingsprovider.h +++ b/src/core/providers/sensorthings/qgssensorthingsprovider.h @@ -30,7 +30,7 @@ * * \since QGIS 3.36 */ -class QgsSensorThingsProvider : public QgsVectorDataProvider +class CORE_EXPORT QgsSensorThingsProvider final : public QgsVectorDataProvider { Q_OBJECT @@ -41,28 +41,32 @@ class QgsSensorThingsProvider : public QgsVectorDataProvider QgsSensorThingsProvider( const QString &uri, const QgsDataProvider::ProviderOptions &providerOptions, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ); - QgsAbstractFeatureSource *featureSource() const override; - QString storageType() const override; - QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const override; - Qgis::WkbType wkbType() const override; - long long featureCount() const override; - QgsFields fields() const override; - QgsLayerMetadata layerMetadata() const override; - QString htmlMetadata() const override; - - QgsVectorDataProvider::Capabilities capabilities() const override; - - QgsCoordinateReferenceSystem crs() const override; - void setDataSourceUri( const QString &uri ) override; - QgsRectangle extent() const override; - bool isValid() const override { return mValid; } - - QString name() const override; - QString description() const override; + QgsAbstractFeatureSource *featureSource() const final; + QString storageType() const final; + QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const final; + Qgis::WkbType wkbType() const final; + long long featureCount() const final; + QgsFields fields() const final; + QgsLayerMetadata layerMetadata() const final; + QString htmlMetadata() const final; + + Qgis::DataProviderFlags flags() const final; + QgsVectorDataProvider::Capabilities capabilities() const final; + bool supportsSubsetString() const final; + QString subsetString() const final; + bool setSubsetString( const QString &subset, bool updateFeatureCount = true ) final; + QgsCoordinateReferenceSystem crs() const final; + void setDataSourceUri( const QString &uri ) final; + QgsRectangle extent() const final; + bool isValid() const final { return mValid; } + + QString name() const final; + QString description() const final; + bool renderInPreview( const QgsDataProvider::PreviewContext &context ) final; static QString providerKey(); - void handlePostCloneOperations( QgsVectorDataProvider *source ) override; + void handlePostCloneOperations( QgsVectorDataProvider *source ) final; private: bool mValid = false; @@ -70,28 +74,28 @@ class QgsSensorThingsProvider : public QgsVectorDataProvider QgsLayerMetadata mLayerMetadata; - void reloadProviderData() override; + void reloadProviderData() final; }; -class QgsSensorThingsProviderMetadata: public QgsProviderMetadata +class QgsSensorThingsProviderMetadata final: public QgsProviderMetadata { Q_OBJECT public: QgsSensorThingsProviderMetadata(); - QIcon icon() const override; - QList dataItemProviders() const override; - QVariantMap decodeUri( const QString &uri ) const override; - QString encodeUri( const QVariantMap &parts ) const override; - QgsSensorThingsProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ) override; - QList< Qgis::LayerType > supportedLayerTypes() const override; + QIcon icon() const final; + QList dataItemProviders() const final; + QVariantMap decodeUri( const QString &uri ) const final; + QString encodeUri( const QVariantMap &parts ) const final; + QgsSensorThingsProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ) final; + QList< Qgis::LayerType > supportedLayerTypes() const final; // handling of stored connections - QMap connections( bool cached ) override; - QgsAbstractProviderConnection *createConnection( const QString &name ) override; - void deleteConnection( const QString &name ) override; - void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) override; + QMap connections( bool cached ) final; + QgsAbstractProviderConnection *createConnection( const QString &name ) final; + void deleteConnection( const QString &name ) final; + void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) final; }; diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp index 1e695cbb1da6..3ff383dd6d52 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.cpp @@ -19,6 +19,7 @@ #include "qgslogger.h" #include "qgsreadwritelocker.h" #include "qgsblockingnetworkrequest.h" +#include "qgssetrequestinitiator_p.h" #include "qgsnetworkaccessmanager.h" #include "qgsjsonutils.h" @@ -37,28 +38,43 @@ QgsSensorThingsSharedData::QgsSensorThingsSharedData( const QString &uri ) mGeometryField = QgsSensorThingsUtils::geometryFieldForEntityType( mEntityType ); // use initial value of maximum page size as default mMaximumPageSize = uriParts.value( QStringLiteral( "pageSize" ), mMaximumPageSize ).toInt(); + // will default to 0 if not specified, i.e. no limit + mFeatureLimit = uriParts.value( QStringLiteral( "featureLimit" ) ).toInt(); + mFilterExtent = uriParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >(); + mSubsetString = uriParts.value( QStringLiteral( "sql" ) ).toString(); 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 { @@ -130,6 +146,16 @@ QUrl QgsSensorThingsSharedData::parseUrl( const QUrl &url, bool *isTestEndpoint return modifiedUrl; } +QgsRectangle QgsSensorThingsSharedData::extent() const +{ + QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read ); + + // Since we can't retrieve the actual layer extent via SensorThings API, we use a pessimistic + // global extent until we've retrieved all the features from the layer + return hasCachedAllFeatures() ? mFetchedFeatureExtent + : ( !mFilterExtent.isNull() ? mFilterExtent : QgsRectangle( -180, -90, 180, 90 ) ); +} + long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const { QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read ); @@ -142,8 +168,12 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const // return no features, just the total count QString countUri = QStringLiteral( "%1?$top=0&$count=true" ).arg( mEntityBaseUri ); const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType ); - if ( !typeFilter.isEmpty() ) - countUri += QStringLiteral( "&$filter=" ) + typeFilter; + const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent ); + QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } ); + if ( !filterString.isEmpty() ) + filterString = QStringLiteral( "&$filter=" ) + filterString; + if ( !filterString.isEmpty() ) + countUri += filterString; const QUrl url = parseUrl( QUrl( countUri ) ); @@ -177,6 +207,8 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const } mFeatureCount = rootContent["@iot.count"].get(); + if ( mFeatureLimit > 0 && mFeatureCount > mFeatureLimit ) + mFeatureCount = mFeatureLimit; } catch ( const json::parse_error &ex ) { @@ -187,10 +219,17 @@ long long QgsSensorThingsSharedData::featureCount( QgsFeedback *feedback ) const return mFeatureCount; } +QString QgsSensorThingsSharedData::subsetString() const +{ + return mSubsetString; +} + bool QgsSensorThingsSharedData::hasCachedAllFeatures() const { QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read ); - return mHasCachedAllFeatures || ( mFeatureCount > 0 && mCachedFeatures.size() == mFeatureCount ); + return mHasCachedAllFeatures + || ( mFeatureCount > 0 && mCachedFeatures.size() == mFeatureCount ) + || ( mFeatureLimit > 0 && mCachedFeatures.size() >= mFeatureLimit ); } bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsFeedback *feedback ) @@ -213,10 +252,17 @@ bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsF if ( mNextPage.isEmpty() ) { locker.changeMode( QgsReadWriteLocker::Write ); - mNextPage = QStringLiteral( "%1?$top=%2&$count=false" ).arg( mEntityBaseUri ).arg( mMaximumPageSize ); + + int thisPageSize = mMaximumPageSize; + if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit ) + thisPageSize = mFeatureLimit - mCachedFeatures.size(); + + mNextPage = QStringLiteral( "%1?$top=%2&$count=false" ).arg( mEntityBaseUri ).arg( thisPageSize ); const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType ); - if ( !typeFilter.isEmpty() ) - mNextPage += QStringLiteral( "&$filter=" ) + typeFilter; + const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, mFilterExtent ); + const QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } ); + if ( !filterString.isEmpty() ) + mNextPage += QStringLiteral( "&$filter=" ) + filterString; } locker.unlock(); @@ -243,18 +289,39 @@ bool QgsSensorThingsSharedData::getFeature( QgsFeatureId id, QgsFeature &f, QgsF QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectangle &extent, QgsFeedback *feedback, const QString &thisPage, QString &nextPage, const QgsFeatureIds &alreadyFetchedIds ) { - const QgsGeometry extentGeom = QgsGeometry::fromRect( extent ); + const QgsRectangle requestExtent = mFilterExtent.isNull() ? extent : extent.intersect( mFilterExtent ); + const QgsGeometry extentGeom = QgsGeometry::fromRect( requestExtent ); QgsReadWriteLocker locker( mReadWriteLock, QgsReadWriteLocker::Read ); if ( hasCachedAllFeatures() || mCachedExtent.contains( extentGeom ) ) { // all features cached locally, rely on local spatial index - return qgis::listToSet( mSpatialIndex.intersects( extent ) ); + return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) ); } - // TODO -- is using 'geography' always correct here? const QString typeFilter = QgsSensorThingsUtils::filterForWkbType( mEntityType, mGeometryType ); - QString queryUrl = !thisPage.isEmpty() ? thisPage : QStringLiteral( "%1?$top=%2&$count=false&$filter=geo.intersects(%3, geography'%4')%5" ).arg( mEntityBaseUri ).arg( mMaximumPageSize ).arg( mGeometryField, extent.asWktPolygon(), typeFilter.isEmpty() ? QString() : ( QStringLiteral( " and " ) + typeFilter ) ); + const QString extentFilter = QgsSensorThingsUtils::filterForExtent( mGeometryField, requestExtent ); + QString filterString = QgsSensorThingsUtils::combineFilters( { typeFilter, extentFilter, mSubsetString } ); + if ( !filterString.isEmpty() ) + filterString = QStringLiteral( "&$filter=" ) + filterString; + int thisPageSize = mMaximumPageSize; + QString queryUrl; + if ( !thisPage.isEmpty() ) + { + queryUrl = thisPage; + const thread_local QRegularExpression topRe( QStringLiteral( "\\$top=\\d+" ) ); + const QRegularExpressionMatch match = topRe.match( queryUrl ); + if ( match.hasMatch() ) + { + if ( mFeatureLimit > 0 && ( mCachedFeatures.size() + thisPageSize ) > mFeatureLimit ) + thisPageSize = mFeatureLimit - mCachedFeatures.size(); + queryUrl = queryUrl.left( match.capturedStart( 0 ) ) + QStringLiteral( "$top=%1" ).arg( thisPageSize ) + queryUrl.mid( match.capturedEnd( 0 ) ); + } + } + else + { + queryUrl = QStringLiteral( "%1?$top=%2&$count=false%3" ).arg( mEntityBaseUri ).arg( thisPageSize ).arg( filterString ); + } if ( thisPage.isEmpty() && mCachedExtent.intersects( extentGeom ) ) { @@ -262,7 +329,7 @@ QgsFeatureIds QgsSensorThingsSharedData::getFeatureIdsInExtent( const QgsRectang // This is slightly nicer from a rendering point of view, because panning the map won't see features // previously visible disappear temporarily while we wait for them to be included in the service's result set... nextPage = queryUrl; - return qgis::listToSet( mSpatialIndex.intersects( extent ) ); + return qgis::listToSet( mSpatialIndex.intersects( requestExtent ) ); } locker.unlock(); @@ -306,6 +373,7 @@ void QgsSensorThingsSharedData::clearCache() mCachedFeatures.clear(); mIotIdToFeatureId.clear(); mSpatialIndex = QgsSpatialIndex(); + mFetchedFeatureExtent = QgsRectangle(); } bool QgsSensorThingsSharedData::processFeatureRequest( QString &nextPage, QgsFeedback *feedback, const std::function< void( const QgsFeature & ) > &fetchedFeatureCallback, const std::function &continueFetchingCallback, const std::function &onNoMoreFeaturesCallback ) @@ -427,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 ) ) @@ -602,24 +678,57 @@ 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) // Set geometry - if ( mGeometryType != Qgis::WkbType::NoGeometry && featureData.contains( mGeometryField.toLocal8Bit().constData() ) ) + if ( mGeometryType != Qgis::WkbType::NoGeometry ) { - feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( featureData[mGeometryField.toLocal8Bit().constData()] ) ); + if ( featureData.contains( mGeometryField.toLocal8Bit().constData() ) ) + { + const auto &geometryPart = featureData[mGeometryField.toLocal8Bit().constData()]; + if ( geometryPart.contains( "geometry" ) ) + feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart["geometry"] ) ); + else + feature.setGeometry( QgsJsonUtils::geometryFromGeoJson( geometryPart ) ); + } } mCachedFeatures.insert( feature.id(), feature ); mIotIdToFeatureId.insert( iotId, feature.id() ); mSpatialIndex.addFeature( feature ); + mFetchedFeatureExtent.combineExtentWith( feature.geometry().boundingBox() ); fetchedFeatureCallback( feature ); + + if ( mFeatureLimit > 0 && mFeatureLimit <= mCachedFeatures.size() ) + break; } locker.unlock(); - if ( rootContent.contains( "@iot.nextLink" ) ) + if ( rootContent.contains( "@iot.nextLink" ) && ( mFeatureLimit == 0 || mFeatureLimit > mCachedFeatures.size() ) ) { nextPage = QString::fromStdString( rootContent["@iot.nextLink"].get() ); } diff --git a/src/core/providers/sensorthings/qgssensorthingsshareddata.h b/src/core/providers/sensorthings/qgssensorthingsshareddata.h index fb618f0df456..f54bf7343e4c 100644 --- a/src/core/providers/sensorthings/qgssensorthingsshareddata.h +++ b/src/core/providers/sensorthings/qgssensorthingsshareddata.h @@ -50,7 +50,9 @@ class QgsSensorThingsSharedData QString error() const { return mError; } QgsCoordinateReferenceSystem crs() const { return mSourceCRS; } + QgsRectangle extent() const; long long featureCount( QgsFeedback *feedback = nullptr ) const; + QString subsetString() const; bool hasCachedAllFeatures() const; bool getFeature( QgsFeatureId id, QgsFeature &f, QgsFeedback *feedback = nullptr ); @@ -76,12 +78,20 @@ class QgsSensorThingsSharedData mutable QString mError; QString mEntityBaseUri; + QString mSubsetString; Qgis::SensorThingsEntity mEntityType = Qgis::SensorThingsEntity::Invalid; + int mFeatureLimit = 0; Qgis::WkbType mGeometryType = Qgis::WkbType::Unknown; QString mGeometryField; QgsFields mFields; + + QgsRectangle mFilterExtent; + + //! Extent calculated from features actually fetched so far + QgsRectangle mFetchedFeatureExtent; + QgsCoordinateReferenceSystem mSourceCRS; mutable long long mFeatureCount = static_cast< long long >( Qgis::FeatureCountState::Uncounted ); diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.cpp b/src/core/providers/sensorthings/qgssensorthingsutils.cpp index 64eddcf16c6f..87f9ba757a44 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.cpp +++ b/src/core/providers/sensorthings/qgssensorthingsutils.cpp @@ -17,9 +17,11 @@ #include "qgsfield.h" #include "qgsfields.h" #include "qgswkbtypes.h" +#include "qgssetrequestinitiator_p.h" #include "qgsnetworkaccessmanager.h" #include "qgsblockingnetworkrequest.h" #include "qgslogger.h" +#include "qgsrectangle.h" #include #include #include @@ -43,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; } @@ -69,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 } @@ -92,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; } @@ -178,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; @@ -201,6 +223,9 @@ QString QgsSensorThingsUtils::geometryFieldForEntityType( Qgis::SensorThingsEnti case Qgis::SensorThingsEntity::FeatureOfInterest: return QStringLiteral( "feature" ); + + case Qgis::SensorThingsEntity::MultiDatastream: + return QStringLiteral( "observedArea" ); } BUILTIN_UNREACHABLE } @@ -220,48 +245,84 @@ bool QgsSensorThingsUtils::entityTypeHasGeometry( Qgis::SensorThingsEntity type case Qgis::SensorThingsEntity::Location: case Qgis::SensorThingsEntity::FeatureOfInterest: + case Qgis::SensorThingsEntity::MultiDatastream: return true; } BUILTIN_UNREACHABLE } -QString QgsSensorThingsUtils::filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ) +Qgis::GeometryType QgsSensorThingsUtils::geometryTypeForEntity( Qgis::SensorThingsEntity type ) { - QString filterTarget; - switch ( entityType ) + switch ( type ) { - case Qgis::SensorThingsEntity::Location: - filterTarget = QStringLiteral( "location/type" ); - break; - - case Qgis::SensorThingsEntity::FeatureOfInterest: - filterTarget = QStringLiteral( "feature/type" ); - break; - case Qgis::SensorThingsEntity::Invalid: case Qgis::SensorThingsEntity::Thing: case Qgis::SensorThingsEntity::HistoricalLocation: case Qgis::SensorThingsEntity::Datastream: case Qgis::SensorThingsEntity::Sensor: - case Qgis::SensorThingsEntity::ObservedProperty: case Qgis::SensorThingsEntity::Observation: - break; + 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; switch ( QgsWkbTypes::geometryType( wkbType ) ) { case Qgis::GeometryType::Point: - return QStringLiteral( "%1 eq 'Point'" ).arg( filterTarget ); + geometryTypeString = QStringLiteral( "Point" ); + break; case Qgis::GeometryType::Polygon: - return QStringLiteral( "%1 eq 'Polygon'" ).arg( filterTarget ); + geometryTypeString = QStringLiteral( "Polygon" ); + break; case Qgis::GeometryType::Line: - // TODO -- confirm - return QStringLiteral( "%1 eq 'LineString'" ).arg( filterTarget ); + geometryTypeString = QStringLiteral( "LineString" ); + break; + case Qgis::GeometryType::Unknown: case Qgis::GeometryType::Null: - break; + return QString(); } - return QString(); + + const QString filterTarget = geometryFieldForEntityType( entityType ); + if ( filterTarget.isEmpty() ) + return QString(); + + return QStringLiteral( "%1/type eq '%2' or %1/geometry/type eq '%2'" ).arg( filterTarget, geometryTypeString ); +} + +QString QgsSensorThingsUtils::filterForExtent( const QString &geometryField, const QgsRectangle &extent ) +{ + // TODO -- confirm using 'geography' is always correct here + return ( extent.isNull() || geometryField.isEmpty() ) + ? QString() + : QStringLiteral( "geo.intersects(%1, geography'%2')" ).arg( geometryField, extent.asWktPolygon() ); +} + +QString QgsSensorThingsUtils::combineFilters( const QStringList &filters ) +{ + QStringList nonEmptyFilters; + for ( const QString &filter : filters ) + { + if ( !filter.isEmpty() ) + nonEmptyFilters.append( filter ); + } + if ( nonEmptyFilters.empty() ) + return QString(); + if ( nonEmptyFilters.size() == 1 ) + return nonEmptyFilters.at( 0 ); + + return QStringLiteral( "(" ) + nonEmptyFilters.join( QLatin1String( ") and (" ) ) + QStringLiteral( ")" ); } QList QgsSensorThingsUtils::availableGeometryTypes( const QString &uri, Qgis::SensorThingsEntity type, QgsFeedback *feedback, const QString &authCfg ) diff --git a/src/core/providers/sensorthings/qgssensorthingsutils.h b/src/core/providers/sensorthings/qgssensorthingsutils.h index d73f3059addf..873abe1afa91 100644 --- a/src/core/providers/sensorthings/qgssensorthingsutils.h +++ b/src/core/providers/sensorthings/qgssensorthingsutils.h @@ -21,6 +21,7 @@ class QgsFields; class QgsFeedback; +class QgsRectangle; /** * \ingroup core @@ -36,6 +37,9 @@ class CORE_EXPORT QgsSensorThingsUtils //! Default page size static constexpr int DEFAULT_PAGE_SIZE = 200; SIP_SKIP + //! Default limit on number of features fetched + static constexpr int DEFAULT_FEATURE_LIMIT = 10000; SIP_SKIP + /** * Converts a string value to a Qgis::SensorThingsEntity type. * @@ -72,12 +76,40 @@ 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. */ static QString filterForWkbType( Qgis::SensorThingsEntity entityType, Qgis::WkbType wkbType ); + /** + * Returns a filter string which restricts results to those within the specified + * \a extent. + * + * The \a extent should always be specified in EPSG:4326. + * + * \since QGIS 3.38 + */ + static QString filterForExtent( const QString &geometryField, const QgsRectangle &extent ); + + /** + * Combines a set of SensorThings API filter operators. + * + * See https://docs.ogc.org/is/18-088/18-088.html#requirement-request-data-filter + * + * \since QGIS 3.38 + */ + static QString combineFilters( const QStringList &filters ); + /** * Returns a list of available geometry types for the server at the specified \a uri * and entity \a type. diff --git a/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp b/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp index d47cc3cd6573..418d7eb29174 100644 --- a/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp +++ b/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp @@ -58,6 +58,11 @@ QgsVirtualPointCloudProvider::QgsVirtualPointCloudProvider( parseFile(); } +Qgis::DataProviderFlags QgsVirtualPointCloudProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsVirtualPointCloudProvider::~QgsVirtualPointCloudProvider() = default; QgsPointCloudDataProvider::Capabilities QgsVirtualPointCloudProvider::capabilities() const diff --git a/src/core/providers/vpc/qgsvirtualpointcloudprovider.h b/src/core/providers/vpc/qgsvirtualpointcloudprovider.h index 21661c2f155b..711a804cc365 100644 --- a/src/core/providers/vpc/qgsvirtualpointcloudprovider.h +++ b/src/core/providers/vpc/qgsvirtualpointcloudprovider.h @@ -39,6 +39,7 @@ class CORE_EXPORT QgsVirtualPointCloudProvider: public QgsPointCloudDataProvider ~QgsVirtualPointCloudProvider(); + Qgis::DataProviderFlags flags() const override; QgsPointCloudDataProvider::Capabilities capabilities() const override; QgsCoordinateReferenceSystem crs() const override; diff --git a/src/core/qgis.h b/src/core/qgis.h index dc9c625e4f5f..56a599e59dcb 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -1163,6 +1163,25 @@ class CORE_EXPORT Qgis Q_ENUM( RasterRendererFlag ) Q_FLAG( RasterRendererFlags ) + /** + * Raster renderer capabilities. + * + * \since QGIS 3.48 + */ + enum class RasterRendererCapability : int SIP_ENUM_BASETYPE( IntFlag ) + { + UsesMultipleBands = 1 << 0, //!< The renderer utilizes multiple raster bands for color data (note that alpha bands are not considered for this capability) + }; + Q_ENUM( RasterRendererCapability ) + + /** + * Raster renderer capabilities. + * + * \since QGIS 3.38 + */ + Q_DECLARE_FLAGS( RasterRendererCapabilities, RasterRendererCapability ) + Q_FLAG( RasterRendererCapabilities ) + /** * \brief The RasterAttributeTableFieldUsage enum represents the usage of a Raster Attribute Table field. * \note Directly mapped from GDALRATFieldUsage enum values. @@ -1827,6 +1846,8 @@ class CORE_EXPORT Qgis enum class DataProviderFlag : int SIP_ENUM_BASETYPE( IntFlag ) { IsBasemapSource = 1 << 1, //!< Associated source should be considered a 'basemap' layer. See Qgis::MapLayerProperty::IsBasemapLayer. + FastExtent2D = 1 << 2, //!< Provider's 2D extent retrieval via QgsDataProvider::extent() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38. + FastExtent3D = 1 << 3, //!< Provider's 3D extent retrieval via QgsDataProvider::extent3D() is always guaranteed to be trivial/fast to calculate. Since QGIS 3.38. }; //! Data provider flags Q_DECLARE_FLAGS( DataProviderFlags, DataProviderFlag ) @@ -2123,6 +2144,7 @@ class CORE_EXPORT Qgis FixedTemporalRange SIP_MONKEYPATCH_COMPAT_NAME( ModeFixedTemporalRange ) = 0, //!< Mode when temporal properties have fixed start and end datetimes. 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) }; Q_ENUM( RasterTemporalMode ) @@ -3232,6 +3254,46 @@ class CORE_EXPORT Qgis }; Q_ENUM( AltitudeBinding ) + /** + * Describes how the limits of a range are handled. + * + * \since QGIS 3.38 + */ + enum class RangeLimits : int + { + IncludeBoth = 0, //!< Both lower and upper values are included in the range + IncludeLowerExcludeUpper, //!< Lower value is included in the range, upper value is excluded + ExcludeLowerIncludeUpper, //!< Lower value is excluded from the range, upper value in inccluded + ExcludeBoth, //!< Both lower and upper values are excluded from the range + }; + Q_ENUM( RangeLimits ) + + /** + * Raster layer elevation modes. + * + * \since QGIS 3.38 + */ + enum class RasterElevationMode : int + { + FixedElevationRange = 0, //!< Layer has a fixed elevation range + RepresentsElevationSurface = 1, //!< Pixel values represent an elevation surface + FixedRangePerBand = 2, //!< Layer has a fixed (manually specified) elevation range per band + DynamicRangePerBand = 3, //!< Layer has a elevation range per band, calculated dynamically from an expression + }; + Q_ENUM( RasterElevationMode ) + + /** + * Mesh layer elevation modes. + * + * \since QGIS 3.38 + */ + enum class MeshElevationMode : int + { + FixedElevationRange = 0, //!< Layer has a fixed elevation range + FromVertices = 1 //!< Elevation should be taken from mesh vertices + }; + Q_ENUM( MeshElevationMode ) + /** * Between line constraints which can be enabled * @@ -4810,6 +4872,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 ) @@ -4948,6 +5011,7 @@ Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProfileGeneratorFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProjectCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::ProjectReadFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterRendererFlags ) +Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterRendererCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RasterTemporalCapabilityFlags ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RelationshipCapabilities ) Q_DECLARE_OPERATORS_FOR_FLAGS( Qgis::RenderContextFlags ) diff --git a/src/core/qgsabstractcontentcache.cpp b/src/core/qgsabstractcontentcache.cpp index 9d6ebaa447f9..56ab94d7c292 100644 --- a/src/core/qgsabstractcontentcache.cpp +++ b/src/core/qgsabstractcontentcache.cpp @@ -16,6 +16,7 @@ ***************************************************************************/ #include "qgsabstractcontentcache.h" +#include "qgssetrequestinitiator_p.h" // // QgsAbstractContentCacheEntry @@ -39,8 +40,3 @@ void QgsAbstractContentCacheBase::onRemoteContentFetched( const QString &, bool { } - - - - - diff --git a/src/core/qgsabstractcontentcache.h b/src/core/qgsabstractcontentcache.h index ac59b24b69c7..22043074a53b 100644 --- a/src/core/qgsabstractcontentcache.h +++ b/src/core/qgsabstractcontentcache.h @@ -260,177 +260,7 @@ class CORE_EXPORT QgsAbstractContentCache : public QgsAbstractContentCacheBase * be TRUE from GUI based applications (like the main QGIS application) or crashes will result. Only for * use in external scripts or QGIS server. */ - QByteArray getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking = false ) const - { - // is it a path to local file? - QFile file( path ); - if ( file.exists() ) - { - if ( file.open( QIODevice::ReadOnly ) ) - { - return file.readAll(); - } - else - { - return missingContent; - } - } - - // maybe it's an embedded base64 string - if ( path.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) ) - { - const QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix - return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals ); - } - - // maybe it's a url... - if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs - { - return missingContent; - } - - const QUrl url( path ); - if ( !url.isValid() ) - { - return missingContent; - } - - // check whether it's a url pointing to a local file - if ( url.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 ) - { - file.setFileName( url.toLocalFile() ); - if ( file.exists() ) - { - if ( file.open( QIODevice::ReadOnly ) ) - { - return file.readAll(); - } - } - - // not found... - return missingContent; - } - - const QMutexLocker locker( &mMutex ); - - // already a request in progress for this url - if ( mPendingRemoteUrls.contains( path ) ) - { - // it's a non blocking request so return fetching content - if ( !blocking ) - { - return fetchingContent; - } - - // it's a blocking request so try to find the task and wait for task finished - const auto constActiveTasks = QgsApplication::taskManager()->activeTasks(); - for ( QgsTask *task : constActiveTasks ) - { - // the network content fetcher task's description ends with the path - if ( !task->description().endsWith( path ) ) - { - continue; - } - - // cast task to network content fetcher task - QgsNetworkContentFetcherTask *ncfTask = qobject_cast( task ); - if ( ncfTask ) - { - // wait for task finished - if ( waitForTaskFinished( ncfTask ) ) - { - if ( mRemoteContentCache.contains( path ) ) - { - // We got the file! - return *mRemoteContentCache[ path ]; - } - } - } - // task found, no needs to continue - break; - } - // if no content returns the content is probably in remote content cache - // or a new task will be created - } - - if ( mRemoteContentCache.contains( path ) ) - { - // already fetched this content - phew. Just return what we already got. - return *mRemoteContentCache[ path ]; - } - - mPendingRemoteUrls.insert( path ); - //fire up task to fetch content in background - QNetworkRequest request( url ); - QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsAbstractContentCache<%1>" ).arg( mTypeString ) ); - request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache ); - request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true ); - - QgsNetworkContentFetcherTask *task = new QgsNetworkContentFetcherTask( request ); - connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path, missingContent] - { - const QMutexLocker locker( &mMutex ); - - QNetworkReply *reply = task->reply(); - if ( !reply ) - { - // canceled - QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) ); - return; - } - - if ( reply->error() != QNetworkReply::NoError ) - { - QgsMessageLog::logMessage( tr( "%3 request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path, mTypeString ), mTypeString ); - return; - } - - bool ok = true; - - const QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ); - if ( !QgsVariantUtils::isNull( status ) && status.toInt() >= 400 ) - { - const QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ); - QgsMessageLog::logMessage( tr( "%4 request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path, mTypeString ), mTypeString ); - mRemoteContentCache.insert( path, new QByteArray( missingContent ) ); - ok = false; - } - - if ( !checkReply( reply, path ) ) - { - mRemoteContentCache.insert( path, new QByteArray( missingContent ) ); - ok = false; - } - - if ( ok ) - { - // read the content data - const QByteArray ba = reply->readAll(); - - // because of the fragility listed below in waitForTaskFinished, this slot may get called twice. In that case - // the second time will have an empty reply (we've already read it all...) - if ( !ba.isEmpty() ) - mRemoteContentCache.insert( path, new QByteArray( ba ) ); - } - QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) ); - } ); - - QgsApplication::taskManager()->addTask( task ); - - // if blocking, wait for finished - if ( blocking ) - { - if ( waitForTaskFinished( task ) ) - { - if ( mRemoteContentCache.contains( path ) ) - { - // We got the file! - return *mRemoteContentCache[ path ]; - } - } - } - return fetchingContent; - } + QByteArray getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking = false ) const; void onRemoteContentFetched( const QString &url, bool success ) override { @@ -636,7 +466,7 @@ class CORE_EXPORT QgsAbstractContentCache : public QgsAbstractContentCacheBase { QgsDebugMsgLevel( QStringLiteral( "***Entry:" ), 1 ); entry->dump(); - entry = entry->nextEntry; + entry = static_cast< T * >( entry->nextEntry ); } } diff --git a/src/core/qgsabstractcontentcache_p.h b/src/core/qgsabstractcontentcache_p.h new file mode 100644 index 000000000000..e6c424591b84 --- /dev/null +++ b/src/core/qgsabstractcontentcache_p.h @@ -0,0 +1,197 @@ +/*************************************************************************** + qgsabstractcontentcache_p.h + --------------- + begin : February 2024 + copyright : (C) 2024 by Matthias Kuhn + email : matthias@opengis.ch + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSABSTRACTCONTENTCACHE_P_H +#define QGSABSTRACTCONTENTCACHE_P_H + +#include "qgsabstractcontentcache.h" +#include "qgssetrequestinitiator_p.h" + +template +QByteArray QgsAbstractContentCache::getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking ) const +{ + // is it a path to local file? + QFile file( path ); + if ( file.exists() ) + { + if ( file.open( QIODevice::ReadOnly ) ) + { + return file.readAll(); + } + else + { + return missingContent; + } + } + + // maybe it's an embedded base64 string + if ( path.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) ) + { + const QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix + return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals ); + } + + // maybe it's a url... + if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs + { + return missingContent; + } + + const QUrl url( path ); + if ( !url.isValid() ) + { + return missingContent; + } + + // check whether it's a url pointing to a local file + if ( url.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 ) + { + file.setFileName( url.toLocalFile() ); + if ( file.exists() ) + { + if ( file.open( QIODevice::ReadOnly ) ) + { + return file.readAll(); + } + } + + // not found... + return missingContent; + } + + const QMutexLocker locker( &mMutex ); + + // already a request in progress for this url + if ( mPendingRemoteUrls.contains( path ) ) + { + // it's a non blocking request so return fetching content + if ( !blocking ) + { + return fetchingContent; + } + + // it's a blocking request so try to find the task and wait for task finished + const auto constActiveTasks = QgsApplication::taskManager()->activeTasks(); + for ( QgsTask *task : constActiveTasks ) + { + // the network content fetcher task's description ends with the path + if ( !task->description().endsWith( path ) ) + { + continue; + } + + // cast task to network content fetcher task + QgsNetworkContentFetcherTask *ncfTask = qobject_cast( task ); + if ( ncfTask ) + { + // wait for task finished + if ( waitForTaskFinished( ncfTask ) ) + { + if ( mRemoteContentCache.contains( path ) ) + { + // We got the file! + return *mRemoteContentCache[ path ]; + } + } + } + // task found, no needs to continue + break; + } + // if no content returns the content is probably in remote content cache + // or a new task will be created + } + + if ( mRemoteContentCache.contains( path ) ) + { + // already fetched this content - phew. Just return what we already got. + return *mRemoteContentCache[ path ]; + } + + mPendingRemoteUrls.insert( path ); + //fire up task to fetch content in background + QNetworkRequest request( url ); + QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsAbstractContentCache<%1>" ).arg( mTypeString ) ); + request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache ); + request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true ); + + QgsNetworkContentFetcherTask *task = new QgsNetworkContentFetcherTask( request ); + connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path, missingContent] + { + const QMutexLocker locker( &mMutex ); + + QNetworkReply *reply = task->reply(); + if ( !reply ) + { + // canceled + QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) ); + return; + } + + if ( reply->error() != QNetworkReply::NoError ) + { + QgsMessageLog::logMessage( tr( "%3 request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path, mTypeString ), mTypeString ); + return; + } + + bool ok = true; + + const QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ); + if ( !QgsVariantUtils::isNull( status ) && status.toInt() >= 400 ) + { + const QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ); + QgsMessageLog::logMessage( tr( "%4 request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path, mTypeString ), mTypeString ); + mRemoteContentCache.insert( path, new QByteArray( missingContent ) ); + ok = false; + } + + if ( !checkReply( reply, path ) ) + { + mRemoteContentCache.insert( path, new QByteArray( missingContent ) ); + ok = false; + } + + if ( ok ) + { + // read the content data + const QByteArray ba = reply->readAll(); + + // because of the fragility listed below in waitForTaskFinished, this slot may get called twice. In that case + // the second time will have an empty reply (we've already read it all...) + if ( !ba.isEmpty() ) + mRemoteContentCache.insert( path, new QByteArray( ba ) ); + } + QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) ); + } ); + + QgsApplication::taskManager()->addTask( task ); + + // if blocking, wait for finished + if ( blocking ) + { + if ( waitForTaskFinished( task ) ) + { + if ( mRemoteContentCache.contains( path ) ) + { + // We got the file! + return *mRemoteContentCache[ path ]; + } + } + } + return fetchingContent; +} + +#endif // QGSABSTRACTCONTENTCACHE_P_H diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index 254180e10c8d..9bb2f12161de 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -305,6 +305,8 @@ void QgsApplication::init( QString profileFolder ) #endif qRegisterMetaType( "QPainter::CompositionMode" ); qRegisterMetaType( "QgsDateTimeRange" ); + qRegisterMetaType( "QgsDoubleRange" ); + qRegisterMetaType( "QgsIntRange" ); qRegisterMetaType>( "QList" ); qRegisterMetaType>( "QMap" ); qRegisterMetaType>( "QMap" ); diff --git a/src/core/qgsbookmarkmodel.cpp b/src/core/qgsbookmarkmodel.cpp index e1b2dabd5c87..5e833d3e6018 100644 --- a/src/core/qgsbookmarkmodel.cpp +++ b/src/core/qgsbookmarkmodel.cpp @@ -222,11 +222,9 @@ bool QgsBookmarkManagerModel::setData( const QModelIndex &index, const QVariant return false; } -bool QgsBookmarkManagerModel::insertRows( int, int count, const QModelIndex &parent ) +bool QgsBookmarkManagerModel::insertRows( int, int count, const QModelIndex & ) { // append - const int oldCount = mManager->bookmarks().count(); - beginInsertRows( parent, oldCount, oldCount + count ); bool result = true; for ( int i = 0; i < count; ++i ) { @@ -236,14 +234,11 @@ bool QgsBookmarkManagerModel::insertRows( int, int count, const QModelIndex &par mBlocked = false; result &= res; } - endInsertRows(); return result; } -bool QgsBookmarkManagerModel::removeRows( int row, int count, const QModelIndex &parent ) +bool QgsBookmarkManagerModel::removeRows( int row, int count, const QModelIndex & ) { - beginRemoveRows( parent, row, row + count ); - const QList< QgsBookmark > appBookmarks = mManager->bookmarks(); const QList< QgsBookmark > projectBookmarks = mProjectManager->bookmarks(); for ( int r = row + count - 1; r >= row; --r ) @@ -253,7 +248,6 @@ bool QgsBookmarkManagerModel::removeRows( int row, int count, const QModelIndex else mManager->removeBookmark( appBookmarks.at( r ).id() ); } - endRemoveRows(); return true; } diff --git a/src/core/qgsconnectionpool.h b/src/core/qgsconnectionpool.h index 3ffc1e34203e..8de06879a515 100644 --- a/src/core/qgsconnectionpool.h +++ b/src/core/qgsconnectionpool.h @@ -342,7 +342,7 @@ class QgsConnectionPool /** * Invalidates all connections to the specified resource. * The internal state of certain handles (for instance OGR) are altered - * when a dataset is modified. Consquently, all open handles need to be + * when a dataset is modified. Consequently, all open handles need to be * invalidated when such datasets are changed to ensure the handles are * refreshed. See the OGR provider for an example where this is needed. */ diff --git a/src/core/qgscplhttpfetchoverrider.h b/src/core/qgscplhttpfetchoverrider.h index bcf4654a7202..3cb64e51c558 100644 --- a/src/core/qgscplhttpfetchoverrider.h +++ b/src/core/qgscplhttpfetchoverrider.h @@ -21,17 +21,13 @@ #include #include #include -#include "qgsnetworkaccessmanager.h" // for QgsSetRequestInitiatorClass +#include "qgis_core.h" #include "cpl_http.h" #include "gdal.h" class QgsFeedback; -#ifndef SIP_RUN -#define QgsSetCPLHTTPFetchOverriderInitiatorClass(overrider, _class) QgsSetRequestInitiatorClass((overrider), _class) -#endif - /** * \ingroup core * \class QgsCPLHTTPFetchOverrider diff --git a/src/core/qgsdbquerylog.h b/src/core/qgsdbquerylog.h index e4eb6b0cb525..d777216192a8 100644 --- a/src/core/qgsdbquerylog.h +++ b/src/core/qgsdbquerylog.h @@ -102,13 +102,6 @@ class CORE_EXPORT QgsDatabaseQueryLogEntry Q_DECLARE_METATYPE( QgsDatabaseQueryLogEntry ); -#ifndef SIP_RUN -#include "qgsconfig.h" -constexpr int sQueryLoggerFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); -#define QgsSetQueryLogClass(entry, _class) entry.initiatorClass = _class; entry.origin = QString(QString( __FILE__ ).mid( sQueryLoggerFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")"); -#define QGS_QUERY_LOG_ORIGIN QString(QString( __FILE__ ).mid( sQueryLoggerFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")") -#endif - /** * \ingroup core * \class QgsDatabaseQueryLog diff --git a/src/core/qgsdbquerylog_p.h b/src/core/qgsdbquerylog_p.h new file mode 100644 index 000000000000..605d46abecaf --- /dev/null +++ b/src/core/qgsdbquerylog_p.h @@ -0,0 +1,24 @@ +/*************************************************************************** + qgsdbquerylog_p.h + ------------ + Date : February 2024 + Copyright : (C) 2024 by Matthias Kuhn + Email : matthias@opengis.ch + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSDBQUERYLOG_P_H +#define QGSDBQUERYLOG_P_H + +#include "qgsconfig.h" +constexpr int sQueryLoggerFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); +#define QgsSetQueryLogClass(entry, _class) ( entry ).initiatorClass = _class; ( entry ).origin = QString(QString( __FILE__ ).mid( sQueryLoggerFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")"); +#define QGS_QUERY_LOG_ORIGIN QString(QString( __FILE__ ).mid( sQueryLoggerFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + ( __FUNCTION__ ) + ")") + +#endif // QGSDBQUERYLOG_P_H diff --git a/src/core/qgselevationutils.cpp b/src/core/qgselevationutils.cpp index 6870075a5881..8370b3458a15 100644 --- a/src/core/qgselevationutils.cpp +++ b/src/core/qgselevationutils.cpp @@ -68,6 +68,7 @@ bool QgsElevationUtils::enableElevationForLayer( QgsMapLayer *layer ) if ( QgsRasterLayerElevationProperties *properties = qobject_cast( layer->elevationProperties() ) ) { properties->setEnabled( true ); + properties->setMode( Qgis::RasterElevationMode::RepresentsElevationSurface ); // This could potentially be made smarter, eg by checking the data type of bands. But that's likely overkill..! properties->setBandNumber( 1 ); return true; diff --git a/src/core/qgsfieldproxymodel.cpp b/src/core/qgsfieldproxymodel.cpp index 34696ee224ba..42fedac53b02 100644 --- a/src/core/qgsfieldproxymodel.cpp +++ b/src/core/qgsfieldproxymodel.cpp @@ -15,7 +15,7 @@ #include "qgsfieldproxymodel.h" #include "qgsfieldmodel.h" -#include "qgsvectorlayer.h" +#include "qgsvariantutils.h" QgsFieldProxyModel::QgsFieldProxyModel( QObject *parent ) : QSortFilterProxyModel( parent ) @@ -85,6 +85,22 @@ bool QgsFieldProxyModel::filterAcceptsRow( int source_row, const QModelIndex &so if ( mFilters.testFlag( HideReadOnly ) && isReadOnly( index ) ) return false; + if ( mFilters.testFlag( QgsFieldProxyModel::OriginProvider ) ) + { + const QgsFields::FieldOrigin origin = static_cast< QgsFields::FieldOrigin >( sourceModel()->data( index, static_cast< int >( QgsFieldModel::CustomRole::FieldOrigin ) ).toInt() ); + switch ( origin ) + { + case QgsFields::OriginUnknown: + case QgsFields::OriginJoin: + case QgsFields::OriginEdit: + case QgsFields::OriginExpression: + return false; + + case QgsFields::OriginProvider: + break; + } + } + if ( mFilters.testFlag( AllTypes ) ) return true; diff --git a/src/core/qgsfieldproxymodel.h b/src/core/qgsfieldproxymodel.h index 94bf7e05a389..d5a8dd5bf2ca 100644 --- a/src/core/qgsfieldproxymodel.h +++ b/src/core/qgsfieldproxymodel.h @@ -37,17 +37,18 @@ class CORE_EXPORT QgsFieldProxyModel : public QSortFilterProxyModel //! Field type filters enum Filter SIP_ENUM_BASETYPE( IntFlag ) { - String = 1, //!< String fields - Int = 2, //!< Integer fields - LongLong = 4, //!< Longlong fields - Double = 8, //!< Double fields + String = 1 << 0, //!< String fields + Int = 1 << 1, //!< Integer fields + LongLong = 1 << 2, //!< Longlong fields + Double = 1 << 3, //!< Double fields Numeric = Int | LongLong | Double, //!< All numeric fields - Date = 16, //!< Date or datetime fields - Time = 32, //!< Time fields - HideReadOnly = 64, //!< Hide read-only fields - DateTime = 128, //!< Datetime fields - Binary = 256, //!< Binary fields, since QGIS 3.34 - Boolean = 512, //!< Boolean fields, since QGIS 3.34 + Date = 1 << 4, //!< Date or datetime fields + Time = 1 << 5, //!< Time fields + HideReadOnly = 1 << 6, //!< Hide read-only fields + DateTime = 1 << 7, //!< Datetime fields + Binary = 1 << 8, //!< Binary fields, since QGIS 3.34 + Boolean = 1 << 9, //!< Boolean fields, since QGIS 3.34 + OriginProvider = 1 << 10, //!< Fields with a provider origin, since QGIS 3.38 AllTypes = Numeric | Date | String | Time | DateTime | Binary | Boolean, //!< All field types }; Q_DECLARE_FLAGS( Filters, Filter ) diff --git a/src/core/qgsgml.cpp b/src/core/qgsgml.cpp index f22c99c397b1..1f43441ba76d 100644 --- a/src/core/qgsgml.cpp +++ b/src/core/qgsgml.cpp @@ -20,6 +20,7 @@ #include "qgslogger.h" #include "qgsmessagelog.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgswkbptr.h" #include "qgsogcutils.h" #include "qgsogrutils.h" diff --git a/src/core/qgsidentifycontext.cpp b/src/core/qgsidentifycontext.cpp index fc369f42abd0..77db3dc1bf81 100644 --- a/src/core/qgsidentifycontext.cpp +++ b/src/core/qgsidentifycontext.cpp @@ -30,3 +30,13 @@ bool QgsIdentifyContext::isTemporal() const { return mTemporalRange.begin().isValid() || mTemporalRange.end().isValid(); } + +QgsDoubleRange QgsIdentifyContext::zRange() const +{ + return mZRange; +} + +void QgsIdentifyContext::setZRange( const QgsDoubleRange &range ) +{ + mZRange = range; +} diff --git a/src/core/qgsidentifycontext.h b/src/core/qgsidentifycontext.h index ceedef29bf81..d2f3727583fc 100644 --- a/src/core/qgsidentifycontext.h +++ b/src/core/qgsidentifycontext.h @@ -56,9 +56,29 @@ class CORE_EXPORT QgsIdentifyContext */ bool isTemporal() const; + /** + * Returns the range of z-values to identify within, or an infinite range if no filtering by + * z should be applied. + * + * \see setZRange() + * \since QGIS 3.38 + */ + QgsDoubleRange zRange() const; + + /** + * Sets the \a range of z-values to identify within. + * + * Set to an infinite range if no filtering by z should be applied. + * + * \see zRange() + * \since QGIS 3.38 + */ + void setZRange( const QgsDoubleRange &range ); + private: QgsDateTimeRange mTemporalRange; + QgsDoubleRange mZRange; }; diff --git a/src/core/qgsimagecache.cpp b/src/core/qgsimagecache.cpp index 513ebb294735..9c3e340d62dd 100644 --- a/src/core/qgsimagecache.cpp +++ b/src/core/qgsimagecache.cpp @@ -24,6 +24,7 @@ #include "qgsmessagelog.h" #include "qgsnetworkcontentfetchertask.h" #include "qgssettings.h" +#include "qgsabstractcontentcache_p.h" #include #include @@ -513,3 +514,5 @@ QImage QgsImageCache::getFrameFromReader( QImageReader &reader, int frameNumber } return reader.read(); } + +template class QgsAbstractContentCache; // clazy:exclude=missing-qobject-macro 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/qgslegendrenderer.cpp b/src/core/qgslegendrenderer.cpp index 629289226bab..b19fb9bf6bfb 100644 --- a/src/core/qgslegendrenderer.cpp +++ b/src/core/qgslegendrenderer.cpp @@ -927,13 +927,21 @@ QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTr ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth; + QgsExpressionContextScope *symbolScope = nullptr; if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) ) + { + symbolScope = symbolNode->createSymbolScope(); + context.expressionContext().appendScope( symbolScope ); ctx.patchShape = symbolNode->patchShape(); + } ctx.patchSize = symbolItem->userPatchSize(); QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, &ctx ); + if ( symbolScope ) + delete context.expressionContext().popScope(); + if ( layerScope ) delete context.expressionContext().popScope(); diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 165113e65386..d50f1e1540a0 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -1490,7 +1490,7 @@ class CORE_EXPORT QgsMapLayer : public QObject Q_DECL_DEPRECATED bool hasAutoRefreshEnabled() const SIP_DEPRECATED; /** - * Returns the layer's automatical refresh mode. + * Returns the layer's automatic refresh mode. * \see autoRefreshInterval() * \see setAutoRefreshMode() * \since QGIS 3.34 @@ -1634,7 +1634,7 @@ class CORE_EXPORT QgsMapLayer : public QObject /** * Returns path to the placeholder image or an empty string if a generated legend is shown - * \return placholder image path + * \return placeholder image path * \since QGIS 3.22 */ QString legendPlaceholderImage() const { return mLegendPlaceholderImage;} diff --git a/src/core/qgsmaplayerelevationproperties.cpp b/src/core/qgsmaplayerelevationproperties.cpp index 46877551b5e5..877c2b204e90 100644 --- a/src/core/qgsmaplayerelevationproperties.cpp +++ b/src/core/qgsmaplayerelevationproperties.cpp @@ -68,7 +68,7 @@ void QgsMapLayerElevationProperties::copyCommonProperties( const QgsMapLayerElev mZOffset = other->zOffset(); } -bool QgsMapLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsMapLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &, QgsMapLayer * ) const { return true; } @@ -133,5 +133,7 @@ void QgsMapLayerElevationProperties::initPropertyDefinitions() { { static_cast< int >( QgsMapLayerElevationProperties::Property::ZOffset ), QgsPropertyDefinition( "ZOffset", QObject::tr( "Offset" ), QgsPropertyDefinition::Double, origin ) }, { static_cast< int >( QgsMapLayerElevationProperties::Property::ExtrusionHeight ), QgsPropertyDefinition( "ExtrusionHeight", QObject::tr( "Extrusion height" ), QgsPropertyDefinition::DoublePositive, origin ) }, + { static_cast< int >( QgsMapLayerElevationProperties::Property::RasterPerBandLowerElevation ), QgsPropertyDefinition( "RasterPerBandLowerElevation", QObject::tr( "Lower elevation for band" ), QgsPropertyDefinition::Double, origin ) }, + { static_cast< int >( QgsMapLayerElevationProperties::Property::RasterPerBandUpperElevation ), QgsPropertyDefinition( "RasterPerBandUpperElevation", QObject::tr( "Upper elevation for band" ), QgsPropertyDefinition::Double, origin ) }, }; } diff --git a/src/core/qgsmaplayerelevationproperties.h b/src/core/qgsmaplayerelevationproperties.h index d34a1e08e2b3..db7a84136a2b 100644 --- a/src/core/qgsmaplayerelevationproperties.h +++ b/src/core/qgsmaplayerelevationproperties.h @@ -94,6 +94,8 @@ class CORE_EXPORT QgsMapLayerElevationProperties : public QObject { ZOffset, //! Z offset ExtrusionHeight, //!< Extrusion height + RasterPerBandLowerElevation, //!< Lower elevation for each raster band (since QGIS 3.38) + RasterPerBandUpperElevation, //!< Upper elevation for each raster band (since QGIS 3.38) }; // *INDENT-ON* @@ -153,8 +155,10 @@ class CORE_EXPORT QgsMapLayerElevationProperties : public QObject /** * Returns TRUE if the layer should be visible and rendered for the specified z \a range. + * + * Since QGIS 3.38 the \a layer argument can be used to specify the target layer. */ - virtual bool isVisibleInZRange( const QgsDoubleRange &range ) const; + virtual bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = nullptr ) const; /** * Returns flags associated to the elevation properties. diff --git a/src/core/qgsmapsettingsutils.cpp b/src/core/qgsmapsettingsutils.cpp index 33e283fbb9da..93d24ac141c0 100644 --- a/src/core/qgsmapsettingsutils.cpp +++ b/src/core/qgsmapsettingsutils.cpp @@ -93,11 +93,11 @@ void QgsMapSettingsUtils::worldFileParameters( const QgsMapSettings &mapSettings // scaling matrix double s[6]; - s[0] = ms.mapUnitsPerPixel(); + s[0] = ms.mapUnitsPerPixel() / ms.devicePixelRatio(); s[1] = 0; s[2] = xOrigin; s[3] = 0; - s[4] = -ms.mapUnitsPerPixel(); + s[4] = -ms.mapUnitsPerPixel() / ms.devicePixelRatio(); s[5] = yOrigin; // rotation matrix diff --git a/src/core/qgsmatrix4x4.h b/src/core/qgsmatrix4x4.h index a7213ecd718b..e1920c632c69 100644 --- a/src/core/qgsmatrix4x4.h +++ b/src/core/qgsmatrix4x4.h @@ -76,7 +76,7 @@ class CORE_EXPORT QgsMatrix4x4 */ void translate( const QgsVector3D &vector ); - //! Matrix-vector multiplication (vector is converted to homogenous coordinates [X,Y,Z,1] and back) + //! Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,Z,1] and back) QgsVector3D map( const QgsVector3D &vector ) const SIP_HOLDGIL { return *this * vector; @@ -126,7 +126,7 @@ class CORE_EXPORT QgsMatrix4x4 //! Matrix-matrix multiplication (useful to concatenate transforms) CORE_EXPORT QgsVector3D operator*( const QgsMatrix4x4 &matrix, const QgsVector3D &vector ); -//! Matrix-vector multiplication (vector is converted to homogenous coordinates [X,Y,Z,1] and back) +//! Matrix-vector multiplication (vector is converted to homogeneous coordinates [X,Y,Z,1] and back) CORE_EXPORT QgsMatrix4x4 operator*( const QgsMatrix4x4 &m1, const QgsMatrix4x4 &m2 ); diff --git a/src/core/qgsogcutils.cpp b/src/core/qgsogcutils.cpp index 3658e3211e36..bdd959ee1357 100644 --- a/src/core/qgsogcutils.cpp +++ b/src/core/qgsogcutils.cpp @@ -923,7 +923,7 @@ QDomElement QgsOgcUtils::filterElement( QDomDocument &doc, GMLVersion gmlVersion bool QgsOgcUtils::readGMLCoordinates( QgsPolylineXY &coords, const QDomElement &elem ) { QString coordSeparator = QStringLiteral( "," ); - QString tupelSeparator = QStringLiteral( " " ); + QString tupleSeparator = QStringLiteral( " " ); //"decimal" has to be "." coords.clear(); @@ -934,10 +934,10 @@ bool QgsOgcUtils::readGMLCoordinates( QgsPolylineXY &coords, const QDomElement & } if ( elem.hasAttribute( QStringLiteral( "ts" ) ) ) { - tupelSeparator = elem.attribute( QStringLiteral( "ts" ) ); + tupleSeparator = elem.attribute( QStringLiteral( "ts" ) ); } - const QStringList tupels = elem.text().split( tupelSeparator, Qt::SkipEmptyParts ); + const QStringList tupels = elem.text().split( tupleSeparator, Qt::SkipEmptyParts ); QStringList tuple_coords; double x, y; bool conversionSuccess; @@ -975,22 +975,22 @@ QgsRectangle QgsOgcUtils::rectangleFromGMLBox( const QDomNode &boxNode ) const QDomElement bElem = boxElem.firstChild().toElement(); QString coordSeparator = QStringLiteral( "," ); - QString tupelSeparator = QStringLiteral( " " ); + QString tupleSeparator = QStringLiteral( " " ); if ( bElem.hasAttribute( QStringLiteral( "cs" ) ) ) { coordSeparator = bElem.attribute( QStringLiteral( "cs" ) ); } if ( bElem.hasAttribute( QStringLiteral( "ts" ) ) ) { - tupelSeparator = bElem.attribute( QStringLiteral( "ts" ) ); + tupleSeparator = bElem.attribute( QStringLiteral( "ts" ) ); } const QString bString = bElem.text(); bool ok1, ok2, ok3, ok4; - const double xmin = bString.section( tupelSeparator, 0, 0 ).section( coordSeparator, 0, 0 ).toDouble( &ok1 ); - const double ymin = bString.section( tupelSeparator, 0, 0 ).section( coordSeparator, 1, 1 ).toDouble( &ok2 ); - const double xmax = bString.section( tupelSeparator, 1, 1 ).section( coordSeparator, 0, 0 ).toDouble( &ok3 ); - const double ymax = bString.section( tupelSeparator, 1, 1 ).section( coordSeparator, 1, 1 ).toDouble( &ok4 ); + const double xmin = bString.section( tupleSeparator, 0, 0 ).section( coordSeparator, 0, 0 ).toDouble( &ok1 ); + const double ymin = bString.section( tupleSeparator, 0, 0 ).section( coordSeparator, 1, 1 ).toDouble( &ok2 ); + const double xmax = bString.section( tupleSeparator, 1, 1 ).section( coordSeparator, 0, 0 ).toDouble( &ok3 ); + const double ymax = bString.section( tupleSeparator, 1, 1 ).section( coordSeparator, 1, 1 ).toDouble( &ok4 ); if ( ok1 && ok2 && ok3 && ok4 ) { diff --git a/src/core/qgsogcutils.h b/src/core/qgsogcutils.h index 4c5860fcc2d2..4482a613f260 100644 --- a/src/core/qgsogcutils.h +++ b/src/core/qgsogcutils.h @@ -514,7 +514,7 @@ class QgsOgcUtilsExpressionFromFilter /** * Returns an expression node from a WFS filter embedded in a document with - * boudnaries operator. + * boundaries operator. */ QgsExpressionNode *nodeIsBetweenFromOgcFilter( const QDomElement &element ); diff --git a/src/core/qgsowsconnection.h b/src/core/qgsowsconnection.h index f438b4bf71df..7ef9082f2169 100644 --- a/src/core/qgsowsconnection.h +++ b/src/core/qgsowsconnection.h @@ -40,7 +40,7 @@ template class QgsSettingsEntryEnumFlag; /** * \ingroup core - * \brief Connections settingss for XYZ + * \brief Connections settings for XYZ * \since QGIS 3.30 */ class CORE_EXPORT QgsXyzConnectionSettings SIP_SKIP @@ -64,7 +64,7 @@ class CORE_EXPORT QgsXyzConnectionSettings SIP_SKIP /** * \ingroup core - * \brief Connections settingss for Arcgis + * \brief Connections settings for Arcgis * \since QGIS 3.30 */ class CORE_EXPORT QgsArcGisConnectionSettings SIP_SKIP diff --git a/src/core/qgsrange.h b/src/core/qgsrange.h index c7ca801d827b..163121301662 100644 --- a/src/core/qgsrange.h +++ b/src/core/qgsrange.h @@ -57,6 +57,19 @@ class QgsRange , mIncludeUpper( includeUpper ) {} + /** + * Constructor for QgsRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsRange( T lower, T upper, Qgis::RangeLimits limits ) + : mLower( lower ) + , mUpper( upper ) + , mIncludeLower( limits == Qgis::RangeLimits::IncludeLowerExcludeUpper || limits == Qgis::RangeLimits::IncludeBoth ) + , mIncludeUpper( limits == Qgis::RangeLimits::ExcludeLowerIncludeUpper || limits == Qgis::RangeLimits::IncludeBoth ) + {} + /** * Returns the lower bound of the range. * \see upper() @@ -87,6 +100,23 @@ class QgsRange */ bool includeUpper() const { return mIncludeUpper; } + /** + * Returns the limit handling of the range. + * + * \since QGIS 3.38 + */ + Qgis::RangeLimits rangeLimits() const + { + if ( mIncludeLower && mIncludeUpper ) + return Qgis::RangeLimits::IncludeBoth; + else if ( mIncludeLower && !mIncludeUpper ) + return Qgis::RangeLimits::IncludeLowerExcludeUpper; + else if ( !mIncludeLower && mIncludeUpper ) + return Qgis::RangeLimits::ExcludeLowerIncludeUpper; + else + return Qgis::RangeLimits::ExcludeBoth; + } + /** * Returns TRUE if the range is empty, ie the lower bound equals (or exceeds) the upper bound * and either the bounds are exclusive. @@ -201,6 +231,16 @@ class CORE_EXPORT QgsDoubleRange : public QgsRange< double > { public: + /** + * Constructor for QgsDoubleRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsDoubleRange( double lower, double upper, Qgis::RangeLimits limits ) + : QgsRange( lower, upper, limits ) + {} + #ifndef SIP_RUN /** @@ -273,6 +313,8 @@ class CORE_EXPORT QgsDoubleRange : public QgsRange< double > }; +Q_DECLARE_METATYPE( QgsDoubleRange ) + /** * \brief QgsRange which stores a range of integer values. @@ -285,6 +327,16 @@ class CORE_EXPORT QgsIntRange : public QgsRange< int > { public: + /** + * Constructor for QgsIntRange. The \a lower and \a upper bounds are specified, + * and whether or not these bounds are included in the range. + * + * \since QGIS 3.38 + */ + QgsIntRange( int lower, int upper, Qgis::RangeLimits limits ) + : QgsRange( lower, upper, limits ) + {} + #ifndef SIP_RUN /** @@ -344,6 +396,8 @@ class CORE_EXPORT QgsIntRange : public QgsRange< int > }; +Q_DECLARE_METATYPE( QgsIntRange ) + /** * \class QgsTemporalRange diff --git a/src/core/qgsrenderchecker.cpp b/src/core/qgsrenderchecker.cpp index c75985a161ce..ec6b99a910e8 100644 --- a/src/core/qgsrenderchecker.cpp +++ b/src/core/qgsrenderchecker.cpp @@ -596,7 +596,32 @@ bool QgsRenderChecker::compareImages( const QString &testName, const QString &re myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 ); expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 ); } + if ( expectedImage.format() != QImage::Format_RGB32 + && expectedImage.format() != QImage::Format_ARGB32 + && expectedImage.format() != QImage::Format_ARGB32_Premultiplied ) + { + mReport += QLatin1String( "" ); + mReport += QStringLiteral( "Expected image for %1 is not a compatible format (%2). Must be 32 bit RGB format." ).arg( testName, qgsEnumValueToKey( expectedImage.format() ) ); + mReport += QLatin1String( "" ); + mReport += myImagesString; + mMarkdownReport += QStringLiteral( "Failed because expected image has an incompatible format - %1 (32 bit format is expected)\n" ).arg( qgsEnumValueToKey( expectedImage.format() ) ); + performPostTestActions( flags ); + return mResult; + } + if ( myResultImage.format() != QImage::Format_RGB32 + && myResultImage.format() != QImage::Format_ARGB32 + && myResultImage.format() != QImage::Format_ARGB32_Premultiplied ) + { + mReport += QLatin1String( "" ); + mReport += QStringLiteral( "Rendered image for %1 is not a compatible format (%2). Must be 32 bit RGB format." ).arg( testName, qgsEnumValueToKey( myResultImage.format() ) ); + mReport += QLatin1String( "" ); + mReport += myImagesString; + + mMarkdownReport += QStringLiteral( "Failed because rendered image has an incompatible format - %1 (32 bit format is expected)\n" ).arg( qgsEnumValueToKey( myResultImage.format() ) ); + performPostTestActions( flags ); + return mResult; + } // // Now iterate through them counting how many diff --git a/src/core/qgssourcecache.cpp b/src/core/qgssourcecache.cpp index 57f1f1bc78b7..49663899c688 100644 --- a/src/core/qgssourcecache.cpp +++ b/src/core/qgssourcecache.cpp @@ -16,6 +16,7 @@ ***************************************************************************/ #include "qgssourcecache.h" +#include "qgsabstractcontentcache_p.h" #include "qgis.h" #include "qgslogger.h" @@ -114,3 +115,5 @@ QString QgsSourceCache::fetchSource( const QString &path, bool &isBroken, bool b return filePath; } + +template class QgsAbstractContentCache; // clazy:exclude=missing-qobject-macro diff --git a/src/core/raster/qgshillshaderenderer.cpp b/src/core/raster/qgshillshaderenderer.cpp index 62015ec1e5e4..6c61a29e1054 100644 --- a/src/core/raster/qgshillshaderenderer.cpp +++ b/src/core/raster/qgshillshaderenderer.cpp @@ -521,7 +521,7 @@ QgsRasterBlock *QgsHillshadeRenderer::block( int bandNo, const QgsRectangle &ext double currentAlpha = mOpacity; if ( mRasterTransparency ) { - currentAlpha = mRasterTransparency->alphaValue( x22, mOpacity * 255 ) / 255.0; + currentAlpha *= mRasterTransparency->opacityForValue( x22 ); } if ( mAlphaBand > 0 ) { @@ -569,13 +569,29 @@ QList QgsHillshadeRenderer::usesBands() const } +int QgsHillshadeRenderer::inputBand() const +{ + return mBand; +} + void QgsHillshadeRenderer::setBand( int bandNo ) { - if ( bandNo > mInput->bandCount() || bandNo <= 0 ) + setInputBand( bandNo ); +} + +bool QgsHillshadeRenderer::setInputBand( int band ) +{ + if ( !mInput ) { - return; + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mBand = band; + return true; } - mBand = bandNo; + return false; } void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props ) const @@ -594,7 +610,7 @@ void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const // add Channel Selection tags (if band is not default 1) // Need to insert channelSelection in the correct sequence as in SLD standard e.g. // after opacity or geometry or as first element after sld:RasterSymbolizer - if ( band() != 1 ) + if ( mBand != 1 ) { QDomElement channelSelectionElem = doc.createElement( QStringLiteral( "sld:ChannelSelection" ) ); elements = rasterSymbolizerElem.elementsByTagName( QStringLiteral( "sld:Opacity" ) ); @@ -621,7 +637,7 @@ void QgsHillshadeRenderer::toSld( QDomDocument &doc, QDomElement &element, const // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); } diff --git a/src/core/raster/qgshillshaderenderer.h b/src/core/raster/qgshillshaderenderer.h index b75a3034052c..22a51e39789e 100644 --- a/src/core/raster/qgshillshaderenderer.h +++ b/src/core/raster/qgshillshaderenderer.h @@ -61,19 +61,25 @@ class CORE_EXPORT QgsHillshadeRenderer : public QgsRasterRenderer QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; QList usesBands() const override; + int inputBand() const override; void toSld( QDomDocument &doc, QDomElement &element, const QVariantMap &props = QVariantMap() ) const override; /** * Returns the band used by the renderer + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } /** * Sets the band used by the renderer. * \see band + * + * \deprecated since QGIS 3.38 use setInputBand() instead */ - void setBand( int bandNo ); + Q_DECL_DEPRECATED void setBand( int bandNo ) SIP_DEPRECATED; + bool setInputBand( int band ) override; /** * Returns the direction of the light over the raster between 0-360. diff --git a/src/core/raster/qgsmultibandcolorrenderer.cpp b/src/core/raster/qgsmultibandcolorrenderer.cpp index c52f4bf56c8d..cf8ecc82d024 100644 --- a/src/core/raster/qgsmultibandcolorrenderer.cpp +++ b/src/core/raster/qgsmultibandcolorrenderer.cpp @@ -18,9 +18,7 @@ #include "qgsmultibandcolorrenderer.h" #include "qgscontrastenhancement.h" #include "qgsrastertransparency.h" -#include "qgsrasterviewport.h" #include "qgslayertreemodellegendnode.h" -#include "qgssymbol.h" #include #include @@ -366,7 +364,7 @@ QgsRasterBlock *QgsMultiBandColorRenderer::block( int bandNo, QgsRectangle cons double currentOpacity = mOpacity; if ( mRasterTransparency ) { - currentOpacity = mRasterTransparency->alphaValue( redVal, greenVal, blueVal, mOpacity * 255 ) / 255.0; + currentOpacity *= mRasterTransparency->opacityForRgbValues( redVal, greenVal, blueVal ); } if ( mAlphaBand > 0 ) { diff --git a/src/core/raster/qgspalettedrasterrenderer.cpp b/src/core/raster/qgspalettedrasterrenderer.cpp index 9d8973ef5690..0b79f62570de 100644 --- a/src/core/raster/qgspalettedrasterrenderer.cpp +++ b/src/core/raster/qgspalettedrasterrenderer.cpp @@ -17,7 +17,6 @@ #include "qgspalettedrasterrenderer.h" #include "qgsrastertransparency.h" -#include "qgsrasterviewport.h" #include "qgssymbollayerutils.h" #include "qgsstyleentityvisitor.h" #include "qgsmessagelog.h" @@ -175,6 +174,26 @@ void QgsPalettedRasterRenderer::setLabel( double idx, const QString &label ) } } +int QgsPalettedRasterRenderer::inputBand() const +{ + return mBand; +} + +bool QgsPalettedRasterRenderer::setInputBand( int band ) +{ + if ( !mInput ) + { + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mBand = band; + return true; + } + return false; +} + QgsRasterBlock *QgsPalettedRasterRenderer::block( int, QgsRectangle const &extent, int width, int height, QgsRasterBlockFeedback *feedback ) { std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); @@ -246,7 +265,7 @@ QgsRasterBlock *QgsPalettedRasterRenderer::block( int, QgsRectangle const &exte double currentOpacity = mOpacity; if ( mRasterTransparency ) { - currentOpacity = mRasterTransparency->alphaValue( value, mOpacity * 255 ) / 255.0; + currentOpacity *= mRasterTransparency->opacityForValue( value ); } if ( mAlphaBand > 0 ) { @@ -337,7 +356,7 @@ void QgsPalettedRasterRenderer::toSld( QDomDocument &doc, QDomElement &element, // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // add ColorMap tag diff --git a/src/core/raster/qgspalettedrasterrenderer.h b/src/core/raster/qgspalettedrasterrenderer.h index 23028265a219..58f6e5c1c3a7 100644 --- a/src/core/raster/qgspalettedrasterrenderer.h +++ b/src/core/raster/qgspalettedrasterrenderer.h @@ -143,8 +143,13 @@ class CORE_EXPORT QgsPalettedRasterRenderer: public QgsRasterRenderer /** * Returns the raster band used for rendering the raster. + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } + + int inputBand() const override; + bool setInputBand( int band ) override; void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; QList< QPair< QString, QColor > > legendSymbologyItems() const override; diff --git a/src/core/raster/qgsrasterattributetable.cpp b/src/core/raster/qgsrasterattributetable.cpp index ee45be536628..f007a45a0088 100644 --- a/src/core/raster/qgsrasterattributetable.cpp +++ b/src/core/raster/qgsrasterattributetable.cpp @@ -1078,7 +1078,7 @@ QgsRasterAttributeTable *QgsRasterAttributeTable::createFromRaster( QgsRasterLay if ( bandNumber ) { - *bandNumber = palettedRenderer->band(); + *bandNumber = palettedRenderer->inputBand(); } return rat; } @@ -1174,7 +1174,7 @@ QgsRasterAttributeTable *QgsRasterAttributeTable::createFromRaster( QgsRasterLay if ( bandNumber ) { - *bandNumber = pseudoColorRenderer->band(); + *bandNumber = pseudoColorRenderer->inputBand(); } return rat; diff --git a/src/core/raster/qgsrastercontourrenderer.cpp b/src/core/raster/qgsrastercontourrenderer.cpp index 1905ffe54829..0d5e57febb1a 100644 --- a/src/core/raster/qgsrastercontourrenderer.cpp +++ b/src/core/raster/qgsrastercontourrenderer.cpp @@ -232,6 +232,26 @@ QList QgsRasterContourRenderer::createLegendNodes return nodes; } +int QgsRasterContourRenderer::inputBand() const +{ + return mInputBand; +} + +bool QgsRasterContourRenderer::setInputBand( int band ) +{ + if ( !mInput ) + { + mInputBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mInputBand = band; + return true; + } + return false; +} + void QgsRasterContourRenderer::setContourSymbol( QgsLineSymbol *symbol ) { mContourSymbol.reset( symbol ); diff --git a/src/core/raster/qgsrastercontourrenderer.h b/src/core/raster/qgsrastercontourrenderer.h index 9953f336e360..1ef9955110c7 100644 --- a/src/core/raster/qgsrastercontourrenderer.h +++ b/src/core/raster/qgsrastercontourrenderer.h @@ -50,13 +50,8 @@ class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer QList usesBands() const override; QList createLegendNodes( QgsLayerTreeLayer *nodeLayer ) SIP_FACTORY override; - - // - - //! Returns the number of the input raster band - int inputBand() const { return mInputBand; } - //! Sets the number of the input raster band - void setInputBand( int band ) { mInputBand = band; } + int inputBand() const override; + bool setInputBand( int band ) override; //! Returns the interval of contour lines generation double contourInterval() const { return mContourInterval; } diff --git a/src/core/raster/qgsrasterlayer.cpp b/src/core/raster/qgsrasterlayer.cpp index 526728bf4509..e1bd136dc8f8 100644 --- a/src/core/raster/qgsrasterlayer.cpp +++ b/src/core/raster/qgsrasterlayer.cpp @@ -1369,7 +1369,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh { return; } - myBands << myGrayRenderer->grayBand(); + myBands << myGrayRenderer->inputBand(); myRasterRenderer = myGrayRenderer; myMinMaxOrigin = myGrayRenderer->minMaxOrigin(); } @@ -1391,7 +1391,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh { return; } - myBands << myPseudoColorRenderer->band(); + myBands << myPseudoColorRenderer->inputBand(); myRasterRenderer = myPseudoColorRenderer; myMinMaxOrigin = myPseudoColorRenderer->minMaxOrigin(); } @@ -1442,7 +1442,7 @@ void QgsRasterLayer::setContrastEnhancement( QgsContrastEnhancement::ContrastEnh QgsColorRampShader *colorRampShader = dynamic_cast( myPseudoColorRenderer->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( myPseudoColorRenderer->band(), extent, myPseudoColorRenderer->input() ); + colorRampShader->classifyColorRamp( myPseudoColorRenderer->inputBand(), extent, myPseudoColorRenderer->input() ); } } } @@ -1573,7 +1573,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q mLastRectangleUsedByRefreshContrastEnhancementIfNeeded = extent; double min; double max; - computeMinMax( sbpcr->band(), + computeMinMax( sbpcr->inputBand(), rasterRenderer->minMaxOrigin(), rasterRenderer->minMaxOrigin().limits(), extent, static_cast( SAMPLE_SIZE ), min, max ); @@ -1585,7 +1585,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q QgsColorRampShader *colorRampShader = dynamic_cast( sbpcr->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( sbpcr->band(), extent, rasterRenderer->input() ); + colorRampShader->classifyColorRamp( sbpcr->inputBand(), extent, rasterRenderer->input() ); } } @@ -1598,7 +1598,7 @@ void QgsRasterLayer::refreshRenderer( QgsRasterRenderer *rasterRenderer, const Q QgsColorRampShader *colorRampShader = dynamic_cast( r->shader()->rasterShaderFunction() ); if ( colorRampShader ) { - colorRampShader->classifyColorRamp( sbpcr->band(), extent, rasterRenderer->input() ); + colorRampShader->classifyColorRamp( sbpcr->inputBand(), extent, rasterRenderer->input() ); } } diff --git a/src/core/raster/qgsrasterlayerelevationproperties.cpp b/src/core/raster/qgsrasterlayerelevationproperties.cpp index 1b338ac33b1e..be3b97744e01 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.cpp +++ b/src/core/raster/qgsrasterlayerelevationproperties.cpp @@ -24,6 +24,7 @@ #include "qgsfillsymbollayer.h" #include "qgsapplication.h" #include "qgscolorschemeregistry.h" +#include "qgsexpressioncontextutils.h" QgsRasterLayerElevationProperties::QgsRasterLayerElevationProperties( QObject *parent ) : QgsMapLayerElevationProperties( parent ) @@ -44,12 +45,46 @@ QDomElement QgsRasterLayerElevationProperties::writeXml( QDomElement &parentElem { QDomElement element = document.createElement( QStringLiteral( "elevation" ) ); element.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + element.setAttribute( QStringLiteral( "mode" ), qgsEnumValueToKey( mMode ) ); element.setAttribute( QStringLiteral( "symbology" ), qgsEnumValueToKey( mSymbology ) ); if ( !std::isnan( mElevationLimit ) ) element.setAttribute( QStringLiteral( "elevationLimit" ), qgsDoubleToString( mElevationLimit ) ); writeCommonProperties( element, document, context ); - element.setAttribute( QStringLiteral( "band" ), mBandNumber ); + + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + element.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( mFixedRange.lower() ) ); + element.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( mFixedRange.upper() ) ); + element.setAttribute( QStringLiteral( "includeLower" ), mFixedRange.includeLower() ? "1" : "0" ); + element.setAttribute( QStringLiteral( "includeUpper" ), mFixedRange.includeUpper() ? "1" : "0" ); + break; + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + QDomElement ranges = document.createElement( QStringLiteral( "ranges" ) ); + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + QDomElement range = document.createElement( QStringLiteral( "range" ) ); + range.setAttribute( QStringLiteral( "band" ), it.key() ); + range.setAttribute( QStringLiteral( "lower" ), qgsDoubleToString( it.value().lower() ) ); + range.setAttribute( QStringLiteral( "upper" ), qgsDoubleToString( it.value().upper() ) ); + range.setAttribute( QStringLiteral( "includeLower" ), it.value().includeLower() ? "1" : "0" ); + range.setAttribute( QStringLiteral( "includeUpper" ), it.value().includeUpper() ? "1" : "0" ); + ranges.appendChild( range ); + } + element.appendChild( ranges ); + break; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + element.setAttribute( QStringLiteral( "band" ), mBandNumber ); + break; + } QDomElement profileLineSymbolElement = document.createElement( QStringLiteral( "profileLineSymbol" ) ); profileLineSymbolElement.appendChild( QgsSymbolLayerUtils::saveSymbol( QString(), mProfileLineSymbol.get(), document, context ) ); @@ -67,6 +102,7 @@ bool QgsRasterLayerElevationProperties::readXml( const QDomElement &element, con { const QDomElement elevationElement = element.firstChildElement( QStringLiteral( "elevation" ) ).toElement(); mEnabled = elevationElement.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt(); + mMode = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "mode" ) ), Qgis::RasterElevationMode::RepresentsElevationSurface ); mSymbology = qgsEnumKeyToValue( elevationElement.attribute( QStringLiteral( "symbology" ) ), Qgis::ProfileSurfaceSymbology::Line ); if ( elevationElement.hasAttribute( QStringLiteral( "elevationLimit" ) ) ) mElevationLimit = elevationElement.attribute( QStringLiteral( "elevationLimit" ) ).toDouble(); @@ -74,7 +110,44 @@ bool QgsRasterLayerElevationProperties::readXml( const QDomElement &element, con mElevationLimit = std::numeric_limits< double >::quiet_NaN(); readCommonProperties( elevationElement, context ); - mBandNumber = elevationElement.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + { + const double lower = elevationElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = elevationElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = elevationElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = elevationElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mFixedRange = QgsDoubleRange( lower, upper, includeLower, includeUpper ); + break; + } + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + mRangePerBand.clear(); + + const QDomNodeList ranges = elevationElement.firstChildElement( QStringLiteral( "ranges" ) ).childNodes(); + for ( int i = 0; i < ranges.size(); ++i ) + { + const QDomElement rangeElement = ranges.at( i ).toElement(); + const int band = rangeElement.attribute( QStringLiteral( "band" ) ).toInt(); + const double lower = rangeElement.attribute( QStringLiteral( "lower" ) ).toDouble(); + const double upper = rangeElement.attribute( QStringLiteral( "upper" ) ).toDouble(); + const bool includeLower = rangeElement.attribute( QStringLiteral( "includeLower" ) ).toInt(); + const bool includeUpper = rangeElement.attribute( QStringLiteral( "includeUpper" ) ).toInt(); + mRangePerBand.insert( band, QgsDoubleRange( lower, upper, includeLower, includeUpper ) ); + } + break; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + mBandNumber = elevationElement.attribute( QStringLiteral( "band" ), QStringLiteral( "1" ) ).toInt(); + break; + } const QColor defaultColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); @@ -95,11 +168,14 @@ QgsRasterLayerElevationProperties *QgsRasterLayerElevationProperties::clone() co { std::unique_ptr< QgsRasterLayerElevationProperties > res = std::make_unique< QgsRasterLayerElevationProperties >( nullptr ); res->setEnabled( mEnabled ); + res->setMode( mMode ); res->setProfileLineSymbol( mProfileLineSymbol->clone() ); res->setProfileFillSymbol( mProfileFillSymbol->clone() ); res->setProfileSymbology( mSymbology ); res->setElevationLimit( mElevationLimit ); res->setBandNumber( mBandNumber ); + res->setFixedRange( mFixedRange ); + res->setFixedRangePerBand( mRangePerBand ); res->copyCommonProperties( this ); return res.release(); } @@ -107,22 +183,177 @@ QgsRasterLayerElevationProperties *QgsRasterLayerElevationProperties::clone() co QString QgsRasterLayerElevationProperties::htmlSummary() const { QStringList properties; - properties << tr( "Elevation band: %1" ).arg( mBandNumber ); - properties << tr( "Scale: %1" ).arg( mZScale ); - properties << tr( "Offset: %1" ).arg( mZOffset ); + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + properties << tr( "Elevation range: %1 to %2" ).arg( mFixedRange.lower() ).arg( mFixedRange.upper() ); + break; + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + properties << tr( "Elevation for band %1: %2 to %3" ).arg( it.key() ).arg( it.value().lower() ).arg( it.value().upper() ); + } + break; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + properties << tr( "Elevation band: %1" ).arg( mBandNumber ); + properties << tr( "Scale: %1" ).arg( mZScale ); + properties << tr( "Offset: %1" ).arg( mZOffset ); + break; + } + return QStringLiteral( "
  • %1
  • " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } -bool QgsRasterLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsRasterLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer ) const { - // TODO -- test actual raster z range - return true; + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange.overlaps( range ); + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + return true; + } + return false; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( QgsRasterLayer *rl = qobject_cast< QgsRasterLayer * >( layer ) ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), 1, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), rl->dataProvider()->displayBandName( 1 ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), rl->dataProvider()->bandDescription( 1 ), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = mDataDefinedProperties.property( Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = mDataDefinedProperties.property( Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + for ( int band = 1; band <= rl->bandCount(); ++band ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), rl->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), rl->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; + + if ( QgsDoubleRange( lower, upper ).overlaps( range ) ) + return true; + } + } + return false; + } + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + // TODO -- test actual raster z range + return true; + } + BUILTIN_UNREACHABLE } -QgsDoubleRange QgsRasterLayerElevationProperties::calculateZRange( QgsMapLayer * ) const +QgsDoubleRange QgsRasterLayerElevationProperties::calculateZRange( QgsMapLayer *layer ) const { - // TODO -- determine actual z range from raster statistics - return QgsDoubleRange(); + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange; + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + double lower = std::numeric_limits< double >::max(); + double upper = std::numeric_limits< double >::min(); + bool includeLower = true; + bool includeUpper = true; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().lower() < lower ) + { + lower = it.value().lower(); + includeLower = it.value().includeLower(); + } + else if ( !includeLower && it.value().lower() == lower && it.value().includeLower() ) + { + includeLower = true; + } + if ( it.value().upper() > upper ) + { + upper = it.value().upper(); + includeUpper = it.value().includeUpper(); + } + else if ( !includeUpper && it.value().upper() == upper && it.value().includeUpper() ) + { + includeUpper = true; + } + } + return QgsDoubleRange( lower, upper, includeLower, includeUpper ); + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( QgsRasterLayer *rl = qobject_cast< QgsRasterLayer * >( layer ) ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), 1, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), rl->dataProvider()->displayBandName( 1 ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), rl->dataProvider()->bandDescription( 1 ), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = mDataDefinedProperties.property( Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = mDataDefinedProperties.property( Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + double minLower = std::numeric_limits::max(); + double maxUpper = std::numeric_limits::lowest(); + for ( int band = 1; band <= rl->bandCount(); ++band ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), rl->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), rl->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; + + minLower = std::min( minLower, lower ); + maxUpper = std::max( maxUpper, upper ); + } + return ( minLower == std::numeric_limits::max() && maxUpper == std::numeric_limits::lowest() ) ? QgsDoubleRange() : QgsDoubleRange( minLower, maxUpper ); + } + return QgsDoubleRange(); + } + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + // TODO -- determine actual z range from raster statistics + return QgsDoubleRange(); + } + BUILTIN_UNREACHABLE } bool QgsRasterLayerElevationProperties::showByDefaultInElevationProfilePlots() const @@ -130,6 +361,24 @@ bool QgsRasterLayerElevationProperties::showByDefaultInElevationProfilePlots() c return mEnabled; } +QgsMapLayerElevationProperties::Flags QgsRasterLayerElevationProperties::flags() const +{ + if ( mEnabled ) + { + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return QgsMapLayerElevationProperties::Flag::FlagDontInvalidateCachedRendersWhenRangeChanges; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + case Qgis::RasterElevationMode::FixedRangePerBand: + case Qgis::RasterElevationMode::DynamicRangePerBand: + break; + } + } + return QgsMapLayerElevationProperties::Flags(); +} + void QgsRasterLayerElevationProperties::setEnabled( bool enabled ) { if ( enabled == mEnabled ) @@ -140,6 +389,20 @@ void QgsRasterLayerElevationProperties::setEnabled( bool enabled ) emit profileGenerationPropertyChanged(); } +Qgis::RasterElevationMode QgsRasterLayerElevationProperties::mode() const +{ + return mMode; +} + +void QgsRasterLayerElevationProperties::setMode( Qgis::RasterElevationMode mode ) +{ + if ( mMode == mode ) + return; + + mMode = mode; + emit changed(); +} + void QgsRasterLayerElevationProperties::setBandNumber( int band ) { if ( mBandNumber == band ) @@ -150,6 +413,147 @@ void QgsRasterLayerElevationProperties::setBandNumber( int band ) emit profileGenerationPropertyChanged(); } +QgsDoubleRange QgsRasterLayerElevationProperties::elevationRangeForPixelValue( QgsRasterLayer *layer, int band, double pixelValue ) const +{ + if ( !mEnabled || std::isnan( pixelValue ) ) + return QgsDoubleRange(); + + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + return mFixedRange; + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + auto it = mRangePerBand.constFind( band ); + if ( it != mRangePerBand.constEnd() ) + return it.value(); + return QgsDoubleRange(); + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( layer && band > 0 && band <= layer->bandCount() ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), band, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), layer->dataProvider()->displayBandName( band ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), layer->dataProvider()->bandDescription( band ), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = mDataDefinedProperties.property( Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = mDataDefinedProperties.property( Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + + bool ok = false; + const double lower = lowerProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + return QgsDoubleRange(); + const double upper = upperProperty.valueAsDouble( context, 0, &ok ); + if ( !ok ) + return QgsDoubleRange(); + + return QgsDoubleRange( lower, upper ); + } + + return QgsDoubleRange(); + } + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + { + if ( band != mBandNumber ) + return QgsDoubleRange(); + + const double z = pixelValue * mZScale + mZOffset; + return QgsDoubleRange( z, z ); + } + } + BUILTIN_UNREACHABLE +} + +int QgsRasterLayerElevationProperties::bandForElevationRange( QgsRasterLayer *layer, const QgsDoubleRange &range ) const +{ + switch ( mMode ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + case Qgis::RasterElevationMode::RepresentsElevationSurface: + return -1; + + case Qgis::RasterElevationMode::FixedRangePerBand: + { + // find the top-most band which matches the map range + int currentMatchingBand = -1; + QgsDoubleRange currentMatchingRange; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + { + if ( currentMatchingRange.isInfinite() + || ( it.value().includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && it.value().upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingBand = it.key(); + currentMatchingRange = it.value(); + } + } + } + return currentMatchingBand; + } + + case Qgis::RasterElevationMode::DynamicRangePerBand: + { + if ( layer ) + { + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + context.appendScope( bandScope ); + + QgsProperty lowerProperty = mDataDefinedProperties.property( Property::RasterPerBandLowerElevation ); + QgsProperty upperProperty = mDataDefinedProperties.property( Property::RasterPerBandUpperElevation ); + lowerProperty.prepare( context ); + upperProperty.prepare( context ); + + int currentMatchingBand = -1; + QgsDoubleRange currentMatchingRange; + + for ( int band = 1; band <= layer->bandCount(); ++band ) + { + 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( range ) ) + { + if ( currentMatchingRange.isInfinite() + || ( bandRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) + || ( !currentMatchingRange.includeUpper() && bandRange.upper() >= currentMatchingRange.upper() ) ) + { + currentMatchingBand = band; + currentMatchingRange = bandRange; + } + } + } + return currentMatchingBand; + } + return -1; + } + } + BUILTIN_UNREACHABLE +} + QgsLineSymbol *QgsRasterLayerElevationProperties::profileLineSymbol() const { return mProfileLineSymbol.get(); @@ -287,3 +691,31 @@ void QgsRasterLayerElevationProperties::setDefaultProfileFillSymbol( const QColo profileFillLayer->setStrokeStyle( Qt::NoPen ); mProfileFillSymbol = std::make_unique< QgsFillSymbol>( QgsSymbolLayerList( { profileFillLayer.release() } ) ); } + +QMap QgsRasterLayerElevationProperties::fixedRangePerBand() const +{ + return mRangePerBand; +} + +void QgsRasterLayerElevationProperties::setFixedRangePerBand( const QMap &ranges ) +{ + if ( ranges == mRangePerBand ) + return; + + mRangePerBand = ranges; + emit changed(); +} + +QgsDoubleRange QgsRasterLayerElevationProperties::fixedRange() const +{ + return mFixedRange; +} + +void QgsRasterLayerElevationProperties::setFixedRange( const QgsDoubleRange &range ) +{ + if ( range == mFixedRange ) + return; + + mFixedRange = range; + emit changed(); +} diff --git a/src/core/raster/qgsrasterlayerelevationproperties.h b/src/core/raster/qgsrasterlayerelevationproperties.h index 4036d6f12c7e..f40e05db25e1 100644 --- a/src/core/raster/qgsrasterlayerelevationproperties.h +++ b/src/core/raster/qgsrasterlayerelevationproperties.h @@ -51,9 +51,10 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; QgsRasterLayerElevationProperties *clone() const override SIP_FACTORY; QString htmlSummary() const override; - bool isVisibleInZRange( const QgsDoubleRange &range ) const override; + bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = nullptr ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; + QgsMapLayerElevationProperties::Flags flags() const override; /** * Returns TRUE if the elevation properties are enabled, i.e. the raster layer values represent an elevation surface. @@ -69,9 +70,27 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio */ void setEnabled( bool enabled ); + /** + * Returns the elevation mode. + * + * \see setMode() + * \since QGIS 3.38 + */ + Qgis::RasterElevationMode mode() const; + + /** + * Sets the elevation \a mode. + * + * \see mode() + * \since QGIS 3.38 + */ + void setMode( Qgis::RasterElevationMode mode ); + /** * Returns the band number from which the elevation should be taken. * + * \note This is only considered when mode() is Qgis::RasterElevationMode::RepresentsElevationSurface. + * * \see setBandNumber() */ int bandNumber() const { return mBandNumber; } @@ -79,10 +98,79 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio /** * Sets the \a band number from which the elevation should be taken. * + * \note This is only considered when mode() is Qgis::RasterElevationMode::RepresentsElevationSurface. + * * \see bandNumber() */ void setBandNumber( int band ); + /** + * Returns the fixed elevation range for the raster. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRange() + * \since QGIS 3.38 + */ + QgsDoubleRange fixedRange() const; + + /** + * Sets the fixed elevation \a range for the raster. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedElevationRange. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRange() + * \since QGIS 3.38 + */ + void setFixedRange( const QgsDoubleRange &range ); + + /** + * Returns the fixed elevation range for each band. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedRangePerBand. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see setFixedRangePerBand() + * \since QGIS 3.38 + */ + QMap fixedRangePerBand() const; + + /** + * Sets the fixed elevation range for each band. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedRangePerBand. + * + * \note When a fixed range is set any zOffset() and zScale() is ignored. + * + * \see fixedRangePerBand() + * \since QGIS 3.38 + */ + void setFixedRangePerBand( const QMap &ranges ); + + /** + * Returns the elevation range corresponding to a raw pixel value from the specified \a band. + * + * Returns an infinite range if the pixel value does not correspond to an elevation value. + * + * \since QGIS 3.38 + */ + QgsDoubleRange elevationRangeForPixelValue( QgsRasterLayer *layer, int band, double pixelValue ) const; + + /** + * Returns the band corresponding to the specified \a range. + * + * \note This is only considered when mode() is Qgis::RasterElevationMode::FixedRangePerBand or + * Qgis::RasterElevationMode::DynamicRangePerBand. For other modes it will always return -1. + * + * \since QGIS 3.38 + */ + int bandForElevationRange( QgsRasterLayer *layer, const QgsDoubleRange &range ) const; + /** * Returns the line symbol used to render the raster profile in elevation profile plots. * @@ -176,12 +264,17 @@ class CORE_EXPORT QgsRasterLayerElevationProperties : public QgsMapLayerElevatio void setDefaultProfileFillSymbol( const QColor &color ); bool mEnabled = false; + + Qgis::RasterElevationMode mMode = Qgis::RasterElevationMode::RepresentsElevationSurface; + std::unique_ptr< QgsLineSymbol > mProfileLineSymbol; std::unique_ptr< QgsFillSymbol > mProfileFillSymbol; Qgis::ProfileSurfaceSymbology mSymbology = Qgis::ProfileSurfaceSymbology::Line; double mElevationLimit = std::numeric_limits< double >::quiet_NaN(); int mBandNumber = 1; + QgsDoubleRange mFixedRange; + QMap< int, QgsDoubleRange > mRangePerBand; }; #endif // QGSRASTERLAYERELEVATIONPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 0e03553fcf48..472a457aaf8e 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -33,6 +33,8 @@ #include "qgsrasterlayerelevationproperties.h" #include "qgsruntimeprofiler.h" #include "qgsapplication.h" +#include "qgsrastertransparency.h" +#include "qgsrasterlayerutils.h" #include #include @@ -271,12 +273,32 @@ 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: + case Qgis::RasterTemporalMode::FixedRangePerBand: break; case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: @@ -297,14 +319,55 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer ); - if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) + if ( elevationProperties && elevationProperties->hasElevation() ) { - QgsRasterLayerElevationProperties *elevProp - = static_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 ( elevationProperties->mode() ) + { + case Qgis::RasterElevationMode::FixedElevationRange: + // don't need to handle anything here -- the layer renderer will never be created if the + // render context range doesn't match the layer's fixed elevation range + break; + + case Qgis::RasterElevationMode::FixedRangePerBand: + case Qgis::RasterElevationMode::DynamicRangePerBand: + // temporal/elevation band based filtering was already handled earlier in this method + break; + + case Qgis::RasterElevationMode::RepresentsElevationSurface: + { + if ( mPipe->renderer()->usesBands().contains( mElevationBand ) ) + { + // if layer has elevation settings and we are only rendering a slice of z values => we need to filter pixels by elevation + + 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(); + + // account for z offset/zscale by reversing these calculations, so that we get the z range in + // raw pixel values + const double adjustedLower = ( rendererContext.zRange().lower() - mElevationOffset ) / mElevationScale; + const double adjustedUpper = ( rendererContext.zRange().upper() - mElevationOffset ) / mElevationScale; + 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; + } + } + } } mFeedback->setRenderContext( rendererContext ); diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index 9f1a8f7c0bf3..4cde844e5272 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -34,6 +34,16 @@ bool QgsRasterLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTi case Qgis::RasterTemporalMode::FixedTemporalRange: return range.isInfinite() || mFixedRange.isInfinite() || mFixedRange.overlaps( range ); + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + return true; + } + return false; + } + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: case Qgis::RasterTemporalMode::RedrawLayerOnly: return true; @@ -55,6 +65,36 @@ QgsDateTimeRange QgsRasterLayerTemporalProperties::calculateTemporalExtent( QgsM case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: return rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange(); + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + QDateTime begin; + QDateTime end; + bool includeBeginning = true; + bool includeEnd = true; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().begin() < begin || !begin.isValid() ) + { + begin = it.value().begin(); + includeBeginning = it.value().includeBeginning(); + } + else if ( !includeBeginning && it.value().begin() == begin && it.value().includeBeginning() ) + { + includeBeginning = true; + } + if ( it.value().end() > end || !end.isValid() ) + { + end = it.value().end(); + includeEnd = it.value().includeEnd(); + } + else if ( !includeEnd && it.value().end() == end && it.value().includeEnd() ) + { + includeEnd = true; + } + } + return QgsDateTimeRange( begin, end, includeBeginning, includeEnd ); + } + case Qgis::RasterTemporalMode::RedrawLayerOnly: break; } @@ -65,16 +105,28 @@ QgsDateTimeRange QgsRasterLayerTemporalProperties::calculateTemporalExtent( QgsM QList QgsRasterLayerTemporalProperties::allTemporalRanges( QgsMapLayer *layer ) const { QgsRasterLayer *rasterLayer = qobject_cast< QgsRasterLayer *>( layer ); - if ( !rasterLayer ) - return {}; switch ( mMode ) { case Qgis::RasterTemporalMode::FixedTemporalRange: return { mFixedRange }; + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + QList results; + results.reserve( mRangePerBand.size() ); + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + results.append( it.value() ); + } + return results; + } + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: { + if ( !rasterLayer || !rasterLayer->dataProvider() ) + return {}; + const QList< QgsDateTimeRange > ranges = rasterLayer->dataProvider()->temporalCapabilities()->allAvailableTemporalRanges(); return ranges.empty() ? QList< QgsDateTimeRange > { rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange() } : ranges; } @@ -100,7 +152,17 @@ void QgsRasterLayerTemporalProperties::setMode( Qgis::RasterTemporalMode mode ) QgsTemporalProperty::Flags QgsRasterLayerTemporalProperties::flags() const { - return mode() == Qgis::RasterTemporalMode::FixedTemporalRange ? QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges : QgsTemporalProperty::Flags(); + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + return QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges; + + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::RedrawLayerOnly: + case Qgis::RasterTemporalMode::FixedRangePerBand: + return QgsTemporalProperty::Flags(); + } + BUILTIN_UNREACHABLE } Qgis::TemporalIntervalMatchMethod QgsRasterLayerTemporalProperties::intervalHandlingMethod() const @@ -125,6 +187,88 @@ const QgsDateTimeRange &QgsRasterLayerTemporalProperties::fixedTemporalRange() c return mFixedRange; } +QMap QgsRasterLayerTemporalProperties::fixedRangePerBand() const +{ + return mRangePerBand; +} + +void QgsRasterLayerTemporalProperties::setFixedRangePerBand( const QMap &ranges ) +{ + if ( mRangePerBand == ranges ) + return; + + mRangePerBand = ranges; + emit changed(); +} + +int QgsRasterLayerTemporalProperties::bandForTemporalRange( QgsRasterLayer *, const QgsDateTimeRange &range ) const +{ + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::RedrawLayerOnly: + return -1; + + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + // find the latest-most band which matches the map range + int currentMatchingBand = -1; + QgsDateTimeRange currentMatchingRange; + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + if ( it.value().overlaps( range ) ) + { + if ( currentMatchingRange.isInfinite() + || ( it.value().includeEnd() && it.value().end() >= currentMatchingRange.end() ) // cppcheck-suppress mismatchingContainerExpression + || ( !currentMatchingRange.includeEnd() && it.value().end() >= currentMatchingRange.end() ) ) // cppcheck-suppress mismatchingContainerExpression + { + currentMatchingBand = it.key(); + currentMatchingRange = it.value(); + } + } + } + return currentMatchingBand; + } + } + 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; + } + } + BUILTIN_UNREACHABLE +} + bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) @@ -137,16 +281,45 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons mMode = static_cast< Qgis::RasterTemporalMode >( temporalNode.attribute( QStringLiteral( "mode" ), QStringLiteral( "0" ) ). toInt() ); mIntervalHandlingMethod = static_cast< Qgis::TemporalIntervalMatchMethod >( temporalNode.attribute( QStringLiteral( "fetchMode" ), QStringLiteral( "0" ) ). toInt() ); - const QDomNode rangeElement = temporalNode.namedItem( QStringLiteral( "fixedRange" ) ); + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + { + const QDomNode rangeElement = temporalNode.namedItem( QStringLiteral( "fixedRange" ) ); - const QDomNode begin = rangeElement.namedItem( QStringLiteral( "start" ) ); - const QDomNode end = rangeElement.namedItem( QStringLiteral( "end" ) ); + const QDomNode begin = rangeElement.namedItem( QStringLiteral( "start" ) ); + const QDomNode end = rangeElement.namedItem( QStringLiteral( "end" ) ); - const QDateTime beginDate = QDateTime::fromString( begin.toElement().text(), Qt::ISODate ); - const QDateTime endDate = QDateTime::fromString( end.toElement().text(), Qt::ISODate ); + const QDateTime beginDate = QDateTime::fromString( begin.toElement().text(), Qt::ISODate ); + const QDateTime endDate = QDateTime::fromString( end.toElement().text(), Qt::ISODate ); + + const QgsDateTimeRange range = QgsDateTimeRange( beginDate, endDate ); + setFixedTemporalRange( range ); + break; + } - const QgsDateTimeRange range = QgsDateTimeRange( beginDate, endDate ); - setFixedTemporalRange( range ); + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + mRangePerBand.clear(); + + const QDomNodeList ranges = temporalNode.firstChildElement( QStringLiteral( "ranges" ) ).childNodes(); + for ( int i = 0; i < ranges.size(); ++i ) + { + const QDomElement rangeElement = ranges.at( i ).toElement(); + const int band = rangeElement.attribute( QStringLiteral( "band" ) ).toInt(); + const QDateTime begin = QDateTime::fromString( rangeElement.attribute( QStringLiteral( "begin" ) ), Qt::ISODate ); + const QDateTime end = QDateTime::fromString( rangeElement.attribute( QStringLiteral( "end" ) ), Qt::ISODate ); + const bool includeBeginning = rangeElement.attribute( QStringLiteral( "includeBeginning" ) ).toInt(); + const bool includeEnd = rangeElement.attribute( QStringLiteral( "includeEnd" ) ).toInt(); + mRangePerBand.insert( band, QgsDateTimeRange( begin, end, includeBeginning, includeEnd ) ); + } + break; + } + + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + case Qgis::RasterTemporalMode::RedrawLayerOnly: + break; + } return true; } @@ -162,19 +335,48 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD temporalElement.setAttribute( QStringLiteral( "mode" ), QString::number( static_cast< int >( mMode ) ) ); temporalElement.setAttribute( QStringLiteral( "fetchMode" ), QString::number( static_cast< int >( mIntervalHandlingMethod ) ) ); - QDomElement rangeElement = document.createElement( QStringLiteral( "fixedRange" ) ); + switch ( mMode ) + { + case Qgis::RasterTemporalMode::FixedTemporalRange: + { + + QDomElement rangeElement = document.createElement( QStringLiteral( "fixedRange" ) ); + + QDomElement startElement = document.createElement( QStringLiteral( "start" ) ); + QDomElement endElement = document.createElement( QStringLiteral( "end" ) ); - QDomElement startElement = document.createElement( QStringLiteral( "start" ) ); - QDomElement endElement = document.createElement( QStringLiteral( "end" ) ); + const QDomText startText = document.createTextNode( mFixedRange.begin().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); + const QDomText endText = document.createTextNode( mFixedRange.end().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); + startElement.appendChild( startText ); + endElement.appendChild( endText ); + rangeElement.appendChild( startElement ); + rangeElement.appendChild( endElement ); - const QDomText startText = document.createTextNode( mFixedRange.begin().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); - const QDomText endText = document.createTextNode( mFixedRange.end().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); - startElement.appendChild( startText ); - endElement.appendChild( endText ); - rangeElement.appendChild( startElement ); - rangeElement.appendChild( endElement ); + temporalElement.appendChild( rangeElement ); + break; + } + + case Qgis::RasterTemporalMode::FixedRangePerBand: + { + QDomElement ranges = document.createElement( QStringLiteral( "ranges" ) ); + for ( auto it = mRangePerBand.constBegin(); it != mRangePerBand.constEnd(); ++it ) + { + QDomElement range = document.createElement( QStringLiteral( "range" ) ); + range.setAttribute( QStringLiteral( "band" ), it.key() ); + range.setAttribute( QStringLiteral( "begin" ), it.value().begin().toString( Qt::ISODate ) ); + range.setAttribute( QStringLiteral( "end" ), it.value().end().toString( Qt::ISODate ) ); + range.setAttribute( QStringLiteral( "includeBeginning" ), it.value().includeBeginning() ? "1" : "0" ); + range.setAttribute( QStringLiteral( "includeEnd" ), it.value().includeEnd() ? "1" : "0" ); + ranges.appendChild( range ); + } + temporalElement.appendChild( ranges ); + break; + } - temporalElement.appendChild( rangeElement ); + case Qgis::RasterTemporalMode::RedrawLayerOnly: + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + break; + } element.appendChild( temporalElement ); diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 88687906c463..aa2fabf15b92 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -25,6 +25,8 @@ #include "qgsrange.h" #include "qgsmaplayertemporalproperties.h" +class QgsRasterLayer; + /** * \class QgsRasterLayerTemporalProperties * \ingroup core @@ -90,7 +92,7 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP * a render context intersects the specified \a range. * * \warning This setting is only effective when mode() is - * QgsRasterLayerTemporalProperties::ModeFixedTemporalRange + * Qgis::RasterTemporalMode::FixedTemporalRange * * \see fixedTemporalRange() */ @@ -99,13 +101,49 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP /** * Returns the fixed temporal range for the layer. * - * \warning To be used only when mode() is - * QgsRasterLayerTemporalProperties::ModeFixedTemporalRange + * \warning To be used only when mode() is Qgis::RasterTemporalMode::FixedTemporalRange * * \see setFixedTemporalRange() */ const QgsDateTimeRange &fixedTemporalRange() const; + /** + * Returns the fixed temporal range for each band. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::FixedRangePerBand. + * + * \see setFixedRangePerBand() + * \since QGIS 3.38 + */ + QMap fixedRangePerBand() const; + + /** + * Sets the fixed temporal range for each band. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::FixedRangePerBand. + * + * \see fixedRangePerBand() + * \since QGIS 3.38 + */ + void setFixedRangePerBand( const QMap &ranges ); + + /** + * Returns the band corresponding to the specified \a range. + * + * \note This is only considered when mode() is Qgis::RasterTemporalMode::FixedRangePerBand. + * For other modes it will always return -1. + * + * \since QGIS 3.38 + */ + 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; + QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; @@ -122,6 +160,8 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP //! Represents fixed temporal range. QgsDateTimeRange mFixedRange; + + QMap< int, QgsDateTimeRange > mRangePerBand; }; #endif // QGSRASTERLAYERTEMPORALPROPERTIES_H diff --git a/src/core/raster/qgsrasterlayerutils.cpp b/src/core/raster/qgsrasterlayerutils.cpp new file mode 100644 index 000000000000..3dc8b536cc98 --- /dev/null +++ b/src/core/raster/qgsrasterlayerutils.cpp @@ -0,0 +1,151 @@ +/*************************************************************************** + 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 + const QList< int > temporalBands = temporalProperties->filteredBandsForTemporalRange( + layer, temporalRange ); + 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/qgsrasterrenderer.cpp b/src/core/raster/qgsrasterrenderer.cpp index 87e47e30a9b2..799f0930f5d3 100644 --- a/src/core/raster/qgsrasterrenderer.cpp +++ b/src/core/raster/qgsrasterrenderer.cpp @@ -97,6 +97,16 @@ bool QgsRasterRenderer::setInput( QgsRasterInterface *input ) return true; } +int QgsRasterRenderer::inputBand() const +{ + return -1; +} + +bool QgsRasterRenderer::setInputBand( int ) +{ + return false; +} + bool QgsRasterRenderer::usesTransparency() const { if ( !mInput ) diff --git a/src/core/raster/qgsrasterrenderer.h b/src/core/raster/qgsrasterrenderer.h index e696b0af05f2..28fdb77c31ca 100644 --- a/src/core/raster/qgsrasterrenderer.h +++ b/src/core/raster/qgsrasterrenderer.h @@ -85,6 +85,34 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface bool setInput( QgsRasterInterface *input ) override; + /** + * Returns the input band for the renderer, or -1 if no input band is available. + * + * For renderers which utilize multiple input bands -1 will be returned. In these + * cases usesBands() will return a list of all utilized bands (including alpha + * bands). + * + * \see setInputBand() + * \see usesBands() + * + * \since QGIS 3.38 + */ + virtual int inputBand() const; + + /** + * Attempts to set the input \a band for the renderer. + * + * Returns TRUE if the band was successfully set, or FALSE if the band could not be set. + * + * \note Not all renderers support setting the input band. + * + * \see inputBand() + * \see usesBands() + * + * \since QGIS 3.38 + */ + virtual bool setInputBand( int band ); + QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, @@ -164,7 +192,11 @@ class CORE_EXPORT QgsRasterRenderer : public QgsRasterInterface */ void copyCommonProperties( const QgsRasterRenderer *other, bool copyMinMaxOrigin = true ); - //! Returns a list of band numbers used by the renderer + /** + * Returns a list of band numbers used by the renderer. + * + * \see setInputBand() + */ virtual QList usesBands() const { return QList(); } //! Returns const reference to origin of min/max values diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index 6398d725d0d6..772b362d5e28 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -35,9 +35,10 @@ QgsRasterRendererRegistryEntry::QgsRasterRendererRegistryEntry( const QString &name, const QString &visibleName, QgsRasterRendererCreateFunc rendererFunction, - QgsRasterRendererWidgetCreateFunc widgetFunction ) + QgsRasterRendererWidgetCreateFunc widgetFunction, Qgis::RasterRendererCapabilities capabilities ) : name( name ) , visibleName( visibleName ) + , capabilities( capabilities ) , rendererCreateFunction( rendererFunction ) , widgetCreateFunction( widgetFunction ) { @@ -52,7 +53,8 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() { // insert items in a particular order, which is returned in renderersList() insert( QgsRasterRendererRegistryEntry( QStringLiteral( "multibandcolor" ), QObject::tr( "Multiband color" ), - QgsMultiBandColorRenderer::create, nullptr ) ); + QgsMultiBandColorRenderer::create, nullptr, + Qgis::RasterRendererCapability::UsesMultipleBands ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "paletted" ), QObject::tr( "Paletted/Unique values" ), QgsPalettedRasterRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "singlebandgray" ), QObject::tr( "Singleband gray" ), QgsSingleBandGrayRenderer::create, nullptr ) ); @@ -109,6 +111,16 @@ QList< QgsRasterRendererRegistryEntry > QgsRasterRendererRegistry::entries() con return result; } +Qgis::RasterRendererCapabilities QgsRasterRendererRegistry::rendererCapabilities( const QString &rendererName ) const +{ + const QHash< QString, QgsRasterRendererRegistryEntry >::const_iterator it = mEntries.constFind( rendererName ); + if ( it != mEntries.constEnd() ) + { + return it.value().capabilities; + } + return Qgis::RasterRendererCapabilities(); +} + QgsRasterRenderer *QgsRasterRendererRegistry::defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const { if ( !provider || provider->bandCount() < 1 ) @@ -239,13 +251,11 @@ QgsRasterRenderer *QgsRasterRendererRegistry::defaultRendererForDrawingStyle( Qg const int bandCount = renderer->usesBands().size(); if ( bandCount == 1 ) { - const QList transparentSingleList; - tr->setTransparentSingleValuePixelList( transparentSingleList ); + tr->setTransparentSingleValuePixelList( {} ); } else if ( bandCount == 3 ) { - const QList transparentThreeValueList; - tr->setTransparentThreeValuePixelList( transparentThreeValueList ); + tr->setTransparentThreeValuePixelList( {} ); } renderer->setRasterTransparency( tr.release() ); return renderer.release(); diff --git a/src/core/raster/qgsrasterrendererregistry.h b/src/core/raster/qgsrasterrendererregistry.h index 8230221d4402..424b2a945964 100644 --- a/src/core/raster/qgsrasterrendererregistry.h +++ b/src/core/raster/qgsrasterrendererregistry.h @@ -18,11 +18,6 @@ #ifndef QGSRASTERRENDERERREGISTRY_H #define QGSRASTERRENDERERREGISTRY_H - -#define SIP_NO_FILE - - - #include "qgis_core.h" #include "qgis.h" #include @@ -36,17 +31,26 @@ class QgsRasterRendererWidget; class QgsRasterDataProvider; class QgsRectangle; +#ifndef SIP_RUN typedef QgsRasterRenderer *( *QgsRasterRendererCreateFunc )( const QDomElement &, QgsRasterInterface *input ); typedef QgsRasterRendererWidget *( *QgsRasterRendererWidgetCreateFunc )( QgsRasterLayer *, const QgsRectangle &extent ); /** * \ingroup core * \brief Registry for raster renderer entries. + * + * \note Not available in Python bindings */ struct CORE_EXPORT QgsRasterRendererRegistryEntry { + + /** + * Constructor for QgsRasterRendererRegistryEntry. + * + * Since QGIS 3.38, the \a capabilities argument can be used to specify renderer capabilities. + */ QgsRasterRendererRegistryEntry( const QString &name, const QString &visibleName, QgsRasterRendererCreateFunc rendererFunction, - QgsRasterRendererWidgetCreateFunc widgetFunction ); + QgsRasterRendererWidgetCreateFunc widgetFunction, Qgis::RasterRendererCapabilities capabilities = Qgis::RasterRendererCapabilities() ); /** * Constructor for QgsRasterRendererRegistryEntry. @@ -54,11 +58,21 @@ struct CORE_EXPORT QgsRasterRendererRegistryEntry QgsRasterRendererRegistryEntry() = default; QString name; QString visibleName; //visible (and translatable) name + + /** + * Renderer capabilities. + * + * \since QGIS 3.38 + */ + Qgis::RasterRendererCapabilities capabilities; + QIcon icon(); QgsRasterRendererCreateFunc rendererCreateFunction = nullptr ; //pointer to create function QgsRasterRendererWidgetCreateFunc widgetCreateFunction = nullptr ; //pointer to create function for renderer widget }; +#endif + /** * \ingroup core * \brief Registry for raster renderers. @@ -66,25 +80,67 @@ struct CORE_EXPORT QgsRasterRendererRegistryEntry * QgsRasterRendererRegistry is not usually directly created, but rather accessed through * QgsApplication::rasterRendererRegistry(). * - * \note not available in Python bindings + * \note Exposed to Python bindings in QGIS 3.38 */ class CORE_EXPORT QgsRasterRendererRegistry { public: + /** + * Constructor for QgsRasterRendererRegistry. + * + * QgsRasterRendererRegistry is not usually directly created, but rather accessed through + * QgsApplication::rasterRendererRegistry(). + * + * The registry is pre-populated with standard raster renderers. + */ QgsRasterRendererRegistry(); - void insert( const QgsRasterRendererRegistryEntry &entry ); - void insertWidgetFunction( const QString &rendererName, QgsRasterRendererWidgetCreateFunc func ); - bool rendererData( const QString &rendererName, QgsRasterRendererRegistryEntry &data ) const; + /** + * Inserts a new \a entry into the registry. + * + * \note Not available in Python bindings + */ + void insert( const QgsRasterRendererRegistryEntry &entry ) SIP_SKIP; + + /** + * Sets the widget creation function for a renderer. + * + * \note Not available in Python bindings + */ + void insertWidgetFunction( const QString &rendererName, QgsRasterRendererWidgetCreateFunc func ) SIP_SKIP; + + /** + * Retrieves renderer data from the registry. + * + * \note Not available in Python bindings + */ + bool rendererData( const QString &rendererName, QgsRasterRendererRegistryEntry &data ) const SIP_SKIP; + + /** + * Returns a list of the names of registered renderers. + */ QStringList renderersList() const; - QList< QgsRasterRendererRegistryEntry > entries() const; + + /** + * Returns the list of registered renderers. + * + * \note Not available in Python bindings + */ + QList< QgsRasterRendererRegistryEntry > entries() const SIP_SKIP; + + /** + * Returns the capabilities for the renderer with the specified name. + * + * \since QGIS 3.38 + */ + Qgis::RasterRendererCapabilities rendererCapabilities( const QString &rendererName ) const; /** * Creates a default renderer for a raster drawing style (considering user options such as default contrast enhancement). * Caller takes ownership. */ - QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const; + QgsRasterRenderer *defaultRendererForDrawingStyle( Qgis::RasterDrawingStyle drawingStyle, QgsRasterDataProvider *provider ) const SIP_FACTORY; private: QHash< QString, QgsRasterRendererRegistryEntry > mEntries; diff --git a/src/core/raster/qgsrastertransparency.cpp b/src/core/raster/qgsrastertransparency.cpp index 30be6e030066..786fff816644 100644 --- a/src/core/raster/qgsrastertransparency.cpp +++ b/src/core/raster/qgsrastertransparency.cpp @@ -19,17 +19,16 @@ email : ersts@amnh.org #include "qgsrasterinterface.h" #include "qgsrastertransparency.h" #include "qgis.h" -#include "qgslogger.h" #include #include -QList QgsRasterTransparency::transparentSingleValuePixelList() const +QVector QgsRasterTransparency::transparentSingleValuePixelList() const { return mTransparentSingleValuePixelList; } -QList QgsRasterTransparency::transparentThreeValuePixelList() const +QVector QgsRasterTransparency::transparentThreeValuePixelList() const { return mTransparentThreeValuePixelList; } @@ -40,38 +39,34 @@ void QgsRasterTransparency::initializeTransparentPixelList( double value ) mTransparentSingleValuePixelList.clear(); //add the initial value - TransparentSingleValuePixel myTransparentSingleValuePixel; - myTransparentSingleValuePixel.min = value; - myTransparentSingleValuePixel.max = value; - myTransparentSingleValuePixel.percentTransparent = 100.0; - mTransparentSingleValuePixelList.append( myTransparentSingleValuePixel ); + mTransparentSingleValuePixelList.append( TransparentSingleValuePixel( value, value, 0 ) ); } void QgsRasterTransparency::initializeTransparentPixelList( double redValue, double greenValue, double blueValue ) { - //clearn the existing list + //clear the existing list mTransparentThreeValuePixelList.clear(); //add the initial values - TransparentThreeValuePixel myTransparentThreeValuePixel; - myTransparentThreeValuePixel.red = redValue; - myTransparentThreeValuePixel.green = greenValue; - myTransparentThreeValuePixel.blue = blueValue; - myTransparentThreeValuePixel.percentTransparent = 100.0; - mTransparentThreeValuePixelList.append( myTransparentThreeValuePixel ); + mTransparentThreeValuePixelList.append( TransparentThreeValuePixel( redValue, greenValue, blueValue, 0 ) ); } -void QgsRasterTransparency::setTransparentSingleValuePixelList( const QList &newList ) +void QgsRasterTransparency::setTransparentSingleValuePixelList( const QVector &newList ) { mTransparentSingleValuePixelList = newList; } -void QgsRasterTransparency::setTransparentThreeValuePixelList( const QList &newList ) +void QgsRasterTransparency::setTransparentThreeValuePixelList( const QVector &newList ) { mTransparentThreeValuePixelList = newList; } int QgsRasterTransparency::alphaValue( double value, int globalTransparency ) const +{ + return static_cast< int >( opacityForValue( value ) * globalTransparency ); +} + +double QgsRasterTransparency::opacityForValue( double value ) const { //if NaN return 0, transparent if ( std::isnan( value ) ) @@ -80,30 +75,27 @@ int QgsRasterTransparency::alphaValue( double value, int globalTransparency ) co } //Search through the transparency list looking for a match - bool myTransparentPixelFound = false; - TransparentSingleValuePixel myTransparentPixel = {0, 0, 100}; - for ( int myListRunner = 0; myListRunner < mTransparentSingleValuePixelList.count(); myListRunner++ ) + auto it = std::find_if( mTransparentSingleValuePixelList.constBegin(), mTransparentSingleValuePixelList.constEnd(), [value]( const TransparentSingleValuePixel & p ) { - myTransparentPixel = mTransparentSingleValuePixelList[myListRunner]; - if ( ( value >= myTransparentPixel.min && value <= myTransparentPixel.max ) || - qgsDoubleNear( value, myTransparentPixel.min ) || - qgsDoubleNear( value, myTransparentPixel.max ) ) - { - myTransparentPixelFound = true; - break; - } - } + return ( value > p.min && value < p.max ) + || ( p.includeMinimum && qgsDoubleNear( value, p.min ) ) + || ( p.includeMaximum && qgsDoubleNear( value, p.max ) ); + } ); - //if a match was found use the stored transparency percentage - if ( myTransparentPixelFound ) + if ( it != mTransparentSingleValuePixelList.constEnd() ) { - return static_cast< int >( static_cast< float >( globalTransparency ) * ( 1.0 - ( myTransparentPixel.percentTransparent / 100.0 ) ) ); + return it->opacity; } - return globalTransparency; + return 1; } int QgsRasterTransparency::alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency ) const +{ + return static_cast< int >( opacityForRgbValues( redValue, greenValue, blueValue ) * globalTransparency ); +} + +double QgsRasterTransparency::opacityForRgbValues( double redValue, double greenValue, double blueValue ) const { //if NaN return 0, transparent if ( std::isnan( redValue ) || std::isnan( greenValue ) || std::isnan( blueValue ) ) @@ -112,31 +104,19 @@ int QgsRasterTransparency::alphaValue( double redValue, double greenValue, doubl } //Search through the transparency list looking for a match - bool myTransparentPixelFound = false; - TransparentThreeValuePixel myTransparentPixel = {0, 0, 0, 100}; - for ( int myListRunner = 0; myListRunner < mTransparentThreeValuePixelList.count(); myListRunner++ ) + auto it = std::find_if( mTransparentThreeValuePixelList.constBegin(), mTransparentThreeValuePixelList.constEnd(), [redValue, greenValue, blueValue]( const TransparentThreeValuePixel & p ) { - myTransparentPixel = mTransparentThreeValuePixelList[myListRunner]; - if ( qgsDoubleNear( myTransparentPixel.red, redValue ) ) - { - if ( qgsDoubleNear( myTransparentPixel.green, greenValue ) ) - { - if ( qgsDoubleNear( myTransparentPixel.blue, blueValue ) ) - { - myTransparentPixelFound = true; - break; - } - } - } - } + return qgsDoubleNear( p.red, redValue ) + && qgsDoubleNear( p.green, greenValue ) + && qgsDoubleNear( p.blue, blueValue ); + } ); - //if a match was found use the stored transparency percentage - if ( myTransparentPixelFound ) + if ( it != mTransparentThreeValuePixelList.constEnd() ) { - return static_cast< int >( static_cast< float >( globalTransparency ) * ( 1.0 - ( myTransparentPixel.percentTransparent / 100.0 ) ) ); + return it->opacity; } - return globalTransparency; + return 1; } bool QgsRasterTransparency::isEmpty() const @@ -150,13 +130,13 @@ void QgsRasterTransparency::writeXml( QDomDocument &doc, QDomElement &parentElem if ( !mTransparentSingleValuePixelList.isEmpty() ) { QDomElement singleValuePixelListElement = doc.createElement( QStringLiteral( "singleValuePixelList" ) ); - QList::const_iterator it = mTransparentSingleValuePixelList.constBegin(); + auto it = mTransparentSingleValuePixelList.constBegin(); for ( ; it != mTransparentSingleValuePixelList.constEnd(); ++it ) { QDomElement pixelListElement = doc.createElement( QStringLiteral( "pixelListEntry" ) ); pixelListElement.setAttribute( QStringLiteral( "min" ), QgsRasterBlock::printValue( it->min ) ); pixelListElement.setAttribute( QStringLiteral( "max" ), QgsRasterBlock::printValue( it->max ) ); - pixelListElement.setAttribute( QStringLiteral( "percentTransparent" ), QString::number( it->percentTransparent ) ); + pixelListElement.setAttribute( QStringLiteral( "percentTransparent" ), QString::number( 100.0 * ( 1 - it->opacity ) ) ); singleValuePixelListElement.appendChild( pixelListElement ); } rasterTransparencyElem.appendChild( singleValuePixelListElement ); @@ -165,14 +145,14 @@ void QgsRasterTransparency::writeXml( QDomDocument &doc, QDomElement &parentElem if ( !mTransparentThreeValuePixelList.isEmpty() ) { QDomElement threeValuePixelListElement = doc.createElement( QStringLiteral( "threeValuePixelList" ) ); - QList::const_iterator it = mTransparentThreeValuePixelList.constBegin(); + auto it = mTransparentThreeValuePixelList.constBegin(); for ( ; it != mTransparentThreeValuePixelList.constEnd(); ++it ) { QDomElement pixelListElement = doc.createElement( QStringLiteral( "pixelListEntry" ) ); pixelListElement.setAttribute( QStringLiteral( "red" ), QgsRasterBlock::printValue( it->red ) ); pixelListElement.setAttribute( QStringLiteral( "green" ), QgsRasterBlock::printValue( it->green ) ); pixelListElement.setAttribute( QStringLiteral( "blue" ), QgsRasterBlock::printValue( it->blue ) ); - pixelListElement.setAttribute( QStringLiteral( "percentTransparent" ), QString::number( it->percentTransparent ) ); + pixelListElement.setAttribute( QStringLiteral( "percentTransparent" ), QString::number( 100.0 * ( 1 - it->opacity ) ) ); threeValuePixelListElement.appendChild( pixelListElement ); } rasterTransparencyElem.appendChild( threeValuePixelListElement ); @@ -195,37 +175,37 @@ void QgsRasterTransparency::readXml( const QDomElement &elem ) if ( !singlePixelListElem.isNull() ) { const QDomNodeList entryList = singlePixelListElem.elementsByTagName( QStringLiteral( "pixelListEntry" ) ); - TransparentSingleValuePixel sp; for ( int i = 0; i < entryList.size(); ++i ) { currentEntryElem = entryList.at( i ).toElement(); - sp.percentTransparent = currentEntryElem.attribute( QStringLiteral( "percentTransparent" ) ).toDouble(); + double min = 0; + double max = 0; + const double opacity = 1.0 - currentEntryElem.attribute( QStringLiteral( "percentTransparent" ) ).toDouble() / 100.0; // Backward compoatibility < 1.9 : pixelValue (before ranges) if ( currentEntryElem.hasAttribute( QStringLiteral( "pixelValue" ) ) ) { - sp.min = sp.max = currentEntryElem.attribute( QStringLiteral( "pixelValue" ) ).toDouble(); + min = max = currentEntryElem.attribute( QStringLiteral( "pixelValue" ) ).toDouble(); } else { - sp.min = currentEntryElem.attribute( QStringLiteral( "min" ) ).toDouble(); - sp.max = currentEntryElem.attribute( QStringLiteral( "max" ) ).toDouble(); + min = currentEntryElem.attribute( QStringLiteral( "min" ) ).toDouble(); + max = currentEntryElem.attribute( QStringLiteral( "max" ) ).toDouble(); } - mTransparentSingleValuePixelList.append( sp ); + mTransparentSingleValuePixelList.append( TransparentSingleValuePixel( min, max, opacity ) ); } } const QDomElement threeValuePixelListElem = elem.firstChildElement( QStringLiteral( "threeValuePixelList" ) ); if ( !threeValuePixelListElem.isNull() ) { const QDomNodeList entryList = threeValuePixelListElem.elementsByTagName( QStringLiteral( "pixelListEntry" ) ); - TransparentThreeValuePixel tp; for ( int i = 0; i < entryList.size(); ++i ) { currentEntryElem = entryList.at( i ).toElement(); - tp.red = currentEntryElem.attribute( QStringLiteral( "red" ) ).toDouble(); - tp.green = currentEntryElem.attribute( QStringLiteral( "green" ) ).toDouble(); - tp.blue = currentEntryElem.attribute( QStringLiteral( "blue" ) ).toDouble(); - tp.percentTransparent = currentEntryElem.attribute( QStringLiteral( "percentTransparent" ) ).toDouble(); - mTransparentThreeValuePixelList.append( tp ); + const double red = currentEntryElem.attribute( QStringLiteral( "red" ) ).toDouble(); + const double green = currentEntryElem.attribute( QStringLiteral( "green" ) ).toDouble(); + const double blue = currentEntryElem.attribute( QStringLiteral( "blue" ) ).toDouble(); + const double opacity = 1.0 - currentEntryElem.attribute( QStringLiteral( "percentTransparent" ) ).toDouble() / 100.0; + mTransparentThreeValuePixelList.append( TransparentThreeValuePixel( red, green, blue, opacity ) ); } } } diff --git a/src/core/raster/qgsrastertransparency.h b/src/core/raster/qgsrastertransparency.h index 82e396aca404..7375c0883991 100644 --- a/src/core/raster/qgsrastertransparency.h +++ b/src/core/raster/qgsrastertransparency.h @@ -20,6 +20,7 @@ email : ersts@amnh.org #include "qgis_core.h" #include "qgis_sip.h" +#include "qgis.h" #include class QDomDocument; class QDomElement; @@ -39,39 +40,161 @@ class CORE_EXPORT QgsRasterTransparency */ QgsRasterTransparency() = default; - // - // Structs to hold transparent pixel values - // + /** + * \ingroup core + * \brief Defines the transparency for a RGB pixel value. + */ struct TransparentThreeValuePixel { + + /** + * Constructor for TransparentThreeValuePixel. + * \param red red pixel value + * \param green green pixel value + * \param blue blue pixel value + * \param opacity opacity for pixel, between 0 and 1.0 + * \since QGIS 3.38 + */ + TransparentThreeValuePixel( double red = 0, double green = 0, double blue = 0, double opacity = 0 ) + : red( red ) + , green( green ) + , blue( blue ) + , opacity( opacity ) + {} + + /** + * Red pixel value. + */ double red; + + /** + * Green pixel value. + */ double green; + + /** + * Blue pixel value. + */ double blue; - double percentTransparent; + + /** + * Opacity for pixel, between 0 and 1.0. + * + * \since QGIS 3.38 + */ + double opacity = 0; + + bool operator==( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const + { + return qgsDoubleNear( red, other.red ) + && qgsDoubleNear( green, other.green ) + && qgsDoubleNear( blue, other.blue ) + && qgsDoubleNear( opacity, other.opacity ); + } + bool operator!=( const QgsRasterTransparency::TransparentThreeValuePixel &other ) const + { + return !( *this == other ); + } + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->red ).arg( sipCpp->green ).arg( sipCpp->blue ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif }; + /** + * \ingroup core + * \brief Defines the transparency for a range of single-band pixel values. + */ struct TransparentSingleValuePixel { + + /** + * Constructor for TransparentSingleValuePixel. + * \param minimum minimum pixel value to include in range + * \param maximum maximum pixel value to include in range + * \param opacity opacity for pixel, between 0 and 1.0 + * \param includeMinimum whether the minimum value should be included in the range + * \param includeMaximum whether the maximum value should be included in the range + * + * \since QGIS 3.38 + */ + TransparentSingleValuePixel( double minimum = 0, double maximum = 0, double opacity = 0, bool includeMinimum = true, bool includeMaximum = true ) + : min( minimum ) + , max( maximum ) + , opacity( opacity ) + , includeMinimum( includeMinimum ) + , includeMaximum( includeMaximum ) + {} + + /** + * Minimum pixel value to include in range. + */ double min; + + /** + * Maximum pixel value to include in range. + */ double max; - double percentTransparent; - }; - // - // Initializer, Accessor and mutator for transparency tables. - // + /** + * Opacity for pixel, between 0 and 1.0. + * + * \since QGIS 3.38 + */ + double opacity = 0; + + /** + * TRUE if pixels matching the min value should be considered transparent, + * or FALSE if only pixels greater than the min value should be transparent. + * + * \since QGIS 3.38 + */ + bool includeMinimum = true; + + /** + * TRUE if pixels matching the max value should be considered transparent, + * or FALSE if only pixels less than the max value should be transparent. + * + * \since QGIS 3.38 + */ + bool includeMaximum = true; + + bool operator==( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const + { + return qgsDoubleNear( min, other.min ) + && qgsDoubleNear( max, other.max ) + && qgsDoubleNear( opacity, other.opacity ) + && includeMinimum == other.includeMinimum && includeMaximum == other.includeMaximum; + } + bool operator!=( const QgsRasterTransparency::TransparentSingleValuePixel &other ) const + { + return !( *this == other ); + } + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + const QString str = QStringLiteral( "" ).arg( sipCpp->min ).arg( sipCpp->max ).arg( sipCpp->opacity ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif + }; /** * Returns the transparent single value pixel list. * \see setTransparentSingleValuePixelList() */ - QList transparentSingleValuePixelList() const; + QVector transparentSingleValuePixelList() const; /** * Returns the transparent three value pixel list. * \see setTransparentThreeValuePixelList() */ - QList transparentThreeValuePixelList() const; + QVector transparentThreeValuePixelList() const; /** * Resets the transparency list to a single \a value. @@ -87,13 +210,13 @@ class CORE_EXPORT QgsRasterTransparency * Sets the transparent single value pixel list, replacing the whole existing list. * \see transparentSingleValuePixelList() */ - void setTransparentSingleValuePixelList( const QList &newList ); + void setTransparentSingleValuePixelList( const QVector &newList ); /** * Sets the transparent three value pixel list, replacing the whole existing list. * \see transparentThreeValuePixelList() */ - void setTransparentThreeValuePixelList( const QList &newList ); + void setTransparentThreeValuePixelList( const QVector &newList ); /** * Returns the transparency value for a single \a value pixel. @@ -103,10 +226,20 @@ class CORE_EXPORT QgsRasterTransparency * * \param value the needle to search for in the transparency hay stack * \param globalTransparency the overall transparency level for the layer + * + * \deprecated use opacityForValue() instead. */ - int alphaValue( double value, int globalTransparency = 255 ) const; + Q_DECL_DEPRECATED int alphaValue( double value, int globalTransparency = 255 ) const SIP_DEPRECATED; - //! \brief + /** + * Returns the opacity (as a value from 0 to 1) for a single \a value pixel. + * + * Searches through the transparency list, and if a match is found, returns + * the opacity corresponding to the value. Returns 1 if no matches are found. + * + * \since QGIS 3.38 + */ + double opacityForValue( double value ) const; /** * Returns the transparency value for a RGB pixel. @@ -117,8 +250,22 @@ class CORE_EXPORT QgsRasterTransparency * \param greenValue the green portion of the needle to search for in the transparency hay stack * \param blueValue the green portion of the needle to search for in the transparency hay stack * \param globalTransparency the overall transparency level for the layer + * + * \deprecated use opacityForRgbValues() instead. + */ + Q_DECL_DEPRECATED int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const SIP_DEPRECATED; + + /** + * Returns the opacity (as a value from 0 to 1) for a set of RGB pixel values. + * + * Searches through the transparency list, and if a match is found, returns + * the opacity corresponding to the values. Returns 1 if no matches are found. + * + * If any of the red, green or blue values are NaN, 0 will be returned. + * + * \since QGIS 3.38 */ - int alphaValue( double redValue, double greenValue, double blueValue, int globalTransparency = 255 ) const; + double opacityForRgbValues( double redValue, double greenValue, double blueValue ) const; //! True if there are no entries in the pixel lists except the nodata value bool isEmpty() const; @@ -135,10 +282,10 @@ class CORE_EXPORT QgsRasterTransparency private: //! \brief The list to hold transparency values for RGB layers - QList mTransparentThreeValuePixelList; + QVector mTransparentThreeValuePixelList; //! \brief The list to hold transparency values for single value pixel layers - QList mTransparentSingleValuePixelList; + QVector mTransparentSingleValuePixelList; }; #endif diff --git a/src/core/raster/qgssinglebandcolordatarenderer.cpp b/src/core/raster/qgssinglebandcolordatarenderer.cpp index 870d7d7235d8..e9ed71cfa46a 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.cpp +++ b/src/core/raster/qgssinglebandcolordatarenderer.cpp @@ -23,8 +23,9 @@ #include #include -QgsSingleBandColorDataRenderer::QgsSingleBandColorDataRenderer( QgsRasterInterface *input, int band ): - QgsRasterRenderer( input, QStringLiteral( "singlebandcolordata" ) ), mBand( band ) +QgsSingleBandColorDataRenderer::QgsSingleBandColorDataRenderer( QgsRasterInterface *input, int band ) + : QgsRasterRenderer( input, QStringLiteral( "singlebandcolordata" ) ) + , mBand( band ) { } @@ -139,3 +140,23 @@ bool QgsSingleBandColorDataRenderer::setInput( QgsRasterInterface *input ) } return false; } + +int QgsSingleBandColorDataRenderer::inputBand() const +{ + return mBand; +} + +bool QgsSingleBandColorDataRenderer::setInputBand( int band ) +{ + if ( !mInput ) + { + mBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mBand = band; + return true; + } + return false; +} diff --git a/src/core/raster/qgssinglebandcolordatarenderer.h b/src/core/raster/qgssinglebandcolordatarenderer.h index a1c94e8735b8..aaf0694bda44 100644 --- a/src/core/raster/qgssinglebandcolordatarenderer.h +++ b/src/core/raster/qgssinglebandcolordatarenderer.h @@ -44,6 +44,8 @@ class CORE_EXPORT QgsSingleBandColorDataRenderer: public QgsRasterRenderer static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; bool setInput( QgsRasterInterface *input ) override; + int inputBand() const override; + bool setInputBand( int band ) override; QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; diff --git a/src/core/raster/qgssinglebandgrayrenderer.cpp b/src/core/raster/qgssinglebandgrayrenderer.cpp index fda76d2818cd..35c85d9fb7fc 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.cpp +++ b/src/core/raster/qgssinglebandgrayrenderer.cpp @@ -22,7 +22,6 @@ #include "qgscolorramplegendnodesettings.h" #include "qgsreadwritecontext.h" #include "qgscolorrampimpl.h" -#include "qgssymbol.h" #include #include @@ -149,7 +148,7 @@ QgsRasterBlock *QgsSingleBandGrayRenderer::block( int bandNo, const QgsRectangle double currentAlpha = mOpacity; if ( mRasterTransparency ) { - currentAlpha = mRasterTransparency->alphaValue( grayVal, mOpacity * 255 ) / 255.0; + currentAlpha *= mRasterTransparency->opacityForValue( grayVal ); } if ( mAlphaBand > 0 ) { @@ -193,6 +192,31 @@ QgsRasterBlock *QgsSingleBandGrayRenderer::block( int bandNo, const QgsRectangle return outputBlock.release(); } +void QgsSingleBandGrayRenderer::setGrayBand( int band ) +{ + setInputBand( band ); +} + +int QgsSingleBandGrayRenderer::inputBand() const +{ + return mGrayBand; +} + +bool QgsSingleBandGrayRenderer::setInputBand( int band ) +{ + if ( !mInput ) + { + mGrayBand = band; + return true; + } + else if ( band > 0 && band <= mInput->bandCount() ) + { + mGrayBand = band; + return true; + } + return false; +} + void QgsSingleBandGrayRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const { if ( parentElem.isNull() ) @@ -314,7 +338,7 @@ void QgsSingleBandGrayRenderer::toSld( QDomDocument &doc, QDomElement &element, // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( grayBand() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mGrayBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // set ContrastEnhancement @@ -333,7 +357,7 @@ void QgsSingleBandGrayRenderer::toSld( QDomDocument &doc, QDomElement &element, case QgsContrastEnhancement::ClipToMinimumMaximum: { // with this renderer export have to be check against real min/max values of the raster - const QgsRasterBandStats myRasterBandStats = mInput->bandStatistics( grayBand(), Qgis::RasterBandStatistic::Min | Qgis::RasterBandStatistic::Max ); + const QgsRasterBandStats myRasterBandStats = mInput->bandStatistics( mGrayBand, Qgis::RasterBandStatistic::Min | Qgis::RasterBandStatistic::Max ); // if minimum range differ from the real minimum => set is in exported SLD vendor option if ( !qgsDoubleNear( lContrastEnhancement->minimumValue(), myRasterBandStats.minimumValue ) ) diff --git a/src/core/raster/qgssinglebandgrayrenderer.h b/src/core/raster/qgssinglebandgrayrenderer.h index 99fd1ba1416d..60a92bf46a95 100644 --- a/src/core/raster/qgssinglebandgrayrenderer.h +++ b/src/core/raster/qgssinglebandgrayrenderer.h @@ -55,8 +55,19 @@ class CORE_EXPORT QgsSingleBandGrayRenderer: public QgsRasterRenderer QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; - int grayBand() const { return mGrayBand; } - void setGrayBand( int band ) { mGrayBand = band; } + /** + * \deprecated since QGIS 3.38 use inputBand() instead + */ + Q_DECL_DEPRECATED int grayBand() const SIP_DEPRECATED { return mGrayBand; } + + /** + * \deprecated since QGIS 3.38 use setInputBand() instead + */ + Q_DECL_DEPRECATED void setGrayBand( int band ) SIP_DEPRECATED; + + int inputBand() const override; + bool setInputBand( int band ) override; + const QgsContrastEnhancement *contrastEnhancement() const { return mContrastEnhancement.get(); } //! Takes ownership void setContrastEnhancement( QgsContrastEnhancement *ce SIP_TRANSFER ); diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp index 03e9395b25eb..bbccbd2ff325 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.cpp +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.cpp @@ -20,7 +20,6 @@ #include "qgscolorrampshader.h" #include "qgsrastershader.h" #include "qgsrastertransparency.h" -#include "qgsrasterviewport.h" #include "qgsstyleentityvisitor.h" #include "qgscolorramplegendnode.h" @@ -38,17 +37,28 @@ QgsSingleBandPseudoColorRenderer::QgsSingleBandPseudoColorRenderer( QgsRasterInt } void QgsSingleBandPseudoColorRenderer::setBand( int bandNo ) +{ + setInputBand( bandNo ); +} + +int QgsSingleBandPseudoColorRenderer::inputBand() const +{ + return mBand; +} + +bool QgsSingleBandPseudoColorRenderer::setInputBand( int band ) { if ( !mInput ) { - mBand = bandNo; - return; + mBand = band; + return true; } - - if ( bandNo <= mInput->bandCount() || bandNo > 0 ) + else if ( band > 0 && band <= mInput->bandCount() ) { - mBand = bandNo; + mBand = band; + return true; } + return false; } void QgsSingleBandPseudoColorRenderer::setClassificationMin( double min ) @@ -112,13 +122,13 @@ void QgsSingleBandPseudoColorRenderer::setShader( QgsRasterShader *shader ) void QgsSingleBandPseudoColorRenderer::createShader( QgsColorRamp *colorRamp, QgsColorRampShader::Type colorRampType, QgsColorRampShader::ClassificationMode classificationMode, int classes, bool clip, const QgsRectangle &extent ) { - if ( band() == -1 || classificationMin() >= classificationMax() ) + if ( mBand == -1 || classificationMin() >= classificationMax() ) { return; } QgsColorRampShader *colorRampShader = new QgsColorRampShader( classificationMin(), classificationMax(), colorRamp, colorRampType, classificationMode ); - colorRampShader->classifyColorRamp( classes, band(), extent, input() ); + colorRampShader->classifyColorRamp( classes, mBand, extent, input() ); colorRampShader->setClip( clip ); QgsRasterShader *rasterShader = new QgsRasterShader(); @@ -276,7 +286,7 @@ QgsRasterBlock *QgsSingleBandPseudoColorRenderer::block( int bandNo, QgsRectangl double currentOpacity = mOpacity; if ( mRasterTransparency ) { - currentOpacity = mRasterTransparency->alphaValue( val, mOpacity * 255 ) / 255.0; + currentOpacity *= mRasterTransparency->opacityForValue( val ); } if ( mAlphaBand > 0 ) { @@ -366,7 +376,7 @@ void QgsSingleBandPseudoColorRenderer::toSld( QDomDocument &doc, QDomElement &el // set band QDomElement sourceChannelNameElem = doc.createElement( QStringLiteral( "sld:SourceChannelName" ) ); - sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( band() ) ) ); + sourceChannelNameElem.appendChild( doc.createTextNode( QString::number( mBand ) ) ); channelElem.appendChild( sourceChannelNameElem ); // add ColorMap tag @@ -482,3 +492,4 @@ bool QgsSingleBandPseudoColorRenderer::canCreateRasterAttributeTable() const { return true; } + diff --git a/src/core/raster/qgssinglebandpseudocolorrenderer.h b/src/core/raster/qgssinglebandpseudocolorrenderer.h index d6278b0f95bc..be3c2d60d193 100644 --- a/src/core/raster/qgssinglebandpseudocolorrenderer.h +++ b/src/core/raster/qgssinglebandpseudocolorrenderer.h @@ -87,14 +87,21 @@ class CORE_EXPORT QgsSingleBandPseudoColorRenderer: public QgsRasterRenderer /** * Returns the band used by the renderer + * + * \deprecated since QGIS 3.38 use inputBand() instead */ - int band() const { return mBand; } + Q_DECL_DEPRECATED int band() const SIP_DEPRECATED { return mBand; } /** * Sets the band used by the renderer. * \see band + * + * \deprecated since QGIS 3.38 use setInputBand() instead */ - void setBand( int bandNo ); + Q_DECL_DEPRECATED void setBand( int bandNo ) SIP_DEPRECATED; + + int inputBand() const override; + bool setInputBand( int band ) override; double classificationMin() const { return mClassificationMin; } double classificationMax() const { return mClassificationMax; } diff --git a/src/core/sensor/qgsiodevicesensor.cpp b/src/core/sensor/qgsiodevicesensor.cpp index 622b48d0e031..b12a66c7a0bb 100644 --- a/src/core/sensor/qgsiodevicesensor.cpp +++ b/src/core/sensor/qgsiodevicesensor.cpp @@ -366,11 +366,62 @@ void QgsSerialPortSensor::setBaudRate( const QSerialPort::BaudRate &baudRate ) mBaudRate = baudRate; } +QByteArray QgsSerialPortSensor::delimiter() const +{ + return mDelimiter; +} + +void QgsSerialPortSensor::setDelimiter( const QByteArray &delimiter ) +{ + if ( mDelimiter == delimiter ) + return; + + mDelimiter = delimiter; +} + + +void QgsSerialPortSensor::parseData() +{ + if ( !mDelimiter.isEmpty() ) + { + if ( mFirstDelimiterHit ) + { + mDataBuffer += mSerialPort->readAll(); + const auto lastIndex = mDataBuffer.lastIndexOf( mDelimiter ); + if ( lastIndex > -1 ) + { + QgsAbstractSensor::SensorData data; + data.lastValue = mDataBuffer.mid( 0, lastIndex ); + mDataBuffer = mDataBuffer.mid( lastIndex + mDelimiter.size() ); + data.lastTimestamp = QDateTime::currentDateTime(); + setData( data ); + } + } + else + { + QByteArray data = mSerialPort->readAll(); + const auto lastIndex = data.lastIndexOf( mDelimiter ); + if ( lastIndex > -1 ) + { + mFirstDelimiterHit = true; + mDataBuffer = data.mid( lastIndex + mDelimiter.size() ); + } + } + } + else + { + QgsAbstractSensor::SensorData data; + data.lastValue = mSerialPort->readAll(); + data.lastTimestamp = QDateTime::currentDateTime(); + setData( data ); + } +} void QgsSerialPortSensor::handleConnect() { mSerialPort->setPortName( mPortName ); mSerialPort->setBaudRate( mBaudRate ); + mFirstDelimiterHit = false; if ( mSerialPort->open( QIODevice::ReadOnly ) ) { @@ -417,6 +468,7 @@ bool QgsSerialPortSensor::writePropertiesToElement( QDomElement &element, QDomDo { element.setAttribute( QStringLiteral( "portName" ), mPortName ); element.setAttribute( QStringLiteral( "baudRate" ), static_cast( mBaudRate ) ); + element.setAttribute( QStringLiteral( "delimiter" ), QString( mDelimiter ) ); return true; } @@ -424,6 +476,7 @@ bool QgsSerialPortSensor::readPropertiesFromElement( const QDomElement &element, { mPortName = element.attribute( QStringLiteral( "portName" ) ); mBaudRate = static_cast< QSerialPort::BaudRate >( element.attribute( QStringLiteral( "baudRate" ) ).toInt() ); + mDelimiter = element.attribute( QStringLiteral( "delimiter" ) ).toLocal8Bit(); return true; } #endif diff --git a/src/core/sensor/qgsiodevicesensor.h b/src/core/sensor/qgsiodevicesensor.h index 3fee43d5e7e0..61fa6f32a453 100644 --- a/src/core/sensor/qgsiodevicesensor.h +++ b/src/core/sensor/qgsiodevicesensor.h @@ -274,6 +274,22 @@ class CORE_EXPORT QgsSerialPortSensor : public QgsIODeviceSensor */ void setBaudRate( const QSerialPort::BaudRate &baudRate ); + /** + * Returns the current delimiter used to separate data frames. If empty, + * each serial port data update will be considered a data frame. + * \since QGIS 3.38 + */ + QByteArray delimiter() const; + + /** + * Sets the delimiter used to identify data frames out of the data received + * from the serial port. If empty, each serial port data update will be + * considered a data frame. + * \param delimiter Character used to identify data frames + * \since QGIS 3.38 + */ + void setDelimiter( const QByteArray &delimiter ); + bool writePropertiesToElement( QDomElement &element, QDomDocument &document ) const override; bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document ) override; @@ -282,6 +298,10 @@ class CORE_EXPORT QgsSerialPortSensor : public QgsIODeviceSensor void handleConnect() override; void handleDisconnect() override; + protected slots: + + void parseData() override; + private slots: void handleError( QSerialPort::SerialPortError error ); @@ -291,8 +311,10 @@ class CORE_EXPORT QgsSerialPortSensor : public QgsIODeviceSensor QSerialPort *mSerialPort = nullptr; QString mPortName; - - QSerialPort::BaudRate mBaudRate; + QSerialPort::BaudRate mBaudRate = QSerialPort::Baud9600; + QByteArray mDelimiter; + bool mFirstDelimiterHit = false; + QByteArray mDataBuffer; }; SIP_END diff --git a/src/core/settings/qgssettingsentry.cpp b/src/core/settings/qgssettingsentry.cpp index cae997a59baa..9244717966c4 100644 --- a/src/core/settings/qgssettingsentry.cpp +++ b/src/core/settings/qgssettingsentry.cpp @@ -235,21 +235,24 @@ QVariant QgsSettingsEntryBase::formerValueAsVariant( const QStringList &dynamicK bool QgsSettingsEntryBase::copyValueFromKey( const QString &key, const QStringList &dynamicKeyPartList, bool removeSettingAtKey ) const { - if ( exists( dynamicKeyPartList ) ) - return false; - auto settings = QgsSettings::get(); const QString oldCompleteKey = completeKeyPrivate( key, dynamicKeyPartList ); if ( settings->contains( oldCompleteKey ) ) { - QVariant oldValue = settings->value( oldCompleteKey, mDefaultValue ); - setVariantValue( oldValue, dynamicKeyPartList ); + if ( !exists( dynamicKeyPartList ) ) + { + QVariant oldValue = settings->value( oldCompleteKey, mDefaultValue ); + // do not copy if it is equal to the default value + if ( oldValue != defaultValueAsVariant() ) + setVariantValue( oldValue, dynamicKeyPartList ); + } if ( removeSettingAtKey ) settings->remove( oldCompleteKey ); return true; } + return false; } diff --git a/src/core/settings/qgssettingsentryimpl.cpp b/src/core/settings/qgssettingsentryimpl.cpp index fe30f79d8426..59b261044617 100644 --- a/src/core/settings/qgssettingsentryimpl.cpp +++ b/src/core/settings/qgssettingsentryimpl.cpp @@ -101,7 +101,7 @@ bool QgsSettingsEntryInteger::checkValuePrivate( const int &value ) const if ( value > mMaxValue ) { - QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greather than maximum value '%2'." ) + QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greater than maximum value '%2'." ) .arg( QString::number( value ) ) .arg( QString::number( mMaxValue ) ) ); return false; @@ -142,7 +142,7 @@ bool QgsSettingsEntryInteger64::checkValuePrivate( const qlonglong &value ) cons if ( value > mMaxValue ) { - QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greather than maximum value '%2'." ) + QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greater than maximum value '%2'." ) .arg( QString::number( value ) ) .arg( QString::number( mMaxValue ) ) ); return false; @@ -184,7 +184,7 @@ bool QgsSettingsEntryDouble::checkValuePrivate( const double &value ) const if ( value > mMaxValue ) { - QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greather than maximum value '%2'." ) + QgsDebugError( QObject::tr( "Can't set value for setting. Value '%1' is greater than maximum value '%2'." ) .arg( QString::number( value ), QString::number( mMaxValue ) ) ); return false; } diff --git a/src/core/settings/qgssettingsregistrycore.cpp b/src/core/settings/qgssettingsregistrycore.cpp index 51b16f3009ac..2a59bcfad1d3 100644 --- a/src/core/settings/qgssettingsregistrycore.cpp +++ b/src/core/settings/qgssettingsregistrycore.cpp @@ -276,9 +276,9 @@ void QgsSettingsRegistryCore::migrateOldSettings() settings.endGroup(); Q_NOWARN_DEPRECATED_POP - QgsOwsConnection::settingsUsername->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/username" ), {service, connection}, true ); - QgsOwsConnection::settingsPassword->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/password" ), {service, connection}, true ); - QgsOwsConnection::settingsAuthCfg->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/authcfg" ), {service, connection}, true ); + QgsOwsConnection::settingsUsername->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/username" ).arg( service, connection ), {service.toLower(), connection}, true ); + QgsOwsConnection::settingsPassword->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/password" ).arg( service, connection ), {service.toLower(), connection}, true ); + QgsOwsConnection::settingsAuthCfg->copyValueFromKey( QStringLiteral( "qgis/connections/%1/%2/authcfg" ).arg( service, connection ), {service.toLower(), connection}, true ); } if ( settings.contains( QStringLiteral( "selected" ) ) ) QgsOwsConnection::sTreeOwsConnections->setSelectedItem( settings.value( QStringLiteral( "selected" ) ).toString(), {service.toLower()} ); @@ -493,9 +493,9 @@ void QgsSettingsRegistryCore::backwardCompatibility() Q_NOWARN_DEPRECATED_POP } - QgsOwsConnection::settingsUsername->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/username" ), {service, connection} ); - QgsOwsConnection::settingsPassword->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/password" ), {service, connection} ); - QgsOwsConnection::settingsAuthCfg->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/authcfg" ), {service, connection} ); + QgsOwsConnection::settingsUsername->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/username" ).arg( service, connection ), {service.toLower(), connection} ); + QgsOwsConnection::settingsPassword->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/password" ).arg( service, connection ), {service.toLower(), connection} ); + QgsOwsConnection::settingsAuthCfg->copyValueToKey( QStringLiteral( "qgis/connections/%1/%2/authcfg" ).arg( service, connection ), {service.toLower(), connection} ); } } } diff --git a/src/core/symbology/qgscategorizedsymbolrenderer.cpp b/src/core/symbology/qgscategorizedsymbolrenderer.cpp index 1a9388328d05..2d4e2f07da2b 100644 --- a/src/core/symbology/qgscategorizedsymbolrenderer.cpp +++ b/src/core/symbology/qgscategorizedsymbolrenderer.cpp @@ -740,9 +740,9 @@ QgsFeatureRenderer *QgsCategorizedSymbolRenderer::create( QDomElement &element, } else if ( valueType == QLatin1String( "bool" ) ) { - if ( value.toLower() == QStringLiteral( "false" ) ) + if ( value.toLower() == QLatin1String( "false" ) ) return false; - if ( value.toLower() == QStringLiteral( "true" ) ) + if ( value.toLower() == QLatin1String( "true" ) ) return true; } else if ( valueType == QLatin1String( "NULL" ) ) diff --git a/src/core/symbology/qgsfillsymbollayer.h b/src/core/symbology/qgsfillsymbollayer.h index 4e33817da86e..5dc3a697c2da 100644 --- a/src/core/symbology/qgsfillsymbollayer.h +++ b/src/core/symbology/qgsfillsymbollayer.h @@ -766,7 +766,7 @@ class CORE_EXPORT QgsImageFillSymbolLayer: public QgsFillSymbolLayer SIP_ABSTRAC void renderPolygon( const QPolygonF &points, const QVector *rings, QgsSymbolRenderContext &context ) override; /** - * Sets the \a units fo the symbol's stroke width. + * Sets the \a units for the symbol's stroke width. * \see strokeWidthUnit() * \see setStrokeWidthMapUnitScale() */ diff --git a/src/core/symbology/qgssvgcache.cpp b/src/core/symbology/qgssvgcache.cpp index f8823a19c6be..4b96f9dbe108 100644 --- a/src/core/symbology/qgssvgcache.cpp +++ b/src/core/symbology/qgssvgcache.cpp @@ -22,6 +22,7 @@ #include "qgsmessagelog.h" #include "qgssymbollayerutils.h" #include "qgsnetworkcontentfetchertask.h" +#include "qgsabstractcontentcache_p.h" #include #include @@ -855,3 +856,4 @@ QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) cons return image; } +template class QgsAbstractContentCache; // clazy:exclude=missing-qobject-macro diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index a667ce287645..7616af8fb6e0 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -943,7 +943,7 @@ void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext const bool prevForceVector = context->forceVectorOutput(); context->setForceVectorOutput( true ); - const double opacity = expressionContext ? dataDefinedProperties().valueAsDouble( QgsSymbol::Property::Opacity, *expressionContext, mOpacity ) : mOpacity; + const double opacity = expressionContext ? dataDefinedProperties().valueAsDouble( QgsSymbol::Property::Opacity, *expressionContext, mOpacity * 100 ) * 0.01 : mOpacity; QgsSymbolRenderContext symbolContext( *context, Qgis::RenderUnit::Unknown, opacity, false, mRenderHints, nullptr ); symbolContext.setSelected( selected ); diff --git a/src/core/tiledscene/qgscesiumtilesdataprovider.cpp b/src/core/tiledscene/qgscesiumtilesdataprovider.cpp index 1f043a5a962c..ade6dd2c110d 100644 --- a/src/core/tiledscene/qgscesiumtilesdataprovider.cpp +++ b/src/core/tiledscene/qgscesiumtilesdataprovider.cpp @@ -22,6 +22,7 @@ #include "qgsprovidersublayerdetails.h" #include "qgsthreadingutils.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgscesiumutils.h" #include "qgssphere.h" @@ -914,6 +915,11 @@ QgsCesiumTilesDataProvider::QgsCesiumTilesDataProvider( const QgsCesiumTilesData mShared = other.mShared; } +Qgis::DataProviderFlags QgsCesiumTilesDataProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + Qgis::TiledSceneProviderCapabilities QgsCesiumTilesDataProvider::capabilities() const { return Qgis::TiledSceneProviderCapability::ReadLayerMetadata; diff --git a/src/core/tiledscene/qgscesiumtilesdataprovider.h b/src/core/tiledscene/qgscesiumtilesdataprovider.h index 4fbd56294a2f..7cf50d658e14 100644 --- a/src/core/tiledscene/qgscesiumtilesdataprovider.h +++ b/src/core/tiledscene/qgscesiumtilesdataprovider.h @@ -46,6 +46,7 @@ class CORE_EXPORT QgsCesiumTilesDataProvider final: public QgsTiledSceneDataProv QgsCesiumTilesDataProvider &operator=( const QgsCesiumTilesDataProvider &other ) = delete; ~QgsCesiumTilesDataProvider() final; + Qgis::DataProviderFlags flags() const override; Qgis::TiledSceneProviderCapabilities capabilities() const final; QgsCesiumTilesDataProvider *clone() const final; QgsCoordinateReferenceSystem crs() const final; diff --git a/src/core/vector/qgsvectorlayer.cpp b/src/core/vector/qgsvectorlayer.cpp index 7487d63ce530..eac50e31e0ef 100644 --- a/src/core/vector/qgsvectorlayer.cpp +++ b/src/core/vector/qgsvectorlayer.cpp @@ -1032,22 +1032,34 @@ QgsRectangle QgsVectorLayer::extent() const if ( !isSpatial() ) return rect; - if ( !mValidExtent2D && mLazyExtent2D && mReadExtentFromXml && !mXmlExtent2D.isNull() ) + if ( mDataProvider && mDataProvider->isValid() && ( mDataProvider->flags() & Qgis::DataProviderFlag::FastExtent2D ) ) { - updateExtent( mXmlExtent2D ); + // Provider has a trivial 2D extent calculation => always get extent from provider. + // Things are nice and simple this way, e.g. we can always trust that this extent is + // accurate and up to date. + updateExtent( mDataProvider->extent() ); mValidExtent2D = true; mLazyExtent2D = false; } - - if ( !mValidExtent2D && mLazyExtent2D && mDataProvider && mDataProvider->isValid() ) + else { - // store the extent - updateExtent( mDataProvider->extent() ); - mValidExtent2D = true; - mLazyExtent2D = false; + if ( !mValidExtent2D && mLazyExtent2D && mReadExtentFromXml && !mXmlExtent2D.isNull() ) + { + updateExtent( mXmlExtent2D ); + mValidExtent2D = true; + mLazyExtent2D = false; + } - // show the extent - QgsDebugMsgLevel( QStringLiteral( "2D Extent of layer: %1" ).arg( mExtent2D.toString() ), 3 ); + if ( !mValidExtent2D && mLazyExtent2D && mDataProvider && mDataProvider->isValid() ) + { + // store the extent + updateExtent( mDataProvider->extent() ); + mValidExtent2D = true; + mLazyExtent2D = false; + + // show the extent + QgsDebugMsgLevel( QStringLiteral( "2D Extent of layer: %1" ).arg( mExtent2D.toString() ), 3 ); + } } if ( mValidExtent2D ) @@ -1133,22 +1145,34 @@ QgsBox3D QgsVectorLayer:: extent3D() const if ( !isSpatial() ) return extent; - if ( !mValidExtent3D && mLazyExtent3D && mReadExtentFromXml && !mXmlExtent3D.isNull() ) + if ( mDataProvider && mDataProvider->isValid() && ( mDataProvider->flags() & Qgis::DataProviderFlag::FastExtent3D ) ) { - updateExtent( mXmlExtent3D ); + // Provider has a trivial 3D extent calculation => always get extent from provider. + // Things are nice and simple this way, e.g. we can always trust that this extent is + // accurate and up to date. + updateExtent( mDataProvider->extent3D() ); mValidExtent3D = true; mLazyExtent3D = false; } - - if ( !mValidExtent3D && mLazyExtent3D && mDataProvider && mDataProvider->isValid() ) + else { - // store the extent - updateExtent( mDataProvider->extent3D() ); - mValidExtent3D = true; - mLazyExtent3D = false; + if ( !mValidExtent3D && mLazyExtent3D && mReadExtentFromXml && !mXmlExtent3D.isNull() ) + { + updateExtent( mXmlExtent3D ); + mValidExtent3D = true; + mLazyExtent3D = false; + } - // show the extent - QgsDebugMsgLevel( QStringLiteral( "3D Extent of layer: %1" ).arg( mExtent3D.toString() ), 3 ); + if ( !mValidExtent3D && mLazyExtent3D && mDataProvider && mDataProvider->isValid() ) + { + // store the extent + updateExtent( mDataProvider->extent3D() ); + mValidExtent3D = true; + mLazyExtent3D = false; + + // show the extent + QgsDebugMsgLevel( QStringLiteral( "3D Extent of layer: %1" ).arg( mExtent3D.toString() ), 3 ); + } } if ( mValidExtent3D ) diff --git a/src/core/vector/qgsvectorlayereditbuffergroup.cpp b/src/core/vector/qgsvectorlayereditbuffergroup.cpp index 297ab35892cb..7ddb6593d816 100644 --- a/src/core/vector/qgsvectorlayereditbuffergroup.cpp +++ b/src/core/vector/qgsvectorlayereditbuffergroup.cpp @@ -172,7 +172,7 @@ bool QgsVectorLayerEditBufferGroup::commitChanges( QStringList &commitErrors, bo break; } - // Order layers childrens to parents + // Order layers children to parents const QList orderedLayers = orderLayersParentsToChildren( constModifiedLayers ); QList::const_iterator orderedLayersIterator; diff --git a/src/core/vector/qgsvectorlayereditutils.cpp b/src/core/vector/qgsvectorlayereditutils.cpp index c8d83e07a25f..bd1a45a1e26b 100644 --- a/src/core/vector/qgsvectorlayereditutils.cpp +++ b/src/core/vector/qgsvectorlayereditutils.cpp @@ -787,7 +787,7 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPoint &p ) if ( qgsDoubleNear( threshold, 0.0 ) ) { - threshold = 0.0000001; + threshold = 1e-8; if ( mLayer->crs().mapUnits() == Qgis::DistanceUnit::Meters ) { @@ -799,9 +799,8 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPoint &p ) } } - QgsRectangle searchRect( p.x() - threshold, p.y() - threshold, - p.x() + threshold, p.y() + threshold ); - double sqrSnappingTolerance = threshold * threshold; + QgsRectangle searchRect( p, p, false ); + searchRect.grow( threshold ); QgsFeature f; QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest() @@ -809,45 +808,14 @@ int QgsVectorLayerEditUtils::addTopologicalPoints( const QgsPoint &p ) .setFlags( Qgis::FeatureRequestFlag::ExactIntersect ) .setNoAttributes() ); - QMap features; - QMap segments; - - while ( fit.nextFeature( f ) ) - { - int afterVertex; - QgsPointXY snappedPoint; - double sqrDistSegmentSnap = f.geometry().closestSegmentWithContext( p, snappedPoint, afterVertex, nullptr, segmentSearchEpsilon ); - if ( sqrDistSegmentSnap < sqrSnappingTolerance ) - { - segments[f.id()] = afterVertex; - features[f.id()] = f.geometry(); - } - } - - if ( segments.isEmpty() ) - return 2; - bool pointsAdded = false; - for ( QMap::const_iterator it = segments.constBegin(); it != segments.constEnd(); ++it ) + while ( fit.nextFeature( f ) ) { - QgsFeatureId fid = it.key(); - int segmentAfterVertex = it.value(); - QgsGeometry geom = features[fid]; - - int atVertex, beforeVertex, afterVertex; - double sqrDistVertexSnap; - geom.closestVertex( p, atVertex, beforeVertex, afterVertex, sqrDistVertexSnap ); - - if ( sqrDistVertexSnap < sqrSnappingTolerance ) - continue; // the vertex already exists - do not insert it - - if ( !mLayer->insertVertex( p, fid, segmentAfterVertex ) ) - { - QgsDebugError( QStringLiteral( "failed to insert topo point" ) ); - } - else + QgsGeometry geom = f.geometry(); + if ( geom.addTopologicalPoint( p, threshold, segmentSearchEpsilon ) ) { pointsAdded = true; + mLayer->changeGeometry( f.id(), geom ); } } diff --git a/src/core/vector/qgsvectorlayerelevationproperties.cpp b/src/core/vector/qgsvectorlayerelevationproperties.cpp index 592afa7b70b6..3d2eee7e3bd0 100644 --- a/src/core/vector/qgsvectorlayerelevationproperties.cpp +++ b/src/core/vector/qgsvectorlayerelevationproperties.cpp @@ -232,7 +232,7 @@ QString QgsVectorLayerElevationProperties::htmlSummary() const return QStringLiteral( "
  • %1
  • " ).arg( properties.join( QLatin1String( "
  • " ) ) ); } -bool QgsVectorLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange & ) const +bool QgsVectorLayerElevationProperties::isVisibleInZRange( const QgsDoubleRange &, QgsMapLayer * ) const { // TODO -- test actual layer z range return true; diff --git a/src/core/vector/qgsvectorlayerelevationproperties.h b/src/core/vector/qgsvectorlayerelevationproperties.h index 823c654fc60a..7a021e1582ff 100644 --- a/src/core/vector/qgsvectorlayerelevationproperties.h +++ b/src/core/vector/qgsvectorlayerelevationproperties.h @@ -54,7 +54,7 @@ class CORE_EXPORT QgsVectorLayerElevationProperties : public QgsMapLayerElevatio void setDefaultsFromLayer( QgsMapLayer *layer ) override; QgsVectorLayerElevationProperties *clone() const override SIP_FACTORY; QString htmlSummary() const override; - bool isVisibleInZRange( const QgsDoubleRange &range ) const override; + bool isVisibleInZRange( const QgsDoubleRange &range, QgsMapLayer *layer = nullptr ) const override; QgsDoubleRange calculateZRange( QgsMapLayer *layer ) const override; bool showByDefaultInElevationProfilePlots() const override; @@ -292,7 +292,7 @@ class CORE_EXPORT QgsVectorLayerElevationProperties : public QgsMapLayerElevatio bool showMarkerSymbolInSurfacePlots() const { return mShowMarkerSymbolInSurfacePlots; } /** - * Sets whehter the marker symbol should also be shown in continuous surface plots. + * Sets whether the marker symbol should also be shown in continuous surface plots. * * \note This setting is only used when type() is Qgis::VectorProfileType::ContinuousSurface. * diff --git a/src/core/vector/qgsvectorlayerfeatureiterator.cpp b/src/core/vector/qgsvectorlayerfeatureiterator.cpp index 8379a97172e7..6bb9989a019a 100644 --- a/src/core/vector/qgsvectorlayerfeatureiterator.cpp +++ b/src/core/vector/qgsvectorlayerfeatureiterator.cpp @@ -622,7 +622,7 @@ void QgsVectorLayerFeatureIterator::setInterruptionChecker( QgsFeedback *interru bool QgsVectorLayerFeatureIterator::isValid() const { - return mProviderIterator.isValid(); + return mChangedFeaturesIterator.isValid() || mProviderIterator.isValid(); } bool QgsVectorLayerFeatureIterator::fetchNextAddedFeature( QgsFeature &f ) diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.cpp b/src/core/vector/qgsvectorlayerprofilegenerator.cpp index 524e2ee4c57d..5ab958abccd5 100644 --- a/src/core/vector/qgsvectorlayerprofilegenerator.cpp +++ b/src/core/vector/qgsvectorlayerprofilegenerator.cpp @@ -308,7 +308,7 @@ void QgsVectorLayerProfileResults::visitFeaturesAtPoint( const QgsProfilePoint & const double snappedDistance = point.distance() < partBounds.xMinimum() ? partBounds.xMinimum() : point.distance() > partBounds.xMaximum() ? partBounds.xMaximum() : point.distance(); - const QgsGeometry cutLine( new QgsLineString( QgsPoint( snappedDistance, minZ ), QgsPoint( snappedDistance, maxZ ) ) ); + const QgsGeometry cutLine( new QgsLineString( QgsPoint( snappedDistance, qgsDoubleNear( minZ, maxZ ) ? minZ - 1 : minZ ), QgsPoint( snappedDistance, maxZ ) ) ); QgsGeos cutLineGeos( cutLine.constGet() ); const QgsGeometry points( cutLineGeos.intersection( exterior ) ); @@ -750,6 +750,18 @@ bool QgsVectorLayerProfileGenerator::generateProfile( const QgsProfileGeneration mProfileCurveEngine.reset( new QgsGeos( mProfileCurve.get() ) ); mProfileCurveEngine->prepareGeometry(); + if ( mTolerance == 0.0 ) // geos does not handle very well buffer with 0 size + { + mProfileBufferedCurve = std::unique_ptr( mProfileCurve->clone() ); + } + else + { + mProfileBufferedCurve = std::unique_ptr( mProfileCurveEngine->buffer( mTolerance, 8, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Round, 2 ) ); + } + + mProfileBufferedCurveEngine.reset( new QgsGeos( mProfileBufferedCurve.get() ) ); + mProfileBufferedCurveEngine->prepareGeometry(); + mDataDefinedProperties.prepare( mExpressionContext ); if ( mFeedback->isCanceled() ) @@ -802,70 +814,155 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPoints() // our feature request is using the optimised distance within check (allowing use of spatial index) // BUT this will also include points which are within the tolerance distance before/after the end of line. // So we also need to double check that they fall within the flat buffered curve too. - std::unique_ptr< QgsAbstractGeometry > bufferedCurve( mProfileCurveEngine->buffer( mTolerance, 8, Qgis::EndCapStyle::Flat, Qgis::JoinStyle::Round, 2 ) ); - QgsGeos bufferedCurveEngine( bufferedCurve.get() ); - bufferedCurveEngine.prepareGeometry(); - auto processPoint = [this, &bufferedCurveEngine]( const QgsFeature & feature, const QgsPoint * point ) + QgsFeature feature; + QgsFeatureIterator it = mSource->getFeatures( request ); + while ( !mFeedback->isCanceled() && it.nextFeature( feature ) ) { - if ( !bufferedCurveEngine.intersects( point ) ) - return; + mExpressionContext.setFeature( feature ); - const double offset = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ZOffset, mExpressionContext, mOffset ); + const QgsGeometry g = feature.geometry(); + for ( auto it = g.const_parts_begin(); !mFeedback->isCanceled() && it != g.const_parts_end(); ++it ) + { + if ( mProfileBufferedCurveEngine->intersects( *it ) ) + { + processIntersectionPoint( qgsgeometry_cast< const QgsPoint * >( *it ), feature ); + } + } + } + return !mFeedback->isCanceled(); +} + +void QgsVectorLayerProfileGenerator::processIntersectionPoint( const QgsPoint *point, const QgsFeature &feature ) +{ + QString error; + const double offset = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ZOffset, mExpressionContext, mOffset ); + + const double height = featureZToHeight( point->x(), point->y(), point->z(), offset ); + mResults->mRawPoints.append( QgsPoint( point->x(), point->y(), height ) ); + mResults->minZ = std::min( mResults->minZ, height ); + mResults->maxZ = std::max( mResults->maxZ, height ); + + const double distanceAlongProfileCurve = mProfileCurveEngine->lineLocatePoint( *point, &error ); + mResults->mDistanceToHeightMap.insert( distanceAlongProfileCurve, height ); + + QgsVectorLayerProfileResults::Feature resultFeature; + resultFeature.featureId = feature.id(); + if ( mExtrusionEnabled ) + { + const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); + + resultFeature.geometry = QgsGeometry( new QgsLineString( QgsPoint( point->x(), point->y(), height ), + QgsPoint( point->x(), point->y(), height + extrusion ) ) ); + resultFeature.crossSectionGeometry = QgsGeometry( new QgsLineString( QgsPoint( distanceAlongProfileCurve, height ), + QgsPoint( distanceAlongProfileCurve, height + extrusion ) ) ); + mResults->minZ = std::min( mResults->minZ, height + extrusion ); + mResults->maxZ = std::max( mResults->maxZ, height + extrusion ); + } + else + { + resultFeature.geometry = QgsGeometry( new QgsPoint( point->x(), point->y(), height ) ); + resultFeature.crossSectionGeometry = QgsGeometry( new QgsPoint( distanceAlongProfileCurve, height ) ); + } + + mResults->features[resultFeature.featureId].append( resultFeature ); +} + +void QgsVectorLayerProfileGenerator::processIntersectionCurve( const QgsLineString *intersectionCurve, const QgsFeature &feature ) +{ + QString error; + + QgsVectorLayerProfileResults::Feature resultFeature; + resultFeature.featureId = feature.id(); + double maxDistanceAlongProfileCurve = std::numeric_limits::lowest(); + + const double offset = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ZOffset, mExpressionContext, mOffset ); + const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); + + const int numPoints = intersectionCurve->numPoints(); + QVector< double > newX( numPoints ); + QVector< double > newY( numPoints ); + QVector< double > newZ( numPoints ); + QVector< double > newDistance( numPoints ); + + const double *inX = intersectionCurve->xData(); + const double *inY = intersectionCurve->yData(); + const double *inZ = intersectionCurve->is3D() ? intersectionCurve->zData() : nullptr; + double *outX = newX.data(); + double *outY = newY.data(); + double *outZ = newZ.data(); + double *outDistance = newDistance.data(); + + QVector< double > extrudedZ; + double *extZOut = nullptr; + if ( mExtrusionEnabled ) + { + extrudedZ.resize( numPoints ); + extZOut = extrudedZ.data(); + } + + for ( int i = 0 ; ! mFeedback->isCanceled() && i < numPoints; ++i ) + { + QgsPoint intersectionPoint( *inX, *inY, ( inZ ? *inZ : std::numeric_limits::quiet_NaN() ) ); + + const double height = featureZToHeight( intersectionPoint.x(), intersectionPoint.y(), intersectionPoint.z(), offset ); + const double distanceAlongProfileCurve = mProfileCurveEngine->lineLocatePoint( intersectionPoint, &error ); - const double height = featureZToHeight( point->x(), point->y(), point->z(), offset ); - mResults->mRawPoints.append( QgsPoint( point->x(), point->y(), height ) ); + maxDistanceAlongProfileCurve = std::max( maxDistanceAlongProfileCurve, distanceAlongProfileCurve ); + + mResults->mRawPoints.append( QgsPoint( intersectionPoint.x(), intersectionPoint.y(), height ) ); mResults->minZ = std::min( mResults->minZ, height ); mResults->maxZ = std::max( mResults->maxZ, height ); - QString lastError; - const double distance = mProfileCurveEngine->lineLocatePoint( *point, &lastError ); - mResults->mDistanceToHeightMap.insert( distance, height ); + mResults->mDistanceToHeightMap.insert( distanceAlongProfileCurve, height ); + *outDistance++ = distanceAlongProfileCurve; + + *outX++ = intersectionPoint.x(); + *outY++ = intersectionPoint.y(); + *outZ++ = height; + if ( extZOut ) + *extZOut++ = height + extrusion; - QgsVectorLayerProfileResults::Feature resultFeature; - resultFeature.featureId = feature.id(); if ( mExtrusionEnabled ) { - const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); - - resultFeature.geometry = QgsGeometry( new QgsLineString( QgsPoint( point->x(), point->y(), height ), - QgsPoint( point->x(), point->y(), height + extrusion ) ) ); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsLineString( QgsPoint( distance, height ), - QgsPoint( distance, height + extrusion ) ) ); mResults->minZ = std::min( mResults->minZ, height + extrusion ); mResults->maxZ = std::max( mResults->maxZ, height + extrusion ); } - else - { - resultFeature.geometry = QgsGeometry( new QgsPoint( point->x(), point->y(), height ) ); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsPoint( distance, height ) ); - } - mResults->features[resultFeature.featureId].append( resultFeature ); - }; + inX++; + inY++; + if ( inZ ) + inZ++; + } - QgsFeature feature; - QgsFeatureIterator it = mSource->getFeatures( request ); - while ( it.nextFeature( feature ) ) - { - if ( mFeedback->isCanceled() ) - return false; + mResults->mDistanceToHeightMap.insert( maxDistanceAlongProfileCurve + 0.000001, std::numeric_limits::quiet_NaN() ); - mExpressionContext.setFeature( feature ); + if ( mFeedback->isCanceled() ) + return; - const QgsGeometry g = feature.geometry(); - if ( g.isMultipart() ) - { - for ( auto it = g.const_parts_begin(); it != g.const_parts_end(); ++it ) - { - processPoint( feature, qgsgeometry_cast< const QgsPoint * >( *it ) ); - } - } - else - { - processPoint( feature, qgsgeometry_cast< const QgsPoint * >( g.constGet() ) ); - } + // create geometries from vector data + if ( mExtrusionEnabled ) + { + std::unique_ptr< QgsLineString > ring = std::make_unique< QgsLineString >( newX, newY, newZ ); + std::unique_ptr< QgsLineString > extrudedRing = std::make_unique< QgsLineString >( newX, newY, extrudedZ ); + std::unique_ptr< QgsLineString > reversedExtrusion( extrudedRing->reversed() ); + ring->append( reversedExtrusion.get() ); + ring->close(); + resultFeature.geometry = QgsGeometry( new QgsPolygon( ring.release() ) ); + + std::unique_ptr< QgsLineString > distanceVHeightRing = std::make_unique< QgsLineString >( newDistance, newZ ); + std::unique_ptr< QgsLineString > extrudedDistanceVHeightRing = std::make_unique< QgsLineString >( newDistance, extrudedZ ); + std::unique_ptr< QgsLineString > reversedDistanceVHeightExtrusion( extrudedDistanceVHeightRing->reversed() ); + distanceVHeightRing->append( reversedDistanceVHeightExtrusion.get() ); + distanceVHeightRing->close(); + resultFeature.crossSectionGeometry = QgsGeometry( new QgsPolygon( distanceVHeightRing.release() ) ); } - return true; + else + { + resultFeature.geometry = QgsGeometry( new QgsLineString( newX, newY, newZ ) ) ; + resultFeature.crossSectionGeometry = QgsGeometry( new QgsLineString( newDistance, newZ ) ); + } + + mResults->features[resultFeature.featureId].append( resultFeature ); } bool QgsVectorLayerProfileGenerator::generateProfileForLines() @@ -873,352 +970,344 @@ bool QgsVectorLayerProfileGenerator::generateProfileForLines() // get features from layer QgsFeatureRequest request; request.setDestinationCrs( mTargetCrs, mTransformContext ); - request.setFilterRect( mProfileCurve->boundingBox() ); + if ( mTolerance > 0 ) + { + request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), mTolerance ); + } + else + { + request.setFilterRect( mProfileCurve->boundingBox() ); + } request.setSubsetOfAttributes( mDataDefinedProperties.referencedFields( mExpressionContext ), mFields ); request.setFeedback( mFeedback.get() ); - auto processCurve = [this]( const QgsFeature & feature, const QgsCurve * curve ) + auto processCurve = [this]( const QgsFeature & feature, const QgsCurve * featGeomPart ) { QString error; - std::unique_ptr< QgsAbstractGeometry > intersection( mProfileCurveEngine->intersection( curve, &error ) ); + std::unique_ptr< QgsAbstractGeometry > intersection( mProfileBufferedCurveEngine->intersection( featGeomPart, &error ) ); if ( !intersection ) return; if ( mFeedback->isCanceled() ) return; - QgsGeos curveGeos( curve ); - curveGeos.prepareGeometry(); - if ( mFeedback->isCanceled() ) - return; - - for ( auto it = intersection->const_parts_begin(); it != intersection->const_parts_end(); ++it ) + // Intersection is empty : GEOS issue for vertical intersection : use feature geometry as intersection + if ( intersection->isEmpty() ) { - if ( mFeedback->isCanceled() ) - return; + intersection.reset( featGeomPart->clone() ); + } + QgsGeos featGeomPartGeos( featGeomPart ); + featGeomPartGeos.prepareGeometry(); + + for ( auto it = intersection->const_parts_begin(); + !mFeedback->isCanceled() && it != intersection->const_parts_end(); + ++it ) + { if ( const QgsPoint *intersectionPoint = qgsgeometry_cast< const QgsPoint * >( *it ) ) { // unfortunately we need to do some work to interpolate the z value for the line -- GEOS doesn't give us this - const double distance = curveGeos.lineLocatePoint( *intersectionPoint, &error ); - std::unique_ptr< QgsPoint > interpolatedPoint( curve->interpolatePoint( distance ) ); - - const double offset = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ZOffset, mExpressionContext, mOffset ); + QString error; + const double distance = featGeomPartGeos.lineLocatePoint( *intersectionPoint, &error ); + std::unique_ptr< QgsPoint > interpolatedPoint( featGeomPart->interpolatePoint( distance ) ); - const double height = featureZToHeight( interpolatedPoint->x(), interpolatedPoint->y(), interpolatedPoint->z(), offset ); - mResults->mRawPoints.append( QgsPoint( interpolatedPoint->x(), interpolatedPoint->y(), height ) ); - mResults->minZ = std::min( mResults->minZ, height ); - mResults->maxZ = std::max( mResults->maxZ, height ); + processIntersectionPoint( interpolatedPoint.get(), feature ); + } + else if ( const QgsLineString *intersectionCurve = qgsgeometry_cast< const QgsLineString * >( *it ) ) + { + processIntersectionCurve( intersectionCurve, feature ); + } + } + }; - const double distanceAlongProfileCurve = mProfileCurveEngine->lineLocatePoint( *interpolatedPoint, &error ); - mResults->mDistanceToHeightMap.insert( distanceAlongProfileCurve, height ); + QgsFeature feature; + QgsFeatureIterator it = mSource->getFeatures( request ); + while ( !mFeedback->isCanceled() && it.nextFeature( feature ) ) + { + mExpressionContext.setFeature( feature ); - QgsVectorLayerProfileResults::Feature resultFeature; - resultFeature.featureId = feature.id(); - if ( mExtrusionEnabled ) - { - const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); - - resultFeature.geometry = QgsGeometry( new QgsLineString( QgsPoint( interpolatedPoint->x(), interpolatedPoint->y(), height ), - QgsPoint( interpolatedPoint->x(), interpolatedPoint->y(), height + extrusion ) ) ); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsLineString( QgsPoint( distanceAlongProfileCurve, height ), - QgsPoint( distanceAlongProfileCurve, height + extrusion ) ) ); - mResults->minZ = std::min( mResults->minZ, height + extrusion ); - mResults->maxZ = std::max( mResults->maxZ, height + extrusion ); - } - else - { - resultFeature.geometry = QgsGeometry( new QgsPoint( interpolatedPoint->x(), interpolatedPoint->y(), height ) ); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsPoint( distanceAlongProfileCurve, height ) ); - } - mResults->features[resultFeature.featureId].append( resultFeature ); - } - else if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( *it ) ) + const QgsGeometry g = feature.geometry(); + for ( auto it = g.const_parts_begin(); !mFeedback->isCanceled() && it != g.const_parts_end(); ++it ) + { + if ( mProfileBufferedCurveEngine->intersects( *it ) ) { - const int numPoints = ls->numPoints(); - QVector< double > newX; - newX.resize( numPoints ); - QVector< double > newY; - newY.resize( numPoints ); - QVector< double > newZ; - newZ.resize( numPoints ); - QVector< double > newDistance; - newDistance.resize( numPoints ); - - const double *inX = ls->xData(); - const double *inY = ls->yData(); - const double *inZ = ls->is3D() ? ls->zData() : nullptr; - double *outX = newX.data(); - double *outY = newY.data(); - double *outZ = newZ.data(); - double *outDistance = newDistance.data(); - - QVector< double > extrudedZ; - double *extZOut = nullptr; - double extrusion = 0; - if ( mExtrusionEnabled ) - { - extrudedZ.resize( numPoints ); - extZOut = extrudedZ.data(); + processCurve( feature, qgsgeometry_cast< const QgsCurve * >( *it ) ); + } + } + } - extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); - } + return !mFeedback->isCanceled(); +} - QString lastError; - for ( int i = 0 ; i < numPoints; ++i ) - { - double x = *inX++; - double y = *inY++; +QgsPoint QgsVectorLayerProfileGenerator::interpolatePointOnTriangle( const QgsPolygon *triangle, double x, double y ) const +{ + QgsPoint p1, p2, p3; + Qgis::VertexType vt; + triangle->exteriorRing()->pointAt( 0, p1, vt ); + triangle->exteriorRing()->pointAt( 1, p2, vt ); + triangle->exteriorRing()->pointAt( 2, p3, vt ); + const double z = QgsMeshLayerUtils::interpolateFromVerticesData( p1, p2, p3, p1.z(), p2.z(), p3.z(), QgsPointXY( x, y ) ); + return QgsPoint( x, y, z ); +}; + +void QgsVectorLayerProfileGenerator::processTriangleIntersectForPoint( const QgsPolygon *triangle, const QgsPoint *p, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ) +{ + const QgsPoint interpolatedPoint = interpolatePointOnTriangle( triangle, p->x(), p->y() ); + mResults->mRawPoints.append( interpolatedPoint ); + mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() ); + mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() ); - // find z value from original curve by interpolating to this point - const double distanceAlongOriginalGeometry = curveGeos.lineLocatePoint( QgsPoint( x, y ) ); - std::unique_ptr< QgsPoint > closestOriginalPoint( curve->interpolatePoint( distanceAlongOriginalGeometry ) ); + QString lastError; + const double distance = mProfileCurveEngine->lineLocatePoint( *p, &lastError ); + mResults->mDistanceToHeightMap.insert( distance, interpolatedPoint.z() ); - double z = inZ ? *inZ++ : 0; + if ( mExtrusionEnabled ) + { + const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); + + transformedParts.append( QgsGeometry( new QgsLineString( interpolatedPoint, + QgsPoint( interpolatedPoint.x(), interpolatedPoint.y(), interpolatedPoint.z() + extrusion ) ) ) ); + crossSectionParts.append( QgsGeometry( new QgsLineString( QgsPoint( distance, interpolatedPoint.z() ), + QgsPoint( distance, interpolatedPoint.z() + extrusion ) ) ) ); + mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() + extrusion ); + mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() + extrusion ); + } + else + { + transformedParts.append( QgsGeometry( new QgsPoint( interpolatedPoint ) ) ); + crossSectionParts.append( QgsGeometry( new QgsPoint( distance, interpolatedPoint.z() ) ) ); + } +} - *outX++ = x; - *outY++ = y; - *outZ++ = std::isnan( closestOriginalPoint->z() ) ? 0 : closestOriginalPoint->z(); - if ( extZOut ) - *extZOut++ = z + extrusion; +void QgsVectorLayerProfileGenerator::processTriangleIntersectForLine( const QgsPolygon *triangle, const QgsLineString *intersectionLine, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ) +{ + if ( triangle->exteriorRing()->numPoints() < 4 ) // not a polygon + return; - mResults->mRawPoints.append( QgsPoint( x, y, z ) ); - mResults->minZ = std::min( mResults->minZ, z ); - mResults->maxZ = std::max( mResults->maxZ, z ); - if ( mExtrusionEnabled ) - { - mResults->minZ = std::min( mResults->minZ, z + extrusion ); - mResults->maxZ = std::max( mResults->maxZ, z + extrusion ); - } + int numPoints = intersectionLine->numPoints(); + QVector< double > newX( numPoints ); + QVector< double > newY( numPoints ); + QVector< double > newZ( numPoints ); + QVector< double > newDistance( numPoints ); + + const double *inX = intersectionLine->xData(); + const double *inY = intersectionLine->yData(); + const double *inZ = intersectionLine->is3D() ? intersectionLine->zData() : nullptr; + double *outX = newX.data(); + double *outY = newY.data(); + double *outZ = newZ.data(); + double *outDistance = newDistance.data(); + + double lastDistanceAlongProfileCurve = 0.0; + QVector< double > extrudedZ; + double *extZOut = nullptr; + double extrusion = 0; + + if ( mExtrusionEnabled ) + { + extrudedZ.resize( numPoints ); + extZOut = extrudedZ.data(); - const double distance = mProfileCurveEngine->lineLocatePoint( QgsPoint( x, y ), &lastError ); - *outDistance++ = distance; + extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); + } - mResults->mDistanceToHeightMap.insert( distance, z ); - } + QString lastError; + for ( int i = 0 ; ! mFeedback->isCanceled() && i < numPoints; ++i ) + { + double x = *inX++; + double y = *inY++; + double z = inZ ? *inZ++ : 0; - QgsVectorLayerProfileResults::Feature resultFeature; - resultFeature.featureId = feature.id(); + QgsPoint interpolatedPoint( x, y, z ); // general case (not a triangle) - if ( mExtrusionEnabled ) - { - std::unique_ptr< QgsLineString > ring = std::make_unique< QgsLineString >( newX, newY, newZ ); - std::unique_ptr< QgsLineString > extrudedRing = std::make_unique< QgsLineString >( newX, newY, extrudedZ ); - std::unique_ptr< QgsLineString > reversedExtrusion( extrudedRing->reversed() ); - ring->append( reversedExtrusion.get() ); - ring->close(); - resultFeature.geometry = QgsGeometry( new QgsPolygon( ring.release() ) ); - - - std::unique_ptr< QgsLineString > distanceVHeightRing = std::make_unique< QgsLineString >( newDistance, newZ ); - std::unique_ptr< QgsLineString > extrudedDistanceVHeightRing = std::make_unique< QgsLineString >( newDistance, extrudedZ ); - std::unique_ptr< QgsLineString > reversedDistanceVHeightExtrusion( extrudedDistanceVHeightRing->reversed() ); - distanceVHeightRing->append( reversedDistanceVHeightExtrusion.get() ); - distanceVHeightRing->close(); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsPolygon( distanceVHeightRing.release() ) ); - } - else - { - resultFeature.geometry = QgsGeometry( new QgsLineString( newX, newY, newZ ) ); - resultFeature.crossSectionGeometry = QgsGeometry( new QgsLineString( newDistance, newZ ) ); - } - mResults->features[resultFeature.featureId].append( resultFeature ); - } + *outX++ = x; + *outY++ = y; + if ( triangle->exteriorRing()->numPoints() == 4 ) // triangle case + { + interpolatedPoint = interpolatePointOnTriangle( triangle, x, y ); } - }; + double tempOutZ = std::isnan( interpolatedPoint.z() ) ? 0.0 : interpolatedPoint.z(); + *outZ++ = tempOutZ; - QgsFeature feature; - QgsFeatureIterator it = mSource->getFeatures( request ); - while ( it.nextFeature( feature ) ) - { - if ( mFeedback->isCanceled() ) - return false; + if ( mExtrusionEnabled ) + *extZOut++ = tempOutZ + extrusion; - if ( !mProfileCurveEngine->intersects( feature.geometry().constGet() ) ) - continue; + mResults->mRawPoints.append( interpolatedPoint ); + mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() ); + mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() ); + if ( mExtrusionEnabled ) + { + mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() + extrusion ); + mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() + extrusion ); + } - mExpressionContext.setFeature( feature ); + const double distance = mProfileCurveEngine->lineLocatePoint( interpolatedPoint, &lastError ); + *outDistance++ = distance; - const QgsGeometry g = feature.geometry(); - if ( g.isMultipart() ) - { - for ( auto it = g.const_parts_begin(); it != g.const_parts_end(); ++it ) - { - if ( !mProfileCurveEngine->intersects( *it ) ) - continue; + mResults->mDistanceToHeightMap.insert( distance, interpolatedPoint.z() ); + lastDistanceAlongProfileCurve = distance; + } - processCurve( feature, qgsgeometry_cast< const QgsCurve * >( *it ) ); - } + // insert nan point to end the line + mResults->mDistanceToHeightMap.insert( lastDistanceAlongProfileCurve + 0.000001, std::numeric_limits::quiet_NaN() ); + + if ( mFeedback->isCanceled() ) + return; + + if ( mExtrusionEnabled ) + { + std::unique_ptr< QgsLineString > ring = std::make_unique< QgsLineString >( newX, newY, newZ ); + std::unique_ptr< QgsLineString > extrudedRing = std::make_unique< QgsLineString >( newX, newY, extrudedZ ); + std::unique_ptr< QgsLineString > reversedExtrusion( extrudedRing->reversed() ); + ring->append( reversedExtrusion.get() ); + ring->close(); + transformedParts.append( QgsGeometry( new QgsPolygon( ring.release() ) ) ); + + std::unique_ptr< QgsLineString > distanceVHeightRing = std::make_unique< QgsLineString >( newDistance, newZ ); + std::unique_ptr< QgsLineString > extrudedDistanceVHeightRing = std::make_unique< QgsLineString >( newDistance, extrudedZ ); + std::unique_ptr< QgsLineString > reversedDistanceVHeightExtrusion( extrudedDistanceVHeightRing->reversed() ); + distanceVHeightRing->append( reversedDistanceVHeightExtrusion.get() ); + distanceVHeightRing->close(); + crossSectionParts.append( QgsGeometry( new QgsPolygon( distanceVHeightRing.release() ) ) ); + } + else + { + transformedParts.append( QgsGeometry( new QgsLineString( newX, newY, newZ ) ) ); + crossSectionParts.append( QgsGeometry( new QgsLineString( newDistance, newZ ) ) ); + } +}; + +void QgsVectorLayerProfileGenerator::processTriangleIntersectForPolygon( const QgsPolygon *sourcePolygon, const QgsPolygon *intersectionPolygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ) +{ + bool oldExtrusion = mExtrusionEnabled; + + /* Polyone extrusion produces I or C or inverted C shapes because the starting and ending points are the same. + We observe the same case with linestrings if the starting and ending points are not at the ends. + In the case below, the Z polygon projected onto the curve produces a shape that cannot be used to represent the extrusion ==> we would obtain a 3D volume. + In order to avoid having strange shapes that cannot be understood by the end user, extrusion is deactivated in the case of polygons. + + .^.. + ./ | \.. + ../ | \... + ../ | \... + ../ | \.. ....^.. + ../ | ........\.../ \... ^ + ../ ......|......./ \... \.... .../ \ + /,........../ | \.. \... / \ + v | \... ..../ \... \ + | \ ./ \... \ + | v \.. \ + | `v + | + .^.. + ./ \.. + ../ \... + ../ \... + ../ \.. ....^.. + ../ ........\.../ \... ^ + ../ ............../ \... \.... .../ \ + /,........../ \.. \... / \ + v \... ..../ \... \ + \ ./ \... \ + v \.. \ + `v + */ + mExtrusionEnabled = false; + if ( mProfileBufferedCurveEngine->contains( sourcePolygon ) ) // sourcePolygon is entirely inside curve buffer, we keep it as whole + { + if ( const QgsCurve *exterior = sourcePolygon->exteriorRing() ) + { + QgsLineString *exteriorLine = qgsgeometry_cast( exterior ); + processTriangleIntersectForLine( sourcePolygon, exteriorLine, transformedParts, crossSectionParts ); } - else + for ( int i = 0; i < sourcePolygon->numInteriorRings(); ++i ) { - processCurve( feature, qgsgeometry_cast< const QgsCurve * >( g.constGet() ) ); + QgsLineString *interiorLine = qgsgeometry_cast( sourcePolygon->interiorRing( i ) ); + processTriangleIntersectForLine( sourcePolygon, interiorLine, transformedParts, crossSectionParts ); + } + } + else // sourcePolygon is partially inside curve buffer, the intersectionPolygon is closed due to the intersection operation then + // it must be 'reopened' + { + if ( const QgsCurve *exterior = intersectionPolygon->exteriorRing() ) + { + QgsLineString *exteriorLine = qgsgeometry_cast( exterior )->clone(); + exteriorLine->deleteVertex( QgsVertexId( 0, 0, exteriorLine->numPoints() - 1 ) ); // open linestring + processTriangleIntersectForLine( sourcePolygon, exteriorLine, transformedParts, crossSectionParts ); + delete exteriorLine; + } + for ( int i = 0; i < intersectionPolygon->numInteriorRings(); ++i ) + { + QgsLineString *interiorLine = qgsgeometry_cast( intersectionPolygon->interiorRing( i ) ); + if ( mProfileBufferedCurveEngine->contains( interiorLine ) ) // interiorLine is entirely inside curve buffer + { + processTriangleIntersectForLine( sourcePolygon, interiorLine, transformedParts, crossSectionParts ); + } + else + { + interiorLine = qgsgeometry_cast( intersectionPolygon->interiorRing( i ) )->clone(); + interiorLine->deleteVertex( QgsVertexId( 0, 0, interiorLine->numPoints() - 1 ) ); // open linestring + processTriangleIntersectForLine( sourcePolygon, interiorLine, transformedParts, crossSectionParts ); + delete interiorLine; + } } } - return true; -} + + mExtrusionEnabled = oldExtrusion; +}; bool QgsVectorLayerProfileGenerator::generateProfileForPolygons() { // get features from layer QgsFeatureRequest request; request.setDestinationCrs( mTargetCrs, mTransformContext ); - request.setFilterRect( mProfileCurve->boundingBox() ); + if ( mTolerance > 0 ) + { + request.setDistanceWithin( QgsGeometry( mProfileCurve->clone() ), mTolerance ); + } + else + { + request.setFilterRect( mProfileCurve->boundingBox() ); + } request.setSubsetOfAttributes( mDataDefinedProperties.referencedFields( mExpressionContext ), mFields ); request.setFeedback( mFeedback.get() ); - auto interpolatePointOnTriangle = []( const QgsPolygon * triangle, double x, double y ) -> QgsPoint - { - QgsPoint p1, p2, p3; - Qgis::VertexType vt; - triangle->exteriorRing()->pointAt( 0, p1, vt ); - triangle->exteriorRing()->pointAt( 1, p2, vt ); - triangle->exteriorRing()->pointAt( 2, p3, vt ); - const double z = QgsMeshLayerUtils::interpolateFromVerticesData( p1, p2, p3, p1.z(), p2.z(), p3.z(), QgsPointXY( x, y ) ); - return QgsPoint( x, y, z ); - }; - std::function< void( const QgsPolygon *triangle, const QgsAbstractGeometry *intersect, QVector< QgsGeometry > &, QVector< QgsGeometry > & ) > processTriangleLineIntersect; - processTriangleLineIntersect = [this, &interpolatePointOnTriangle, &processTriangleLineIntersect]( const QgsPolygon * triangle, const QgsAbstractGeometry * intersect, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ) + processTriangleLineIntersect = [this]( const QgsPolygon * triangle, const QgsAbstractGeometry * intersection, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ) { - // intersect may be a (multi)point or (multi)linestring - switch ( QgsWkbTypes::geometryType( intersect->wkbType() ) ) + for ( auto it = intersection->const_parts_begin(); + ! mFeedback->isCanceled() && it != intersection->const_parts_end(); + ++it ) { - case Qgis::GeometryType::Point: - if ( const QgsMultiPoint *mp = qgsgeometry_cast< const QgsMultiPoint * >( intersect ) ) - { - const int numPoint = mp->numGeometries(); - for ( int i = 0; i < numPoint; ++i ) - { - processTriangleLineIntersect( triangle, mp->geometryN( i ), transformedParts, crossSectionParts ); - } - } - else if ( const QgsPoint *p = qgsgeometry_cast< const QgsPoint * >( intersect ) ) - { - const QgsPoint interpolatedPoint = interpolatePointOnTriangle( triangle, p->x(), p->y() ); - mResults->mRawPoints.append( interpolatedPoint ); - mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() ); - mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() ); - - QString lastError; - const double distance = mProfileCurveEngine->lineLocatePoint( *p, &lastError ); - mResults->mDistanceToHeightMap.insert( distance, interpolatedPoint.z() ); - - if ( mExtrusionEnabled ) - { - const double extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); - - transformedParts.append( QgsGeometry( new QgsLineString( interpolatedPoint, - QgsPoint( interpolatedPoint.x(), interpolatedPoint.y(), interpolatedPoint.z() + extrusion ) ) ) ); - crossSectionParts.append( QgsGeometry( new QgsLineString( QgsPoint( distance, interpolatedPoint.z() ), - QgsPoint( distance, interpolatedPoint.z() + extrusion ) ) ) ); - mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() + extrusion ); - mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() + extrusion ); - } - else - { - transformedParts.append( QgsGeometry( new QgsPoint( interpolatedPoint ) ) ); - crossSectionParts.append( QgsGeometry( new QgsPoint( distance, interpolatedPoint.z() ) ) ); - } - } - break; - case Qgis::GeometryType::Line: - if ( const QgsMultiLineString *ml = qgsgeometry_cast< const QgsMultiLineString * >( intersect ) ) - { - const int numLines = ml->numGeometries(); - for ( int i = 0; i < numLines; ++i ) - { - processTriangleLineIntersect( triangle, ml->geometryN( i ), transformedParts, crossSectionParts ); - } - } - else if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( intersect ) ) - { - const int numPoints = ls->numPoints(); - QVector< double > newX; - newX.resize( numPoints ); - QVector< double > newY; - newY.resize( numPoints ); - QVector< double > newZ; - newZ.resize( numPoints ); - QVector< double > newDistance; - newDistance.resize( numPoints ); - - const double *inX = ls->xData(); - const double *inY = ls->yData(); - double *outX = newX.data(); - double *outY = newY.data(); - double *outZ = newZ.data(); - double *outDistance = newDistance.data(); - - QVector< double > extrudedZ; - double *extZOut = nullptr; - double extrusion = 0; - if ( mExtrusionEnabled ) + // intersect may be a (multi)point or (multi)linestring + switch ( QgsWkbTypes::geometryType( ( *it )->wkbType() ) ) + { + case Qgis::GeometryType::Point: + if ( const QgsPoint *p = qgsgeometry_cast< const QgsPoint * >( *it ) ) { - extrudedZ.resize( numPoints ); - extZOut = extrudedZ.data(); - - extrusion = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ExtrusionHeight, mExpressionContext, mExtrusionHeight ); + processTriangleIntersectForPoint( triangle, p, transformedParts, crossSectionParts ); } + break; - QString lastError; - for ( int i = 0 ; i < numPoints; ++i ) + case Qgis::GeometryType::Line: + if ( const QgsLineString *intersectionLine = qgsgeometry_cast< const QgsLineString * >( *it ) ) { - double x = *inX++; - double y = *inY++; - - QgsPoint interpolatedPoint = interpolatePointOnTriangle( triangle, x, y ); - *outX++ = x; - *outY++ = y; - *outZ++ = interpolatedPoint.z(); - if ( extZOut ) - *extZOut++ = interpolatedPoint.z() + extrusion; - - mResults->mRawPoints.append( interpolatedPoint ); - mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() ); - mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() ); - if ( mExtrusionEnabled ) - { - mResults->minZ = std::min( mResults->minZ, interpolatedPoint.z() + extrusion ); - mResults->maxZ = std::max( mResults->maxZ, interpolatedPoint.z() + extrusion ); - } - - const double distance = mProfileCurveEngine->lineLocatePoint( interpolatedPoint, &lastError ); - *outDistance++ = distance; - - mResults->mDistanceToHeightMap.insert( distance, interpolatedPoint.z() ); + processTriangleIntersectForLine( triangle, intersectionLine, transformedParts, crossSectionParts ); } + break; - if ( mExtrusionEnabled ) + case Qgis::GeometryType::Polygon: + if ( const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( *it ) ) { - std::unique_ptr< QgsLineString > ring = std::make_unique< QgsLineString >( newX, newY, newZ ); - std::unique_ptr< QgsLineString > extrudedRing = std::make_unique< QgsLineString >( newX, newY, extrudedZ ); - std::unique_ptr< QgsLineString > reversedExtrusion( extrudedRing->reversed() ); - ring->append( reversedExtrusion.get() ); - ring->close(); - transformedParts.append( QgsGeometry( new QgsPolygon( ring.release() ) ) ); - - - std::unique_ptr< QgsLineString > distanceVHeightRing = std::make_unique< QgsLineString >( newDistance, newZ ); - std::unique_ptr< QgsLineString > extrudedDistanceVHeightRing = std::make_unique< QgsLineString >( newDistance, extrudedZ ); - std::unique_ptr< QgsLineString > reversedDistanceVHeightExtrusion( extrudedDistanceVHeightRing->reversed() ); - distanceVHeightRing->append( reversedDistanceVHeightExtrusion.get() ); - distanceVHeightRing->close(); - crossSectionParts.append( QgsGeometry( new QgsPolygon( distanceVHeightRing.release() ) ) ); + processTriangleIntersectForPolygon( triangle, poly, transformedParts, crossSectionParts ); } - else - { - transformedParts.append( QgsGeometry( new QgsLineString( newX, newY, newZ ) ) ); - crossSectionParts.append( QgsGeometry( new QgsLineString( newDistance, newZ ) ) ); - } - } - break; + break; - case Qgis::GeometryType::Polygon: - case Qgis::GeometryType::Unknown: - case Qgis::GeometryType::Null: - return; + case Qgis::GeometryType::Unknown: + case Qgis::GeometryType::Null: + return; + } } }; @@ -1230,7 +1319,7 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons() ring->xAt( 2 ), ring->yAt( 2 ), 0.005 ); }; - auto processPolygon = [this, &triangleIsCollinearInXYPlane, &processTriangleLineIntersect]( const QgsCurvePolygon * polygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts, double offset, bool & wasCollinear ) + auto processPolygon = [this, &processTriangleLineIntersect, &triangleIsCollinearInXYPlane]( const QgsCurvePolygon * polygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts, double offset, bool & wasCollinear ) { std::unique_ptr< QgsPolygon > clampedPolygon; if ( const QgsPolygon *p = qgsgeometry_cast< const QgsPolygon * >( polygon ) ) @@ -1246,95 +1335,138 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons() if ( mFeedback->isCanceled() ) return; - QgsGeometry tessellation; - if ( clampedPolygon->numInteriorRings() == 0 && clampedPolygon->exteriorRing() && clampedPolygon->exteriorRing()->numPoints() == 4 && clampedPolygon->exteriorRing()->isClosed() ) - { - // special case -- polygon is already a triangle, so no need to tessellate - std::unique_ptr< QgsMultiPolygon > multiPolygon = std::make_unique< QgsMultiPolygon >(); - multiPolygon->addGeometry( clampedPolygon.release() ); - tessellation = QgsGeometry( std::move( multiPolygon ) ); - } - else + if ( mTolerance > 0.0 ) // if the tolerance is not 0.0 we will have a polygon / polygon intersection, we do not need tessellation { - const QgsRectangle bounds = clampedPolygon->boundingBox(); - QgsTessellator t( bounds, false, false, false, false ); - t.addPolygon( *clampedPolygon, 0 ); + QString error; + if ( mProfileBufferedCurveEngine->intersects( clampedPolygon.get(), &error ) ) + { + std::unique_ptr< QgsAbstractGeometry > intersection; + intersection.reset( mProfileBufferedCurveEngine->intersection( clampedPolygon.get(), &error ) ); + if ( error.isEmpty() ) + { + processTriangleLineIntersect( clampedPolygon.get(), intersection.get(), transformedParts, crossSectionParts ); + } + else + { + // this case may occur with vertical object as geos does not handle very well 3D data. + // Geos works in 2D from the 3D coordinates then re-add the Z values, but when 2D-from-3D objects are vertical, they are topologically incorrects! + // This piece of code is just a fix to handle this case, a better and real 3D capable library is needed (like SFCGAL). + QgsLineString *ring = qgsgeometry_cast< QgsLineString * >( clampedPolygon->exteriorRing() ); + int numPoints = ring->numPoints(); + QVector< double > newX( numPoints ); + QVector< double > newY( numPoints ); + QVector< double > newZ( numPoints ); + double *outX = newX.data(); + double *outY = newY.data(); + double *outZ = newZ.data(); - tessellation = QgsGeometry( t.asMultiPolygon() ); - if ( mFeedback->isCanceled() ) - return; + const double *inX = ring->xData(); + const double *inY = ring->yData(); + const double *inZ = ring->zData(); + for ( int i = 0 ; ! mFeedback->isCanceled() && i < ring->numPoints() - 1; ++i ) + { + *outX++ = inX[i] + i * 1.0e-9; + *outY++ = inY[i] + i * 1.0e-9; + *outZ++ = inZ[i]; + } + std::unique_ptr< QgsPolygon > shiftedPoly; + shiftedPoly.reset( new QgsPolygon( new QgsLineString( newX, newY, newZ ) ) ); - tessellation.translate( bounds.xMinimum(), bounds.yMinimum() ); - } + intersection.reset( mProfileBufferedCurveEngine->intersection( shiftedPoly.get(), &error ) ); + if ( intersection.get() ) + processTriangleLineIntersect( clampedPolygon.get(), intersection.get(), transformedParts, crossSectionParts ); + else + QgsDebugMsgLevel( QStringLiteral( "processPolygon after shift bad geom! error: %1" ).arg( error ), 0 ); + } + } - // iterate through the tessellation, finding triangles which intersect the line - const int numTriangles = qgsgeometry_cast< const QgsMultiPolygon * >( tessellation.constGet() )->numGeometries(); - for ( int i = 0; i < numTriangles; ++i ) + } + else // ie. polygon / line intersection ==> need tessellation { - if ( mFeedback->isCanceled() ) - return; + QgsGeometry tessellation; + if ( clampedPolygon->numInteriorRings() == 0 && clampedPolygon->exteriorRing() && clampedPolygon->exteriorRing()->numPoints() == 4 && clampedPolygon->exteriorRing()->isClosed() ) + { + // special case -- polygon is already a triangle, so no need to tessellate + std::unique_ptr< QgsMultiPolygon > multiPolygon = std::make_unique< QgsMultiPolygon >(); + multiPolygon->addGeometry( clampedPolygon.release() ); + tessellation = QgsGeometry( std::move( multiPolygon ) ); + } + else + { + const QgsRectangle bounds = clampedPolygon->boundingBox(); + QgsTessellator t( bounds, false, false, false, false ); + t.addPolygon( *clampedPolygon, 0 ); - const QgsPolygon *triangle = qgsgeometry_cast< const QgsPolygon * >( qgsgeometry_cast< const QgsMultiPolygon * >( tessellation.constGet() )->geometryN( i ) ); + tessellation = QgsGeometry( t.asMultiPolygon() ); + if ( mFeedback->isCanceled() ) + return; - if ( triangleIsCollinearInXYPlane( triangle ) ) + tessellation.translate( bounds.xMinimum(), bounds.yMinimum() ); + } + + // iterate through the tessellation, finding triangles which intersect the line + const int numTriangles = qgsgeometry_cast< const QgsMultiPolygon * >( tessellation.constGet() )->numGeometries(); + for ( int i = 0; ! mFeedback->isCanceled() && i < numTriangles; ++i ) { - wasCollinear = true; - const QgsLineString *ring = qgsgeometry_cast< const QgsLineString * >( polygon->exteriorRing() ); + const QgsPolygon *triangle = qgsgeometry_cast< const QgsPolygon * >( qgsgeometry_cast< const QgsMultiPolygon * >( tessellation.constGet() )->geometryN( i ) ); - QString lastError; - if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( mProfileCurve.get() ) ) + if ( triangleIsCollinearInXYPlane( triangle ) ) { - for ( int curveSegmentIndex = 0; curveSegmentIndex < mProfileCurve->numPoints() - 1; ++curveSegmentIndex ) + wasCollinear = true; + const QgsLineString *ring = qgsgeometry_cast< const QgsLineString * >( polygon->exteriorRing() ); + + QString lastError; + if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( mProfileCurve.get() ) ) { - const QgsPoint p1 = ls->pointN( curveSegmentIndex ); - const QgsPoint p2 = ls->pointN( curveSegmentIndex + 1 ); + for ( int curveSegmentIndex = 0; curveSegmentIndex < mProfileCurve->numPoints() - 1; ++curveSegmentIndex ) + { + const QgsPoint p1 = ls->pointN( curveSegmentIndex ); + const QgsPoint p2 = ls->pointN( curveSegmentIndex + 1 ); - QgsPoint intersectionPoint; - double minZ = std::numeric_limits< double >::max(); - double maxZ = std::numeric_limits< double >::lowest(); + QgsPoint intersectionPoint; + double minZ = std::numeric_limits< double >::max(); + double maxZ = std::numeric_limits< double >::lowest(); - for ( auto vertexPair : std::array, 3> {{ { 0, 1}, {1, 2}, {2, 0} }} ) - { - bool isIntersection = false; - if ( QgsGeometryUtils::segmentIntersection( ring->pointN( vertexPair.first ), ring->pointN( vertexPair.second ), p1, p2, intersectionPoint, isIntersection ) ) + for ( auto vertexPair : std::array, 3> {{ { 0, 1}, {1, 2}, {2, 0} }} ) { - const double fraction = QgsGeometryUtilsBase::pointFractionAlongLine( ring->xAt( vertexPair.first ), ring->yAt( vertexPair.first ), ring->xAt( vertexPair.second ), ring->yAt( vertexPair.second ), intersectionPoint.x(), intersectionPoint.y() ); - const double intersectionZ = ring->zAt( vertexPair.first ) + ( ring->zAt( vertexPair.second ) - ring->zAt( vertexPair.first ) ) * fraction; - minZ = std::min( minZ, intersectionZ ); - maxZ = std::max( maxZ, intersectionZ ); + bool isIntersection = false; + if ( QgsGeometryUtils::segmentIntersection( ring->pointN( vertexPair.first ), ring->pointN( vertexPair.second ), p1, p2, intersectionPoint, isIntersection ) ) + { + const double fraction = QgsGeometryUtilsBase::pointFractionAlongLine( ring->xAt( vertexPair.first ), ring->yAt( vertexPair.first ), ring->xAt( vertexPair.second ), ring->yAt( vertexPair.second ), intersectionPoint.x(), intersectionPoint.y() ); + const double intersectionZ = ring->zAt( vertexPair.first ) + ( ring->zAt( vertexPair.second ) - ring->zAt( vertexPair.first ) ) * fraction; + minZ = std::min( minZ, intersectionZ ); + maxZ = std::max( maxZ, intersectionZ ); + } } - } - if ( !intersectionPoint.isEmpty() ) - { - // need z? - mResults->mRawPoints.append( intersectionPoint ); - mResults->minZ = std::min( mResults->minZ, minZ ); - mResults->maxZ = std::max( mResults->maxZ, maxZ ); + if ( !intersectionPoint.isEmpty() ) + { + // need z? + mResults->mRawPoints.append( intersectionPoint ); + mResults->minZ = std::min( mResults->minZ, minZ ); + mResults->maxZ = std::max( mResults->maxZ, maxZ ); - const double distance = mProfileCurveEngine->lineLocatePoint( intersectionPoint, &lastError ); + const double distance = mProfileCurveEngine->lineLocatePoint( intersectionPoint, &lastError ); - crossSectionParts.append( QgsGeometry( new QgsLineString( QVector< double > {distance, distance}, QVector< double > {minZ, maxZ} ) ) ); + crossSectionParts.append( QgsGeometry( new QgsLineString( QVector< double > {distance, distance}, QVector< double > {minZ, maxZ} ) ) ); - mResults->mDistanceToHeightMap.insert( distance, minZ ); - mResults->mDistanceToHeightMap.insert( distance, maxZ ); + mResults->mDistanceToHeightMap.insert( distance, minZ ); + mResults->mDistanceToHeightMap.insert( distance, maxZ ); + } } } + else + { + // curved geometries, not supported yet, but not possible through the GUI anyway + QgsDebugError( QStringLiteral( "Collinear triangles with curved profile lines are not supported yet" ) ); + } } - else - { - // curved geometries, not supported yet, but not possible through the GUI anyway - QgsDebugError( QStringLiteral( "Collinear triangles with curved profile lines are not supported yet" ) ); - } - } - else - { - if ( mProfileCurveEngine->intersects( triangle ) ) + else // not collinear { QString error; - std::unique_ptr< QgsAbstractGeometry > intersection( mProfileCurveEngine->intersection( triangle, &error ) ); - if ( intersection && !intersection->isEmpty() ) + if ( mProfileBufferedCurveEngine->intersects( triangle, &error ) ) { + std::unique_ptr< QgsAbstractGeometry > intersection( mProfileBufferedCurveEngine->intersection( triangle, &error ) ); processTriangleLineIntersect( triangle, intersection.get(), transformedParts, crossSectionParts ); } } @@ -1342,45 +1474,35 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons() } }; + // ========= MAIN JOB QgsFeature feature; QgsFeatureIterator it = mSource->getFeatures( request ); - while ( it.nextFeature( feature ) ) + while ( ! mFeedback->isCanceled() && it.nextFeature( feature ) ) { - if ( mFeedback->isCanceled() ) - return false; - - if ( !mProfileCurveEngine->intersects( feature.geometry().constGet() ) ) + if ( !mProfileBufferedCurveEngine->intersects( feature.geometry().constGet() ) ) continue; mExpressionContext.setFeature( feature ); const double offset = mDataDefinedProperties.valueAsDouble( QgsMapLayerElevationProperties::Property::ZOffset, mExpressionContext, mOffset ); - const QgsGeometry g = feature.geometry(); QVector< QgsGeometry > transformedParts; QVector< QgsGeometry > crossSectionParts; bool wasCollinear = false; - if ( g.isMultipart() ) + + // === process intersection of geometry feature parts with the mProfileBoxEngine + for ( auto it = g.const_parts_begin(); ! mFeedback->isCanceled() && it != g.const_parts_end(); ++it ) { - for ( auto it = g.const_parts_begin(); it != g.const_parts_end(); ++it ) + if ( mProfileBufferedCurveEngine->intersects( *it ) ) { - if ( mFeedback->isCanceled() ) - break; - - if ( !mProfileCurveEngine->intersects( *it ) ) - continue; - processPolygon( qgsgeometry_cast< const QgsCurvePolygon * >( *it ), transformedParts, crossSectionParts, offset, wasCollinear ); } } - else - { - processPolygon( qgsgeometry_cast< const QgsCurvePolygon * >( g.constGet() ), transformedParts, crossSectionParts, offset, wasCollinear ); - } if ( mFeedback->isCanceled() ) return false; + // === aggregate results for this feature QgsVectorLayerProfileResults::Feature resultFeature; resultFeature.featureId = feature.id(); resultFeature.geometry = transformedParts.size() > 1 ? QgsGeometry::collectGeometry( transformedParts ) : transformedParts.value( 0 ); @@ -1389,9 +1511,18 @@ bool QgsVectorLayerProfileGenerator::generateProfileForPolygons() if ( !wasCollinear ) { QgsGeometry unioned = QgsGeometry::unaryUnion( crossSectionParts ); - if ( unioned.type() == Qgis::GeometryType::Line ) - unioned = unioned.mergeLines(); - resultFeature.crossSectionGeometry = unioned; + if ( unioned.isEmpty() ) + { + resultFeature.crossSectionGeometry = QgsGeometry::collectGeometry( crossSectionParts ); + } + else + { + if ( unioned.type() == Qgis::GeometryType::Line ) + { + unioned = unioned.mergeLines(); + } + resultFeature.crossSectionGeometry = unioned; + } } else { @@ -1548,4 +1679,3 @@ bool QgsVectorLayerProfileGenerator::clampAltitudes( QgsPolygon *polygon, double } return true; } - diff --git a/src/core/vector/qgsvectorlayerprofilegenerator.h b/src/core/vector/qgsvectorlayerprofilegenerator.h index e5c26dc168b1..17ae6130298c 100644 --- a/src/core/vector/qgsvectorlayerprofilegenerator.h +++ b/src/core/vector/qgsvectorlayerprofilegenerator.h @@ -124,6 +124,14 @@ class CORE_EXPORT QgsVectorLayerProfileGenerator : public QgsAbstractProfileSurf bool generateProfileForLines(); bool generateProfileForPolygons(); + void processIntersectionPoint( const QgsPoint *intersectionPoint, const QgsFeature &feature ); + void processIntersectionCurve( const QgsLineString *intersectionCurve, const QgsFeature &feature ); + + QgsPoint interpolatePointOnTriangle( const QgsPolygon *triangle, double x, double y ) const; + void processTriangleIntersectForPoint( const QgsPolygon *triangle, const QgsPoint *intersect, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ); + void processTriangleIntersectForLine( const QgsPolygon *triangle, const QgsLineString *intersect, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ); + void processTriangleIntersectForPolygon( const QgsPolygon *triangle, const QgsPolygon *intersectionPolygon, QVector< QgsGeometry > &transformedParts, QVector< QgsGeometry > &crossSectionParts ); + double terrainHeight( double x, double y ); double featureZToHeight( double x, double y, double z, double offset ); @@ -136,6 +144,9 @@ class CORE_EXPORT QgsVectorLayerProfileGenerator : public QgsAbstractProfileSurf std::unique_ptr< QgsCurve > mProfileCurve; std::unique_ptr< QgsGeos > mProfileCurveEngine; + std::unique_ptr mProfileBufferedCurve; + std::unique_ptr< QgsGeos > mProfileBufferedCurveEngine; + std::unique_ptr< QgsAbstractTerrainProvider > mTerrainProvider; std::unique_ptr< QgsCurve > mTransformedCurve; diff --git a/src/core/vector/qgsvectorlayerutils.cpp b/src/core/vector/qgsvectorlayerutils.cpp index 600563841bbf..06c5471977c5 100644 --- a/src/core/vector/qgsvectorlayerutils.cpp +++ b/src/core/vector/qgsvectorlayerutils.cpp @@ -1250,7 +1250,7 @@ QString QgsVectorLayerUtils::guessFriendlyIdentifierField( const QgsFields &fiel // So try to look at another field whose name would end with _name // And fallback to using the "id" field that should always be filled. if ( candidateName == QLatin1String( "gml_name" ) && - fields.indexOf( QStringLiteral( "id" ) ) >= 0 ) + fields.indexOf( QLatin1String( "id" ) ) >= 0 ) { candidateName.clear(); // Try to find a field ending with "_name", which is not "gml_name" diff --git a/src/core/vectortile/qgsarcgisvectortileservicedataprovider.cpp b/src/core/vectortile/qgsarcgisvectortileservicedataprovider.cpp index 6f4aae4322de..e2cee18f6573 100644 --- a/src/core/vectortile/qgsarcgisvectortileservicedataprovider.cpp +++ b/src/core/vectortile/qgsarcgisvectortileservicedataprovider.cpp @@ -18,6 +18,7 @@ #include "qgsapplication.h" #include "qgsblockingnetworkrequest.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsvectortileutils.h" #include "qgsarcgisrestutils.h" #include "qgslogger.h" @@ -72,6 +73,11 @@ QgsArcGisVectorTileServiceDataProvider::QgsArcGisVectorTileServiceDataProvider( mLayerMetadata = other.mLayerMetadata; } +Qgis::DataProviderFlags QgsArcGisVectorTileServiceDataProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + Qgis::VectorTileProviderFlags QgsArcGisVectorTileServiceDataProvider::providerFlags() const { return QgsXyzVectorTileDataProviderBase::providerFlags() | Qgis::VectorTileProviderFlag::AlwaysUseTileMatrixSetFromProvider; diff --git a/src/core/vectortile/qgsarcgisvectortileservicedataprovider.h b/src/core/vectortile/qgsarcgisvectortileservicedataprovider.h index c5309bead132..2603055ea4c7 100644 --- a/src/core/vectortile/qgsarcgisvectortileservicedataprovider.h +++ b/src/core/vectortile/qgsarcgisvectortileservicedataprovider.h @@ -41,6 +41,7 @@ class CORE_EXPORT QgsArcGisVectorTileServiceDataProvider : public QgsXyzVectorTi */ QgsArcGisVectorTileServiceDataProvider &operator=( const QgsArcGisVectorTileServiceDataProvider &other ) = delete; + Qgis::DataProviderFlags flags() const override; Qgis::VectorTileProviderFlags providerFlags() const override; Qgis::VectorTileProviderCapabilities providerCapabilities() const override; QString name() const override; diff --git a/src/core/vectortile/qgsmbtilesvectortiledataprovider.cpp b/src/core/vectortile/qgsmbtilesvectortiledataprovider.cpp index 9fe7dd348eda..11a37b0981f4 100644 --- a/src/core/vectortile/qgsmbtilesvectortiledataprovider.cpp +++ b/src/core/vectortile/qgsmbtilesvectortiledataprovider.cpp @@ -93,6 +93,11 @@ QgsMbTilesVectorTileDataProvider::QgsMbTilesVectorTileDataProvider( const QgsMbT mMatrixSet = other.mMatrixSet; } +Qgis::DataProviderFlags QgsMbTilesVectorTileDataProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QString QgsMbTilesVectorTileDataProvider::name() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS diff --git a/src/core/vectortile/qgsmbtilesvectortiledataprovider.h b/src/core/vectortile/qgsmbtilesvectortiledataprovider.h index 5b931bcc24f7..18668e70250c 100644 --- a/src/core/vectortile/qgsmbtilesvectortiledataprovider.h +++ b/src/core/vectortile/qgsmbtilesvectortiledataprovider.h @@ -44,6 +44,7 @@ class CORE_EXPORT QgsMbTilesVectorTileDataProvider : public QgsVectorTileDataPro */ QgsMbTilesVectorTileDataProvider &operator=( const QgsMbTilesVectorTileDataProvider &other ) = delete; + Qgis::DataProviderFlags flags() const override; QString name() const override; QString description() const override; QgsVectorTileDataProvider *clone() const override; diff --git a/src/core/vectortile/qgsvectortilelayer.cpp b/src/core/vectortile/qgsvectortilelayer.cpp index 010894628c63..73eecf3990a3 100644 --- a/src/core/vectortile/qgsvectortilelayer.cpp +++ b/src/core/vectortile/qgsvectortilelayer.cpp @@ -23,6 +23,7 @@ #include "qgsvectortileloader.h" #include "qgsvectortileutils.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsdatasourceuri.h" #include "qgslayermetadataformatter.h" #include "qgsblockingnetworkrequest.h" diff --git a/src/core/vectortile/qgsvectortileutils.cpp b/src/core/vectortile/qgsvectortileutils.cpp index 209448d0ef35..1504d09549bd 100644 --- a/src/core/vectortile/qgsvectortileutils.cpp +++ b/src/core/vectortile/qgsvectortileutils.cpp @@ -33,6 +33,7 @@ #include "qgsvectortilerenderer.h" #include "qgsmapboxglstyleconverter.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsjsonutils.h" diff --git a/src/core/vectortile/qgsvtpktiles.cpp b/src/core/vectortile/qgsvtpktiles.cpp index 7eab6ebce310..272bc4f3cbf0 100644 --- a/src/core/vectortile/qgsvtpktiles.cpp +++ b/src/core/vectortile/qgsvtpktiles.cpp @@ -339,7 +339,7 @@ QgsLayerMetadata QgsVtpkTiles::layerMetadata() const QVariantMap QgsVtpkTiles::rootTileMap() const { - // make sure metadata has been read alread + // make sure metadata has been read already ( void )metadata(); if ( mHasReadTileMap || mTileMapPath.isEmpty() ) diff --git a/src/core/vectortile/qgsvtpkvectortiledataprovider.cpp b/src/core/vectortile/qgsvtpkvectortiledataprovider.cpp index 9713b707f21b..897dcafcb2ba 100644 --- a/src/core/vectortile/qgsvtpkvectortiledataprovider.cpp +++ b/src/core/vectortile/qgsvtpkvectortiledataprovider.cpp @@ -83,6 +83,11 @@ QgsVtpkVectorTileDataProvider::QgsVtpkVectorTileDataProvider( const QgsVtpkVecto mSpriteImage = other.mSpriteImage; } +Qgis::DataProviderFlags QgsVtpkVectorTileDataProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + Qgis::VectorTileProviderFlags QgsVtpkVectorTileDataProvider::providerFlags() const { return Qgis::VectorTileProviderFlag::AlwaysUseTileMatrixSetFromProvider; diff --git a/src/core/vectortile/qgsvtpkvectortiledataprovider.h b/src/core/vectortile/qgsvtpkvectortiledataprovider.h index f8271697feb9..c1ae72be8d5c 100644 --- a/src/core/vectortile/qgsvtpkvectortiledataprovider.h +++ b/src/core/vectortile/qgsvtpkvectortiledataprovider.h @@ -45,6 +45,7 @@ class CORE_EXPORT QgsVtpkVectorTileDataProvider : public QgsVectorTileDataProvid */ QgsVtpkVectorTileDataProvider &operator=( const QgsVtpkVectorTileDataProvider &other ) = delete; + Qgis::DataProviderFlags flags() const override; Qgis::VectorTileProviderFlags providerFlags() const override; Qgis::VectorTileProviderCapabilities providerCapabilities() const override; QString name() const override; diff --git a/src/core/vectortile/qgsxyzvectortiledataprovider.cpp b/src/core/vectortile/qgsxyzvectortiledataprovider.cpp index 37cfff3fde32..543441fc2963 100644 --- a/src/core/vectortile/qgsxyzvectortiledataprovider.cpp +++ b/src/core/vectortile/qgsxyzvectortiledataprovider.cpp @@ -19,6 +19,7 @@ #include "qgsvectortileloader.h" #include "qgsvectortileutils.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsapplication.h" #include "qgsauthmanager.h" #include "qgsmessagelog.h" @@ -332,6 +333,11 @@ QgsXyzVectorTileDataProvider::QgsXyzVectorTileDataProvider( const QgsXyzVectorTi mMatrixSet = other.mMatrixSet; } +Qgis::DataProviderFlags QgsXyzVectorTileDataProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QString QgsXyzVectorTileDataProvider::name() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS diff --git a/src/core/vectortile/qgsxyzvectortiledataprovider.h b/src/core/vectortile/qgsxyzvectortiledataprovider.h index 9fed5c73d503..2031c929feef 100644 --- a/src/core/vectortile/qgsxyzvectortiledataprovider.h +++ b/src/core/vectortile/qgsxyzvectortiledataprovider.h @@ -79,6 +79,7 @@ class CORE_EXPORT QgsXyzVectorTileDataProvider : public QgsXyzVectorTileDataProv */ QgsXyzVectorTileDataProvider &operator=( const QgsXyzVectorTileDataProvider &other ) = delete; + Qgis::DataProviderFlags flags() const override; QString name() const override; QString description() const override; QgsVectorTileDataProvider *clone() const override; diff --git a/src/crssync/CMakeLists.txt b/src/crssync/CMakeLists.txt index 278272161a98..06fd69c11264 100644 --- a/src/crssync/CMakeLists.txt +++ b/src/crssync/CMakeLists.txt @@ -12,6 +12,8 @@ else () qgis_core ) + target_compile_definitions(crssync PRIVATE "CMAKE_INSTALL_PREFIX=\"${CMAKE_INSTALL_PREFIX}\"") + if(MSVC AND NOT USING_NMAKE) add_custom_target(synccrsdb COMMENT "Running crssync" diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 694a97029165..09b327700271 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -233,6 +233,7 @@ set(QGIS_GUI_SRCS editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp editorwidgets/qgsvaluerelationwidgetfactory.cpp + elevation/qgselevationcontrollerwidget.cpp elevation/qgselevationprofilecanvas.cpp elevation/qgselevationprofilelayertreeview.cpp @@ -464,6 +465,7 @@ set(QGIS_GUI_SRCS providers/sensorthings/qgssensorthingsguiprovider.cpp providers/sensorthings/qgssensorthingssourceselect.cpp providers/sensorthings/qgssensorthingssourcewidget.cpp + providers/sensorthings/qgssensorthingssubseteditor.cpp providers/vtpkvectortiles/qgsvtpkvectortileguiprovider.cpp providers/vtpkvectortiles/qgsvtpkvectortilesourcewidget.cpp @@ -682,6 +684,7 @@ set(QGIS_GUI_SRCS qgsoptionsdialoghighlightwidget.cpp qgsoptionsdialoghighlightwidgetsimpl.cpp qgsorderbydialog.cpp + qgsoverlaywidgetlayout.cpp qgsowssourceselect.cpp qgsowssourcewidget.cpp qgspanelwidget.cpp @@ -955,6 +958,7 @@ set(QGIS_GUI_HDRS qgsoptionsdialoghighlightwidgetsimpl.h qgsoptionswidgetfactory.h qgsorderbydialog.h + qgsoverlaywidgetlayout.h qgsowssourceselect.h qgsowssourcewidget.h qgspanelwidget.h @@ -1194,6 +1198,7 @@ set(QGIS_GUI_HDRS effects/qgspainteffectpropertieswidget.h effects/qgspainteffectwidget.h + elevation/qgselevationcontrollerwidget.h elevation/qgselevationprofilecanvas.h elevation/qgselevationprofilelayertreeview.h @@ -1426,6 +1431,7 @@ set(QGIS_GUI_HDRS providers/sensorthings/qgssensorthingsguiprovider.h providers/sensorthings/qgssensorthingssourceselect.h providers/sensorthings/qgssensorthingssourcewidget.h + providers/sensorthings/qgssensorthingssubseteditor.h providers/vtpkvectortiles/qgsvtpkvectortileguiprovider.h providers/vtpkvectortiles/qgsvtpkvectortilesourcewidget.h @@ -1792,6 +1798,8 @@ if(ENABLE_MODELTEST) target_link_libraries(qgis_gui ${QT_VERSION_BASE}::Test) endif() +# We use private headers from core that need this +target_compile_definitions(qgis_gui PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(qgis_gui PRIVATE "-DQT_NO_FOREACH") if(WIN32) diff --git a/src/gui/auth/qgsauthsettingswidget.h b/src/gui/auth/qgsauthsettingswidget.h index 3153339c1520..388ac53c65a8 100644 --- a/src/gui/auth/qgsauthsettingswidget.h +++ b/src/gui/auth/qgsauthsettingswidget.h @@ -159,7 +159,7 @@ class GUI_EXPORT QgsAuthSettingsWidget : public QWidget, private Ui::QgsAuthSett void setStoreUsernameChecked( bool checked ); /** - * \brief setStorePasswordCheched check the "Store" checkbox for the password + * \brief setStorePasswordChecked check the "Store" checkbox for the password * \param checked * \see showStoreCheckboxes */ diff --git a/src/gui/codeeditors/qgscodeeditorpython.h b/src/gui/codeeditors/qgscodeeditorpython.h index e53ceae25282..b56a7cf49ae6 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.h +++ b/src/gui/codeeditors/qgscodeeditorpython.h @@ -104,7 +104,7 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor QString characterBeforeCursor() const; /** - * Returns the character after the cursor, or an empty string if the cursot is set at end + * Returns the character after the cursor, or an empty string if the cursor is set at end * * \since QGIS 3.30 */ 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/editorwidgets/qgsrelationreferenceconfigdlg.cpp b/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp index d1259f6f874e..40e5f2621032 100644 --- a/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp +++ b/src/gui/editorwidgets/qgsrelationreferenceconfigdlg.cpp @@ -70,6 +70,7 @@ QgsRelationReferenceConfigDlg::QgsRelationReferenceConfigDlg( QgsVectorLayer *vl connect( mExpressionWidget, static_cast( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsEditorConfigWidget::changed ); connect( mEditExpression, &QAbstractButton::clicked, this, &QgsRelationReferenceConfigDlg::mEditExpression_clicked ); connect( mFilterExpression, &QTextEdit::textChanged, this, &QgsEditorConfigWidget::changed ); + connect( mFetchLimitCheckBox, &QCheckBox::toggled, mFetchLimit, &QSpinBox::setEnabled ); } void QgsRelationReferenceConfigDlg::mEditExpression_clicked() @@ -113,7 +114,7 @@ void QgsRelationReferenceConfigDlg::setConfig( const QVariantMap &config ) mCbxMapIdentification->setChecked( config.value( QStringLiteral( "MapIdentification" ), false ).toBool() ); mCbxAllowAddFeatures->setChecked( config.value( QStringLiteral( "AllowAddFeatures" ), false ).toBool() ); mCbxReadOnly->setChecked( config.value( QStringLiteral( "ReadOnly" ), false ).toBool() ); - mFetchLimitGroupBox->setChecked( config.value( QStringLiteral( "FetchLimitActive" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ).toInt() > 0 ).toBool() ); + mFetchLimitCheckBox->setChecked( config.value( QStringLiteral( "FetchLimitActive" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ).toInt() > 0 ).toBool() ); mFetchLimit->setValue( config.value( QStringLiteral( "FetchLimitNumber" ), QgsSettings().value( QStringLiteral( "maxEntriesRelationWidget" ), 100, QgsSettings::Gui ) ).toInt() ); mFilterExpression->setPlainText( config.value( QStringLiteral( "FilterExpression" ) ).toString() ); @@ -175,7 +176,7 @@ QVariantMap QgsRelationReferenceConfigDlg::config() myConfig.insert( QStringLiteral( "ReadOnly" ), mCbxReadOnly->isChecked() ); myConfig.insert( QStringLiteral( "Relation" ), mComboRelation->currentData() ); myConfig.insert( QStringLiteral( "AllowAddFeatures" ), mCbxAllowAddFeatures->isChecked() ); - myConfig.insert( QStringLiteral( "FetchLimitActive" ), mFetchLimitGroupBox->isChecked() ); + myConfig.insert( QStringLiteral( "FetchLimitActive" ), mFetchLimitCheckBox->isChecked() ); myConfig.insert( QStringLiteral( "FetchLimitNumber" ), mFetchLimit->value() ); if ( mFilterGroupBox->isChecked() ) diff --git a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp index 553a0e90a29e..864c01872232 100644 --- a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp @@ -27,9 +27,12 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi mLayerName->setFilters( Qgis::LayerFilter::VectorLayer ); mKeyColumn->setLayer( mLayerName->currentLayer() ); mValueColumn->setLayer( mLayerName->currentLayer() ); + mGroupColumn->setLayer( mLayerName->currentLayer() ); + mGroupColumn->setAllowEmptyFieldName( true ); mDescriptionExpression->setLayer( mLayerName->currentLayer() ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mKeyColumn, &QgsFieldComboBox::setLayer ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mValueColumn, &QgsFieldComboBox::setLayer ); + connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mGroupColumn, &QgsFieldComboBox::setLayer ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mDescriptionExpression, &QgsFieldExpressionWidget::setLayer ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, this, &QgsValueRelationConfigDlg::layerChanged ); connect( mEditExpression, &QAbstractButton::clicked, this, &QgsValueRelationConfigDlg::editExpression ); @@ -41,6 +44,11 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi connect( mLayerName, &QgsMapLayerComboBox::layerChanged, this, &QgsEditorConfigWidget::changed ); connect( mKeyColumn, static_cast( &QComboBox::currentIndexChanged ), this, &QgsEditorConfigWidget::changed ); connect( mValueColumn, static_cast( &QComboBox::currentIndexChanged ), this, &QgsEditorConfigWidget::changed ); + connect( mGroupColumn, static_cast( &QComboBox::currentIndexChanged ), this, [ = ]( int index ) + { + mDisplayGroupName->setEnabled( index != 0 ); + emit changed(); + } ); connect( mDescriptionExpression, static_cast( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsEditorConfigWidget::changed ); connect( mAllowMulti, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed ); connect( mAllowNull, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed ); @@ -51,8 +59,14 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi { label_nofColumns->setEnabled( checked ); mNofColumns->setEnabled( checked ); - } - ); + } ); + + connect( mUseCompleter, &QCheckBox::stateChanged, this, [ = ]( int state ) + { + mCompleterMatchFromStart->setEnabled( static_cast( state ) == Qt::CheckState::Checked ); + } ); + + mCompleterMatchFromStart->setEnabled( mUseCompleter->isChecked() ); connect( mNofColumns, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsEditorConfigWidget::changed ); @@ -71,6 +85,8 @@ QVariantMap QgsValueRelationConfigDlg::config() QString() ); cfg.insert( QStringLiteral( "Key" ), mKeyColumn->currentField() ); cfg.insert( QStringLiteral( "Value" ), mValueColumn->currentField() ); + cfg.insert( QStringLiteral( "Group" ), mGroupColumn->currentField() ); + cfg.insert( QStringLiteral( "DisplayGroupName" ), mDisplayGroupName->isChecked() ); cfg.insert( QStringLiteral( "Description" ), mDescriptionExpression->expression() ); cfg.insert( QStringLiteral( "AllowMulti" ), mAllowMulti->isChecked() ); cfg.insert( QStringLiteral( "NofColumns" ), mNofColumns->value() ); @@ -78,6 +94,8 @@ QVariantMap QgsValueRelationConfigDlg::config() cfg.insert( QStringLiteral( "OrderByValue" ), mOrderByValue->isChecked() ); cfg.insert( QStringLiteral( "FilterExpression" ), mFilterExpression->toPlainText() ); cfg.insert( QStringLiteral( "UseCompleter" ), mUseCompleter->isChecked() ); + const Qt::MatchFlags completerMatchFlags { mCompleterMatchFromStart->isChecked() ? Qt::MatchFlag::MatchStartsWith : Qt::MatchFlag::MatchContains }; + cfg.insert( QStringLiteral( "CompleterMatchFlags" ), static_cast( completerMatchFlags ) ); return cfg; } @@ -88,6 +106,8 @@ void QgsValueRelationConfigDlg::setConfig( const QVariantMap &config ) mLayerName->setLayer( lyr ); mKeyColumn->setField( config.value( QStringLiteral( "Key" ) ).toString() ); mValueColumn->setField( config.value( QStringLiteral( "Value" ) ).toString() ); + mGroupColumn->setField( config.value( QStringLiteral( "Group" ) ).toString() ); + mDisplayGroupName->setChecked( config.value( QStringLiteral( "DisplayGroupName" ) ).toBool() ); mDescriptionExpression->setField( config.value( QStringLiteral( "Description" ) ).toString() ); mAllowMulti->setChecked( config.value( QStringLiteral( "AllowMulti" ) ).toBool() ); mNofColumns->setValue( config.value( QStringLiteral( "NofColumns" ), 1 ).toInt() ); @@ -100,6 +120,9 @@ void QgsValueRelationConfigDlg::setConfig( const QVariantMap &config ) mOrderByValue->setChecked( config.value( QStringLiteral( "OrderByValue" ) ).toBool() ); mFilterExpression->setPlainText( config.value( QStringLiteral( "FilterExpression" ) ).toString() ); mUseCompleter->setChecked( config.value( QStringLiteral( "UseCompleter" ) ).toBool() ); + // Default is MatchStartsWith for backwards compatibility + const Qt::MatchFlags completerMatchFlags { config.contains( QStringLiteral( "CompleterMatchFlags" ) ) ? static_cast( config.value( QStringLiteral( "CompleterMatchFlags" ), Qt::MatchFlag::MatchStartsWith ).toInt( ) ) : Qt::MatchFlag::MatchStartsWith }; + mCompleterMatchFromStart->setChecked( completerMatchFlags.testFlag( Qt::MatchFlag::MatchStartsWith ) ); } void QgsValueRelationConfigDlg::layerChanged() diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp index 6e7369203617..da74c4c2a98d 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp @@ -37,13 +37,15 @@ #include #include #include +#include #include using namespace nlohmann; ///@cond PRIVATE -QgsFilteredTableWidget::QgsFilteredTableWidget( QWidget *parent, bool showSearch ) +QgsFilteredTableWidget::QgsFilteredTableWidget( QWidget *parent, bool showSearch, bool displayGroupName ) : QWidget( parent ) + , mDisplayGroupName( displayGroupName ) { mSearchWidget = new QgsFilterLineEdit( this ); mSearchWidget->setShowSearchIcon( true ); @@ -95,32 +97,63 @@ void QgsFilteredTableWidget::filterStringChanged( const QString &filterString ) { auto signalBlockedTableWidget = whileBlocking( mTableWidget ); Q_UNUSED( signalBlockedTableWidget ) - mTableWidget->clearContents(); - const int rCount = std::max( 1, ( int ) std::ceil( ( float ) mCache.count() / ( float ) mColumnCount ) ); - mTableWidget->setRowCount( rCount ); - int row = 0; - int column = 0; - for ( const QPair &pair : std::as_const( mCache ) ) + mTableWidget->clearContents(); + if ( !mCache.isEmpty() ) { - if ( column == mColumnCount ) + QVariantList groups; + groups << QVariant(); + for ( const QPair &pair : std::as_const( mCache ) ) { - row++; - column = 0; + if ( !groups.contains( pair.first.group ) ) + { + groups << pair.first.group; + } } - if ( pair.first.value.contains( filterString, Qt::CaseInsensitive ) ) + const int groupsCount = mDisplayGroupName ? groups.count() : groups.count() - 1; + + const int rCount = std::max( 1, ( int ) std::ceil( ( float )( mCache.count() + groupsCount ) / ( float ) mColumnCount ) ); + mTableWidget->setRowCount( rCount ); + + int row = 0; + int column = 0; + QVariant currentGroup; + for ( const QPair &pair : std::as_const( mCache ) ) { - QTableWidgetItem *item = nullptr; - item = new QTableWidgetItem( pair.first.value ); - item->setData( Qt::UserRole, pair.first.key ); - item->setData( Qt::ToolTipRole, pair.first.description ); - item->setCheckState( pair.second ); - item->setFlags( mEnabledTable ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled ); - mTableWidget->setItem( row, column, item ); - column++; + if ( column == mColumnCount ) + { + row++; + column = 0; + } + if ( currentGroup != pair.first.group ) + { + currentGroup = pair.first.group; + if ( mDisplayGroupName || !( row == 0 && column == 0 ) ) + { + QTableWidgetItem *item = new QTableWidgetItem( mDisplayGroupName ? pair.first.group.toString() : QString() ); + item->setFlags( item->flags() & ~Qt::ItemIsEnabled ); + mTableWidget->setItem( row, column, item ); + column++; + if ( column == mColumnCount ) + { + row++; + column = 0; + } + } + } + if ( pair.first.value.contains( filterString, Qt::CaseInsensitive ) ) + { + QTableWidgetItem *item = new QTableWidgetItem( pair.first.value ); + item->setData( Qt::UserRole, pair.first.key ); + item->setData( Qt::ToolTipRole, pair.first.description ); + item->setCheckState( pair.second ); + item->setFlags( mEnabledTable ? item->flags() | Qt::ItemIsEnabled : item->flags() & ~Qt::ItemIsEnabled ); + mTableWidget->setItem( row, column, item ); + column++; + } } + mTableWidget->setRowCount( row + 1 ); } - mTableWidget->setRowCount( row + 1 ); } QStringList QgsFilteredTableWidget::selection() const @@ -290,7 +323,8 @@ QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent ) const bool useCompleter = config( QStringLiteral( "UseCompleter" ) ).toBool(); if ( allowMulti ) { - return new QgsFilteredTableWidget( parent, useCompleter ); + const bool displayGroupName = config( QStringLiteral( "DisplayGroupName" ) ).toBool(); + return new QgsFilteredTableWidget( parent, useCompleter, displayGroupName ); } else if ( useCompleter ) { @@ -531,19 +565,45 @@ void QgsValueRelationWidgetWrapper::populate() if ( mComboBox ) { - whileBlocking( mComboBox )->clear(); - if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) ) + mComboBox->blockSignals( true ); + mComboBox->clear(); + const bool allowNull = config( QStringLiteral( "AllowNull" ) ).toBool(); + if ( allowNull ) { - whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) ); + mComboBox->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) ); } - for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) ) + if ( !mCache.isEmpty() ) { - whileBlocking( mComboBox )->addItem( element.value, element.key ); - if ( !element.description.isEmpty() ) - mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole ); + QVariant currentGroup; + QStandardItemModel *model = qobject_cast( mComboBox->model() ); + const bool displayGroupName = config( QStringLiteral( "DisplayGroupName" ) ).toBool(); + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : std::as_const( mCache ) ) + { + if ( currentGroup != element.group ) + { + if ( mComboBox->count() > ( allowNull ? 1 : 0 ) ) + { + mComboBox->insertSeparator( mComboBox->count() ); + } + if ( displayGroupName ) + { + mComboBox->addItem( element.group.toString() ); + QStandardItem *item = model->item( mComboBox->count() - 1 ); + item->setFlags( item->flags() & ~Qt::ItemIsEnabled ); + } + currentGroup = element.group; + } + + mComboBox->addItem( element.value, element.key ); + + if ( !element.description.isEmpty() ) + { + mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole ); + } + } } - + mComboBox->blockSignals( false ); } else if ( mTableWidget ) { @@ -560,6 +620,17 @@ void QgsValueRelationWidgetWrapper::populate() } QStringListModel *m = new QStringListModel( values, mLineEdit ); QCompleter *completer = new QCompleter( m, mLineEdit ); + + const Qt::MatchFlags completerMatchFlags { config().contains( QStringLiteral( "CompleterMatchFlags" ) ) ? static_cast( config().value( QStringLiteral( "CompleterMatchFlags" ), Qt::MatchFlag::MatchStartsWith ).toInt( ) ) : Qt::MatchFlag::MatchStartsWith }; + + if ( completerMatchFlags.testFlag( Qt::MatchFlag::MatchContains ) ) + { + completer->setFilterMode( Qt::MatchFlag::MatchContains ); + } + else + { + completer->setFilterMode( Qt::MatchFlag::MatchStartsWith ); + } completer->setCaseSensitivity( Qt::CaseInsensitive ); mLineEdit->setCompleter( completer ); } diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h index 0de56c940ab1..a7a8b9623499 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h @@ -47,8 +47,9 @@ class QgsFilteredTableWidget : public QWidget * \brief QgsFilteredTableWidget constructor * \param parent * \param showSearch Whether the search QgsFilterLineEdit should be visible or not + * \param displayGroupName Set to TRUE to display the grouping value as name in section header */ - QgsFilteredTableWidget( QWidget *parent, bool showSearch ); + QgsFilteredTableWidget( QWidget *parent, bool showSearch, bool displayGroupName ); bool eventFilter( QObject *watched, QEvent *event ) override; @@ -104,6 +105,8 @@ class QgsFilteredTableWidget : public QWidget QTableWidget *mTableWidget = nullptr; bool mEnabledTable = true; QVector> mCache; + bool mDisplayGroupName = false; + friend class TestQgsValueRelationWidgetWrapper; }; diff --git a/src/gui/elevation/qgselevationcontrollerwidget.cpp b/src/gui/elevation/qgselevationcontrollerwidget.cpp new file mode 100644 index 000000000000..9f5e52f96853 --- /dev/null +++ b/src/gui/elevation/qgselevationcontrollerwidget.cpp @@ -0,0 +1,397 @@ +/*************************************************************************** + qgselevationcontrollerwidget.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 "qgselevationcontrollerwidget.h" +#include "qgsrangeslider.h" +#include "qgsrange.h" +#include "qgsproject.h" +#include "qgsprojectelevationproperties.h" +#include "qgsapplication.h" +#include "qgsdoublespinbox.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +QgsElevationControllerWidget::QgsElevationControllerWidget( QWidget *parent ) + : QWidget( parent ) +{ + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + + mConfigureButton = new QToolButton(); + mConfigureButton->setPopupMode( QToolButton::InstantPopup ); + mConfigureButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/settings.svg" ) ) ); + QHBoxLayout *hl = new QHBoxLayout(); + hl->setContentsMargins( 0, 0, 0, 0 ); + hl->addWidget( mConfigureButton ); + hl->addStretch(); + vl->addLayout( hl ); + mMenu = new QMenu( this ); + mConfigureButton->setMenu( mMenu ); + + QgsElevationControllerSettingsAction *settingsAction = new QgsElevationControllerSettingsAction( mMenu ); + mMenu->addAction( settingsAction ); + + settingsAction->sizeSpin()->clear(); + connect( settingsAction->sizeSpin(), qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size ) + { + setFixedRangeSize( size < 0 ? -1 : size ); + } ); + + mMenu->addSeparator(); + + mSlider = new QgsRangeSlider( Qt::Vertical ); + mSlider->setFlippedDirection( true ); + mSlider->setRangeLimits( 0, 100000 ); + mSliderLabels = new QgsElevationControllerLabels(); + + QHBoxLayout *hlSlider = new QHBoxLayout(); + hlSlider->setContentsMargins( 0, 0, 0, 0 ); + hlSlider->setSpacing( 2 ); + hlSlider->addWidget( mSlider ); + hlSlider->addWidget( mSliderLabels, 1 ); + hlSlider->addStretch(); + vl->addLayout( hlSlider ); + + setCursor( Qt::ArrowCursor ); + + setLayout( vl ); + updateWidgetMask(); + + const QgsDoubleRange projectRange = QgsProject::instance()->elevationProperties()->elevationRange(); + // if project doesn't have a range, just default to ANY range! + setRangeLimits( projectRange.isInfinite() ? QgsDoubleRange( 0, 100 ) : projectRange ); + connect( QgsProject::instance()->elevationProperties(), &QgsProjectElevationProperties::elevationRangeChanged, this, [this]( const QgsDoubleRange & range ) + { + if ( !range.isInfinite() ) + setRangeLimits( range ); + } ); + + connect( mSlider, &QgsRangeSlider::rangeChanged, this, [this]( int, int ) + { + if ( mBlockSliderChanges ) + return; + + emit rangeChanged( range() ); + mSliderLabels->setRange( range() ); + } ); + + // default initial value to full range + setRange( rangeLimits() ); + mSliderLabels->setRange( rangeLimits() ); +} + +void QgsElevationControllerWidget::resizeEvent( QResizeEvent *event ) +{ + QWidget::resizeEvent( event ); + updateWidgetMask(); +} + +QgsDoubleRange QgsElevationControllerWidget::range() const +{ + // if the current slider range is just the current range, but snapped to the slider precision, then losslessly return the current range + const int snappedLower = static_cast< int >( std::floor( mCurrentRange.lower() * mSliderPrecision ) ); + const int snappedUpper = static_cast< int >( std::ceil( mCurrentRange.upper() * mSliderPrecision ) ); + if ( snappedLower == mSlider->lowerValue() && snappedUpper == mSlider->upperValue() ) + return mCurrentRange; + + const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision ); + if ( mFixedRangeSize >= 0 ) + { + // adjust range so that it has exactly the fixed width (given slider int precision the slider range + // will not have the exact fixed width) + if ( sliderRange.upper() + mFixedRangeSize <= mRangeLimits.upper() ) + return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeSize ); + else + return QgsDoubleRange( sliderRange.upper() - mFixedRangeSize, sliderRange.upper() ); + } + else + { + return sliderRange; + } +} + +QgsDoubleRange QgsElevationControllerWidget::rangeLimits() const +{ + return mRangeLimits; +} + +QgsRangeSlider *QgsElevationControllerWidget::slider() +{ + return mSlider; +} + +QMenu *QgsElevationControllerWidget::menu() +{ + return mMenu; +} + +void QgsElevationControllerWidget::setRange( const QgsDoubleRange &range ) +{ + if ( range == mCurrentRange ) + return; + + mCurrentRange = range; + mBlockSliderChanges = true; + mSlider->setRange( static_cast< int >( std::floor( range.lower() * mSliderPrecision ) ), + static_cast< int >( std::ceil( range.upper() * mSliderPrecision ) ) ); + mBlockSliderChanges = false; + emit rangeChanged( range ); + + mSliderLabels->setRange( mCurrentRange ); +} + +void QgsElevationControllerWidget::setRangeLimits( const QgsDoubleRange &limits ) +{ + if ( limits.isInfinite() ) + return; + + mRangeLimits = limits; + + const double limitRange = limits.upper() - limits.lower(); + + // pick a reasonable slider precision, given that the slider operates in integer values only + mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange; + + mBlockSliderChanges = true; + mSlider->setRangeLimits( static_cast< int >( std::floor( limits.lower() * mSliderPrecision ) ), + static_cast< int >( std::ceil( limits.upper() * mSliderPrecision ) ) ); + + // clip current range to fit limits + const double newCurrentLower = std::max( mCurrentRange.lower(), limits.lower() ); + const double newCurrentUpper = std::min( mCurrentRange.upper(), limits.upper() ); + const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper(); + + mSlider->setRange( static_cast< int >( std::floor( newCurrentLower * mSliderPrecision ) ), + static_cast< int >( std::ceil( newCurrentUpper * mSliderPrecision ) ) ); + mCurrentRange = QgsDoubleRange( newCurrentLower, newCurrentUpper ); + mBlockSliderChanges = false; + if ( rangeHasChanged ) + emit rangeChanged( mCurrentRange ); + + mSliderLabels->setLimits( mRangeLimits ); +} + +void QgsElevationControllerWidget::updateWidgetMask() +{ + // we want mouse events from this widgets children to be caught, but events + // on the widget itself to be ignored and passed to underlying widgets which are NOT THE DIRECT + // PARENT of this widget. + // this is definitively *****NOT***** possible with event filters, by overriding mouse events, or + // with the WA_TransparentForMouseEvents attribute + + QRegion reg( frameGeometry() ); + reg -= QRegion( geometry() ); + reg += childrenRegion(); + setMask( reg ); +} + +double QgsElevationControllerWidget::fixedRangeSize() const +{ + return mFixedRangeSize; +} + +void QgsElevationControllerWidget::setFixedRangeSize( double size ) +{ + if ( size == mFixedRangeSize ) + return; + + mFixedRangeSize = size; + if ( mFixedRangeSize < 0 ) + { + mSlider->setFixedRangeSize( -1 ); + } + else + { + mSlider->setFixedRangeSize( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) ); + } +} + +// +// QgsElevationControllerLabels +// +///@cond PRIVATE +QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent ) + : QWidget( parent ) +{ + // Drop the default widget font size by a couple of points + QFont smallerFont = font(); + int fontSize = smallerFont.pointSize(); +#ifdef Q_OS_WIN + fontSize = std::max( fontSize - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes +#else + fontSize = std::max( fontSize - 2, 7 ); +#endif + smallerFont.setPointSize( fontSize ); + setFont( smallerFont ); + + const QFontMetrics fm( smallerFont ); + setMinimumWidth( fm.horizontalAdvance( '0' ) * 5 ); + setAttribute( Qt::WA_TransparentForMouseEvents ); +} + +void QgsElevationControllerLabels::paintEvent( QPaintEvent * ) +{ + QStyleOptionSlider styleOption; + styleOption.initFrom( this ); + + const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this ); + const int sliderHeight = sliderRect.height(); + + QFont f = font(); + const QFontMetrics fm( f ); + + const int left = rect().left() + 2; + + const double limitRange = mLimits.upper() - mLimits.lower(); + const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange; + const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange; + const int lowerY = std::min( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ), + rect().bottom() - fm.descent() ); + const int upperY = std::max( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ), + rect().top() + fm.ascent() ); + + const bool lowerIsCloseToLimit = lowerY + fm.height() > rect().bottom() - fm.descent(); + const bool upperIsCloseToLimit = upperY - fm.height() < rect().top() + fm.ascent(); + const bool lowerIsCloseToUpperLimit = lowerY - fm.height() < rect().top() + fm.ascent(); + + QLocale locale; + + QPainterPath path; + if ( mLimits.lower() > std::numeric_limits< double >::lowest() ) + { + if ( lowerIsCloseToLimit ) + { + f.setBold( true ); + path.addText( left, lowerY, f, locale.toString( mRange.lower() ) ); + } + else + { + f.setBold( true ); + path.addText( left, lowerY, f, locale.toString( mRange.lower() ) ); + f.setBold( false ); + path.addText( left, rect().bottom() - fm.descent(), f, locale.toString( mLimits.lower() ) ); + } + } + + if ( mLimits.upper() < std::numeric_limits< double >::max() ) + { + if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) ) + { + if ( !lowerIsCloseToUpperLimit ) + { + f.setBold( false ); + path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) ); + } + } + else + { + if ( upperIsCloseToLimit ) + { + f.setBold( true ); + path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); + } + else + { + f.setBold( true ); + path.addText( left, upperY, f, locale.toString( mRange.upper() ) ); + f.setBold( false ); + path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) ); + } + } + } + + QPainter p( this ); + p.setRenderHint( QPainter::Antialiasing, true ); + const QColor bufferColor = palette().color( QPalette::Window ); + const QColor textColor = palette().color( QPalette::WindowText ); + QPen pen( bufferColor ); + pen.setJoinStyle( Qt::RoundJoin ); + pen.setCapStyle( Qt::RoundCap ); + pen.setWidthF( 4 ); + p.setPen( pen ); + p.setBrush( Qt::NoBrush ); + p.drawPath( path ); + p.setPen( Qt::NoPen ); + p.setBrush( QBrush( textColor ) ); + p.drawPath( path ); + p.end(); +} + +void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits ) +{ + if ( limits == mLimits ) + return; + + const QFontMetrics fm( font() ); + const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(), + QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3; + setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars ); + + mLimits = limits; + update(); +} + +void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range ) +{ + if ( range == mRange ) + return; + + mRange = range; + update(); +} + +// +// QgsElevationControllerSettingsAction +// + +QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent ) + : QWidgetAction( parent ) +{ + QGridLayout *gLayout = new QGridLayout(); + gLayout->setContentsMargins( 3, 2, 3, 2 ); + + QLabel *label = new QLabel( tr( "Fixed Range Size" ) ); + gLayout->addWidget( label, 0, 0 ); + + mSizeSpin = new QgsDoubleSpinBox(); + mSizeSpin->setDecimals( 4 ); + mSizeSpin->setMinimum( -1.0 ); + mSizeSpin->setMaximum( 999999999.0 ); + mSizeSpin->setClearValue( -1, tr( "Not set" ) ); + mSizeSpin->setKeyboardTracking( false ); + mSizeSpin->setToolTip( tr( "Limit elevation range to a fixed size" ) ); + + gLayout->addWidget( mSizeSpin, 0, 1 ); + + QWidget *w = new QWidget(); + w->setLayout( gLayout ); + setDefaultWidget( w ); +} + +QgsDoubleSpinBox *QgsElevationControllerSettingsAction::sizeSpin() +{ + return mSizeSpin; +} + +///@endcond PRIVATE diff --git a/src/gui/elevation/qgselevationcontrollerwidget.h b/src/gui/elevation/qgselevationcontrollerwidget.h new file mode 100644 index 000000000000..6ff61a5a0f76 --- /dev/null +++ b/src/gui/elevation/qgselevationcontrollerwidget.h @@ -0,0 +1,181 @@ +/*************************************************************************** + qgselevationcontrollerwidget.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 QGSELEVATIONCONTROLLERWIDGET_H +#define QGSELEVATIONCONTROLLERWIDGET_H + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include "qgsrange.h" +#include +#include + +class QgsRangeSlider; +class QgsDoubleSpinBox; +class QToolButton; +class QMenu; + +///@cond PRIVATE + +class GUI_EXPORT QgsElevationControllerLabels : public QWidget SIP_SKIP +{ + Q_OBJECT + + public: + + QgsElevationControllerLabels( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + void paintEvent( QPaintEvent *event ) override; + + void setLimits( const QgsDoubleRange &limits ); + void setRange( const QgsDoubleRange &range ); + + private: + + QgsDoubleRange mLimits; + QgsDoubleRange mRange; + +}; + +class GUI_EXPORT QgsElevationControllerSettingsAction: public QWidgetAction +{ + Q_OBJECT + + public: + + QgsElevationControllerSettingsAction( QWidget *parent = nullptr ); + + QgsDoubleSpinBox *sizeSpin(); + + private: + + QgsDoubleSpinBox *mSizeSpin = nullptr; +}; + + +///@endcond PRIVATE + +/** + * \ingroup gui + * \brief A widget for configuring vertical elevation slicing behavior for maps. + * + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsElevationControllerWidget : public QWidget +{ + + Q_OBJECT + + public: + + /** + * Constructor for QgsElevationControllerWidget, with the specified \a parent widget. + */ + QgsElevationControllerWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + void resizeEvent( QResizeEvent *event ) override; + + /** + * Returns the current visible range from the widget. + * + * \see setRange() + * \see rangeChanged() + */ + QgsDoubleRange range() const; + + /** + * Returns the limits of the elevation range which can be selected by the widget. + * + * \see rangeLimits() + */ + QgsDoubleRange rangeLimits() const; + + /** + * Returns a reference to the slider component of the widget. + */ + QgsRangeSlider *slider(); + + /** + * Returns a reference to the widget's configuration menu, which can be used + * to add actions to the menu. + */ + QMenu *menu(); + + /** + * Returns the fixed range size, or -1 if no fixed size is set. + * + * A fixed size forces the selected elevation range to have a matching difference between + * the upper and lower elevation. + * + * \see setFixedRangeSize() + */ + double fixedRangeSize() const; + + public slots: + + /** + * Sets the current visible \a range for the widget. + * + * \see range() + * \see rangeChanged() + */ + void setRange( const QgsDoubleRange &range ); + + /** + * Sets the limits of the elevation range which can be selected by the widget. + * + * \see rangeLimits() + */ + void setRangeLimits( const QgsDoubleRange &limits ); + + /** + * Sets the fixed range \a size. Set to -1 if no fixed size is desired. + * + * A fixed size forces the selected elevation range to have a matching difference between + * the upper and lower elevation. + * + * \see fixedRangeSize() + */ + void setFixedRangeSize( double size ); + + signals: + + /** + * Emitted when the visible range from the widget is changed. + * + * \see setRange() + * \see range() + */ + void rangeChanged( const QgsDoubleRange &range ); + + private: + + void updateWidgetMask(); + + QToolButton *mConfigureButton = nullptr; + QMenu *mMenu = nullptr; + QgsRangeSlider *mSlider = nullptr; + QgsElevationControllerLabels *mSliderLabels = nullptr; + QgsDoubleRange mRangeLimits; + QgsDoubleRange mCurrentRange; + double mFixedRangeSize = -1; + int mBlockSliderChanges = 0; + double mSliderPrecision = 100; + +}; + +#endif // QGSELEVATIONCONTROLLERWIDGET_H diff --git a/src/gui/layertree/qgslayertreeview.cpp b/src/gui/layertree/qgslayertreeview.cpp index 0df719576c67..ba811db725b2 100644 --- a/src/gui/layertree/qgslayertreeview.cpp +++ b/src/gui/layertree/qgslayertreeview.cpp @@ -111,6 +111,7 @@ void QgsLayerTreeView::setModel( QAbstractItemModel *model ) #endif mProxyModel->setShowPrivateLayers( mShowPrivateLayers ); + mProxyModel->setHideValidLayers( mHideValidLayers ); QTreeView::setModel( mProxyModel ); connect( treeModel->rootGroup(), &QgsLayerTreeNode::expandedChanged, this, &QgsLayerTreeView::onExpandedChanged ); @@ -601,11 +602,22 @@ void QgsLayerTreeView::setShowPrivateLayers( bool showPrivate ) mProxyModel->setShowPrivateLayers( showPrivate ); } -bool QgsLayerTreeView::showPrivateLayers() +void QgsLayerTreeView::setHideValidLayers( bool hideValid ) +{ + mHideValidLayers = hideValid; + mProxyModel->setHideValidLayers( mHideValidLayers ); +} + +bool QgsLayerTreeView::showPrivateLayers() const { return mShowPrivateLayers; } +bool QgsLayerTreeView::hideValidLayers() const +{ + return mHideValidLayers; +} + void QgsLayerTreeView::mouseDoubleClickEvent( QMouseEvent *event ) { if ( mBlockDoubleClickTimer->isActive() ) @@ -852,6 +864,9 @@ bool QgsLayerTreeProxyModel::nodeShown( QgsLayerTreeNode *node ) const { return false; } + if ( mHideValidLayers && layer->isValid() ) + return false; + return true; } } @@ -863,6 +878,23 @@ bool QgsLayerTreeProxyModel::showPrivateLayers() const void QgsLayerTreeProxyModel::setShowPrivateLayers( bool showPrivate ) { + if ( showPrivate == mShowPrivateLayers ) + return; + mShowPrivateLayers = showPrivate; invalidateFilter(); } + +bool QgsLayerTreeProxyModel::hideValidLayers() const +{ + return mHideValidLayers; +} + +void QgsLayerTreeProxyModel::setHideValidLayers( bool hideValid ) +{ + if ( hideValid == mHideValidLayers ) + return; + + mHideValidLayers = hideValid; + invalidateFilter(); +} diff --git a/src/gui/layertree/qgslayertreeview.h b/src/gui/layertree/qgslayertreeview.h index 452d3a479a47..f7aa3df435b8 100644 --- a/src/gui/layertree/qgslayertreeview.h +++ b/src/gui/layertree/qgslayertreeview.h @@ -69,6 +69,22 @@ class GUI_EXPORT QgsLayerTreeProxyModel : public QSortFilterProxyModel */ void setShowPrivateLayers( bool showPrivate ); + /** + * Returns if valid layers should be hidden (i.e. only invalid layers are shown). + * + * \see setHideValidLayers() + * \since QGIS 3.38 + */ + bool hideValidLayers() const; + + /** + * Sets whether valid layers should be hidden (i.e. only invalid layers are shown). + * + * \see setHideValidLayers() + * \since QGIS 3.38 + */ + void setHideValidLayers( bool hideValid ); + protected: bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; @@ -80,6 +96,7 @@ class GUI_EXPORT QgsLayerTreeProxyModel : public QSortFilterProxyModel QgsLayerTreeModel *mLayerTreeModel = nullptr; QString mFilterText; bool mShowPrivateLayers = false; + bool mHideValidLayers = false; }; @@ -332,6 +349,20 @@ class GUI_EXPORT QgsLayerTreeView : public QTreeView ///@endcond + /** + * Returns the show private layers status + * \since QGIS 3.18 + */ + bool showPrivateLayers() const; + + /** + * Returns if valid layers should be hidden (i.e. only invalid layers are shown). + * + * \see setHideValidLayers() + * \since QGIS 3.38 + */ + bool hideValidLayers() const; + public slots: //! Force refresh of layer symbology. Normally not needed as the changes of layer's renderer are monitored by the model void refreshLayerSymbology( const QString &layerId ); @@ -366,10 +397,12 @@ class GUI_EXPORT QgsLayerTreeView : public QTreeView void setShowPrivateLayers( bool showPrivate ); /** - * Returns the show private layers status - * \since QGIS 3.18 + * Sets whether valid layers should be hidden (i.e. only invalid layers are shown). + * + * \see setHideValidLayers() + * \since QGIS 3.38 */ - bool showPrivateLayers( ); + void setHideValidLayers( bool hideValid ); signals: //! Emitted when a current layer is changed @@ -443,6 +476,7 @@ class GUI_EXPORT QgsLayerTreeView : public QTreeView QgsMessageBar *mMessageBar = nullptr; bool mShowPrivateLayers = false; + bool mHideValidLayers = false; QTimer *mBlockDoubleClickTimer = nullptr; // For model debugging diff --git a/src/gui/layout/qgsgeopdflayertreemodel.h b/src/gui/layout/qgsgeopdflayertreemodel.h index e47d6ff44744..c97a55d445a5 100644 --- a/src/gui/layout/qgsgeopdflayertreemodel.h +++ b/src/gui/layout/qgsgeopdflayertreemodel.h @@ -47,7 +47,7 @@ class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsMapLayerModel { LayerColumn = 0, //!< Layer name GroupColumn, //!< PDF group - InitiallyVisible, //!< Initial visiblity state + InitiallyVisible, //!< Initial visibility state IncludeVectorAttributes //!< Vector attribute }; diff --git a/src/gui/layout/qgslayoutattributeselectiondialog.h b/src/gui/layout/qgslayoutattributeselectiondialog.h index 1dffca60bd46..86a2cff4ad7f 100644 --- a/src/gui/layout/qgslayoutattributeselectiondialog.h +++ b/src/gui/layout/qgslayoutattributeselectiondialog.h @@ -45,7 +45,7 @@ class QgsLayoutTableColumn; /** * \ingroup gui - * \brief A base model to hold the displaying or sortings columns used in a QgsLayoutAttributeTable + * \brief A base model to hold the displaying or sorting columns used in a QgsLayoutAttributeTable * * \note This class is not a part of public API * \since QGIS 3.14 diff --git a/src/gui/layout/qgslayoutitemslistview.cpp b/src/gui/layout/qgslayoutitemslistview.cpp index 30f2904da968..3e5938ab6234 100644 --- a/src/gui/layout/qgslayoutitemslistview.cpp +++ b/src/gui/layout/qgslayoutitemslistview.cpp @@ -145,7 +145,7 @@ void QgsLayoutItemsListView::keyPressEvent( QKeyEvent *event ) void QgsLayoutItemsListView::updateSelection() { - // Do nothing if we are currenlty updating the selection + // Do nothing if we are currently updating the selection // because user has selected/deselected some items in the // graphics view if ( !mModel || mUpdatingFromView ) diff --git a/src/gui/layout/qgslayoutlegendwidget.cpp b/src/gui/layout/qgslayoutlegendwidget.cpp index 408be72e1d20..4b91628ff53c 100644 --- a/src/gui/layout/qgslayoutlegendwidget.cpp +++ b/src/gui/layout/qgslayoutlegendwidget.cpp @@ -110,6 +110,8 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend, QgsMa connect( mRasterStrokeGroupBox, &QgsCollapsibleGroupBoxBasic::toggled, this, &QgsLayoutLegendWidget::mRasterStrokeGroupBox_toggled ); connect( mRasterStrokeWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutLegendWidget::mRasterStrokeWidthSpinBox_valueChanged ); connect( mRasterStrokeColorButton, &QgsColorButton::colorChanged, this, &QgsLayoutLegendWidget::mRasterStrokeColorButton_colorChanged ); + connect( mExpandAllToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::expandLegendTree ); + connect( mCollapseAllToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::collapseLegendTree ); connect( mMoveDownToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mMoveDownToolButton_clicked ); connect( mMoveUpToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mMoveUpToolButton_clicked ); connect( mRemoveToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mRemoveToolButton_clicked ); @@ -163,6 +165,8 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend, QgsMa mMoveDownToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowDown.svg" ) ) ); mCountToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionSum.svg" ) ) ); mLayerExpressionButton->setIcon( QIcon( QgsApplication::iconPath( "mIconExpression.svg" ) ) ); + mExpandAllToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionExpandTree.svg" ) ) ); + mCollapseAllToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionCollapseTree.svg" ) ) ); mMoveDownToolButton->setIconSize( QgsGuiUtils::iconSize( true ) ); mMoveUpToolButton->setIconSize( QgsGuiUtils::iconSize( true ) ); @@ -173,6 +177,8 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend, QgsMa mCountToolButton->setIconSize( QgsGuiUtils::iconSize( true ) ); mExpressionFilterButton->setIconSize( QgsGuiUtils::iconSize( true ) ); mLayerExpressionButton->setIconSize( QgsGuiUtils::iconSize( true ) ); + mExpandAllToolButton->setIconSize( QgsGuiUtils::iconSize( true ) ); + mCollapseAllToolButton->setIconSize( QgsGuiUtils::iconSize( true ) ); mRasterStrokeColorButton->setColorDialogTitle( tr( "Select Stroke Color" ) ); mRasterStrokeColorButton->setAllowOpacity( true ); @@ -812,6 +818,16 @@ void QgsLayoutLegendWidget::mMoveUpToolButton_clicked() mLegend->endCommand(); } +void QgsLayoutLegendWidget::expandLegendTree() +{ + mItemTreeView -> expandAll(); +} + +void QgsLayoutLegendWidget::collapseLegendTree() +{ + mItemTreeView -> collapseAll(); +} + void QgsLayoutLegendWidget::mCheckBoxAutoUpdate_stateChanged( int state, bool userTriggered ) { if ( userTriggered ) @@ -827,7 +843,7 @@ void QgsLayoutLegendWidget::mCheckBoxAutoUpdate_stateChanged( int state, bool us QList widgets; widgets << mMoveDownToolButton << mMoveUpToolButton << mRemoveToolButton << mAddToolButton << mEditPushButton << mCountToolButton << mUpdateAllPushButton << mAddGroupToolButton - << mExpressionFilterButton; + << mExpressionFilterButton << mCollapseAllToolButton << mExpandAllToolButton; for ( QWidget *w : std::as_const( widgets ) ) w->setEnabled( state != Qt::Checked ); @@ -1166,29 +1182,20 @@ void QgsLayoutLegendWidget::mLayerExpressionButton_clicked() QgsExpressionContext legendContext = mLegend->createExpressionContext(); legendContext.appendScope( vl->createExpressionContextScope() ); - QgsExpressionContextScope *symbolLegendScope = new QgsExpressionContextScope( tr( "Symbol scope" ) ); - - QgsFeatureRenderer *r = vl->renderer(); - QStringList highlighted; - if ( r ) + if ( QgsLegendModel *model = mLegend->model() ) { - const QgsLegendSymbolList legendSymbols = r->legendSymbolItems(); - - if ( !legendSymbols.empty() ) + const QList legendNodes = model->layerLegendNodes( layerNode, false ); + if ( !legendNodes.isEmpty() ) { - QgsSymbolLegendNode legendNode( layerNode, legendSymbols.first() ); - - symbolLegendScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_label" ), legendNode.symbolLabel().remove( QStringLiteral( "[%" ) ).remove( QStringLiteral( "%]" ) ), true ) ); - symbolLegendScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_id" ), legendSymbols.first().ruleKey(), true ) ); - highlighted << QStringLiteral( "symbol_label" ) << QStringLiteral( "symbol_id" ); - symbolLegendScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_count" ), QVariant::fromValue( vl->featureCount( legendSymbols.first().ruleKey() ) ), true ) ); - highlighted << QStringLiteral( "symbol_count" ); + if ( QgsSymbolLegendNode *symbolNode = qobject_cast( legendNodes.first() ) ) + { + legendContext.appendScope( symbolNode->createSymbolScope() ); + highlighted << QStringLiteral( "symbol_label" ) << QStringLiteral( "symbol_id" ) << QStringLiteral( "symbol_count" ); + } } } - legendContext.appendScope( symbolLegendScope ); - legendContext.setHighlightedVariables( highlighted ); // Passing the vector layer to expression dialog exposes the fields, but we still want generic @@ -1199,7 +1206,10 @@ void QgsLayoutLegendWidget::mLayerExpressionButton_clicked() QgsExpressionBuilderDialog expressiondialog( nullptr, currentExpression, nullptr, QStringLiteral( "generic" ), legendContext ); if ( expressiondialog.exec() ) + { layerNode->setLabelExpression( expressiondialog.expressionText() ); + mItemTreeView->layerTreeModel()->refreshLayerLegend( layerNode ); + } mLegend->beginCommand( tr( "Update Legend" ) ); mLegend->refresh(); @@ -2101,5 +2111,3 @@ bool QgsLayoutLegendMapFilteringModel::filterAcceptsRow( int source_row, const Q ///@endcond - - diff --git a/src/gui/layout/qgslayoutlegendwidget.h b/src/gui/layout/qgslayoutlegendwidget.h index 64266358d177..53c824b840e1 100644 --- a/src/gui/layout/qgslayoutlegendwidget.h +++ b/src/gui/layout/qgslayoutlegendwidget.h @@ -119,6 +119,9 @@ class GUI_EXPORT QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, public Q void setLegendMapViewData(); + void expandLegendTree(); + void collapseLegendTree(); + private slots: //! Sets GUI according to state of mLegend void setGuiElements(); diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index aa744f49a294..63e27642933e 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -92,6 +92,12 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item, QgsMapCanvas *ma connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsLayoutMapWidget::updateTemporalExtent ); connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsLayoutMapWidget::updateTemporalExtent ); + mZLowerSpin->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + mZUpperSpin->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, tr( "Not set" ) ); + connect( mElevationRangeCheckBox, &QgsCollapsibleGroupBoxBasic::toggled, this, &QgsLayoutMapWidget::mElevationRangeCheckBox_toggled ); + connect( mZLowerSpin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutMapWidget::updateZRange ); + connect( mZUpperSpin, qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, &QgsLayoutMapWidget::updateZRange ); + mStartDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() ); mEndDateTime->setDateTimeRange( QDateTime( QDate( 1, 1, 1 ), QTime( 0, 0, 0 ) ), mStartDateTime->maximumDateTime() ); mStartDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); @@ -191,6 +197,8 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item, QgsMapCanvas *ma registerDataDefinedButton( mCRSDDBtn, QgsLayoutObject::DataDefinedProperty::MapCrs ); registerDataDefinedButton( mStartDateTimeDDBtn, QgsLayoutObject::DataDefinedProperty::StartDateTime ); registerDataDefinedButton( mEndDateTimeDDBtn, QgsLayoutObject::DataDefinedProperty::EndDateTime ); + registerDataDefinedButton( mZLowerDDBtn, QgsLayoutObject::DataDefinedProperty::MapZRangeLower ); + registerDataDefinedButton( mZUpperDDBtn, QgsLayoutObject::DataDefinedProperty::MapZRangeUpper ); updateGuiElements(); loadGridEntries(); @@ -558,6 +566,41 @@ void QgsLayoutMapWidget::updateTemporalExtent() updatePreview(); } +void QgsLayoutMapWidget::mElevationRangeCheckBox_toggled( bool checked ) +{ + if ( !mMapItem ) + { + return; + } + + mMapItem->layout()->undoStack()->beginCommand( mMapItem, tr( "Toggle Z Range" ) ); + mMapItem->setZRangeEnabled( checked ); + mMapItem->layout()->undoStack()->endCommand(); + + updatePreview(); +} + +void QgsLayoutMapWidget::updateZRange() +{ + if ( !mMapItem ) + { + return; + } + + mMapItem->layout()->undoStack()->beginCommand( mMapItem, tr( "Set Z Range" ) ); + double zLower = mZLowerSpin->value(); + if ( zLower == mZLowerSpin->clearValue() ) + zLower = std::numeric_limits< double >::lowest(); + double zUpper = mZUpperSpin->value(); + if ( zUpper == mZUpperSpin->clearValue() ) + zUpper = std::numeric_limits< double >::max(); + + mMapItem->setZRange( QgsDoubleRange( zLower, zUpper ) ); + mMapItem->layout()->undoStack()->endCommand(); + + updatePreview(); +} + void QgsLayoutMapWidget::mAtlasCheckBox_toggled( bool checked ) { if ( !mMapItem ) @@ -925,6 +968,17 @@ void QgsLayoutMapWidget::updateGuiElements() mEndDateTime->setDateTime( mMapItem->temporalRange().end() ); } + whileBlocking( mElevationRangeCheckBox )->setChecked( mMapItem->zRangeEnabled() ); + mElevationRangeCheckBox->setCollapsed( !mMapItem->zRangeEnabled() ); + if ( mMapItem->zRange().lower() != std::numeric_limits< double >::lowest() ) + whileBlocking( mZLowerSpin )->setValue( mMapItem->zRange().lower() ); + else + whileBlocking( mZLowerSpin )->clear(); + if ( mMapItem->zRange().upper() != std::numeric_limits< double >::max() ) + whileBlocking( mZUpperSpin )->setValue( mMapItem->zRange().upper() ); + else + whileBlocking( mZUpperSpin )->clear(); + populateDataDefinedButtons(); loadGridEntries(); loadOverviewEntries(); diff --git a/src/gui/layout/qgslayoutmapwidget.h b/src/gui/layout/qgslayoutmapwidget.h index 2828efea672e..83a56c86f901 100644 --- a/src/gui/layout/qgslayoutmapwidget.h +++ b/src/gui/layout/qgslayoutmapwidget.h @@ -110,6 +110,9 @@ class GUI_EXPORT QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui: void mTemporalCheckBox_toggled( bool checked ); void updateTemporalExtent(); + void mElevationRangeCheckBox_toggled( bool checked ); + void updateZRange(); + protected: bool setNewItem( QgsLayoutItem *item ) override; diff --git a/src/gui/locator/qgslocatorwidget.cpp b/src/gui/locator/qgslocatorwidget.cpp index 70623908feff..0f2beb6a0096 100644 --- a/src/gui/locator/qgslocatorwidget.cpp +++ b/src/gui/locator/qgslocatorwidget.cpp @@ -480,7 +480,7 @@ void QgsLocatorFilterFilter::fetchResults( const QString &string, const QgsLocat void QgsLocatorFilterFilter::triggerResult( const QgsLocatorResult &result ) { - mLocator->search( result.getUserData().toString() ); + mLocator->search( result.userData().toString() ); } QgsLocatorLineEdit::QgsLocatorLineEdit( QgsLocatorWidget *locator, QWidget *parent ) diff --git a/src/gui/mesh/qgsmeshdatasetgrouptreeview.cpp b/src/gui/mesh/qgsmeshdatasetgrouptreeview.cpp index 96e1696797fb..40e247726947 100644 --- a/src/gui/mesh/qgsmeshdatasetgrouptreeview.cpp +++ b/src/gui/mesh/qgsmeshdatasetgrouptreeview.cpp @@ -333,7 +333,7 @@ bool QgsMeshDatasetGroupProxyModel::filterAcceptsRow( int source_row, const QMod ///////////////////////////////////////////////////////////////////////////////////////// -QgsMeshDatasetGroupTreeItemDelagate::QgsMeshDatasetGroupTreeItemDelagate( QObject *parent ) +QgsMeshDatasetGroupTreeItemDelegate::QgsMeshDatasetGroupTreeItemDelegate( QObject *parent ) : QStyledItemDelegate( parent ) , mScalarSelectedPixmap( QStringLiteral( ":/images/themes/default/propertyicons/meshcontours.svg" ) ) , mScalarDeselectedPixmap( QStringLiteral( ":/images/themes/default/propertyicons/meshcontoursoff.svg" ) ) @@ -342,7 +342,7 @@ QgsMeshDatasetGroupTreeItemDelagate::QgsMeshDatasetGroupTreeItemDelagate( QObjec { } -void QgsMeshDatasetGroupTreeItemDelagate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const +void QgsMeshDatasetGroupTreeItemDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { if ( !painter ) return; @@ -359,12 +359,12 @@ void QgsMeshDatasetGroupTreeItemDelagate::paint( QPainter *painter, const QStyle painter->drawPixmap( iconRect( option.rect, false ), isActive ? mScalarSelectedPixmap : mScalarDeselectedPixmap ); } -QRect QgsMeshDatasetGroupTreeItemDelagate::iconRect( const QRect &rect, bool isVector ) const +QRect QgsMeshDatasetGroupTreeItemDelegate::iconRect( const QRect &rect, bool isVector ) const { return iconRect( rect, isVector ? 1 : 2 ); } -QRect QgsMeshDatasetGroupTreeItemDelagate::iconRect( const QRect &rect, int pos ) const +QRect QgsMeshDatasetGroupTreeItemDelegate::iconRect( const QRect &rect, int pos ) const { const int iw = mScalarSelectedPixmap.width(); const int ih = mScalarSelectedPixmap.height(); @@ -372,7 +372,7 @@ QRect QgsMeshDatasetGroupTreeItemDelagate::iconRect( const QRect &rect, int pos return QRect( rect.right() - pos * ( iw + margin ), rect.top() + margin, iw, ih ); } -QSize QgsMeshDatasetGroupTreeItemDelagate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const +QSize QgsMeshDatasetGroupTreeItemDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { QSize hint = QStyledItemDelegate::sizeHint( option, index ); if ( hint.height() < 16 ) diff --git a/src/gui/mesh/qgsmeshdatasetgrouptreeview.h b/src/gui/mesh/qgsmeshdatasetgrouptreeview.h index 05b9d9858d28..c8060fbbd0be 100644 --- a/src/gui/mesh/qgsmeshdatasetgrouptreeview.h +++ b/src/gui/mesh/qgsmeshdatasetgrouptreeview.h @@ -179,15 +179,15 @@ class QgsMeshDatasetGroupProxyModel: public QSortFilterProxyModel /** * \ingroup gui - * \class QgsMeshDatasetGroupTreeItemDelagate + * \class QgsMeshDatasetGroupTreeItemDelegate * * \brief Delegate to display tree item with a contours and vector selector */ -class QgsMeshDatasetGroupTreeItemDelagate: public QStyledItemDelegate +class QgsMeshDatasetGroupTreeItemDelegate: public QStyledItemDelegate { Q_OBJECT public: - QgsMeshDatasetGroupTreeItemDelagate( QObject *parent = nullptr ); + QgsMeshDatasetGroupTreeItemDelegate( QObject *parent = nullptr ); void paint( QPainter *painter, const QStyleOptionViewItem &option, @@ -293,7 +293,7 @@ class GUI_EXPORT QgsMeshActiveDatasetGroupTreeView : public QTreeView void setActiveGroup(); QgsMeshDatasetGroupProxyModel *mProxyModel; - QgsMeshDatasetGroupTreeItemDelagate mDelegate; + QgsMeshDatasetGroupTreeItemDelegate mDelegate; QgsMeshLayer *mMeshLayer = nullptr; // not owned }; diff --git a/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp b/src/gui/processing/qgsprocessingdxflayerswidgetwrapper.cpp index b8c8c7c6c6dd..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 ); } @@ -93,7 +95,7 @@ QgsProcessingDxfLayersPanelWidget::QgsProcessingDxfLayersPanelWidget( seenVectorLayers.insert( layer.layer() ); } - const QList options = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() ); + const QList options = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() << static_cast( Qgis::ProcessingSourceType::VectorAnyGeometry ) ); for ( const QgsVectorLayer *layer : options ) { if ( seenVectorLayers.contains( layer ) ) diff --git a/src/gui/processing/qgsprocessingmeshdatasetwidget.h b/src/gui/processing/qgsprocessingmeshdatasetwidget.h index 152f896f5aea..179d5ebf4ea8 100644 --- a/src/gui/processing/qgsprocessingmeshdatasetwidget.h +++ b/src/gui/processing/qgsprocessingmeshdatasetwidget.h @@ -59,7 +59,7 @@ class GUI_EXPORT QgsProcessingMeshDatasetGroupsWidget : public QWidget QPointer mActionCurrentActiveDatasetGroups; QPointer mActionAvailableDatasetGroups; QgsMeshLayer *mMeshLayer = nullptr; - QMap mDatasetGroupsNames; //used to store the dataet groups name if layer is not referenced + QMap mDatasetGroupsNames; //used to store the dataset groups name if layer is not referenced QStringList datasetGroupsNames(); void updateSummaryText(); @@ -156,7 +156,7 @@ class GUI_EXPORT QgsProcessingMeshDatasetTimeWidget : public QWidget, private Ui void populateTimeSteps(); bool hasTemporalDataset() const; - //! Populates diretly the time steps combo box with the referenced layer, used if layer comes from project + //! Populates directly the time steps combo box with the referenced layer, used if layer comes from project void populateTimeStepsFromLayer(); //! Stores the dataset time steps to use them later depending of chosen dataset groups (setDatasetGroupIndexes()), used if layer does not come from project void storeTimeStepsFromLayer( QgsMeshLayer *layer ); diff --git a/src/gui/proj/qgscrsdefinitionwidget.cpp b/src/gui/proj/qgscrsdefinitionwidget.cpp index 2c66b8d4169c..ebecd65c84cf 100644 --- a/src/gui/proj/qgscrsdefinitionwidget.cpp +++ b/src/gui/proj/qgscrsdefinitionwidget.cpp @@ -133,16 +133,16 @@ void QgsCrsDefinitionWidget::validateCurrent() case Qgis::CrsDefinitionFormat::Wkt: { PROJ_STRING_LIST warnings = nullptr; - PROJ_STRING_LIST grammerErrors = nullptr; - crs.reset( proj_create_from_wkt( context, projDef.toUtf8().constData(), nullptr, &warnings, &grammerErrors ) ); + PROJ_STRING_LIST grammarErrors = nullptr; + crs.reset( proj_create_from_wkt( context, projDef.toUtf8().constData(), nullptr, &warnings, &grammarErrors ) ); QStringList warningStrings; - QStringList grammerStrings; + QStringList grammarStrings; for ( auto iter = warnings; iter && *iter; ++iter ) warningStrings << QString( *iter ); - for ( auto iter = grammerErrors; iter && *iter; ++iter ) - grammerStrings << QString( *iter ); + for ( auto iter = grammarErrors; iter && *iter; ++iter ) + grammarStrings << QString( *iter ); proj_string_list_destroy( warnings ); - proj_string_list_destroy( grammerErrors ); + proj_string_list_destroy( grammarErrors ); if ( crs ) { @@ -152,7 +152,7 @@ void QgsCrsDefinitionWidget::validateCurrent() else { QMessageBox::warning( this, tr( "Custom Coordinate Reference System" ), - tr( "This WKT projection definition is not valid:" ) + QStringLiteral( "\n\n" ) + warningStrings.join( '\n' ) + grammerStrings.join( '\n' ) ); + tr( "This WKT projection definition is not valid:" ) + QStringLiteral( "\n\n" ) + warningStrings.join( '\n' ) + grammarStrings.join( '\n' ) ); } break; } diff --git a/src/gui/proj/qgsprojectionselectiontreewidget.cpp b/src/gui/proj/qgsprojectionselectiontreewidget.cpp index 9eb2e5972313..3f1eaaa413b2 100644 --- a/src/gui/proj/qgsprojectionselectiontreewidget.cpp +++ b/src/gui/proj/qgsprojectionselectiontreewidget.cpp @@ -123,7 +123,7 @@ QgsProjectionSelectionTreeWidget::QgsProjectionSelectionTreeWidget( QWidget *par menu.exec( lstRecent->viewport()->mapToGlobal( pos ) ); } ); - // Install event fiter to catch delete key press on the recent crs list + // Install event filter to catch delete key press on the recent crs list lstRecent->installEventFilter( this ); mCheckBoxNoProjection->setHidden( true ); diff --git a/src/gui/proj/qgsprojectionselectionwidget.cpp b/src/gui/proj/qgsprojectionselectionwidget.cpp index 7570d710c7c3..70f1928d74ea 100644 --- a/src/gui/proj/qgsprojectionselectionwidget.cpp +++ b/src/gui/proj/qgsprojectionselectionwidget.cpp @@ -470,6 +470,7 @@ void QgsProjectionSelectionWidget::setOptionVisible( const QgsProjectionSelectio case QgsProjectionSelectionWidget::CurrentCrs: { mModel->setOption( option, visible ); + updateTooltip(); return; } case QgsProjectionSelectionWidget::RecentCrs: @@ -488,6 +489,7 @@ void QgsProjectionSelectionWidget::setOptionVisible( const QgsProjectionSelectio if ( !mModel->combinedModel()->currentCrs().isValid() ) whileBlocking( mCrsComboBox )->setCurrentIndex( 0 ); } + updateTooltip(); return; } @@ -589,8 +591,8 @@ void QgsProjectionSelectionWidget::selectCrs() mCrsComboBox->setCurrentIndex( mCrsComboBox->findData( QgsProjectionSelectionWidget::CurrentCrs, StandardCoordinateReferenceSystemsModel::Role::RoleOption ) ); mCrsComboBox->blockSignals( false ); const QgsCoordinateReferenceSystem crs = dlg.crs(); + // setCrs will emit crsChanged for us setCrs( crs ); - emit crsChanged( crs ); } else { @@ -719,21 +721,17 @@ void QgsProjectionSelectionWidget::setShowAccuracyWarnings( bool show ) void QgsProjectionSelectionWidget::comboIndexChanged( int idx ) { const QgsCoordinateReferenceSystem crs = mModel->data( mModel->index( idx, 0 ), StandardCoordinateReferenceSystemsModel::RoleCrs ).value< QgsCoordinateReferenceSystem >(); - switch ( static_cast< CrsOption >( mModel->data( mModel->index( idx, 0 ), StandardCoordinateReferenceSystemsModel::RoleOption ).toInt() ) ) + const QVariant optionData = mModel->data( mModel->index( idx, 0 ), StandardCoordinateReferenceSystemsModel::RoleOption ); + if ( !optionData.isValid() || static_cast< CrsOption >( optionData.toInt() ) != QgsProjectionSelectionWidget::CrsNotSet ) { - case QgsProjectionSelectionWidget::Invalid: - case QgsProjectionSelectionWidget::LayerCrs: - case QgsProjectionSelectionWidget::ProjectCrs: - case QgsProjectionSelectionWidget::DefaultCrs: - case QgsProjectionSelectionWidget::RecentCrs: - case QgsProjectionSelectionWidget::CurrentCrs: - emit crsChanged( crs ); - break; - - case QgsProjectionSelectionWidget::CrsNotSet: - emit cleared(); - emit crsChanged( QgsCoordinateReferenceSystem() ); - break; + // RoleOption is only available for items from the standard coordinate reference system model, but we + // are using a combined model which also has items from QgsRecentCoordinateReferenceSystemsModel + emit crsChanged( crs ); + } + else + { + emit cleared(); + emit crsChanged( QgsCoordinateReferenceSystem() ); } updateTooltip(); } diff --git a/src/gui/providers/qgspointcloudsourceselect.cpp b/src/gui/providers/qgspointcloudsourceselect.cpp index 2672a427e24c..1716a9d1dda9 100644 --- a/src/gui/providers/qgspointcloudsourceselect.cpp +++ b/src/gui/providers/qgspointcloudsourceselect.cpp @@ -77,17 +77,14 @@ void QgsPointCloudSourceSelect::addButtonClicked() for ( const QString &path : QgsFileWidget::splitFilePaths( mPath ) ) { - // auto determine preferred provider for each path - - const QList< QgsProviderRegistry::ProviderCandidateDetails > preferredProviders = QgsProviderRegistry::instance()->preferredProvidersForUri( path ); // maybe we should raise an assert if preferredProviders size is 0 or >1? Play it safe for now... - if ( preferredProviders.empty() ) - continue; - + const QList< QgsProviderRegistry::ProviderCandidateDetails > preferredProviders = QgsProviderRegistry::instance()->preferredProvidersForUri( path ); + // if no preferred providers we can still give pdal a try + const QString providerKey = preferredProviders.empty() ? QStringLiteral( "pdal" ) : preferredProviders.first().metadata()->key(); Q_NOWARN_DEPRECATED_PUSH - emit addPointCloudLayer( path, QFileInfo( path ).baseName(), preferredProviders.at( 0 ).metadata()->key() ) ; + emit addPointCloudLayer( path, QFileInfo( path ).baseName(), providerKey ) ; Q_NOWARN_DEPRECATED_POP - emit addLayer( Qgis::LayerType::PointCloud, path, QFileInfo( path ).baseName(), preferredProviders.at( 0 ).metadata()->key() ); + emit addLayer( Qgis::LayerType::PointCloud, path, QFileInfo( path ).baseName(), providerKey ); } } else if ( mDataSourceType == QLatin1String( "remote" ) ) diff --git a/src/gui/providers/sensorthings/qgssensorthingsguiprovider.cpp b/src/gui/providers/sensorthings/qgssensorthingsguiprovider.cpp index d9e4997ba416..eba10eaabe33 100644 --- a/src/gui/providers/sensorthings/qgssensorthingsguiprovider.cpp +++ b/src/gui/providers/sensorthings/qgssensorthingsguiprovider.cpp @@ -30,6 +30,9 @@ #include "qgssensorthingsprovider.h" #include "qgssensorthingsdataitemguiprovider.h" #include "qgsapplication.h" +#include "qgssubsetstringeditorprovider.h" +#include "qgssensorthingssubseteditor.h" +#include "qgsvectorlayer.h" // // QgsSensorThingsSourceSelectProvider @@ -112,4 +115,34 @@ QList QgsSensorThingsProviderGuiMetadata::dataItemGuiP return { new QgsSensorThingsDataItemGuiProvider() }; } +class QgsSensorThingsSubsetStringEditorProvider: public QgsSubsetStringEditorProvider +{ + public: + + QString providerKey() const override { return QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY; } + + bool canHandleLayer( QgsVectorLayer *layer ) const override + { + QgsDataProvider *provider = layer->dataProvider(); + return static_cast< bool >( qobject_cast( provider ) ); + } + + QgsSubsetStringEditorInterface *createDialog( QgsVectorLayer *layer, QWidget *parent, Qt::WindowFlags fl ) override + { + QgsDataProvider *provider = layer->dataProvider(); + QgsSensorThingsProvider *sensorThingsProvider = qobject_cast( provider ); + if ( !sensorThingsProvider ) + return nullptr; + + return new QgsSensorThingsSubsetEditor( layer, QgsFields(), parent, fl ); + } +}; + + +QList QgsSensorThingsProviderGuiMetadata::subsetStringEditorProviders() +{ + return QList() + << new QgsSensorThingsSubsetStringEditorProvider; +} + ///@endcond diff --git a/src/gui/providers/sensorthings/qgssensorthingsguiprovider.h b/src/gui/providers/sensorthings/qgssensorthingsguiprovider.h index 05124977e963..ce24861466b3 100644 --- a/src/gui/providers/sensorthings/qgssensorthingsguiprovider.h +++ b/src/gui/providers/sensorthings/qgssensorthingsguiprovider.h @@ -53,6 +53,8 @@ class QgsSensorThingsProviderGuiMetadata: public QgsProviderGuiMetadata QList sourceSelectProviders() override; QList sourceWidgetProviders() override; QList dataItemGuiProviders() override; + QList subsetStringEditorProviders() override; + }; ///@endcond diff --git a/src/gui/providers/sensorthings/qgssensorthingssourceselect.cpp b/src/gui/providers/sensorthings/qgssensorthingssourceselect.cpp index 44e850d17a5d..96407d1f32c6 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourceselect.cpp +++ b/src/gui/providers/sensorthings/qgssensorthingssourceselect.cpp @@ -24,6 +24,7 @@ #include "qgssensorthingsconnectiondialog.h" #include "qgssensorthingssourcewidget.h" #include "qgssensorthingsprovider.h" +#include "qgssensorthingssubseteditor.h" #include #include @@ -51,6 +52,9 @@ QgsSensorThingsSourceSelect::QgsSensorThingsSourceSelect( QWidget *parent, Qt::W vlayout->addWidget( mSourceWidget ); mLayerSettingsContainerWidget->setLayout( vlayout ); + txtSubsetSQL->setWrapMode( QgsCodeEditor::WrapWord ); + connect( pbnQueryBuilder, &QPushButton::clicked, this, &QgsSensorThingsSourceSelect::buildFilter ); + connect( mSourceWidget, &QgsProviderSourceWidget::validChanged, this, &QgsSensorThingsSourceSelect::validate ); connect( mConnectionWidget, &QgsSensorThingsConnectionWidget::validChanged, this, &QgsSensorThingsSourceSelect::validate ); @@ -158,13 +162,20 @@ void QgsSensorThingsSourceSelect::addButtonClicked() const bool isCustom = cmbConnections->currentData().toString() == QLatin1String( "~~custom~~" ); const QString providerUri = mConnectionWidget->sourceUri(); - const QString layerUri = mSourceWidget->updateUriFromGui( providerUri ); + QString layerUri = mSourceWidget->updateUriFromGui( providerUri ); + + QVariantMap uriParts = QgsProviderRegistry::instance()->decodeUri( + QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, + layerUri + ); + if ( !txtSubsetSQL->text().isEmpty() ) + uriParts.insert( QStringLiteral( "sql" ), txtSubsetSQL->text() ); - const QVariantMap uriParts = QgsProviderRegistry::instance()->decodeUri( - QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, - layerUri - ); + layerUri = QgsProviderRegistry::instance()->encodeUri( + QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, + uriParts + ); const Qgis::SensorThingsEntity type = QgsSensorThingsUtils::stringToEntity( uriParts.value( QStringLiteral( "entity" ) ).toString() ); @@ -212,6 +223,12 @@ void QgsSensorThingsSourceSelect::addButtonClicked() emit addLayer( Qgis::LayerType::Vector, layerUri, baseName, QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY ); } +void QgsSensorThingsSourceSelect::setMapCanvas( QgsMapCanvas *mapCanvas ) +{ + QgsAbstractDataSourceWidget::setMapCanvas( mapCanvas ); + mSourceWidget->setMapCanvas( mapCanvas ); +} + void QgsSensorThingsSourceSelect::populateConnectionList() { cmbConnections->blockSignals( true ); @@ -268,6 +285,17 @@ void QgsSensorThingsSourceSelect::cmbConnections_currentTextChanged( const QStri } } +void QgsSensorThingsSourceSelect::buildFilter() +{ + const QgsFields fields = QgsSensorThingsUtils::fieldsForEntityType( mSourceWidget->currentEntityType() ); + QgsSensorThingsSubsetEditor subsetEditor( nullptr, fields ); + subsetEditor.setSubsetString( txtSubsetSQL->text( ) ); + if ( subsetEditor.exec() ) + { + txtSubsetSQL->setText( subsetEditor.subsetString() ); + } +} + void QgsSensorThingsSourceSelect::validate() { const bool isValid = mConnectionWidget->isValid() && mSourceWidget->isValid(); diff --git a/src/gui/providers/sensorthings/qgssensorthingssourceselect.h b/src/gui/providers/sensorthings/qgssensorthingssourceselect.h index a2522ccf6db3..fa70550a39be 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourceselect.h +++ b/src/gui/providers/sensorthings/qgssensorthingssourceselect.h @@ -33,9 +33,8 @@ class QgsSensorThingsSourceSelect : public QgsAbstractDataSourceWidget, private public: QgsSensorThingsSourceSelect( QWidget *parent = nullptr, Qt::WindowFlags fl = QgsGuiUtils::ModalDialogFlags, QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::None ); - - //! Determines the layers the user selected void addButtonClicked() override; + void setMapCanvas( QgsMapCanvas *mapCanvas ) override; private slots: @@ -51,6 +50,7 @@ class QgsSensorThingsSourceSelect : public QgsAbstractDataSourceWidget, private void btnLoad_clicked(); //! Stores the selected datasource whenerver it is changed void cmbConnections_currentTextChanged( const QString &text ); + void buildFilter(); void validate(); diff --git a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp index b64ac28a8d1e..fd73b8fac82c 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp +++ b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.cpp @@ -24,6 +24,7 @@ #include "qgsiconutils.h" #include "qgssensorthingsconnectionpropertiestask.h" #include "qgsapplication.h" +#include "qgsextentwidget.h" #include #include #include @@ -33,7 +34,18 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent ) { setupUi( this ); + QVBoxLayout *vl = new QVBoxLayout(); + vl->setContentsMargins( 0, 0, 0, 0 ); + mExtentWidget = new QgsExtentWidget( nullptr, QgsExtentWidget::CondensedStyle ); + mExtentWidget->setNullValueAllowed( true, tr( "Not set" ) ); + mExtentWidget->setOutputCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + vl->addWidget( mExtentWidget ); + mExtentLimitFrame->setLayout( vl ); + mSpinPageSize->setClearValue( 0, tr( "Default (%1)" ).arg( QgsSensorThingsUtils::DEFAULT_PAGE_SIZE ) ); + mSpinFeatureLimit->setClearValue( 0, tr( "No limit" ) ); + // set a relatively conservative feature limit by default, to make it so they have to opt-in to shoot themselves in the foot! + mSpinFeatureLimit->setValue( QgsSensorThingsUtils::DEFAULT_FEATURE_LIMIT ); for ( Qgis::SensorThingsEntity type : { @@ -45,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 ) ); @@ -58,6 +71,7 @@ QgsSensorThingsSourceWidget::QgsSensorThingsSourceWidget( QWidget *parent ) connect( mSpinPageSize, qOverload< int >( &QSpinBox::valueChanged ), this, &QgsSensorThingsSourceWidget::validate ); connect( mRetrieveTypesButton, &QToolButton::clicked, this, &QgsSensorThingsSourceWidget::retrieveTypes ); mRetrieveTypesButton->setEnabled( false ); + connect( mExtentWidget, &QgsExtentWidget::extentChanged, this, &QgsSensorThingsSourceWidget::validate ); validate(); } @@ -92,6 +106,34 @@ void QgsSensorThingsSourceWidget::setSourceUri( const QString &uri ) mSpinPageSize->setValue( maxPageSizeParam ); } + ok = false; + const int featureLimitParam = mSourceParts.value( QStringLiteral( "featureLimit" ) ).toInt( &ok ); + if ( ok ) + { + mSpinFeatureLimit->setValue( featureLimitParam ); + } + else if ( type != Qgis::SensorThingsEntity::Invalid ) + { + // if not setting an initial uri for a new layer, use "no limit" if it's not present in the uri + mSpinFeatureLimit->clear(); + } + else + { + // when setting an initial uri, use the default, not "no limit" + mSpinFeatureLimit->setValue( QgsSensorThingsUtils::DEFAULT_FEATURE_LIMIT ); + } + + const QgsRectangle bounds = mSourceParts.value( QStringLiteral( "bounds" ) ).value< QgsRectangle >(); + if ( !bounds.isNull() ) + { + mExtentWidget->setCurrentExtent( bounds, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + mExtentWidget->setOutputExtentFromUser( bounds, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + } + else + { + mExtentWidget->clear(); + } + mIsValid = true; } @@ -108,6 +150,17 @@ QString QgsSensorThingsSourceWidget::groupTitle() const return QObject::tr( "SensorThings Configuration" ); } +void QgsSensorThingsSourceWidget::setMapCanvas( QgsMapCanvas *mapCanvas ) +{ + QgsProviderSourceWidget::setMapCanvas( mapCanvas ); + mExtentWidget->setMapCanvas( mapCanvas, false ); +} + +Qgis::SensorThingsEntity QgsSensorThingsSourceWidget::currentEntityType() const +{ + return mComboEntityType->currentData().value< Qgis::SensorThingsEntity >(); +} + QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connectionUri ) const { QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( @@ -138,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; } @@ -152,6 +208,20 @@ QString QgsSensorThingsSourceWidget::updateUriFromGui( const QString &connection parts.remove( QStringLiteral( "pageSize" ) ); } + if ( mSpinFeatureLimit->value() > 0 ) + { + parts.insert( QStringLiteral( "featureLimit" ), QString::number( mSpinFeatureLimit->value() ) ); + } + else + { + parts.remove( QStringLiteral( "featureLimit" ) ); + } + + if ( mExtentWidget->outputExtent().isNull() ) + parts.remove( QStringLiteral( "bounds" ) ); + else + parts.insert( QStringLiteral( "bounds" ), QVariant::fromValue( mExtentWidget->outputExtent() ) ); + return QgsProviderRegistry::instance()->encodeUri( QgsSensorThingsProvider::SENSORTHINGS_PROVIDER_KEY, parts @@ -231,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 ) ); @@ -241,10 +313,36 @@ 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 ); } } @@ -266,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/providers/sensorthings/qgssensorthingssourcewidget.h b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.h index a54c0ebb9c35..1ae3f03408c1 100644 --- a/src/gui/providers/sensorthings/qgssensorthingssourcewidget.h +++ b/src/gui/providers/sensorthings/qgssensorthingssourcewidget.h @@ -23,7 +23,7 @@ #include #include -class QgsFileWidget; +class QgsExtentWidget; class QgsSensorThingsConnectionPropertiesTask; ///@cond PRIVATE @@ -40,6 +40,8 @@ class QgsSensorThingsSourceWidget : public QgsProviderSourceWidget, protected Ui void setSourceUri( const QString &uri ) override; QString sourceUri() const override; QString groupTitle() const override; + void setMapCanvas( QgsMapCanvas *mapCanvas ) override; + Qgis::SensorThingsEntity currentEntityType() const; /** * Updates a connection uri with the layer specific URI settings defined in the widget. @@ -59,6 +61,7 @@ class QgsSensorThingsSourceWidget : public QgsProviderSourceWidget, protected Ui void rebuildGeometryTypes( Qgis::SensorThingsEntity type ); void setCurrentGeometryTypeFromString( const QString &geometryType ); + QgsExtentWidget *mExtentWidget = nullptr; QVariantMap mSourceParts; bool mIsValid = false; QPointer< QgsSensorThingsConnectionPropertiesTask > mPropertiesTask; diff --git a/src/gui/providers/sensorthings/qgssensorthingssubseteditor.cpp b/src/gui/providers/sensorthings/qgssensorthingssubseteditor.cpp new file mode 100644 index 000000000000..c301ebf44b75 --- /dev/null +++ b/src/gui/providers/sensorthings/qgssensorthingssubseteditor.cpp @@ -0,0 +1,158 @@ +/*************************************************************************** + qgssensorthingssubseteditor.cpp + -------------------------------------- + Date : February 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 "qgssensorthingssubseteditor.h" +#include "qgsvectorlayer.h" +#include "qgscodeeditor.h" +#include "qgsfieldproxymodel.h" +#include "qgsfieldmodel.h" + +#include +#include +#include +#include + +///@cond PRIVATE + +QgsSensorThingsSubsetEditor::QgsSensorThingsSubsetEditor( QgsVectorLayer *layer, const QgsFields &fields, QWidget *parent, Qt::WindowFlags fl ) + : QgsSubsetStringEditorInterface( parent, fl ) + , mLayer( layer ) + , mFields( ( layer && fields.isEmpty() ) ? layer->fields() : fields ) +{ + setupUi( this ); + + mSubsetEditor = new QgsCodeEditor(); + mSubsetEditor->setWrapMode( QsciScintilla::WrapWord ); + + QVBoxLayout *vl = new QVBoxLayout(); + vl->addWidget( mSubsetEditor ); + mEditorGroupBox->setLayout( vl ); + + mModelFields = new QgsFieldProxyModel(); + mModelFields->setFilters( QgsFieldProxyModel::Filter::AllTypes | QgsFieldProxyModel::Filter::OriginProvider ); + mModelFields->sourceFieldModel()->setFields( mFields ); + lstFields->setModel( mModelFields ); + lstFields->setViewMode( QListView::ListMode ); + lstFields->setUniformItemSizes( true ); + lstFields->setAlternatingRowColors( true ); + lstFields->setSelectionBehavior( QAbstractItemView::SelectRows ); + + QFont boldFont = font(); + boldFont.setBold( true ); + mLabelComparisons->setFont( boldFont ); + mLabelLogical->setFont( boldFont ); + mLabelArithmetic->setFont( boldFont ); + + mButtonEq->setToolTip( tr( "Equal" ) ); + mButtonEq->setProperty( "expression", " eq " ); + mButtonNe->setToolTip( tr( "Not equal" ) ); + mButtonNe->setProperty( "expression", " ne " ); + mButtonGt->setToolTip( tr( "Greater than" ) ); + mButtonGt->setProperty( "expression", " gt " ); + mButtonGe->setToolTip( tr( "Greater than or equal" ) ); + mButtonGe->setProperty( "expression", " ge " ); + mButtonLt->setToolTip( tr( "Less than" ) ); + mButtonLt->setProperty( "expression", " lt " ); + mButtonLe->setToolTip( tr( "Less than or equal" ) ); + mButtonLe->setProperty( "expression", " le " ); + mButtonAnd->setToolTip( tr( "Logical and" ) ); + mButtonAnd->setProperty( "expression", " and " ); + mButtonOr->setToolTip( tr( "Logical or" ) ); + mButtonOr->setProperty( "expression", " or " ); + mButtonNot->setToolTip( tr( "Logical negation" ) ); + mButtonNot->setProperty( "expression", " not " ); + mButtonAdd->setToolTip( tr( "Addition" ) ); + mButtonAdd->setProperty( "expression", " add " ); + mButtonSub->setToolTip( tr( "Subtraction" ) ); + mButtonSub->setProperty( "expression", " sub " ); + mButtonMul->setToolTip( tr( "Multiplication" ) ); + mButtonMul->setProperty( "expression", " mul " ); + mButtonDiv->setToolTip( tr( "Division" ) ); + mButtonDiv->setProperty( "expression", " div " ); + mButtonMod->setToolTip( tr( "Modulo" ) ); + mButtonMod->setProperty( "expression", " mod " ); + + if ( mLayer ) + lblDataUri->setText( tr( "Set filter on %1" ).arg( mLayer->name() ) ); + else + lblDataUri->setText( tr( "Set filter for layer" ) ); + + connect( mButtonBox->button( QDialogButtonBox::Reset ), &QPushButton::clicked, this, &QgsSensorThingsSubsetEditor::reset ); + connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsSensorThingsSubsetEditor::accept ); + connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsSensorThingsSubsetEditor::reject ); + + connect( lstFields, &QListView::doubleClicked, this, &QgsSensorThingsSubsetEditor::lstFieldsDoubleClicked ); + + for ( QPushButton *button : + { + mButtonEq, + mButtonNe, + mButtonGt, + mButtonGe, + mButtonLt, + mButtonLe, + mButtonAnd, + mButtonOr, + mButtonNot, + mButtonAdd, + mButtonSub, + mButtonMul, + mButtonDiv, + mButtonMod + } ) + { + connect( button, &QPushButton::clicked, this, [this, button] + { + mSubsetEditor->insertText( button->property( "expression" ).toString() ); + mSubsetEditor->setFocus(); + } ); + + } +} + +QString QgsSensorThingsSubsetEditor::subsetString() const +{ + return mSubsetEditor->text(); +} + +void QgsSensorThingsSubsetEditor::setSubsetString( const QString &subsetString ) +{ + mSubsetEditor->setText( subsetString ); +} + +void QgsSensorThingsSubsetEditor::accept() +{ + if ( mLayer ) + { + mLayer->setSubsetString( subsetString() ); + } + QDialog::accept(); +} + +void QgsSensorThingsSubsetEditor::reset() +{ + mSubsetEditor->clear(); +} + +void QgsSensorThingsSubsetEditor::lstFieldsDoubleClicked( const QModelIndex &index ) +{ + mSubsetEditor->insertText( mModelFields->data( index, static_cast< int >( QgsFieldModel::CustomRole::FieldName ) ).toString() ); + mSubsetEditor->setFocus(); +} + + +///@endcond diff --git a/src/gui/providers/sensorthings/qgssensorthingssubseteditor.h b/src/gui/providers/sensorthings/qgssensorthingssubseteditor.h new file mode 100644 index 000000000000..9009fc4cb194 --- /dev/null +++ b/src/gui/providers/sensorthings/qgssensorthingssubseteditor.h @@ -0,0 +1,61 @@ +/*************************************************************************** + qgssensorthingssubseteditor.h + -------------------------------------- + Date : February 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 QGSSENSORTHINGSSUBSETEDITOR_H +#define QGSSENSORTHINGSSUBSETEDITOR_H + +#include "qgis.h" +#include "ui_qgssensorthingssubseteditorbase.h" +#include "qgssubsetstringeditorinterface.h" +#include "qgsfields.h" +#include +#include + +class QgsVectorLayer; +class QgsCodeEditor; +class QgsFieldProxyModel; + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsSensorThingsSubsetEditor : public QgsSubsetStringEditorInterface, protected Ui::QgsSensorThingsSubsetEditorBase +{ + Q_OBJECT + + public: + QgsSensorThingsSubsetEditor( QgsVectorLayer *layer = nullptr, + const QgsFields &fields = QgsFields(), + QWidget *parent SIP_TRANSFERTHIS = nullptr, + Qt::WindowFlags fl = QgsGuiUtils::ModalDialogFlags ); + QString subsetString() const override; + void setSubsetString( const QString &subsetString ) override; + + private slots: + void accept() override; + void reset(); + void lstFieldsDoubleClicked( const QModelIndex &index ); + private: + + QgsCodeEditor *mSubsetEditor = nullptr; + + QPointer< QgsVectorLayer > mLayer; + QgsFields mFields; + + QgsFieldProxyModel *mModelFields = nullptr; +}; + +///@endcond +#endif // QGSSENSORTHINGSSUBSETEDITOR_H diff --git a/src/gui/qgsabstractrelationeditorwidget.cpp b/src/gui/qgsabstractrelationeditorwidget.cpp index 51870df2a237..3f4ef2b96479 100644 --- a/src/gui/qgsabstractrelationeditorwidget.cpp +++ b/src/gui/qgsabstractrelationeditorwidget.cpp @@ -292,7 +292,7 @@ QgsFeatureIds QgsAbstractRelationEditorWidget::addFeature( const QgsGeometry &ge addedFeatureIds.insert( linkFeature.id() ); - // In multiedit add to other features to but whitout dialog + // In multiedit add to other features to but without dialog for ( const QgsFeature &feature : std::as_const( mFeatureList ) ) { // First feature already added diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 16c190d1335a..36dacdfc4e16 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -3168,7 +3168,7 @@ void QgsAttributeForm::updateFieldDependenciesDefaultValue( QgsEditorWidgetWrapp } else { - //otherwise just enter for the field depening on + //otherwise just enter for the field depending on const QSet referencedColumns = exp.referencedColumns(); for ( const QString &referencedColumn : referencedColumns ) { @@ -3200,7 +3200,7 @@ void QgsAttributeForm::updateFieldDependenciesVirtualFields( QgsEditorWidgetWrap } else { - //otherwise just enter for the field depening on + //otherwise just enter for the field depending on const QSet referencedColumns = exp.referencedColumns(); for ( const QString &referencedColumn : referencedColumns ) { diff --git a/src/gui/qgsexpressionbuilderwidget.cpp b/src/gui/qgsexpressionbuilderwidget.cpp index 4bc0b3a96ed6..10fff0940176 100644 --- a/src/gui/qgsexpressionbuilderwidget.cpp +++ b/src/gui/qgsexpressionbuilderwidget.cpp @@ -617,6 +617,11 @@ bool QgsExpressionBuilderWidget::isExpressionValid() return mExpressionValid; } +void QgsExpressionBuilderWidget::setCustomPreviewGenerator( const QString &label, const QList > &choices, const std::function &previewContextGenerator ) +{ + mExpressionPreviewWidget->setCustomPreviewGenerator( label, choices, previewContextGenerator ); +} + void QgsExpressionBuilderWidget::saveToRecent( const QString &collection ) { mExpressionTreeView->saveToRecent( expressionText(), collection ); diff --git a/src/gui/qgsexpressionbuilderwidget.h b/src/gui/qgsexpressionbuilderwidget.h index aa48c926ba9a..c341e326229f 100644 --- a/src/gui/qgsexpressionbuilderwidget.h +++ b/src/gui/qgsexpressionbuilderwidget.h @@ -151,6 +151,63 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp //! Returns if the expression is valid bool isExpressionValid(); +#ifndef SIP_RUN + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator ); +#else + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); + % MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS + % End +#endif + + /** * Adds the current expression to the given \a collection. * By default it is saved to the collection "generic". diff --git a/src/gui/qgsexpressionpreviewwidget.cpp b/src/gui/qgsexpressionpreviewwidget.cpp index db69734af17d..acdd0984b036 100644 --- a/src/gui/qgsexpressionpreviewwidget.cpp +++ b/src/gui/qgsexpressionpreviewwidget.cpp @@ -33,10 +33,23 @@ QgsExpressionPreviewWidget::QgsExpressionPreviewWidget( QWidget *parent ) mCopyPreviewAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ), tr( "Copy Expression Value" ), this ); mPreviewLabel->addAction( mCopyPreviewAction ); mFeaturePickerWidget->setShowBrowserButtons( true ); + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + mStackedWidget->setCurrentWidget( mPageFeaturePicker ); + mCustomButtonNext->setEnabled( false ); + mCustomButtonPrev->setEnabled( false ); connect( mFeaturePickerWidget, &QgsFeaturePickerWidget::featureChanged, this, &QgsExpressionPreviewWidget::setCurrentFeature ); + connect( mCustomComboBox, qOverload< int >( &QComboBox::currentIndexChanged ), this, &QgsExpressionPreviewWidget::setCustomChoice ); connect( mPreviewLabel, &QLabel::linkActivated, this, &QgsExpressionPreviewWidget::linkActivated ); connect( mCopyPreviewAction, &QAction::triggered, this, &QgsExpressionPreviewWidget::copyFullExpressionValue ); + connect( mCustomButtonPrev, &QToolButton::clicked, this, [this] + { + mCustomComboBox->setCurrentIndex( std::max( 0, mCustomComboBox->currentIndex() - 1 ) ); + } ); + connect( mCustomButtonNext, &QToolButton::clicked, this, [this] + { + mCustomComboBox->setCurrentIndex( std::min( mCustomComboBox->count() - 1, mCustomComboBox->currentIndex() + 1 ) ); + } ); } void QgsExpressionPreviewWidget::setLayer( QgsVectorLayer *layer ) @@ -48,6 +61,21 @@ void QgsExpressionPreviewWidget::setLayer( QgsVectorLayer *layer ) } } +void QgsExpressionPreviewWidget::setCustomPreviewGenerator( const QString &label, const QList > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator ) +{ + mCustomPreviewGeneratorFunction = previewContextGenerator; + mStackedWidget->setCurrentWidget( mPageCustomPicker ); + mCustomLabel->setText( label ); + mCustomComboBox->blockSignals( true ); + mCustomComboBox->clear(); + for ( const auto &choice : choices ) + { + mCustomComboBox->addItem( choice.first, choice.second ); + } + mCustomComboBox->blockSignals( false ); + setCustomChoice( 0 ); +} + void QgsExpressionPreviewWidget::setExpressionText( const QString &expression ) { if ( expression != mExpressionText ) @@ -59,7 +87,7 @@ void QgsExpressionPreviewWidget::setExpressionText( const QString &expression ) void QgsExpressionPreviewWidget::setCurrentFeature( const QgsFeature &feature ) { - // todo: update the combo box if it has been set externaly? + // todo: update the combo box if it has been set externally? if ( feature != mExpressionContext.feature() ) { mExpressionContext.setFeature( feature ); @@ -198,6 +226,11 @@ bool QgsExpressionPreviewWidget::parserError() const return mParserError; } +QString QgsExpressionPreviewWidget::currentPreviewText() const +{ + return mPreviewLabel->text(); +} + void QgsExpressionPreviewWidget::setEvalError( bool evalError ) { if ( evalError == mEvalError ) @@ -220,3 +253,15 @@ void QgsExpressionPreviewWidget::copyFullExpressionValue() QgsDebugMsgLevel( QStringLiteral( "set clipboard: %1" ).arg( copiedValue ), 4 ); clipboard->setText( copiedValue ); } + +void QgsExpressionPreviewWidget::setCustomChoice( int ) +{ + const QVariant selectedValue = mCustomComboBox->currentData(); + + mCustomButtonPrev->setEnabled( mCustomComboBox->currentIndex() > 0 && mCustomComboBox->count() > 0 ); + mCustomButtonNext->setEnabled( mCustomComboBox->currentIndex() < ( mCustomComboBox->count() - 1 ) && mCustomComboBox->count() > 0 ); + + mExpressionContext = mCustomPreviewGeneratorFunction( selectedValue ); + + refreshPreview(); +} diff --git a/src/gui/qgsexpressionpreviewwidget.h b/src/gui/qgsexpressionpreviewwidget.h index 2a20b891d07c..38ff08755763 100644 --- a/src/gui/qgsexpressionpreviewwidget.h +++ b/src/gui/qgsexpressionpreviewwidget.h @@ -25,6 +25,8 @@ #include "qgsexpression.h" #include "qgsexpressioncontext.h" +#include + class QAction; class QgsVectorLayer; @@ -44,6 +46,62 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp //! Sets the layer used in the preview void setLayer( QgsVectorLayer *layer ); +#ifndef SIP_RUN + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator ); +#else + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); + % MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS + % End +#endif + //! Sets the expression void setExpressionText( const QString &expression ); @@ -79,9 +137,16 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp //! Returns the root node of the expression const QgsExpressionNode *rootNode() const {return mExpression.rootNode();} - //! Returns the expression parser erros + //! Returns the expression parser errors QList parserErrors() const {return mExpression.parserErrors();} + /** + * Returns the current expression result preview text. + * + * \since QGIS 3.38 + */ + QString currentPreviewText() const; + signals: /** @@ -117,6 +182,7 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp void setEvalError( bool evalError ); void setParserError( bool parserError ); void copyFullExpressionValue(); + void setCustomChoice( int ); private: void setExpressionToolTip( const QString &toolTip ); @@ -132,6 +198,8 @@ class GUI_EXPORT QgsExpressionPreviewWidget : public QWidget, private Ui::QgsExp QString mExpressionText; QgsExpression mExpression; QAction *mCopyPreviewAction = nullptr; + + std::function< QgsExpressionContext( const QVariant & ) > mCustomPreviewGeneratorFunction; }; #endif // QGSEXPRESSIONPREVIEWWIDGET_H diff --git a/src/gui/qgsfeaturelistcombobox.cpp b/src/gui/qgsfeaturelistcombobox.cpp index 2a23ae3162ab..86e7c15fc1e3 100644 --- a/src/gui/qgsfeaturelistcombobox.cpp +++ b/src/gui/qgsfeaturelistcombobox.cpp @@ -62,6 +62,7 @@ QgsFeatureListComboBox::QgsFeatureListComboBox( QWidget *parent ) setModel( mModel ); connect( mLineEdit, &QgsFilterLineEdit::textEdited, this, &QgsFeatureListComboBox::onCurrentTextChanged ); + connect( mLineEdit, &QgsFilterLineEdit::cleared, this, &QgsFeatureListComboBox::onFilterLineEditCleared ); connect( mModel, &QgsFeatureFilterModel::currentFeatureChanged, this, &QgsFeatureListComboBox::currentFeatureChanged ); @@ -106,6 +107,13 @@ void QgsFeatureListComboBox::onCurrentTextChanged( const QString &text ) mModel->setFilterValue( text ); } +void QgsFeatureListComboBox::onFilterLineEditCleared() +{ + // Reset the combobox when the search is cleared + const QString clearedValue = allowNull() ? mLineEdit->nullValue() : mLineEdit->defaultValue(); + mModel->setFilterValue( clearedValue ); +} + void QgsFeatureListComboBox::onFilterUpdateCompleted() { if ( mPopupRequested ) diff --git a/src/gui/qgsfeaturelistcombobox.h b/src/gui/qgsfeaturelistcombobox.h index c5e5218e2a29..9d6c1cc76af9 100644 --- a/src/gui/qgsfeaturelistcombobox.h +++ b/src/gui/qgsfeaturelistcombobox.h @@ -253,6 +253,7 @@ class GUI_EXPORT QgsFeatureListComboBox : public QComboBox private slots: void onCurrentTextChanged( const QString &text ); + void onFilterLineEditCleared(); void onFilterUpdateCompleted(); void onLoadingChanged(); void onItemSelected( const QModelIndex &index ); diff --git a/src/gui/qgsfieldexpressionwidget.cpp b/src/gui/qgsfieldexpressionwidget.cpp index 63c843e3c1f0..fbc14eab63e6 100644 --- a/src/gui/qgsfieldexpressionwidget.cpp +++ b/src/gui/qgsfieldexpressionwidget.cpp @@ -162,6 +162,13 @@ void QgsFieldExpressionWidget::registerExpressionContextGenerator( const QgsExpr mExpressionContextGenerator = generator; } +void QgsFieldExpressionWidget::setCustomPreviewGenerator( const QString &label, const QList > &choices, const std::function &previewContextGenerator ) +{ + mCustomPreviewLabel = label; + mCustomChoices = choices; + mPreviewContextGenerator = previewContextGenerator; +} + void QgsFieldExpressionWidget::setLayer( QgsMapLayer *layer ) { QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ); @@ -243,6 +250,11 @@ void QgsFieldExpressionWidget::editExpression() dlg.setWindowTitle( mExpressionDialogTitle ); dlg.setAllowEvalErrors( mAllowEvalErrors ); + if ( !mCustomChoices.isEmpty() ) + { + dlg.expressionBuilder()->setCustomPreviewGenerator( mCustomPreviewLabel, mCustomChoices, mPreviewContextGenerator ); + } + if ( !vl ) dlg.expressionBuilder()->expressionTree()->loadFieldNames( mFieldProxyModel->sourceFieldModel()->fields() ); diff --git a/src/gui/qgsfieldexpressionwidget.h b/src/gui/qgsfieldexpressionwidget.h index 59836bb524b0..cd5eedf6cda0 100644 --- a/src/gui/qgsfieldexpressionwidget.h +++ b/src/gui/qgsfieldexpressionwidget.h @@ -152,6 +152,62 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget */ void registerExpressionContextGenerator( const QgsExpressionContextGenerator *generator ); +#ifndef SIP_RUN + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, const std::function< QgsExpressionContext( const QVariant & ) > &previewContextGenerator ); +#else + + /** + * Sets the widget to run using a custom preview generator. + * + * In this mode, the widget will call a callback function to generate a new QgsExpressionContext + * as the previewed object changes. This can be used to provide custom preview values for different + * objects (i.e. for objects which aren't vector layer features). + * + * \param label The label to display for the combo box presenting choices of objects. This should be a representative name, eg "Band" if the widget is showing choices of raster layer bands + * \param choices A list of choices to present to the user. Each choice is a pair of a human-readable label and a QVariant representing the object to preview. + * \param previewContextGenerator A function which takes a QVariant representing the object to preview, and returns a QgsExpressionContext to use for previewing the object. + * + * \since QGIS 3.38 + */ + void setCustomPreviewGenerator( const QString &label, const QList< QPair< QString, QVariant > > &choices, SIP_PYCALLABLE ); + % MethodCode + Py_XINCREF( a2 ); + Py_BEGIN_ALLOW_THREADS + sipCpp->setCustomPreviewGenerator( *a0, *a1, [a2]( const QVariant &value )->QgsExpressionContext + { + QgsExpressionContext res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a2, "D", &value, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QgsExpressionContext *t1 = reinterpret_cast( sipConvertToType( s, sipType_QgsExpressionContext, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QgsExpressionContext( *t1 ); + } + sipReleaseType( t1, sipType_QgsExpressionContext, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS + % End +#endif + /** * Allow accepting expressions with evaluation errors. This can be useful when we are not able to * provide an expression context of which we are sure it's completely populated. @@ -275,6 +331,10 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget QString mBackupExpression; bool mAllowEvalErrors = false; + QString mCustomPreviewLabel; + QList< QPair< QString, QVariant > > mCustomChoices; + std::function< QgsExpressionContext( const QVariant & ) > mPreviewContextGenerator; + friend class TestQgsFieldExpressionWidget; }; diff --git a/src/gui/qgsguiutils.h b/src/gui/qgsguiutils.h index 1aeb37d8878f..331e3075be60 100644 --- a/src/gui/qgsguiutils.h +++ b/src/gui/qgsguiutils.h @@ -135,7 +135,7 @@ namespace QgsGuiUtils QFont GUI_EXPORT getFont( bool &ok, const QFont &initial, const QString &title = QString() ); /** - * Restore the wigget geometry from settings. Will use the objetName() of the widget and if empty, or keyName is set, will + * Restore the wigget geometry from settings. Will use the objectName() of the widget and if empty, or keyName is set, will * use keyName to save state into settings. * \param widget The widget to restore. * \param keyName Override for objectName() if needed. @@ -189,7 +189,7 @@ namespace QgsGuiUtils /** * Returns a localized string representation of the \a value with the appropriate number of - * decimals supported by the \a dataType. Traling zeroes after decimal separator are not + * decimals supported by the \a dataType. Trailing zeroes after decimal separator are not * show unless \a displayTrailingZeroes is set. * Note that for floating point types the number of decimals may exceed the actual internal * precision because the precision is always calculated on the mantissa and the conversion to diff --git a/src/gui/qgshelp.cpp b/src/gui/qgshelp.cpp index 04438a322ce4..080e5a0a1af8 100644 --- a/src/gui/qgshelp.cpp +++ b/src/gui/qgshelp.cpp @@ -23,6 +23,7 @@ #include "qgsexpressioncontextutils.h" #include "qgsblockingnetworkrequest.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include #include 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/qgslayerpropertiesdialog.h b/src/gui/qgslayerpropertiesdialog.h index 2c47f009a58b..964e5345ee27 100644 --- a/src/gui/qgslayerpropertiesdialog.h +++ b/src/gui/qgslayerpropertiesdialog.h @@ -72,7 +72,7 @@ class GUI_EXPORT QgsLayerPropertiesDialog : public QgsOptionsDialogBase SIP_ABST /** * Sets the metadata \a widget and \a page associated with the dialog. * - * This must be called in order for the standard metadata loading/saving functionality to be avialable. + * This must be called in order for the standard metadata loading/saving functionality to be available. */ void setMetadataWidget( QgsMetadataWidget *widget, QWidget *page ); 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 830570c5f871..176629800ef2 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -48,6 +48,7 @@ email : sherman at mrcc.com #include "qgsapplication.h" #include "qgsexception.h" #include "qgsfeatureiterator.h" +#include "qgsgrouplayer.h" #include "qgslogger.h" #include "qgsmapcanvas.h" #include "qgsmapcanvasmap.h" @@ -92,6 +93,7 @@ email : sherman at mrcc.com #include "qgsvectortilelayer.h" #include "qgsscreenhelper.h" #include "qgs2dmapcontroller.h" +#include "qgsoverlaywidgetlayout.h" /** * \ingroup gui @@ -129,6 +131,9 @@ QgsMapCanvas::QgsMapCanvas( QWidget *parent ) , mExpressionContextScope( tr( "Map Canvas" ) ) { mScene = new QGraphicsScene(); + mLayout = new QgsOverlayWidgetLayout(); + setLayout( mLayout ); + setScene( mScene ); setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); @@ -280,6 +285,10 @@ QgsMapCanvas::~QgsMapCanvas() delete mCache; } +void QgsMapCanvas::addOverlayWidget( QWidget *widget, Qt::Edge edge ) +{ + mLayout->addWidget( widget, edge ); +} void QgsMapCanvas::cancelJobs() { @@ -1052,6 +1061,21 @@ void QgsMapCanvas::clearTemporalCache() if ( !alreadyInvalidatedThisLayer ) mCache->invalidateCacheForLayer( layer ); } + else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) + { + const QList childLayerList = gl->childLayers(); + for ( QgsMapLayer *childLayer : childLayerList ) + { + if ( childLayer->temporalProperties() && childLayer->temporalProperties()->isActive() ) + { + if ( childLayer->temporalProperties()->flags() & QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges ) + continue; + + mCache->invalidateCacheForLayer( layer ); + break; + } + } + } } if ( invalidateLabels ) @@ -1083,6 +1107,21 @@ void QgsMapCanvas::clearElevationCache() mCache->invalidateCacheForLayer( layer ); } + else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) + { + const QList childLayerList = gl->childLayers(); + for ( QgsMapLayer *childLayer : childLayerList ) + { + if ( childLayer->elevationProperties() && childLayer->elevationProperties()->hasElevation() ) + { + if ( childLayer->elevationProperties()->flags() & QgsMapLayerElevationProperties::FlagDontInvalidateCachedRendersWhenRangeChanges ) + continue; + + mCache->invalidateCacheForLayer( layer ); + break; + } + } + } } if ( invalidateLabels ) @@ -2649,13 +2688,13 @@ void QgsMapCanvas::setWheelFactor( double factor ) void QgsMapCanvas::zoomIn() { - // magnification is alreday handled in zoomByFactor + // magnification is already handled in zoomByFactor zoomByFactor( zoomInFactor() ); } void QgsMapCanvas::zoomOut() { - // magnification is alreday handled in zoomByFactor + // magnification is already handled in zoomByFactor zoomByFactor( zoomOutFactor() ); } diff --git a/src/gui/qgsmapcanvas.h b/src/gui/qgsmapcanvas.h index 0ca337f9aa6b..72045c576f33 100644 --- a/src/gui/qgsmapcanvas.h +++ b/src/gui/qgsmapcanvas.h @@ -73,6 +73,7 @@ class QgsMapCanvasAnnotationItem; class QgsReferencedRectangle; class QgsRenderedItemResults; class QgsTemporaryCursorOverride; +class QgsOverlayWidgetLayout; class QgsTemporalController; class QgsScreenHelper; @@ -111,6 +112,18 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView, public QgsExpressionContex ~QgsMapCanvas() override; + /** + * Adds an overlay \a widget to the layout, which will be bound to the specified \a edge. + * + * The widget will always float above the map canvas. + * + * \note Widgets on the left and right edges will always be positioned first, with + * top and bottom edge widgets expanding to take the remaining horizontal space. + * + * \since QGIS 3.38 + */ + void addOverlayWidget( QWidget *widget SIP_TRANSFER, Qt::Edge edge ); + /** * Returns the magnification factor */ @@ -1274,6 +1287,8 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView, public QgsExpressionContex double mLockedScale; }; + QgsOverlayWidgetLayout *mLayout = nullptr; + //! encompases all map settings necessary for map rendering QgsMapSettings mSettings; diff --git a/src/gui/qgsmaptoolcapture.cpp b/src/gui/qgsmaptoolcapture.cpp index 4b0c1ac02abe..e1044acb0825 100644 --- a/src/gui/qgsmaptoolcapture.cpp +++ b/src/gui/qgsmaptoolcapture.cpp @@ -40,6 +40,8 @@ #include #include #include +#include +#include QgsMapToolCapture::QgsMapToolCapture( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWidget *cadDockWidget, CaptureMode mode ) @@ -1381,13 +1383,13 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) if ( digitizingFinished ) { QgsGeometry g; - QgsCurve *curveToAdd = captureCurve()->clone(); + std::unique_ptr curveToAdd( captureCurve()->clone() ); if ( mode() == CaptureLine ) { - g = QgsGeometry( curveToAdd ); + g = QgsGeometry( curveToAdd->clone() ); geometryCaptured( g ); - lineCaptured( curveToAdd ); + lineCaptured( curveToAdd.release() ); } else { @@ -1401,22 +1403,22 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) if ( hasCurvedSegments && providerSupportsCurvedSegments ) { - curveToAdd = captureCurve()->clone(); + curveToAdd.reset( captureCurve()->clone() ); } else { - curveToAdd = captureCurve()->curveToLine(); + curveToAdd.reset( captureCurve()->curveToLine() ); } } else { - curveToAdd = captureCurve()->clone(); + curveToAdd.reset( captureCurve()->clone() ); } - QgsCurvePolygon *poly = new QgsCurvePolygon(); - poly->setExteriorRing( curveToAdd ); - g = QgsGeometry( poly ); + std::unique_ptr poly{new QgsCurvePolygon()}; + poly->setExteriorRing( curveToAdd.release() ); + g = QgsGeometry( poly->clone() ); geometryCaptured( g ); - polygonCaptured( poly ); + polygonCaptured( poly.get() ); } stopCapturing(); diff --git a/src/gui/qgsmaptoolidentify.cpp b/src/gui/qgsmaptoolidentify.cpp index 104f07a4ae59..718b57e76b71 100644 --- a/src/gui/qgsmaptoolidentify.cpp +++ b/src/gui/qgsmaptoolidentify.cpp @@ -51,6 +51,7 @@ #include "qgspointcloudlayer.h" #include "qgspointcloudrenderer.h" #include "qgspointcloudlayerelevationproperties.h" +#include "qgsrasterlayerelevationproperties.h" #include "qgssymbol.h" #include "qgsguiutils.h" @@ -213,30 +214,51 @@ bool QgsMapToolIdentify::identifyLayer( QList *results, QgsMapLa bool QgsMapToolIdentify::identifyLayer( QList *results, QgsMapLayer *layer, const QgsGeometry &geometry, const QgsRectangle &viewExtent, double mapUnitsPerPixel, QgsMapToolIdentify::LayerType layerType, const QgsIdentifyContext &identifyContext ) { - if ( layer->type() == Qgis::LayerType::Raster && layerType.testFlag( RasterLayer ) ) - { - return identifyRasterLayer( results, qobject_cast( layer ), geometry, viewExtent, mapUnitsPerPixel, identifyContext ); - } - else if ( layer->type() == Qgis::LayerType::Vector && layerType.testFlag( VectorLayer ) ) - { - return identifyVectorLayer( results, qobject_cast( layer ), geometry, identifyContext ); - } - else if ( layer->type() == Qgis::LayerType::Mesh && layerType.testFlag( MeshLayer ) ) - { - return identifyMeshLayer( results, qobject_cast( layer ), geometry, identifyContext ); - } - else if ( layer->type() == Qgis::LayerType::VectorTile && layerType.testFlag( VectorTileLayer ) ) - { - return identifyVectorTileLayer( results, qobject_cast( layer ), geometry, identifyContext ); - } - else if ( layer->type() == Qgis::LayerType::PointCloud && layerType.testFlag( PointCloudLayer ) ) - { - return identifyPointCloudLayer( results, qobject_cast( layer ), geometry, identifyContext ); - } - else + switch ( layer->type() ) { - return false; + case Qgis::LayerType::Vector: + if ( layerType.testFlag( VectorLayer ) ) + { + return identifyVectorLayer( results, qobject_cast( layer ), geometry, identifyContext ); + } + break; + + case Qgis::LayerType::Raster: + if ( layerType.testFlag( RasterLayer ) ) + { + return identifyRasterLayer( results, qobject_cast( layer ), geometry, viewExtent, mapUnitsPerPixel, identifyContext ); + } + break; + + case Qgis::LayerType::Mesh: + if ( layerType.testFlag( MeshLayer ) ) + { + return identifyMeshLayer( results, qobject_cast( layer ), geometry, identifyContext ); + } + break; + + case Qgis::LayerType::VectorTile: + if ( layerType.testFlag( VectorTileLayer ) ) + { + return identifyVectorTileLayer( results, qobject_cast( layer ), geometry, identifyContext ); + } + break; + + case Qgis::LayerType::PointCloud: + if ( layerType.testFlag( PointCloudLayer ) ) + { + return identifyPointCloudLayer( results, qobject_cast( layer ), geometry, identifyContext ); + } + break; + + // not supported + case Qgis::LayerType::Plugin: + case Qgis::LayerType::Annotation: + case Qgis::LayerType::Group: + case Qgis::LayerType::TiledScene: + break; } + return false; } bool QgsMapToolIdentify::identifyVectorLayer( QList *results, QgsVectorLayer *layer, const QgsPointXY &point, const QgsIdentifyContext &identifyContext ) @@ -256,6 +278,12 @@ bool QgsMapToolIdentify::identifyMeshLayer( QListelevationProperties()->isVisibleInZRange( identifyContext.zRange() ) ) + return false; + } + double searchRadius = mOverrideCanvasSearchRadius < 0 ? searchRadiusMU( mCanvas ) : mOverrideCanvasSearchRadius; bool isTemporal = identifyContext.isTemporal() && layer->temporalProperties()->isActive(); @@ -264,7 +292,7 @@ bool QgsMapToolIdentify::identifyMeshLayer( QListrendererSettings().activeVectorDatasetGroup(); const QList allGroup = layer->enabledDatasetGroupsIndexes(); - if ( isTemporal ) //non active dataset group value are only accesible if temporal is active + if ( isTemporal ) //non active dataset group value are only accessible if temporal is active { const QgsDateTimeRange &time = identifyContext.temporalRange(); if ( activeScalarGroup >= 0 ) @@ -510,11 +538,18 @@ bool QgsMapToolIdentify::identifyVectorTileLayer( QList *results, QgsPointCloudLayer *layer, const QgsGeometry &geometry, const QgsIdentifyContext &identifyContext ) { - Q_UNUSED( identifyContext ) + if ( !identifyContext.zRange().isInfinite() ) + { + if ( !layer->elevationProperties()->isVisibleInZRange( identifyContext.zRange(), layer ) ) + return false; + } + QgsPointCloudRenderer *renderer = layer->renderer(); QgsRenderContext context = QgsRenderContext::fromMapSettings( mCanvas->mapSettings() ); context.setCoordinateTransform( QgsCoordinateTransform( layer->crs(), mCanvas->mapSettings().destinationCrs(), mCanvas->mapSettings().transformContext() ) ); + if ( !identifyContext.zRange().isInfinite() ) + context.setZRange( identifyContext.zRange() ); const double searchRadiusMapUnits = mOverrideCanvasSearchRadius < 0 ? searchRadiusMU( mCanvas ) : mOverrideCanvasSearchRadius; @@ -971,6 +1006,12 @@ bool QgsMapToolIdentify::identifyRasterLayer( QList *results, Qg dprovider->temporalCapabilities()->setRequestedTemporalRange( identifyContext.temporalRange() ); } + if ( !identifyContext.zRange().isInfinite() ) + { + if ( !layer->elevationProperties()->isVisibleInZRange( identifyContext.zRange(), layer ) ) + return false; + } + QgsPointXY pointInCanvasCrs = point; try { @@ -1049,6 +1090,49 @@ bool QgsMapToolIdentify::identifyRasterLayer( QList *results, Qg identifyResult = dprovider->identify( point, format, viewExtent, width, height ); } + QgsRasterLayerElevationProperties *elevationProperties = qobject_cast< QgsRasterLayerElevationProperties *>( layer->elevationProperties() ); + if ( identifyResult.isValid() && !identifyContext.zRange().isInfinite() && elevationProperties && elevationProperties->isEnabled() ) + { + // filter results by z range + switch ( format ) + { + case Qgis::RasterIdentifyFormat::Value: + { + bool foundMatch = false; + QMap values = identifyResult.results(); + QMap filteredValues; + for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) + { + if ( QgsVariantUtils::isNull( it.value() ) ) + { + continue; + } + const double value = it.value().toDouble(); + const QgsDoubleRange elevationRange = elevationProperties->elevationRangeForPixelValue( layer, it.key(), value ); + if ( !elevationRange.isInfinite() && identifyContext.zRange().overlaps( elevationRange ) ) + { + filteredValues.insert( it.key(), it.value() ); + foundMatch = true; + } + } + + if ( !foundMatch ) + return false; + + identifyResult = QgsRasterIdentifyResult( Qgis::RasterIdentifyFormat::Value, filteredValues ); + + break; + } + + // can't filter by z for these formats + case Qgis::RasterIdentifyFormat::Undefined: + case Qgis::RasterIdentifyFormat::Text: + case Qgis::RasterIdentifyFormat::Html: + case Qgis::RasterIdentifyFormat::Feature: + break; + } + } + derivedAttributes.insert( derivedAttributesForPoint( QgsPoint( pointInCanvasCrs ) ) ); const double xres = layer->rasterUnitsPerPixelX(); diff --git a/src/gui/qgsmessagebaritem.cpp b/src/gui/qgsmessagebaritem.cpp index 218fb6bfc367..b315a6c2ea9e 100644 --- a/src/gui/qgsmessagebaritem.cpp +++ b/src/gui/qgsmessagebaritem.cpp @@ -165,7 +165,7 @@ void QgsMessageBarItem::writeContent() connect( mTextBrowser, &QTextBrowser::anchorClicked, this, &QgsMessageBarItem::urlClicked ); mTextBrowser->setFrameShape( QFrame::NoFrame ); - // stylesheet set here so Qt-style substitued scrollbar arrows can show within limited height + // stylesheet set here so Qt-style substituted scrollbar arrows can show within limited height // adjusts to height of font set in app options mTextBrowser->setStyleSheet( "QTextEdit { background-color: rgba(0,0,0,0); margin-top: 0.25em; max-height: 1.75em; min-height: 1.75em; } " "QScrollBar { background-color: rgba(0,0,0,0); } " diff --git a/src/gui/qgsnewvectortabledialog.h b/src/gui/qgsnewvectortabledialog.h index 38216271a4d8..2cff3fa567d1 100644 --- a/src/gui/qgsnewvectortabledialog.h +++ b/src/gui/qgsnewvectortabledialog.h @@ -112,7 +112,7 @@ class GUI_EXPORT QgsNewVectorTableDialog : public QDialog, private Ui_QgsNewVect void setFields( const QgsFields &fields ); /** - * Returns TRUE if spatialindex checkbox is cheched + * Returns TRUE if spatialindex checkbox is checked * @return */ bool createSpatialIndex(); diff --git a/src/gui/qgsoverlaywidgetlayout.cpp b/src/gui/qgsoverlaywidgetlayout.cpp new file mode 100644 index 000000000000..353e826e377f --- /dev/null +++ b/src/gui/qgsoverlaywidgetlayout.cpp @@ -0,0 +1,191 @@ +/*************************************************************************** + qgsoverlaywidgetlayout.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 "qgsoverlaywidgetlayout.h" +#include "qgis.h" +#include + +QgsOverlayWidgetLayout::QgsOverlayWidgetLayout( QWidget *parent ) + : QLayout( parent ) +{ + +} + +QgsOverlayWidgetLayout::~QgsOverlayWidgetLayout() +{ + QLayoutItem *item; + while ( ( item = takeAt( 0 ) ) ) + delete item; +} + +int QgsOverlayWidgetLayout::count() const +{ + return mLeftItems.size() + mRightItems.size() + mTopItems.size() + mBottomItems.size(); +} + +void QgsOverlayWidgetLayout::addItem( QLayoutItem *item ) +{ + if ( !mLeftItems.contains( item ) && !mRightItems.contains( item ) && !mTopItems.contains( item ) && !mBottomItems.contains( item ) ) + mLeftItems.append( item ); +} + +QLayoutItem *QgsOverlayWidgetLayout::itemAt( int index ) const +{ + if ( index < 0 ) + return nullptr; + + if ( index < mLeftItems.size() ) + return mLeftItems.at( index ); + index -= mLeftItems.size(); + + if ( index < mRightItems.size() ) + return mRightItems.at( index ); + index -= mRightItems.size(); + + if ( index < mTopItems.size() ) + return mTopItems.at( index ); + index -= mTopItems.size(); + + if ( index < mBottomItems.size() ) + return mBottomItems.at( index ); + + return nullptr; +} + +QLayoutItem *QgsOverlayWidgetLayout::takeAt( int index ) +{ + if ( index < 0 ) + return nullptr; + + if ( index < mLeftItems.size() ) + return mLeftItems.takeAt( index ); + index -= mLeftItems.size(); + + if ( index < mRightItems.size() ) + return mRightItems.takeAt( index ); + index -= mRightItems.size(); + + if ( index < mTopItems.size() ) + return mTopItems.takeAt( index ); + index -= mTopItems.size(); + + if ( index < mBottomItems.size() ) + return mBottomItems.takeAt( index ); + + return nullptr; +} + +QSize QgsOverlayWidgetLayout::sizeHint() const +{ + if ( QWidget *parent = parentWidget() ) + { + return parent->sizeHint(); + } + return QSize(); +} + +QSize QgsOverlayWidgetLayout::minimumSize() const +{ + if ( QWidget *parent = parentWidget() ) + { + return parent->minimumSize(); + } + return QSize(); +} + +void QgsOverlayWidgetLayout::setGeometry( const QRect &rect ) +{ + QLayout::setGeometry( rect ); + + int leftMargin = 0; + int rightMargin = 0; + int topMargin = 0; + int bottomMargin = 0; + getContentsMargins( &leftMargin, &topMargin, &rightMargin, &bottomMargin ); + + // adjust available rect to account for margins + const int innerLeft = rect.left() + leftMargin; + const int innerRight = rect.right() - rightMargin; + const int innerTop = rect.top() + topMargin; + const int innerBottom = rect.bottom() - bottomMargin; + const int innerHeight = innerBottom - innerTop; + + int left = innerLeft; + for ( QLayoutItem *item : std::as_const( mLeftItems ) ) + { + const QSize sizeHint = item->sizeHint(); + item->setGeometry( QRect( left, innerTop, sizeHint.width(), innerHeight ) ); + left += sizeHint.width() + mHorizontalSpacing; + } + + int right = innerRight; + for ( QLayoutItem *item : std::as_const( mRightItems ) ) + { + const QSize sizeHint = item->sizeHint(); + item->setGeometry( QRect( right - sizeHint.width(), innerTop, sizeHint.width(), innerHeight ) ); + right -= sizeHint.width() + mHorizontalSpacing; + } + + int top = innerTop; + for ( QLayoutItem *item : std::as_const( mTopItems ) ) + { + const QSize sizeHint = item->sizeHint(); + item->setGeometry( QRect( left, top, right - left, sizeHint.height() ) ); + top += sizeHint.height() + mVerticalSpacing; + } + + int bottom = innerBottom; + for ( QLayoutItem *item : std::as_const( mBottomItems ) ) + { + const QSize sizeHint = item->sizeHint(); + item->setGeometry( QRect( left, bottom - sizeHint.height(), right - left, sizeHint.height() ) ); + bottom -= sizeHint.height() + mVerticalSpacing; + } +} + +void QgsOverlayWidgetLayout::addWidget( QWidget *widget, Qt::Edge edge ) +{ + QWidgetItem *widgetItem = new QWidgetItem( widget ); + switch ( edge ) + { + case Qt::LeftEdge: + mLeftItems.append( widgetItem ); + break; + case Qt::RightEdge: + mRightItems.append( widgetItem ); + break; + case Qt::TopEdge: + mTopItems.append( widgetItem ); + break; + case Qt::BottomEdge: + mBottomItems.append( widgetItem ); + break; + } + + addChildWidget( widget ); + invalidate(); +} + +void QgsOverlayWidgetLayout::setHorizontalSpacing( int spacing ) +{ + mHorizontalSpacing = spacing; + invalidate(); +} + +void QgsOverlayWidgetLayout::setVerticalSpacing( int spacing ) +{ + mVerticalSpacing = spacing; + invalidate(); +} diff --git a/src/gui/qgsoverlaywidgetlayout.h b/src/gui/qgsoverlaywidgetlayout.h new file mode 100644 index 000000000000..176d092d112b --- /dev/null +++ b/src/gui/qgsoverlaywidgetlayout.h @@ -0,0 +1,98 @@ +/*************************************************************************** + qgsoverlaywidgetlayout.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 QGSOVERLAYWIDGETLAYOUT_H +#define QGSOVERLAYWIDGETLAYOUT_H + +#include "qgis_gui.h" +#include "qgis_sip.h" + +#include + +/** + * \ingroup gui + * \brief A custom layout which can be used to overlay child widgets over a parent widget. + * + * \since QGIS 3.38 + */ +class GUI_EXPORT QgsOverlayWidgetLayout : public QLayout +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsOverlayWidgetLayout, with the specified \a parent widget. + */ + QgsOverlayWidgetLayout( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + ~QgsOverlayWidgetLayout() override; + + int count() const final; + void addItem( QLayoutItem *item ) final; + QLayoutItem *itemAt( int index ) const final; + QLayoutItem *takeAt( int index ) final; + QSize sizeHint() const final; + QSize minimumSize() const final; + void setGeometry( const QRect &rect ) final; + + /** + * Adds a \a widget to the layout, which will be bound to the specified \a edge. + * + * \note Widgets on the left and right edges will always be positioned first, with + * top and bottom edge widgets expanding to take the remaining horizontal space. + */ + void addWidget( QWidget *widget SIP_TRANSFER, Qt::Edge edge ); + + /** + * Sets the spacing between widgets that are laid out side by side. + * + * \see horizontalSpacing() + */ + void setHorizontalSpacing( int spacing ); + + /** + * Returns the spacing between widgets that are laid out side by side. + * + * \see setHorizontalSpacing() + */ + int horizontalSpacing() const { return mHorizontalSpacing; } + + /** + * Sets the spacing between widgets that are laid out on top of each other. + * + * \see verticalSpacing() + */ + void setVerticalSpacing( int spacing ); + + /** + * Returns the spacing between widgets that are laid out on top of each other. + * + * \see setVerticalSpacing() + */ + int verticalSpacing() const { return mVerticalSpacing; } + + private: + + QList< QLayoutItem *> mLeftItems; + QList< QLayoutItem *> mRightItems; + QList< QLayoutItem *> mTopItems; + QList< QLayoutItem *> mBottomItems; + int mHorizontalSpacing = 0; + int mVerticalSpacing = 0; + + +}; + +#endif // QGSOVERLAYWIDGETLAYOUT_H diff --git a/src/gui/qgsowssourceselect.cpp b/src/gui/qgsowssourceselect.cpp index e9045726adc9..eb66835f4a17 100644 --- a/src/gui/qgsowssourceselect.cpp +++ b/src/gui/qgsowssourceselect.cpp @@ -32,8 +32,6 @@ #include "qgsowsconnection.h" #include "qgsdataprovider.h" #include "qgsowssourceselect.h" -#include "qgsnetworkaccessmanager.h" -#include "qgsapplication.h" #include "qgssettings.h" #include "qgsgui.h" @@ -80,10 +78,6 @@ QgsOWSSourceSelect::QgsOWSSourceSelect( const QString &service, QWidget *parent, clearCrs(); - mTileWidthLineEdit->setValidator( new QIntValidator( 0, 9999, this ) ); - mTileHeightLineEdit->setValidator( new QIntValidator( 0, 9999, this ) ); - mFeatureCountLineEdit->setValidator( new QIntValidator( 0, 9999, this ) ); - mCacheComboBox->addItem( tr( "Always Cache" ), QNetworkRequest::AlwaysCache ); mCacheComboBox->addItem( tr( "Prefer Cache" ), QNetworkRequest::PreferCache ); mCacheComboBox->addItem( tr( "Prefer Network" ), QNetworkRequest::PreferNetwork ); diff --git a/src/gui/qgspropertyoverridebutton.cpp b/src/gui/qgspropertyoverridebutton.cpp index 3394f1c065df..188cdeffc520 100644 --- a/src/gui/qgspropertyoverridebutton.cpp +++ b/src/gui/qgspropertyoverridebutton.cpp @@ -263,6 +263,13 @@ void QgsPropertyOverrideButton::mouseReleaseEvent( QMouseEvent *event ) return; } + // Middle button click to open the Expression Builder dialog + if ( event->button() == Qt::MiddleButton ) + { + showExpressionDialog(); + return; + } + // pass to default behavior QToolButton::mousePressEvent( event ); } diff --git a/src/gui/qgsquerybuilder.cpp b/src/gui/qgsquerybuilder.cpp index deff007fd215..cfcc47369e4c 100644 --- a/src/gui/qgsquerybuilder.cpp +++ b/src/gui/qgsquerybuilder.cpp @@ -14,13 +14,14 @@ ***************************************************************************/ #include "qgsquerybuilder.h" #include "qgslogger.h" -#include "qgsproject.h" #include "qgssettings.h" #include "qgsvectorlayer.h" #include "qgsvectordataprovider.h" #include "qgsapplication.h" #include "qgshelp.h" #include "qgsgui.h" +#include "qgsfieldproxymodel.h" +#include "qgsfieldmodel.h" #include #include @@ -83,6 +84,11 @@ QgsQueryBuilder::QgsQueryBuilder( QgsVectorLayer *layer, setupGuiViews(); + mModelFields = new QgsFieldProxyModel(); + mModelFields->setFilters( QgsFieldProxyModel::Filter::AllTypes | QgsFieldProxyModel::Filter::OriginProvider ); + mModelFields->sourceFieldModel()->setLayer( layer ); + lstFields->setModel( mModelFields ); + mOrigSubsetString = layer->subsetString(); connect( layer, &QgsVectorLayer::subsetStringChanged, this, &QgsQueryBuilder::layerSubsetStringChanged ); layerSubsetStringChanged(); @@ -93,8 +99,6 @@ QgsQueryBuilder::QgsQueryBuilder( QgsVectorLayer *layer, mFilterLineEdit->setShowSearchIcon( true ); mFilterLineEdit->setPlaceholderText( tr( "Search…" ) ); connect( mFilterLineEdit, &QgsFilterLineEdit::textChanged, this, &QgsQueryBuilder::onTextChanged ); - - populateFields(); } void QgsQueryBuilder::showEvent( QShowEvent *event ) @@ -103,36 +107,9 @@ void QgsQueryBuilder::showEvent( QShowEvent *event ) QDialog::showEvent( event ); } -void QgsQueryBuilder::populateFields() -{ - const QgsFields &fields = mLayer->fields(); - mTxtSql->setFields( fields ); - for ( int idx = 0; idx < fields.count(); ++idx ) - { - if ( fields.fieldOrigin( idx ) != QgsFields::OriginProvider ) - { - // only consider native fields - continue; - } - QStandardItem *myItem = new QStandardItem( fields.at( idx ).displayNameWithAlias() ); - myItem->setData( idx ); - myItem->setEditable( false ); - mModelFields->insertRow( mModelFields->rowCount(), myItem ); - } - - // All fields get ... setup - setupLstFieldsModel(); -} - -void QgsQueryBuilder::setupLstFieldsModel() -{ - lstFields->setModel( mModelFields ); -} - void QgsQueryBuilder::setupGuiViews() { //Initialize the models - mModelFields = new QStandardItemModel(); mModelValues = new QStandardItemModel(); mProxyValues = new QSortFilterProxyModel(); mProxyValues->setSourceModel( mModelValues ); @@ -150,13 +127,15 @@ void QgsQueryBuilder::setupGuiViews() lstValues->setModel( mProxyValues ); } -void QgsQueryBuilder::fillValues( int idx, int limit ) +void QgsQueryBuilder::fillValues( const QString &field, int limit ) { // clear the model mModelValues->clear(); + const int fieldIndex = mLayer->fields().lookupField( field ); + // determine the field type - QList values = qgis::setToList( mLayer->uniqueValues( idx, limit ) ); + QList values = qgis::setToList( mLayer->uniqueValues( fieldIndex, limit ) ); std::sort( values.begin(), values.end() ); const QString nullValue = QgsApplication::nullRepresentation(); @@ -204,7 +183,7 @@ void QgsQueryBuilder::btnSampleValues_clicked() } //Clear and fill the mModelValues - fillValues( mModelFields->data( lstFields->currentIndex(), Qt::UserRole + 1 ).toInt(), 25 ); + fillValues( mModelFields->data( lstFields->currentIndex(), static_cast< int >( QgsFieldModel::CustomRole::FieldName ) ).toString(), 25 ); if ( prevSubsetString != mLayer->subsetString() ) { @@ -227,7 +206,7 @@ void QgsQueryBuilder::btnGetAllValues_clicked() } //Clear and fill the mModelValues - fillValues( mModelFields->data( lstFields->currentIndex(), Qt::UserRole + 1 ).toInt(), -1 ); + fillValues( mModelFields->data( lstFields->currentIndex(), static_cast< int >( QgsFieldModel::CustomRole::FieldName ) ).toString(), -1 ); if ( prevSubsetString != mLayer->subsetString() ) { @@ -380,7 +359,7 @@ void QgsQueryBuilder::lstFields_clicked( const QModelIndex &index ) void QgsQueryBuilder::lstFields_doubleClicked( const QModelIndex &index ) { - mTxtSql->insertText( '\"' + mLayer->fields().at( mModelFields->data( index, Qt::UserRole + 1 ).toInt() ).name() + '\"' ); + mTxtSql->insertText( '\"' + mModelFields->data( index, static_cast< int >( QgsFieldModel::CustomRole::FieldName ) ).toString() + '\"' ); mTxtSql->setFocus(); } diff --git a/src/gui/qgsquerybuilder.h b/src/gui/qgsquerybuilder.h index 5844bb41ea1a..2a5e25b53ced 100644 --- a/src/gui/qgsquerybuilder.h +++ b/src/gui/qgsquerybuilder.h @@ -28,6 +28,7 @@ class QgsVectorLayer; class QgsCodeEditor; +class QgsFieldProxyModel; /** * \ingroup gui @@ -169,23 +170,17 @@ class GUI_EXPORT QgsQueryBuilder : public QgsSubsetStringEditorInterface, privat private: - /** - * Populate the field list for the selected table - */ - void populateFields(); - void showHelp(); /** * Setup models for listviews */ void setupGuiViews(); - void setupLstFieldsModel(); - void fillValues( int idx, int limit ); + void fillValues( const QString &field, int limit ); // private members //! Model for fields ListView - QStandardItemModel *mModelFields = nullptr; + QgsFieldProxyModel *mModelFields = nullptr; //! Model for values ListView QStandardItemModel *mModelValues = nullptr; //! Filter proxy Model for values ListView diff --git a/src/gui/qgsrangeslider.cpp b/src/gui/qgsrangeslider.cpp index 5225d93e2051..b0eaed600955 100644 --- a/src/gui/qgsrangeslider.cpp +++ b/src/gui/qgsrangeslider.cpp @@ -120,7 +120,15 @@ void QgsRangeSlider::setLowerValue( int lowerValue ) return; mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lowerValue ) ); - mUpperValue = std::max( mLowerValue, mUpperValue ); + if ( mFixedRangeSize >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } + else + { + mUpperValue = std::max( mLowerValue, mUpperValue ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -137,7 +145,16 @@ void QgsRangeSlider::setUpperValue( int upperValue ) return; mUpperValue = std::max( mStyleOption.minimum, std::min( mStyleOption.maximum, upperValue ) ); - mLowerValue = std::min( mLowerValue, mUpperValue ); + if ( mFixedRangeSize >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } + else + { + mLowerValue = std::min( mLowerValue, mUpperValue ); + } + emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -152,6 +169,15 @@ void QgsRangeSlider::setRange( int lower, int upper ) mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lower ) ); mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) ); + if ( mFixedRangeSize >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } + else + { + mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -317,6 +343,24 @@ QRect QgsRangeSlider::selectedRangeRect() return selectionRect.adjusted( -1, 1, 1, -1 ); } +int QgsRangeSlider::fixedRangeSize() const +{ + return mFixedRangeSize; +} + +void QgsRangeSlider::setFixedRangeSize( int size ) +{ + if ( size == mFixedRangeSize ) + return; + + mFixedRangeSize = size; + + if ( mFixedRangeSize >= 0 ) + setUpperValue( mLowerValue + mFixedRangeSize ); + + emit fixedRangeSizeChanged( mFixedRangeSize ); +} + void QgsRangeSlider::applyStep( int step ) { switch ( mFocusControl ) @@ -327,6 +371,11 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; + if ( mFixedRangeSize >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -339,6 +388,11 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; + if ( mFixedRangeSize >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -354,7 +408,15 @@ void QgsRangeSlider::applyStep( int step ) if ( newLowerValue != mLowerValue ) { mLowerValue = newLowerValue; - mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth ); + if ( mFixedRangeSize >= 0 ) + { + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } + else + { + mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -366,7 +428,15 @@ void QgsRangeSlider::applyStep( int step ) if ( newUpperValue != mUpperValue ) { mUpperValue = newUpperValue; - mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth ); + if ( mFixedRangeSize >= 0 ) + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } + else + { + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth ); + } emit rangeChanged( mLowerValue, mUpperValue ); update(); } @@ -604,6 +674,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } } } else if ( newPosition > mStartDragPos ) @@ -614,6 +690,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mLowerValue = mPreDragLowerValue; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } } } else @@ -623,11 +705,23 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) { changed = true; mUpperValue = mPreDragUpperValue; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } } if ( mLowerValue != mPreDragLowerValue ) { changed = true; mLowerValue = mPreDragLowerValue; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } } } } @@ -645,6 +739,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mLowerValue != newPosition ) { mLowerValue = newPosition; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + } + changed = true; } break; @@ -657,6 +758,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event ) if ( mUpperValue != newPosition ) { mUpperValue = newPosition; + if ( mFixedRangeSize >= 0 ) + { + // don't permit fixed width drags if it pushes the other value out of range + mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeSize ); + mUpperValue = std::min( mLowerValue + mFixedRangeSize, mStyleOption.maximum ); + } + changed = true; } break; diff --git a/src/gui/qgsrangeslider.h b/src/gui/qgsrangeslider.h index b94ded7e1b92..bd92989cffcb 100644 --- a/src/gui/qgsrangeslider.h +++ b/src/gui/qgsrangeslider.h @@ -169,6 +169,30 @@ class GUI_EXPORT QgsRangeSlider : public QWidget */ int pageStep() const; + /** + * Returns the slider's fixed range size, or -1 if not set. + * + * If a fixed range size is set then moving either the lower or upper slider will automatically + * move the other slider accordingly, in order to keep the selected range at the specified + * fixed size. + * + * \see setFixedRangeSize() + * \since QGIS 3.38 + */ + int fixedRangeSize() const; + + /** + * Sets the slider's fixed range \a size. Set to -1 if no fixed size is desired. + * + * If a fixed range size is set then moving either the lower or upper slider will automatically + * move the other slider accordingly, in order to keep the selected range at the specified + * fixed size. + * + * \see fixedRangeSize() + * \since QGIS 3.38 + */ + void setFixedRangeSize( int size ); + public slots: /** @@ -255,6 +279,16 @@ class GUI_EXPORT QgsRangeSlider : public QWidget */ void rangeLimitsChanged( int minimum, int maximum ); + /** + * Emitted when the widget's fixed range size is changed. + * + * \see fixedRangeSize() + * \see setFixedRangeSize() + * + * \since QGIS 3.38 + */ + void fixedRangeSizeChanged( int size ); + private: int pick( const QPoint &pt ) const; @@ -270,6 +304,8 @@ class GUI_EXPORT QgsRangeSlider : public QWidget int mSingleStep = 1; int mPageStep = 10; + int mFixedRangeSize = -1; + QStyleOptionSlider mStyleOption; enum Control { diff --git a/src/gui/qgsrasterlayersaveasdialog.cpp b/src/gui/qgsrasterlayersaveasdialog.cpp index c1157ed3b47f..43202019676d 100644 --- a/src/gui/qgsrasterlayersaveasdialog.cpp +++ b/src/gui/qgsrasterlayersaveasdialog.cpp @@ -679,7 +679,7 @@ void QgsRasterLayerSaveAsDialog::mLoadTransparentNoDataToolButton_clicked() const auto constTransparentSingleValuePixelList = rasterTransparency->transparentSingleValuePixelList(); for ( const QgsRasterTransparency::TransparentSingleValuePixel &transparencyPixel : constTransparentSingleValuePixelList ) { - if ( transparencyPixel.percentTransparent == 100 ) + if ( qgsDoubleNear( transparencyPixel.opacity, 0 ) ) { addNoDataRow( transparencyPixel.min, transparencyPixel.max ); if ( transparencyPixel.min != transparencyPixel.max ) diff --git a/src/gui/qgsscalevisibilitydialog.cpp b/src/gui/qgsscalevisibilitydialog.cpp index 6f18a86639f6..6c4af6c16029 100644 --- a/src/gui/qgsscalevisibilitydialog.cpp +++ b/src/gui/qgsscalevisibilitydialog.cpp @@ -54,7 +54,7 @@ QgsScaleVisibilityDialog::QgsScaleVisibilityDialog( QWidget *parent, const QStri dlgLayout->addWidget( buttonBox, 1, 0 ); } -void QgsScaleVisibilityDialog::setScaleVisiblity( bool hasScaleVisibility ) +void QgsScaleVisibilityDialog::setScaleVisibility( bool hasScaleVisibility ) { mGroupBox->setChecked( hasScaleVisibility ); } diff --git a/src/gui/qgsscalevisibilitydialog.h b/src/gui/qgsscalevisibilitydialog.h index 7a0e521295c8..3dddf31fd055 100644 --- a/src/gui/qgsscalevisibilitydialog.h +++ b/src/gui/qgsscalevisibilitydialog.h @@ -32,7 +32,7 @@ class QgsScaleRangeWidget; class GUI_EXPORT QgsScaleVisibilityDialog : public QDialog { Q_OBJECT - Q_PROPERTY( bool hasScaleVisibility READ hasScaleVisibility WRITE setScaleVisiblity ) + Q_PROPERTY( bool hasScaleVisibility READ hasScaleVisibility WRITE setScaleVisibility ) Q_PROPERTY( double minimumScale READ minimumScale WRITE setMinimumScale ) Q_PROPERTY( double maximumScale READ maximumScale WRITE setMaximumScale ) @@ -71,8 +71,16 @@ class GUI_EXPORT QgsScaleVisibilityDialog : public QDialog /** * Set whether scale based visibility is enabled. * \see hasScaleVisibility() + * \deprecated Use setScaleVisibility() */ - void setScaleVisiblity( bool hasScaleVisibility ); + Q_DECL_DEPRECATED void setScaleVisiblity( bool hasScaleVisibility ) SIP_DEPRECATED { setScaleVisibility( hasScaleVisibility ); } // spellok + + /** + * Set whether scale based visibility is enabled. + * \see hasScaleVisibility() + * \since QGIS 3.38 + */ + void setScaleVisibility( bool hasScaleVisibility ); /** * Set the minimum \a scale, or 0 to indicate the minimum is not set. 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/qgsvaliditycheckresultswidget.h b/src/gui/qgsvaliditycheckresultswidget.h index d26bace402f2..dbf4a85846df 100644 --- a/src/gui/qgsvaliditycheckresultswidget.h +++ b/src/gui/qgsvaliditycheckresultswidget.h @@ -30,7 +30,7 @@ class QgsValidityCheckContext; /** * \class QgsValidityCheckResultsModel * \ingroup gui - * \brief A QAbstractItemModel subclass for displaying the results from a QgsAbtractValidityCheck. + * \brief A QAbstractItemModel subclass for displaying the results from a QgsAbstractValidityCheck. * * \since QGIS 3.6 */ diff --git a/src/gui/qgswidgetstatehelper_p.cpp b/src/gui/qgswidgetstatehelper_p.cpp index f22de13c9def..88e2b3cb896f 100644 --- a/src/gui/qgswidgetstatehelper_p.cpp +++ b/src/gui/qgswidgetstatehelper_p.cpp @@ -52,7 +52,7 @@ bool QgsWidgetStateHelper::eventFilter( QObject *object, QEvent *event ) // there is no need to restore its geometry as it might lead to // an incorrect state of QFlags(WindowMinimized|WindowMaximized) // thus minimizing window after it just has been restored by WM. - // Inability to restore minimzed windows has been observed with + // Inability to restore minimized windows has been observed with // KWin 5.19 and Qt 5.15 running under X11. QWindow *win = widget->windowHandle(); if ( !win ) diff --git a/src/gui/raster/qgshillshaderendererwidget.cpp b/src/gui/raster/qgshillshaderendererwidget.cpp index 03d68fa10925..b7fa042ce86a 100644 --- a/src/gui/raster/qgshillshaderendererwidget.cpp +++ b/src/gui/raster/qgshillshaderendererwidget.cpp @@ -79,7 +79,7 @@ void QgsHillshadeRendererWidget::setFromRenderer( const QgsRasterRenderer *rende const QgsHillshadeRenderer *r = dynamic_cast( renderer ); if ( r ) { - mBandsCombo->setBand( r->band() ); + mBandsCombo->setBand( r->inputBand() ); mLightAngle->setValue( r->altitude() ); mLightAzimuth->setValue( r->azimuth() ); mZFactor->setValue( r->zFactor() ); diff --git a/src/gui/raster/qgspalettedrendererwidget.cpp b/src/gui/raster/qgspalettedrendererwidget.cpp index 219ca4d7148c..e813d957b44f 100644 --- a/src/gui/raster/qgspalettedrendererwidget.cpp +++ b/src/gui/raster/qgspalettedrendererwidget.cpp @@ -155,7 +155,7 @@ void QgsPalettedRendererWidget::setFromRenderer( const QgsRasterRenderer *r ) const QgsPalettedRasterRenderer *pr = dynamic_cast( r ); if ( pr ) { - mBand = pr->band(); + mBand = pr->inputBand(); whileBlocking( mBandComboBox )->setBand( mBand ); //read values and colors and fill into tree widget diff --git a/src/gui/raster/qgsrasterlayerproperties.cpp b/src/gui/raster/qgsrasterlayerproperties.cpp index 858d8684010e..be2d2436b3bb 100644 --- a/src/gui/raster/qgsrasterlayerproperties.cpp +++ b/src/gui/raster/qgsrasterlayerproperties.cpp @@ -995,29 +995,31 @@ void QgsRasterLayerProperties::apply() QgsRasterTransparency *rasterTransparency = new QgsRasterTransparency(); if ( mRasterTransparencyWidget->tableTransparency->columnCount() == 4 ) { - QgsRasterTransparency::TransparentThreeValuePixel myTransparentPixel; - QList myTransparentThreeValuePixelList; + QVector myTransparentThreeValuePixelList; for ( int myListRunner = 0; myListRunner < mRasterTransparencyWidget->tableTransparency->rowCount(); myListRunner++ ) { - myTransparentPixel.red = transparencyCellValue( myListRunner, 0 ); - myTransparentPixel.green = transparencyCellValue( myListRunner, 1 ); - myTransparentPixel.blue = transparencyCellValue( myListRunner, 2 ); - myTransparentPixel.percentTransparent = transparencyCellValue( myListRunner, 3 ); - myTransparentThreeValuePixelList.append( myTransparentPixel ); + const double red = transparencyCellValue( myListRunner, 0 ); + const double green = transparencyCellValue( myListRunner, 1 ); + const double blue = transparencyCellValue( myListRunner, 2 ); + const double opacity = 1.0 - transparencyCellValue( myListRunner, 3 ) / 100.0; + myTransparentThreeValuePixelList.append( + QgsRasterTransparency::TransparentThreeValuePixel( red, green, blue, opacity ) + ); } rasterTransparency->setTransparentThreeValuePixelList( myTransparentThreeValuePixelList ); } else if ( mRasterTransparencyWidget->tableTransparency->columnCount() == 3 ) { - QgsRasterTransparency::TransparentSingleValuePixel myTransparentPixel; - QList myTransparentSingleValuePixelList; + QVector myTransparentSingleValuePixelList; for ( int myListRunner = 0; myListRunner < mRasterTransparencyWidget->tableTransparency->rowCount(); myListRunner++ ) { - myTransparentPixel.min = transparencyCellValue( myListRunner, 0 ); - myTransparentPixel.max = transparencyCellValue( myListRunner, 1 ); - myTransparentPixel.percentTransparent = transparencyCellValue( myListRunner, 2 ); + const double min = transparencyCellValue( myListRunner, 0 ); + const double max = transparencyCellValue( myListRunner, 1 ); + const double opacity = 1.0 - transparencyCellValue( myListRunner, 2 ) / 100.0; - myTransparentSingleValuePixelList.append( myTransparentPixel ); + myTransparentSingleValuePixelList.append( + QgsRasterTransparency::TransparentSingleValuePixel( min, max, opacity ) + ); } rasterTransparency->setTransparentSingleValuePixelList( myTransparentSingleValuePixelList ); } diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp index 230324631199..05f6ff1b006f 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -16,13 +16,15 @@ ***************************************************************************/ #include "qgsrasterlayertemporalpropertieswidget.h" -#include "qgsgui.h" -#include "qgsproject.h" -#include "qgsprojecttimesettings.h" #include "qgsrasterdataprovidertemporalcapabilities.h" #include "qgsrasterlayer.h" #include "qgsrasterlayertemporalproperties.h" #include "qgsmaplayerconfigwidget.h" +#include "qgsdatetimeedit.h" +#include "qgsexpressionbuilderdialog.h" +#include "qgsexpressioncontextutils.h" +#include +#include QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( QWidget *parent, QgsRasterLayer *layer ) : QWidget( parent ) @@ -31,25 +33,55 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( Q_ASSERT( mLayer ); setupUi( this ); + // make a useful default expression for per band ranges, just to give users some hints about how to do this... + mFixedRangeLowerExpression = QStringLiteral( "make_datetime(%1,1,1,0,0,0) + make_interval(days:=@band)" ).arg( QDate::currentDate().year() ); + mFixedRangeUpperExpression = QStringLiteral( "make_datetime(%1,1,1,23,59,59) + make_interval(days:=@band)" ).arg( QDate::currentDate().year() ); + mExtraWidgetLayout = new QVBoxLayout(); mExtraWidgetLayout->setContentsMargins( 0, 0, 0, 0 ); mExtraWidgetLayout->addStretch(); mExtraWidgetContainer->setLayout( mExtraWidgetLayout ); - connect( mModeFixedRangeRadio, &QRadioButton::toggled, mFixedTimeRangeFrame, &QWidget::setEnabled ); + if ( mLayer->dataProvider() && mLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ) + { + mModeComboBox->addItem( tr( "Automatic" ), QVariant::fromValue( Qgis::RasterTemporalMode::TemporalRangeFromDataProvider ) ); + } + 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( "Redraw Layer Only" ), QVariant::fromValue( Qgis::RasterTemporalMode::RedrawLayerOnly ) ); + + mStackedWidget->setSizeMode( QgsStackedWidget::SizeMode::CurrentPageOnly ); + + mFixedRangePerBandModel = new QgsRasterBandFixedTemporalRangeModel( this ); + mBandRangesTable->verticalHeader()->setVisible( false ); + mBandRangesTable->setModel( mFixedRangePerBandModel ); + QgsFixedTemporalRangeDelegate *tableDelegate = new QgsFixedTemporalRangeDelegate( mBandRangesTable ); + mBandRangesTable->setItemDelegateForColumn( 1, tableDelegate ); + mBandRangesTable->setItemDelegateForColumn( 2, tableDelegate ); + + connect( mModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsRasterLayerTemporalPropertiesWidget::modeChanged ); connect( mTemporalGroupBox, &QGroupBox::toggled, this, &QgsRasterLayerTemporalPropertiesWidget::temporalGroupBoxChecked ); - connect( mModeRedrawLayer, &QRadioButton::toggled, mLabelRedrawLayer, &QWidget::setEnabled ); mStartTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); mEndTemporalDateTimeEdit->setDisplayFormat( QStringLiteral( "yyyy-MM-dd HH:mm:ss" ) ); - if ( !mLayer->dataProvider() || !mLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ) + QMenu *calculateFixedRangePerBandMenu = new QMenu( mCalculateFixedRangePerBandButton ); + mCalculateFixedRangePerBandButton->setMenu( calculateFixedRangePerBandMenu ); + mCalculateFixedRangePerBandButton->setPopupMode( QToolButton::InstantPopup ); + QAction *calculateLowerAction = new QAction( "Calculate Beginning by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateLowerAction ); + connect( calculateLowerAction, &QAction::triggered, this, [this] { - mModeAutomaticRadio->setEnabled( false ); - mModeAutomaticRadio->setChecked( false ); - mModeFixedRangeRadio->setChecked( true ); - } + calculateRangeByExpression( false ); + } ); + QAction *calculateUpperAction = new QAction( "Calculate End by Expression…", calculateFixedRangePerBandMenu ); + calculateFixedRangePerBandMenu->addAction( calculateUpperAction ); + connect( calculateUpperAction, &QAction::triggered, this, [this] + { + calculateRangeByExpression( true ); + } ); + syncToLayer(); } @@ -60,17 +92,14 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< QgsRasterLayerTemporalProperties * >( mLayer->temporalProperties() ); - QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), - mEndTemporalDateTimeEdit->dateTime() ); + temporalProperties->setMode( mModeComboBox->currentData().value< Qgis::RasterTemporalMode >() ); - if ( mModeAutomaticRadio->isChecked() ) - temporalProperties->setMode( Qgis::RasterTemporalMode::TemporalRangeFromDataProvider ); - else if ( mModeFixedRangeRadio->isChecked() ) - temporalProperties->setMode( Qgis::RasterTemporalMode::FixedTemporalRange ); - else if ( mModeRedrawLayer->isChecked() ) - temporalProperties->setMode( Qgis::RasterTemporalMode::RedrawLayerOnly ); + const QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), + mEndTemporalDateTimeEdit->dateTime() ); temporalProperties->setFixedTemporalRange( normalRange ); + temporalProperties->setFixedRangePerBand( mFixedRangePerBandModel->rangeData() ); + for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) { widget->apply(); @@ -80,25 +109,32 @@ void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() void QgsRasterLayerTemporalPropertiesWidget::syncToLayer() { const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< const QgsRasterLayerTemporalProperties * >( mLayer->temporalProperties() ); + mModeComboBox->setCurrentIndex( mModeComboBox->findData( QVariant::fromValue( temporalProperties->mode() ) ) ); switch ( temporalProperties->mode() ) { case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: - mModeAutomaticRadio->setChecked( true ); + mStackedWidget->setCurrentWidget( mPageAutomatic ); break; case Qgis::RasterTemporalMode::FixedTemporalRange: - mModeFixedRangeRadio->setChecked( true ); + mStackedWidget->setCurrentWidget( mPageFixedRange ); break; case Qgis::RasterTemporalMode::RedrawLayerOnly: - mModeRedrawLayer->setChecked( true ); + mStackedWidget->setCurrentWidget( mPageRedrawOnly ); + break; + case Qgis::RasterTemporalMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); break; } mStartTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().begin() ); mEndTemporalDateTimeEdit->setDateTime( temporalProperties->fixedTemporalRange().end() ); - mTemporalGroupBox->setChecked( temporalProperties->isActive() ); + mFixedRangePerBandModel->setLayerData( mLayer, temporalProperties->fixedRangePerBand() ); + mBandRangesTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch ); + mBandRangesTable->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch ); + mBandRangesTable->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch ); - mLabelRedrawLayer->setEnabled( mModeRedrawLayer->isChecked() ); + mTemporalGroupBox->setChecked( temporalProperties->isActive() ); for ( QgsMapLayerConfigWidget *widget : std::as_const( mExtraWidgets ) ) { @@ -119,3 +155,308 @@ void QgsRasterLayerTemporalPropertiesWidget::temporalGroupBoxChecked( bool check widget->emit dynamicTemporalControlToggled( checked ); } } + +void QgsRasterLayerTemporalPropertiesWidget::modeChanged() +{ + if ( mModeComboBox->currentData().isValid() ) + { + switch ( mModeComboBox->currentData().value< Qgis::RasterTemporalMode >() ) + { + case Qgis::RasterTemporalMode::TemporalRangeFromDataProvider: + mStackedWidget->setCurrentWidget( mPageAutomatic ); + break; + case Qgis::RasterTemporalMode::FixedTemporalRange: + mStackedWidget->setCurrentWidget( mPageFixedRange ); + break; + case Qgis::RasterTemporalMode::RedrawLayerOnly: + mStackedWidget->setCurrentWidget( mPageRedrawOnly ); + break; + case Qgis::RasterTemporalMode::FixedRangePerBand: + mStackedWidget->setCurrentWidget( mPageFixedRangePerBand ); + break; + } + } +} + +void QgsRasterLayerTemporalPropertiesWidget::calculateRangeByExpression( bool isUpper ) +{ + QgsExpressionContext expressionContext; + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), 1, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( 1 ), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( 1 ), true, false, tr( "Band description" ) ) ); + + expressionContext.appendScope( bandScope ); + expressionContext.setHighlightedVariables( { QStringLiteral( "band" ), QStringLiteral( "band_name" ), QStringLiteral( "band_description" )} ); + + QgsExpressionBuilderDialog dlg = QgsExpressionBuilderDialog( nullptr, isUpper ? mFixedRangeUpperExpression : mFixedRangeLowerExpression, this, QStringLiteral( "generic" ), expressionContext ); + dlg.setExpectedOutputFormat( !isUpper ? tr( "Temporal range start date / time" ) : tr( "Temporal range end date / time" ) ); + + QList > bandChoices; + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandChoices << qMakePair( mLayer->dataProvider()->displayBandName( band ), band ); + } + dlg.expressionBuilder()->setCustomPreviewGenerator( tr( "Band" ), bandChoices, [this]( const QVariant & value )-> QgsExpressionContext + { + return createExpressionContextForBand( value.toInt() ); + } ); + + if ( dlg.exec() ) + { + if ( isUpper ) + mFixedRangeUpperExpression = dlg.expressionText(); + else + mFixedRangeLowerExpression = dlg.expressionText(); + + QgsExpression exp( dlg.expressionText() ); + exp.prepare( &expressionContext ); + for ( int band = 1; band <= mLayer->bandCount(); ++band ) + { + bandScope->setVariable( QStringLiteral( "band" ), band ); + bandScope->setVariable( QStringLiteral( "band_name" ), mLayer->dataProvider()->displayBandName( band ) ); + bandScope->setVariable( QStringLiteral( "band_description" ), mLayer->dataProvider()->bandDescription( band ) ); + + const QVariant res = exp.evaluate( &expressionContext ); + mFixedRangePerBandModel->setData( mFixedRangePerBandModel->index( band - 1, isUpper ? 2 : 1 ), res, Qt::EditRole ); + } + } +} + +QgsExpressionContext QgsRasterLayerTemporalPropertiesWidget::createExpressionContextForBand( int band ) const +{ + QgsExpressionContext context; + context.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); + QgsExpressionContextScope *bandScope = new QgsExpressionContextScope(); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band" ), band, true, false, tr( "Band number" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_name" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->displayBandName( band ) : QString(), true, false, tr( "Band name" ) ) ); + bandScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "band_description" ), ( mLayer && mLayer->dataProvider() ) ? mLayer->dataProvider()->bandDescription( band ) : QString(), true, false, tr( "Band description" ) ) ); + context.appendScope( bandScope ); + context.setHighlightedVariables( { QStringLiteral( "band" ), QStringLiteral( "band_name" ), QStringLiteral( "band_description" )} ); + return context; +} + +///@cond PRIVATE + +// +// QgsRasterBandFixedTemporalRangeModel +// + +QgsRasterBandFixedTemporalRangeModel::QgsRasterBandFixedTemporalRangeModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsRasterBandFixedTemporalRangeModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsRasterBandFixedTemporalRangeModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mBandCount; +} + +QModelIndex QgsRasterBandFixedTemporalRangeModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsRasterBandFixedTemporalRangeModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsRasterBandFixedTemporalRangeModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case 0: + return Qt::ItemFlag::ItemIsEnabled; + case 1: + case 2: + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsRasterBandFixedTemporalRangeModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mBandCount || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const int band = index.row() + 1; + const QgsDateTimeRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return mBandNames.value( band, QString::number( band ) ); + + case 1: + return range.begin().isValid() ? range.begin() : QVariant(); + + case 2: + return range.end().isValid() ? range.end() : QVariant(); + + default: + break; + } + break; + } + + case Qt::TextAlignmentRole: + { + switch ( index.column() ) + { + case 0: + return static_cast( Qt::AlignLeft | Qt::AlignVCenter ); + + case 1: + case 2: + return static_cast( Qt::AlignRight | Qt::AlignVCenter ); + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QVariant QgsRasterBandFixedTemporalRangeModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole && orientation == Qt::Horizontal ) + { + switch ( section ) + { + case 0: + return tr( "Band" ); + case 1: + return tr( "Begin" ); + case 2: + return tr( "End" ); + default: + break; + } + } + return QAbstractItemModel::headerData( section, orientation, role ); +} + +bool QgsRasterBandFixedTemporalRangeModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + if ( index.row() > mBandCount || index.row() < 0 ) + return false; + + const int band = index.row() + 1; + const QgsDateTimeRange range = mRanges.value( band ); + + switch ( role ) + { + case Qt::EditRole: + { + const QDateTime newValue = value.toDateTime(); + if ( !newValue.isValid() ) + return false; + + switch ( index.column() ) + { + case 1: + { + mRanges[band] = QgsDateTimeRange( newValue, range.end(), range.includeBeginning(), range.includeEnd() ); + emit dataChanged( index, index, QVector() << role ); + break; + } + + case 2: + mRanges[band] = QgsDateTimeRange( range.begin(), newValue, range.includeBeginning(), range.includeEnd() ); + emit dataChanged( index, index, QVector() << role ); + break; + + default: + break; + } + return true; + } + + default: + break; + } + + return false; +} + +void QgsRasterBandFixedTemporalRangeModel::setLayerData( QgsRasterLayer *layer, const QMap &ranges ) +{ + beginResetModel(); + + mBandCount = layer->bandCount(); + mRanges = ranges; + + mBandNames.clear(); + for ( int band = 1; band <= mBandCount; ++band ) + { + mBandNames[band] = layer->dataProvider()->displayBandName( band ); + } + + endResetModel(); +} + +// +// QgsFixedTemporalRangeDelegate +// + +QgsFixedTemporalRangeDelegate::QgsFixedTemporalRangeDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +QWidget *QgsFixedTemporalRangeDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex & ) const +{ + QgsDateTimeEdit *editor = new QgsDateTimeEdit( parent ); + editor->setAllowNull( true ); + return editor; +} + +void QgsFixedTemporalRangeDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + if ( QgsDateTimeEdit *dateTimeEdit = qobject_cast< QgsDateTimeEdit * >( editor ) ) + { + model->setData( index, dateTimeEdit->dateTime() ); + } +} +///@endcond PRIVATE diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h index add31c51bb30..ad5d43dad650 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h @@ -20,9 +20,56 @@ #include "ui_qgsrasterlayertemporalpropertieswidgetbase.h" #include "qgis_gui.h" +#include "qgsrange.h" +#include class QgsRasterLayer; class QgsMapLayerConfigWidget; +class QgsExpressionContext; + +#ifndef SIP_RUN +///@cond PRIVATE +class QgsRasterBandFixedTemporalRangeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + QgsRasterBandFixedTemporalRangeModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + void setLayerData( QgsRasterLayer *layer, const QMap &ranges ); + QMap rangeData() const { return mRanges; } + + private: + + int mBandCount = 0; + QMap mBandNames; + QMap mRanges; +}; + +class QgsFixedTemporalRangeDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + QgsFixedTemporalRangeDelegate( QObject *parent ); + + protected: + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + +}; +///@endcond PRIVATE +#endif /** * \ingroup gui @@ -61,8 +108,11 @@ class GUI_EXPORT QgsRasterLayerTemporalPropertiesWidget : public QWidget, privat private slots: void temporalGroupBoxChecked( bool checked ); + void modeChanged(); + void calculateRangeByExpression( bool isUpper ); private: + QgsExpressionContext createExpressionContextForBand( int band ) const; /** * The corresponding map layer with temporal attributes @@ -72,5 +122,8 @@ class GUI_EXPORT QgsRasterLayerTemporalPropertiesWidget : public QWidget, privat QList< QgsMapLayerConfigWidget * > mExtraWidgets; + QgsRasterBandFixedTemporalRangeModel *mFixedRangePerBandModel = nullptr; + QString mFixedRangeLowerExpression; + QString mFixedRangeUpperExpression; }; #endif // QGSRASTERLAYERTEMPORALPROPERTIESWIDGET_H diff --git a/src/gui/raster/qgsrastertransparencywidget.cpp b/src/gui/raster/qgsrastertransparencywidget.cpp index 11df784533b9..5f6451e9f8ec 100644 --- a/src/gui/raster/qgsrastertransparencywidget.cpp +++ b/src/gui/raster/qgsrastertransparencywidget.cpp @@ -474,31 +474,33 @@ void QgsRasterTransparencyWidget::apply() QgsRasterTransparency *rasterTransparency = new QgsRasterTransparency(); if ( tableTransparency->columnCount() == 4 ) { - QgsRasterTransparency::TransparentThreeValuePixel myTransparentPixel; - QList myTransparentThreeValuePixelList; + QVector myTransparentThreeValuePixelList; myTransparentThreeValuePixelList.reserve( tableTransparency->rowCount() ); for ( int myListRunner = 0; myListRunner < tableTransparency->rowCount(); myListRunner++ ) { - myTransparentPixel.red = transparencyCellValue( myListRunner, 0 ); - myTransparentPixel.green = transparencyCellValue( myListRunner, 1 ); - myTransparentPixel.blue = transparencyCellValue( myListRunner, 2 ); - myTransparentPixel.percentTransparent = transparencyCellValue( myListRunner, 3 ); - myTransparentThreeValuePixelList.append( myTransparentPixel ); + const double red = transparencyCellValue( myListRunner, 0 ); + const double green = transparencyCellValue( myListRunner, 1 ); + const double blue = transparencyCellValue( myListRunner, 2 ); + const double opacity = 1.0 - transparencyCellValue( myListRunner, 3 ) / 100.0; + myTransparentThreeValuePixelList.append( + QgsRasterTransparency::TransparentThreeValuePixel( red, green, blue, opacity ) + ); } rasterTransparency->setTransparentThreeValuePixelList( myTransparentThreeValuePixelList ); } else if ( tableTransparency->columnCount() == 3 ) { - QgsRasterTransparency::TransparentSingleValuePixel myTransparentPixel; - QList myTransparentSingleValuePixelList; + QVector myTransparentSingleValuePixelList; myTransparentSingleValuePixelList.reserve( tableTransparency->rowCount() ); for ( int myListRunner = 0; myListRunner < tableTransparency->rowCount(); myListRunner++ ) { - myTransparentPixel.min = transparencyCellValue( myListRunner, 0 ); - myTransparentPixel.max = transparencyCellValue( myListRunner, 1 ); - myTransparentPixel.percentTransparent = transparencyCellValue( myListRunner, 2 ); + const double min = transparencyCellValue( myListRunner, 0 ); + const double max = transparencyCellValue( myListRunner, 1 ); + const double opacity = 1.0 - transparencyCellValue( myListRunner, 2 ) / 100.0; - myTransparentSingleValuePixelList.append( myTransparentPixel ); + myTransparentSingleValuePixelList.append( + QgsRasterTransparency::TransparentSingleValuePixel( min, max, opacity ) + ); } rasterTransparency->setTransparentSingleValuePixelList( myTransparentSingleValuePixelList ); } @@ -630,13 +632,13 @@ void QgsRasterTransparencyWidget::populateTransparencyTable( QgsRasterRenderer * if ( nBands == 1 ) { - QList pixelList = rasterTransparency->transparentSingleValuePixelList(); + QVector pixelList = rasterTransparency->transparentSingleValuePixelList(); for ( int i = 0; i < pixelList.size(); ++i ) { tableTransparency->insertRow( i ); setTransparencyCell( i, 0, pixelList[i].min ); setTransparencyCell( i, 1, pixelList[i].max ); - setTransparencyCell( i, 2, pixelList[i].percentTransparent ); + setTransparencyCell( i, 2, 100 * ( 1 - pixelList[i].opacity ) ); // break synchronization only if values differ if ( pixelList[i].min != pixelList[i].max ) { @@ -646,14 +648,14 @@ void QgsRasterTransparencyWidget::populateTransparencyTable( QgsRasterRenderer * } else if ( nBands == 3 ) { - QList pixelList = rasterTransparency->transparentThreeValuePixelList(); + QVector pixelList = rasterTransparency->transparentThreeValuePixelList(); for ( int i = 0; i < pixelList.size(); ++i ) { tableTransparency->insertRow( i ); setTransparencyCell( i, 0, pixelList[i].red ); setTransparencyCell( i, 1, pixelList[i].green ); setTransparencyCell( i, 2, pixelList[i].blue ); - setTransparencyCell( i, 3, pixelList[i].percentTransparent ); + setTransparencyCell( i, 3, 100 * ( 1 - pixelList[i].opacity ) ); } } diff --git a/src/gui/raster/qgssinglebandgrayrendererwidget.cpp b/src/gui/raster/qgssinglebandgrayrendererwidget.cpp index 2ec1173b64e0..bc7dc13a23fc 100644 --- a/src/gui/raster/qgssinglebandgrayrendererwidget.cpp +++ b/src/gui/raster/qgssinglebandgrayrendererwidget.cpp @@ -185,8 +185,8 @@ void QgsSingleBandGrayRendererWidget::setFromRenderer( const QgsRasterRenderer * if ( gr ) { //band - mGrayBandComboBox->setBand( gr->grayBand() ); - mMinMaxWidget->setBands( QList< int >() << gr->grayBand() ); + mGrayBandComboBox->setBand( gr->inputBand() ); + mMinMaxWidget->setBands( QList< int >() << gr->inputBand() ); mGradientComboBox->setCurrentIndex( mGradientComboBox->findData( gr->gradient() ) ); const QgsContrastEnhancement *ce = gr->contrastEnhancement(); diff --git a/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp b/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp index b1d12c5acc9c..f3dd9bc057f7 100644 --- a/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp +++ b/src/gui/raster/qgssinglebandpseudocolorrendererwidget.cpp @@ -145,9 +145,9 @@ void QgsSingleBandPseudoColorRendererWidget::setFromRenderer( const QgsRasterRen const QgsSingleBandPseudoColorRenderer *pr = dynamic_cast( r ); if ( pr ) { - mBandComboBox->setBand( pr->band() ); - mMinMaxWidget->setBands( QList< int >() << pr->band() ); - mColorRampShaderWidget->setRasterBand( pr->band() ); + mBandComboBox->setBand( pr->inputBand() ); + mMinMaxWidget->setBands( QList< int >() << pr->inputBand() ); + mColorRampShaderWidget->setRasterBand( pr->inputBand() ); // need to set min/max properties here because if we use the raster shader below, // we may set a new color ramp which needs to have min/max values defined. diff --git a/src/gui/sensor/qgssensorwidget.cpp b/src/gui/sensor/qgssensorwidget.cpp index 26da719620a6..fef052eec031 100644 --- a/src/gui/sensor/qgssensorwidget.cpp +++ b/src/gui/sensor/qgssensorwidget.cpp @@ -157,9 +157,13 @@ QgsSerialPortSensorWidget::QgsSerialPortSensorWidget( QWidget *parent ) mBaudRateComboBox->addItem( QStringLiteral( "115200 baud" ), static_cast( QSerialPort::Baud115200 ) ); mBaudRateComboBox->setCurrentIndex( 3 ); + mDataFrameDelimiterComboBox->addItem( tr( "No Delimiter" ), QString() ); + mDataFrameDelimiterComboBox->addItem( tr( "New Line" ), QString( "\n" ) ); + mDataFrameDelimiterComboBox->addItem( tr( "Custom Character" ), QString() ); + updateSerialPortDetails(); - connect( mSerialPortComboBox, static_cast( &QComboBox::currentIndexChanged ), this, [ = ]() + connect( mSerialPortComboBox, static_cast( &QComboBox::currentTextChanged ), this, [ = ]() { updateSerialPortDetails(); emit changed(); @@ -171,13 +175,33 @@ QgsSerialPortSensorWidget::QgsSerialPortSensorWidget( QWidget *parent ) emit changed(); } ); + connect( mDataFrameDelimiterComboBox, static_cast( &QComboBox::currentIndexChanged ), this, [ = ]( int index ) + { + if ( index == mDataFrameDelimiterComboBox->count() - 1 ) + { + mDataFrameDelimiterLineEdit->setEnabled( true ); + mDataFrameDelimiterLineEdit->setFocus(); + } + else + { + mDataFrameDelimiterLineEdit->setEnabled( false ); + } + emit changed(); + } ); + + connect( mDataFrameDelimiterLineEdit, &QLineEdit::textEdited, this, [ = ]() + { + emit changed(); + } ); } QgsAbstractSensor *QgsSerialPortSensorWidget::createSensor() { QgsSerialPortSensor *s = new QgsSerialPortSensor(); - s->setPortName( mSerialPortComboBox->currentData().toString() ); + s->setPortName( mSerialPortComboBox->findText( mSerialPortComboBox->currentText() ) != -1 ? mSerialPortComboBox->currentData().toString() : mSerialPortComboBox->currentText() ); s->setBaudRate( static_cast< QSerialPort::BaudRate >( mBaudRateComboBox->currentData().toInt() ) ); + const QString delimiter = mDataFrameDelimiterComboBox->currentIndex() == mDataFrameDelimiterComboBox->count() - 1 ? mDataFrameDelimiterLineEdit->text() : mDataFrameDelimiterComboBox->currentData().toString(); + s->setDelimiter( delimiter.toLocal8Bit() ); return s; } @@ -187,8 +211,10 @@ bool QgsSerialPortSensorWidget::updateSensor( QgsAbstractSensor *sensor ) if ( !s ) return false; - s->setPortName( mSerialPortComboBox->currentData().toString() ); + s->setPortName( mSerialPortComboBox->findText( mSerialPortComboBox->currentText() ) != -1 ? mSerialPortComboBox->currentData().toString() : mSerialPortComboBox->currentText() ); s->setBaudRate( static_cast< QSerialPort::BaudRate >( mBaudRateComboBox->currentData().toInt() ) ); + const QString delimiter = mDataFrameDelimiterComboBox->currentIndex() == mDataFrameDelimiterComboBox->count() - 1 ? mDataFrameDelimiterLineEdit->text() : mDataFrameDelimiterComboBox->currentData().toString(); + s->setDelimiter( delimiter.toLocal8Bit() ); return true; } @@ -219,18 +245,37 @@ bool QgsSerialPortSensorWidget::setSensor( QgsAbstractSensor *sensor ) mBaudRateComboBox->setCurrentIndex( mBaudRateComboBox->count() - 1 ); } + const QString delimiter = QString( s->delimiter() ); + if ( !delimiter.isEmpty() ) + { + const int delimiterIndex = mDataFrameDelimiterComboBox->findData( delimiter ); + if ( delimiterIndex > -1 ) + { + mDataFrameDelimiterComboBox->setCurrentIndex( delimiterIndex ); + } + else + { + mDataFrameDelimiterComboBox->setCurrentIndex( mDataFrameDelimiterComboBox->count() - 1 ); + mDataFrameDelimiterLineEdit->setText( delimiter ); + } + } + else + { + mDataFrameDelimiterComboBox->setCurrentIndex( 0 ); + mDataFrameDelimiterLineEdit->setText( QString() ); + } return true; } void QgsSerialPortSensorWidget::updateSerialPortDetails() { - if ( mSerialPortComboBox->currentIndex() < 0 ) + if ( mSerialPortComboBox->currentText().isEmpty() ) { return; } - const QString ¤tPortName = mSerialPortComboBox->currentData().toString(); + const QString ¤tPortName = mSerialPortComboBox->findText( mSerialPortComboBox->currentText() ) != -1 ? mSerialPortComboBox->currentData().toString() : mSerialPortComboBox->currentText(); bool serialPortFound = false; for ( const QSerialPortInfo &info : QSerialPortInfo::availablePorts() ) { @@ -251,6 +296,10 @@ void QgsSerialPortSensorWidget::updateSerialPortDetails() mSerialPortDetails->setText( QStringLiteral( "%1:\n- %2: %3" ).arg( tr( "Serial port details" ), tr( "Port name" ), currentPortName ) ); } + else + { + mSerialPortDetails->setText( QString() ); + } } #endif diff --git a/src/gui/symbology/qgssvgselectorwidget.h b/src/gui/symbology/qgssvgselectorwidget.h index ed66b12bde98..e725b86b4a5f 100644 --- a/src/gui/symbology/qgssvgselectorwidget.h +++ b/src/gui/symbology/qgssvgselectorwidget.h @@ -406,8 +406,15 @@ class GUI_EXPORT QgsSvgSelectorWidget : public QWidget, private Ui::WidgetSvgSel /** * Returns if the group box to fill parameters is visible * \since QGIS 3.18 + * \deprecated Use allowParameters() */ - bool allowParamerters() const {return mAllowParameters;} + Q_DECL_DEPRECATED bool allowParamerters() const SIP_DEPRECATED {return mAllowParameters;} // spellok + + /** + * Returns if the group box to fill parameters is visible + * \since QGIS 3.38 + */ + bool allowParameters() const {return mAllowParameters;} /** * Defines if the SVG browser should be visible diff --git a/src/gui/vector/qgsvectorlayerproperties.cpp b/src/gui/vector/qgsvectorlayerproperties.cpp index fe9b9e4bcd2c..cb930e7654db 100644 --- a/src/gui/vector/qgsvectorlayerproperties.cpp +++ b/src/gui/vector/qgsvectorlayerproperties.cpp @@ -574,7 +574,10 @@ void QgsVectorLayerProperties::syncToLayer() } if ( mSourceWidget ) + { + mSourceWidget->setMapCanvas( mCanvas ); mSourceWidget->setSourceUri( mLayer->source() ); + } // populate the general information mLayerOrigNameLineEdit->setText( mLayer->name() ); @@ -759,19 +762,6 @@ void QgsVectorLayerProperties::apply() if ( mMaskingWidget && mMaskingWidget->hasBeenPopulated() ) mMaskingWidget->apply(); - // - // Set up sql subset query if applicable - // - mSubsetGroupBox->setEnabled( true ); - - if ( txtSubsetSQL->text() != mLayer->subsetString() ) - { - // set the subset sql for the layer - mLayer->setSubsetString( txtSubsetSQL->text() ); - mMetadataFilled = false; - } - mOriginalSubsetSQL = mLayer->subsetString(); - // set up the scale based layer visibility stuff.... mLayer->setScaleBasedVisibility( mScaleVisibilityGroupBox->isChecked() ); mLayer->setMaximumScale( mScaleRangeWidget->maximumScale() ); @@ -961,6 +951,7 @@ void QgsVectorLayerProperties::apply() // happens BEFORE we change the source, otherwise we might end up with a renderer which is not // compatible with the new geometry type of the layer. (And likewise for other properties like // fields!) + bool dialogNeedsResync = false; if ( mSourceWidget ) { const QString newSource = mSourceWidget->sourceUri(); @@ -972,9 +963,25 @@ void QgsVectorLayerProperties::apply() // resync dialog to layer's new state -- this allows any changed layer properties // (such as a forced creation of a new renderer compatible with the new layer, new field configuration, etc) // to show in the dialog correctly - syncToLayer(); + dialogNeedsResync = true; } } + // now apply the subset string AFTER setting the layer's source. It's messy, but the subset string + // can form part of the layer's source, but it WON'T be present in the URI returned by the source widget! + // If we don't apply the subset string AFTER changing the source, then the subset string will be lost. + mSubsetGroupBox->setEnabled( true ); + if ( txtSubsetSQL->text() != mLayer->subsetString() ) + { + // set the subset sql for the layer + mLayer->setSubsetString( txtSubsetSQL->text() ); + mMetadataFilled = false; + // need to resync the dialog, the subset string may have changed the layer's geometry type! + dialogNeedsResync = true; + } + mOriginalSubsetSQL = mLayer->subsetString(); + + if ( dialogNeedsResync ) + syncToLayer(); mLayer->triggerRepaint(); // notify the project we've made a change diff --git a/src/providers/arcgisrest/CMakeLists.txt b/src/providers/arcgisrest/CMakeLists.txt index 5d6627d20b97..ce4a7bb81cac 100644 --- a/src/providers/arcgisrest/CMakeLists.txt +++ b/src/providers/arcgisrest/CMakeLists.txt @@ -31,6 +31,9 @@ target_include_directories(provider_arcgisfeatureserver_a PUBLIC # require c++17 target_compile_features(provider_arcgisfeatureserver_a PRIVATE cxx_std_17) +# We use private headers from core that need this +target_compile_definitions(provider_arcgisfeatureserver_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + target_link_libraries (provider_arcgisfeatureserver_a qgis_core ${QCA_LIBRARY} @@ -80,6 +83,9 @@ else() # require c++17 target_compile_features(provider_arcgisfeatureserver PRIVATE cxx_std_17) + # We use private headers from core that need this + target_compile_definitions(provider_arcgisfeatureserver PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + if (WITH_GUI) target_link_libraries(provider_arcgisfeatureserver qgis_gui @@ -108,6 +114,9 @@ target_include_directories(provider_arcgismapserver_a PUBLIC # require c++17 target_compile_features(provider_arcgismapserver_a PRIVATE cxx_std_17) +# We use private headers from core that need this +target_compile_definitions(provider_arcgismapserver_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + target_link_libraries (provider_arcgismapserver_a qgis_core ${QCA_LIBRARY} @@ -121,6 +130,8 @@ else() # require c++17 target_compile_features(provider_arcgismapserver PRIVATE cxx_std_17) + # We use private headers from core that need this + target_compile_definitions(provider_arcgismapserver PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(provider_arcgismapserver qgis_core diff --git a/src/providers/arcgisrest/qgsafsshareddata.cpp b/src/providers/arcgisrest/qgsafsshareddata.cpp index 8b9fe47d2e82..416ef85d1cb0 100644 --- a/src/providers/arcgisrest/qgsafsshareddata.cpp +++ b/src/providers/arcgisrest/qgsafsshareddata.cpp @@ -18,6 +18,7 @@ #include "qgsarcgisrestquery.h" #include "qgslogger.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsblockingnetworkrequest.h" #include "qgsreadwritelocker.h" #include "qgsjsonutils.h" diff --git a/src/providers/arcgisrest/qgsamsprovider.cpp b/src/providers/arcgisrest/qgsamsprovider.cpp index 4a40b93f75e7..df40de30d5df 100644 --- a/src/providers/arcgisrest/qgsamsprovider.cpp +++ b/src/providers/arcgisrest/qgsamsprovider.cpp @@ -25,6 +25,7 @@ #include "qgsgeometry.h" #include "qgsapplication.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgssettings.h" #include "qgsmessagelog.h" #include "qgsauthmanager.h" @@ -346,6 +347,11 @@ QgsAmsProvider::QgsAmsProvider( const QgsAmsProvider &other, const QgsDataProvid mTimestamp = QDateTime::currentDateTime(); } +Qgis::DataProviderFlags QgsAmsProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsRasterDataProvider::ProviderCapabilities QgsAmsProvider::providerCapabilities() const { return ProviderCapability::ReadLayerMetadata | ProviderCapability::ReloadData; diff --git a/src/providers/arcgisrest/qgsamsprovider.h b/src/providers/arcgisrest/qgsamsprovider.h index b00816d9a5f9..4ef78cafcd12 100644 --- a/src/providers/arcgisrest/qgsamsprovider.h +++ b/src/providers/arcgisrest/qgsamsprovider.h @@ -75,6 +75,7 @@ class QgsAmsProvider : public QgsRasterDataProvider QgsAmsProvider( const QString &uri, const QgsDataProvider::ProviderOptions &providerOptions, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ); explicit QgsAmsProvider( const QgsAmsProvider &other, const QgsDataProvider::ProviderOptions &providerOptions ); + Qgis::DataProviderFlags flags() const override; QgsRasterDataProvider::ProviderCapabilities providerCapabilities() const override; /* Inherited from QgsDataProvider */ bool isValid() const override { return mValid; } diff --git a/src/providers/grass/qgsgrass.h b/src/providers/grass/qgsgrass.h index 36a13c741fbe..39a9cfe45790 100644 --- a/src/providers/grass/qgsgrass.h +++ b/src/providers/grass/qgsgrass.h @@ -179,7 +179,7 @@ class GRASS_LIB_EXPORT QgsGrass : public QObject /** * QgsGrass may be running in active or passive mode. * Active mode means that GISRC is set up and GISRC file is available, - * in that case default GISDBASE, LOCATION and MAPSET may be read by GetDefaul*() functions. + * in that case default GISDBASE, LOCATION and MAPSET may be read by GetDefault*() functions. * Passive mode means, that GISRC is not available. */ static bool activeMode(); diff --git a/src/providers/grass/qgsgrassprovidermodule.cpp b/src/providers/grass/qgsgrassprovidermodule.cpp index 12a2ee44a5aa..2cef000f3be3 100644 --- a/src/providers/grass/qgsgrassprovidermodule.cpp +++ b/src/providers/grass/qgsgrassprovidermodule.cpp @@ -972,7 +972,7 @@ void QgsGrassMapsetItem::childrenCreated() if ( mRefreshLater ) { - QgsDebugMsgLevel( QStringLiteral( "directory changed during createChidren() -> refresh() again" ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "directory changed during createChildren() -> refresh() again" ), 2 ); mRefreshLater = false; setState( Qgis::BrowserItemState::Populated ); refresh(); diff --git a/src/providers/mdal/qgsmdalprovider.cpp b/src/providers/mdal/qgsmdalprovider.cpp index 685d709067e2..1cf96b9b9ed9 100644 --- a/src/providers/mdal/qgsmdalprovider.cpp +++ b/src/providers/mdal/qgsmdalprovider.cpp @@ -551,7 +551,7 @@ void QgsMdalProvider::fileMeshFilters( QString &fileMeshFiltersString, QString & // Grind through all the drivers and their respective metadata. // We'll add a file filter for those drivers that have a file // extension defined for them; the others, well, even though - // theoreticaly we can open those files because there exists a + // theoretically we can open those files because there exists a // driver for them, the user will have to use the "All Files" to // open datasets with no explicitly defined file name extension. diff --git a/src/providers/mssql/CMakeLists.txt b/src/providers/mssql/CMakeLists.txt index 1ee3523c7e4d..995e28e41980 100644 --- a/src/providers/mssql/CMakeLists.txt +++ b/src/providers/mssql/CMakeLists.txt @@ -40,6 +40,11 @@ add_library(provider_mssql MODULE ${MSSQL_SRCS} ${MSSQL_HDRS}) # require c++17 target_compile_features(provider_mssql PRIVATE cxx_std_17) +target_compile_definitions(provider_mssql PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + +# We use private headers from core that need this +target_compile_definitions(provider_mssql PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + target_link_libraries(provider_mssql qgis_core ${QT_VERSION_BASE}::Sql @@ -58,6 +63,9 @@ add_library (provider_mssql_a STATIC ${MSSQL_SRCS} ${MSSQL_HDRS}) # require c++17 target_compile_features(provider_mssql_a PRIVATE cxx_std_17) +# We use private headers from core that need this +target_compile_definitions(provider_mssql_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + target_link_libraries (provider_mssql_a qgis_core ${POSTGRES_LIBRARY} diff --git a/src/providers/mssql/qgsmssqldataitems.cpp b/src/providers/mssql/qgsmssqldataitems.cpp index 5fafa8736b06..54b62b16a7ff 100644 --- a/src/providers/mssql/qgsmssqldataitems.cpp +++ b/src/providers/mssql/qgsmssqldataitems.cpp @@ -111,8 +111,8 @@ void QgsMssqlConnectionItem::refresh() stop(); // Clear all children - const QVector allChidren = children(); - for ( QgsDataItem *item : allChidren ) + const QVector allChildren = children(); + for ( QgsDataItem *item : allChildren ) { removeChildItem( item ); delete item; diff --git a/src/providers/mssql/qgsmssqlfeatureiterator.cpp b/src/providers/mssql/qgsmssqlfeatureiterator.cpp index bc0d3aa597d6..4df2ed38def2 100644 --- a/src/providers/mssql/qgsmssqlfeatureiterator.cpp +++ b/src/providers/mssql/qgsmssqlfeatureiterator.cpp @@ -21,6 +21,7 @@ #include "qgsmssqltransaction.h" #include "qgslogger.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsexception.h" #include "qgsmssqldatabase.h" #include "qgsgeometryengine.h" diff --git a/src/providers/mssql/qgsmssqlprovider.cpp b/src/providers/mssql/qgsmssqlprovider.cpp index 94048210f0f2..d404ac53fcd1 100644 --- a/src/providers/mssql/qgsmssqlprovider.cpp +++ b/src/providers/mssql/qgsmssqlprovider.cpp @@ -21,6 +21,8 @@ #include "qgsmssqlproviderconnection.h" #include "qgsfeedback.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" +#include "qgsvariantutils.h" #include #include @@ -709,7 +711,8 @@ QVariant QgsMssqlProvider::defaultValue( int fieldId ) const return QVariant(); } - return query.value( 0 ); + const QVariant res = query.value( 0 ); + return QgsVariantUtils::isNull( res ) ? QVariant() : res; } QString QgsMssqlProvider::storageType() const diff --git a/src/providers/mssql/qgsmssqlprovider.h b/src/providers/mssql/qgsmssqlprovider.h index bc5bf0336f4f..a92db2014691 100644 --- a/src/providers/mssql/qgsmssqlprovider.h +++ b/src/providers/mssql/qgsmssqlprovider.h @@ -136,7 +136,7 @@ class QgsMssqlProvider final: public QgsVectorDataProvider //! Convert a QgsField to work with MSSQL static bool convertField( QgsField &field ); - // Parse type name and num coordinates as stored in geometry_columns tabe and returns normalized (M, Z or ZM) type name + // Parse type name and num coordinates as stored in geometry_columns table and returns normalized (M, Z or ZM) type name static QString typeFromMetadata( const QString &typeName, int numCoords ); //! Convert values to quoted values for database work diff --git a/src/providers/mssql/qgsmssqlproviderconnection.cpp b/src/providers/mssql/qgsmssqlproviderconnection.cpp index 5f5fb9baf0bf..4ca5f00347c4 100644 --- a/src/providers/mssql/qgsmssqlproviderconnection.cpp +++ b/src/providers/mssql/qgsmssqlproviderconnection.cpp @@ -28,6 +28,7 @@ #include "qgsfeedback.h" #include "qgsmssqlsqlquerybuilder.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include #include diff --git a/src/providers/oracle/CMakeLists.txt b/src/providers/oracle/CMakeLists.txt index dd1a552025b3..264917ba5e01 100644 --- a/src/providers/oracle/CMakeLists.txt +++ b/src/providers/oracle/CMakeLists.txt @@ -52,6 +52,7 @@ add_library (provider_oracle MODULE ${ORACLE_SRCS} ${ORACLE_HDRS}) # require c++17 target_compile_features(provider_oracle PRIVATE cxx_std_17) +target_compile_definitions(provider_oracle PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries (provider_oracle qgis_core diff --git a/src/providers/oracle/qgsoracledataitems.cpp b/src/providers/oracle/qgsoracledataitems.cpp index 230ab4444a83..e753ebb41ad6 100644 --- a/src/providers/oracle/qgsoracledataitems.cpp +++ b/src/providers/oracle/qgsoracledataitems.cpp @@ -22,6 +22,7 @@ #include "qgsmessageoutput.h" #include "qgsvectorlayer.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsvectorlayerexporter.h" #include diff --git a/src/providers/oracle/qgsoraclefeatureiterator.cpp b/src/providers/oracle/qgsoraclefeatureiterator.cpp index b5ce52ff210c..1676c8ddb97c 100644 --- a/src/providers/oracle/qgsoraclefeatureiterator.cpp +++ b/src/providers/oracle/qgsoraclefeatureiterator.cpp @@ -19,6 +19,7 @@ #include "qgsoracleexpressioncompiler.h" #include "qgsoracletransaction.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgslogger.h" #include "qgsmessagelog.h" #include "qgsgeometry.h" diff --git a/src/providers/oracle/qgsoracleprovider.cpp b/src/providers/oracle/qgsoracleprovider.cpp index 1a07a94214af..23c226f516e8 100644 --- a/src/providers/oracle/qgsoracleprovider.cpp +++ b/src/providers/oracle/qgsoracleprovider.cpp @@ -28,6 +28,7 @@ #include "qgscoordinatereferencesystem.h" #include "qgslogger.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsprojectstorageguiprovider.h" #include "qgsprojectstorageregistry.h" #include "qgsvectorlayer.h" diff --git a/src/providers/oracle/qgsoracleproviderconnection.cpp b/src/providers/oracle/qgsoracleproviderconnection.cpp index 86e4114b9cca..2e14f286259a 100644 --- a/src/providers/oracle/qgsoracleproviderconnection.cpp +++ b/src/providers/oracle/qgsoracleproviderconnection.cpp @@ -16,6 +16,7 @@ #include "qgsoracleproviderconnection.h" #include "qgsoracleconn.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgssettings.h" #include "qgsoracleprovider.h" #include "qgsexception.h" diff --git a/src/providers/oracle/qgsoracletransaction.cpp b/src/providers/oracle/qgsoracletransaction.cpp index 006585f629c6..7e319c1929de 100644 --- a/src/providers/oracle/qgsoracletransaction.cpp +++ b/src/providers/oracle/qgsoracletransaction.cpp @@ -22,6 +22,7 @@ #include "qgis.h" #include "qgsoracleconn.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" QgsOracleTransaction::QgsOracleTransaction( const QString &connString ) : QgsTransaction( connString ) diff --git a/src/providers/pdal/qgspdalprovider.cpp b/src/providers/pdal/qgspdalprovider.cpp index 6e78ee25ba99..e87f241ac484 100644 --- a/src/providers/pdal/qgspdalprovider.cpp +++ b/src/providers/pdal/qgspdalprovider.cpp @@ -58,6 +58,11 @@ QgsPdalProvider::QgsPdalProvider( loadIndex( ); } +Qgis::DataProviderFlags QgsPdalProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsPdalProvider::~QgsPdalProvider() = default; QgsCoordinateReferenceSystem QgsPdalProvider::crs() const @@ -474,19 +479,35 @@ void QgsPdalProviderMetadata::buildSupportedPointCloudFileFilterAndExtensions() QStringLiteral( "readers.las" ), QStringLiteral( "readers.e57" ), QStringLiteral( "readers.bpf" ) }; + + // the readers.text exposes extensions (csv, txt) which are generally not + // point cloud files. Add these extensions to the filters but do not expose + // them to the list of supported extensions to prevent unexpected behaviors + // such as trying to load a tabular csv file being from a drag and + // drop action. The windows which want to handle the "readers.text" reader + // need to explicitly call the provider. + // see for example qgspointcloudsourceselect.cpp. + const QStringList specificReaders {QStringLiteral( "readers.text" ) }; + + const QStringList readers = allowedReaders + specificReaders; + QStringList filterExtensions; for ( const auto &stage : stages ) { - if ( ! allowedReaders.contains( QString::fromStdString( stage ) ) ) + if ( !readers.contains( QString::fromStdString( stage ) ) ) continue; const pdal::StringList readerExtensions = extensions.extensions( stage ); for ( const auto &extension : readerExtensions ) { - sExtensions.append( QString::fromStdString( extension ) ); + if ( allowedReaders.contains( QString::fromStdString( stage ) ) ) + sExtensions.append( QString::fromStdString( extension ) ); + + filterExtensions.append( QString::fromStdString( extension ) ); } } + filterExtensions.sort(); sExtensions.sort(); - const QString extensionsString = QStringLiteral( "*." ).append( sExtensions.join( QLatin1String( " *." ) ) ); + const QString extensionsString = QStringLiteral( "*." ).append( filterExtensions.join( QLatin1String( " *." ) ) ); sFilterString = tr( "PDAL Point Clouds" ) + QString( " (%1 %2)" ).arg( extensionsString, extensionsString.toUpper() ); } ); } diff --git a/src/providers/pdal/qgspdalprovider.h b/src/providers/pdal/qgspdalprovider.h index 6316a4a3f85f..ae201d6c0b7c 100644 --- a/src/providers/pdal/qgspdalprovider.h +++ b/src/providers/pdal/qgspdalprovider.h @@ -33,6 +33,7 @@ class QgsPdalProvider: public QgsPointCloudDataProvider QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ); ~QgsPdalProvider(); + Qgis::DataProviderFlags flags() const override; QgsCoordinateReferenceSystem crs() const override; QgsRectangle extent() const override; QgsPointCloudAttributeCollection attributes() const override; diff --git a/src/providers/postgres/CMakeLists.txt b/src/providers/postgres/CMakeLists.txt index b074d4d845d7..84e3e4b66445 100644 --- a/src/providers/postgres/CMakeLists.txt +++ b/src/providers/postgres/CMakeLists.txt @@ -52,6 +52,7 @@ target_include_directories(provider_postgres_a PUBLIC # require c++17 target_compile_features(provider_postgres_a PRIVATE cxx_std_17) +target_compile_definitions(provider_postgres_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries (provider_postgres_a qgis_core @@ -77,6 +78,7 @@ if (WITH_GUI) # require c++17 target_compile_features(provider_postgres_gui_a PRIVATE cxx_std_17) + target_compile_definitions(provider_postgres_gui_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(provider_postgres_gui_a qgis_gui @@ -110,6 +112,7 @@ target_include_directories(provider_postgresraster_a PUBLIC # require c++17 target_compile_features(provider_postgresraster_a PRIVATE cxx_std_17) +target_compile_definitions(provider_postgresraster_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries (provider_postgresraster_a qgis_core @@ -151,6 +154,8 @@ else() # require c++17 target_compile_features(provider_postgres PRIVATE cxx_std_17) target_compile_features(provider_postgresraster PRIVATE cxx_std_17) + target_compile_definitions(provider_postgres PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") + target_compile_definitions(provider_postgresraster PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(provider_postgres qgis_core diff --git a/src/providers/postgres/qgspostgresconn.cpp b/src/providers/postgres/qgspostgresconn.cpp index 10e6ce978387..7b057491a06d 100644 --- a/src/providers/postgres/qgspostgresconn.cpp +++ b/src/providers/postgres/qgspostgresconn.cpp @@ -28,6 +28,7 @@ #include "qgspostgresconnpool.h" #include "qgsvariantutils.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsapplication.h" #include diff --git a/src/providers/postgres/qgspostgresconn.h b/src/providers/postgres/qgspostgresconn.h index e71cbc9b347f..877988d8af01 100644 --- a/src/providers/postgres/qgspostgresconn.h +++ b/src/providers/postgres/qgspostgresconn.h @@ -29,6 +29,7 @@ #include "qgswkbtypes.h" #include "qgsconfig.h" #include "qgsvectordataprovider.h" +#include "qgsdbquerylog_p.h" extern "C" { @@ -318,9 +319,7 @@ class QgsPostgresConn : public QObject QString uniqueCursorName(); -#if 0 PGconn *pgConnection() { return mConn; } -#endif // // libpq wrapper diff --git a/src/providers/postgres/qgspostgresfeatureiterator.cpp b/src/providers/postgres/qgspostgresfeatureiterator.cpp index 3cc93a1e14b4..ee0f33ce32cd 100644 --- a/src/providers/postgres/qgspostgresfeatureiterator.cpp +++ b/src/providers/postgres/qgspostgresfeatureiterator.cpp @@ -20,6 +20,7 @@ #include "qgspostgrestransaction.h" #include "qgslogger.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsmessagelog.h" #include "qgsexception.h" #include "qgsgeometryengine.h" diff --git a/src/providers/postgres/qgspostgreslistener.cpp b/src/providers/postgres/qgspostgreslistener.cpp index d314b396f4b0..af54f081f897 100644 --- a/src/providers/postgres/qgspostgreslistener.cpp +++ b/src/providers/postgres/qgspostgreslistener.cpp @@ -19,6 +19,7 @@ #include "qgsdatasourceuri.h" #include "qgscredentials.h" #include "qgslogger.h" +#include "qgspostgresconn.h" #ifdef Q_OS_WIN #include @@ -26,8 +27,6 @@ #include #endif -const int PG_CONNECT_TIMEOUT = 30; - extern "C" { #include @@ -37,17 +36,27 @@ std::unique_ptr< QgsPostgresListener > QgsPostgresListener::create( const QStrin { std::unique_ptr< QgsPostgresListener > res( new QgsPostgresListener( connString ) ); QgsDebugMsgLevel( QStringLiteral( "starting notification listener" ), 2 ); - res->start(); - res->mMutex.lock(); - res->mIsReadyCondition.wait( &res->mMutex ); - res->mMutex.unlock(); + res->start(); return res; } QgsPostgresListener::QgsPostgresListener( const QString &connString ) - : mConnString( connString ) { + mConn = QgsPostgresConn::connectDb( connString, true, false ); + if ( mConn ) + { + mConn->moveToThread( this ); + + QgsPostgresResult result( mConn->LoggedPQexec( "QgsPostgresListener", QStringLiteral( "LISTEN qgis" ) ) ); + if ( result.PQresultStatus() != PGRES_COMMAND_OK ) + { + QgsDebugError( QStringLiteral( "error in listen" ) ); + + mConn->unref(); + mConn = nullptr; + } + } } QgsPostgresListener::~QgsPostgresListener() @@ -56,73 +65,27 @@ QgsPostgresListener::~QgsPostgresListener() QgsDebugMsgLevel( QStringLiteral( "stopping the loop" ), 2 ); wait(); QgsDebugMsgLevel( QStringLiteral( "notification listener stopped" ), 2 ); + + if ( mConn ) + mConn->unref(); } void QgsPostgresListener::run() { - PGconn *conn = nullptr; - QString connectString = mConnString; - - connectString += QStringLiteral( " connect_timeout=%1" ).arg( PG_CONNECT_TIMEOUT ); - conn = PQconnectdb( connectString.toUtf8() ); - - if ( PQstatus( conn ) != CONNECTION_OK ) - { - QgsDataSourceUri uri( connectString ); - QString username = uri.username(); - QString password = uri.password(); - - PQfinish( conn ); - - QgsCredentials::instance()->lock(); - - if ( QgsCredentials::instance()->get( mConnString, username, password, PQerrorMessage( conn ) ) ) - { - uri.setUsername( username ); - uri.setPassword( password ); - connectString = uri.connectionInfo( false ); - connectString += QStringLiteral( " connect_timeout=%1" ).arg( PG_CONNECT_TIMEOUT ); - - conn = PQconnectdb( connectString.toUtf8() ); - if ( PQstatus( conn ) == CONNECTION_OK ) - QgsCredentials::instance()->put( mConnString, username, password ); - } - - QgsCredentials::instance()->unlock(); - - if ( PQstatus( conn ) != CONNECTION_OK ) - { - PQfinish( conn ); - QgsDebugMsgLevel( QStringLiteral( "LISTENer not started" ), 2 ); - return; - } - } - - - PGresult *res = PQexec( conn, "LISTEN qgis" ); - if ( PQresultStatus( res ) != PGRES_COMMAND_OK ) + if ( !mConn ) { QgsDebugError( QStringLiteral( "error in listen" ) ); - PQclear( res ); - PQfinish( conn ); - mMutex.lock(); - mIsReadyCondition.wakeOne(); - mMutex.unlock(); return; } - PQclear( res ); - mMutex.lock(); - mIsReadyCondition.wakeOne(); - mMutex.unlock(); - const int sock = PQsocket( conn ); + const int sock = PQsocket( mConn->pgConnection() ); if ( sock < 0 ) { QgsDebugError( QStringLiteral( "error in socket" ) ); - PQfinish( conn ); return; } + PGconn *pgconn = mConn->pgConnection(); forever { fd_set input_mask; @@ -139,8 +102,8 @@ void QgsPostgresListener::run() break; } - PQconsumeInput( conn ); - PGnotify *n = PQnotifies( conn ); + PQconsumeInput( pgconn ); + PGnotify *n = PQnotifies( pgconn ); if ( n ) { const QString msg( n->extra ); @@ -155,7 +118,4 @@ void QgsPostgresListener::run() break; } } - PQfinish( conn ); } - - diff --git a/src/providers/postgres/qgspostgreslistener.h b/src/providers/postgres/qgspostgreslistener.h index 4d4b20600351..265a44f58662 100644 --- a/src/providers/postgres/qgspostgreslistener.h +++ b/src/providers/postgres/qgspostgreslistener.h @@ -24,6 +24,9 @@ #include #include +class QgsPostgresConn; + + /** * \class QgsPostgresListener * \brief Launch a thread to listen on postgres notifications on the "qgis" channel, the notify signal is emitted on postgres notify. @@ -51,9 +54,8 @@ class QgsPostgresListener : public QThread private: volatile bool mStop = false; - const QString mConnString; - QWaitCondition mIsReadyCondition; - QMutex mMutex; + + QgsPostgresConn *mConn = nullptr; QgsPostgresListener( const QString &connString ); diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index b5b60612abab..e48859dcffa0 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -40,6 +40,7 @@ #include "qgsstringutils.h" #include "qgsjsonutils.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgspostgreslayermetadataprovider.h" #include "qgspostgresprovider.h" @@ -969,7 +970,7 @@ bool QgsPostgresProvider::loadFields() QMap > notNullMap, uniqueMap; if ( result.PQnfields() > 0 ) { - // Collect attribiute oids + // Collect attribute oids QSet attroids; for ( int i = 0; i < result.PQnfields(); i++ ) { @@ -1389,7 +1390,7 @@ bool QgsPostgresProvider::loadFields() .arg( quotedValue( mQuery ) ) .arg( quotedValue( fieldName ) ); QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) ); - if ( seqResult.PQntuples() == 1 ) + if ( seqResult.PQntuples() == 1 && !seqResult.PQgetisnull( 0, 0 ) ) { defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1)" ).arg( quotedValue( seqResult.PQgetvalue( 0, 0 ) ) ); } diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp index 48c10ca2c5ca..b8376fd04896 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp @@ -198,6 +198,10 @@ QgsPostgresRasterProvider::QgsPostgresRasterProvider( const QgsPostgresRasterPro { } +Qgis::DataProviderFlags QgsPostgresRasterProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} bool QgsPostgresRasterProvider::hasSufficientPermsAndCapabilities() { diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.h b/src/providers/postgres/raster/qgspostgresrasterprovider.h index 6bf0580a68c6..24f4abaf55e0 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.h +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.h @@ -42,6 +42,7 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider public: // QgsDataProvider interface + Qgis::DataProviderFlags flags() const override; virtual QgsCoordinateReferenceSystem crs() const override; virtual QgsRectangle extent() const override; virtual bool isValid() const override; diff --git a/src/providers/spatialite/CMakeLists.txt b/src/providers/spatialite/CMakeLists.txt index efaad9a47c81..5d6957a2370c 100644 --- a/src/providers/spatialite/CMakeLists.txt +++ b/src/providers/spatialite/CMakeLists.txt @@ -43,6 +43,7 @@ target_link_libraries(provider_spatialite_a # require c++17 target_compile_features(provider_spatialite_a PRIVATE cxx_std_17) +target_compile_definitions(provider_spatialite_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_spatialite_a PRIVATE "-DQT_NO_FOREACH") @@ -68,6 +69,7 @@ if (WITH_GUI) # require c++17 target_compile_features(provider_spatialite_gui_a PRIVATE cxx_std_17) + target_compile_definitions(provider_spatialite_gui_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_spatialite_gui_a PRIVATE "-DQT_NO_FOREACH") @@ -90,6 +92,7 @@ else() # require c++17 target_compile_features(provider_spatialite PRIVATE cxx_std_17) + target_compile_definitions(provider_spatialite PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_spatialite PRIVATE "-DQT_NO_FOREACH") diff --git a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp index 52d6ddff77e3..b8d180d007be 100644 --- a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp +++ b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp @@ -26,6 +26,7 @@ #include "qgsjsonutils.h" #include "qgsexception.h" #include "qgsgeometryengine.h" +#include "qgsdbquerylog_p.h" QgsSpatiaLiteFeatureIterator::QgsSpatiaLiteFeatureIterator( QgsSpatiaLiteFeatureSource *source, bool ownSource, const QgsFeatureRequest &request ) : QgsAbstractFeatureIteratorFromSource( source, ownSource, request ) diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index 8e0e4ca88d67..7e732b3bfa07 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -31,6 +31,7 @@ email : a.furieri@lqt.it #include "qgsspatialitetransaction.h" #include "qgsspatialiteproviderconnection.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include "qgsjsonutils.h" #include "qgsvectorlayer.h" @@ -656,6 +657,11 @@ QgsSpatiaLiteProvider::~QgsSpatiaLiteProvider() invalidateConnections( mSqlitePath ); } +Qgis::DataProviderFlags QgsSpatiaLiteProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D | Qgis::DataProviderFlag::FastExtent3D; +} + QgsAbstractFeatureSource *QgsSpatiaLiteProvider::featureSource() const { return new QgsSpatiaLiteFeatureSource( this ); @@ -1105,8 +1111,8 @@ QVariant QgsSpatiaLiteProvider::defaultValue( int fieldId ) const } } - ( void )mAttributeFields.at( fieldId ).convertCompatible( resultVar ); - return resultVar; + const bool compatible = mAttributeFields.at( fieldId ).convertCompatible( resultVar ); + return compatible && !QgsVariantUtils::isNull( resultVar ) ? resultVar : QVariant(); } QString QgsSpatiaLiteProvider::defaultValueClause( int fieldIndex ) const diff --git a/src/providers/spatialite/qgsspatialiteprovider.h b/src/providers/spatialite/qgsspatialiteprovider.h index 042b0463f8e0..9845c5b0ed47 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.h +++ b/src/providers/spatialite/qgsspatialiteprovider.h @@ -85,6 +85,7 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider ~ QgsSpatiaLiteProvider() override; + Qgis::DataProviderFlags flags() const override; QgsAbstractFeatureSource *featureSource() const override; QString storageType() const override; QgsCoordinateReferenceSystem crs() const override; diff --git a/src/providers/spatialite/qgsspatialiteproviderconnection.cpp b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp index ebae0ec07c2f..3083da243a60 100644 --- a/src/providers/spatialite/qgsspatialiteproviderconnection.cpp +++ b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp @@ -24,6 +24,7 @@ #include "qgsvectorlayer.h" #include "qgsfeedback.h" #include "qgsdbquerylog.h" +#include "qgsdbquerylog_p.h" #include #include diff --git a/src/providers/virtualraster/qgsvirtualrasterprovider.cpp b/src/providers/virtualraster/qgsvirtualrasterprovider.cpp index ade11f5fa6cf..a0e6d08d1f9e 100644 --- a/src/providers/virtualraster/qgsvirtualrasterprovider.cpp +++ b/src/providers/virtualraster/qgsvirtualrasterprovider.cpp @@ -215,6 +215,11 @@ QgsRasterBlock *QgsVirtualRasterProvider::block( int bandNo, const QgsRectangle return tblock.release(); } +Qgis::DataProviderFlags QgsVirtualRasterProvider::flags() const +{ + return Qgis::DataProviderFlag::FastExtent2D; +} + QgsRectangle QgsVirtualRasterProvider::extent() const { return mExtent; diff --git a/src/providers/virtualraster/qgsvirtualrasterprovider.h b/src/providers/virtualraster/qgsvirtualrasterprovider.h index 533353c39f47..c0423a901061 100644 --- a/src/providers/virtualraster/qgsvirtualrasterprovider.h +++ b/src/providers/virtualraster/qgsvirtualrasterprovider.h @@ -40,6 +40,7 @@ class QgsVirtualRasterProvider : public QgsRasterDataProvider { Q_UNUSED( bandNo ) Q_UNUSED( viewExtent ); Q_UNUSED( width ); Q_UNUSED( height ); Q_UNUSED( data ); Q_UNUSED( feedback ); return true; } // QgsDataProvider interface + Qgis::DataProviderFlags flags() const override; virtual bool isValid() const override; virtual QgsCoordinateReferenceSystem crs() const override; virtual QgsRectangle extent() const override; diff --git a/src/providers/wcs/CMakeLists.txt b/src/providers/wcs/CMakeLists.txt index 09d29e14ce71..823a36c7b667 100644 --- a/src/providers/wcs/CMakeLists.txt +++ b/src/providers/wcs/CMakeLists.txt @@ -29,6 +29,8 @@ target_link_libraries(provider_wcs_a # require c++17 target_compile_features(provider_wcs_a PRIVATE cxx_std_17) +# We use private headers from core that need this +target_compile_definitions(provider_wcs_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_wcs_a PRIVATE "-DQT_NO_FOREACH") if (WITH_GUI) @@ -68,6 +70,9 @@ else() # require c++17 target_compile_features(provider_wcs PRIVATE cxx_std_17) + + # We use private headers from core that need this + target_compile_definitions(provider_wcs PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_link_libraries(provider_wcs qgis_core diff --git a/src/providers/wcs/qgswcscapabilities.cpp b/src/providers/wcs/qgswcscapabilities.cpp index d0e45ab858c6..b892af7031b8 100644 --- a/src/providers/wcs/qgswcscapabilities.cpp +++ b/src/providers/wcs/qgswcscapabilities.cpp @@ -30,6 +30,7 @@ #include "qgsrectangle.h" #include "qgscoordinatereferencesystem.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsmessageoutput.h" #include "qgsmessagelog.h" #include "qgsapplication.h" diff --git a/src/providers/wcs/qgswcsprovider.cpp b/src/providers/wcs/qgswcsprovider.cpp index 052a25e63409..6603b39f2e51 100644 --- a/src/providers/wcs/qgswcsprovider.cpp +++ b/src/providers/wcs/qgswcsprovider.cpp @@ -27,6 +27,7 @@ #include "qgsrectangle.h" #include "qgscoordinatereferencesystem.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsnetworkreplyparser.h" #include "qgsmessagelog.h" #include "qgsexception.h" diff --git a/src/providers/wfs/CMakeLists.txt b/src/providers/wfs/CMakeLists.txt index c553d933653e..d08fcabb4f72 100644 --- a/src/providers/wfs/CMakeLists.txt +++ b/src/providers/wfs/CMakeLists.txt @@ -69,6 +69,9 @@ target_link_libraries(provider_wfs_a # require c++17 target_compile_features(provider_wfs_a PRIVATE cxx_std_17) + +# We use private headers from core that need this +target_compile_definitions(provider_wfs_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_wfs_a PRIVATE "-DQT_NO_FOREACH") if (WITH_GUI) @@ -117,6 +120,8 @@ else() # require c++17 target_compile_features(provider_wfs PRIVATE cxx_std_17) + # We use private headers from core that need this + target_compile_definitions(provider_wfs PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_wfs PRIVATE "-DQT_NO_FOREACH") target_link_libraries (provider_wfs diff --git a/src/providers/wfs/oapif/qgsoapifprovider.h b/src/providers/wfs/oapif/qgsoapifprovider.h index 20a87fb3804a..562c23d6cfc1 100644 --- a/src/providers/wfs/oapif/qgsoapifprovider.h +++ b/src/providers/wfs/oapif/qgsoapifprovider.h @@ -276,7 +276,7 @@ class QgsOapifFeatureDownloaderImpl final: public QObject, public QgsFeatureDown { Q_OBJECT - DEFINE_FEATURE_DOWLOADER_IMPL_SLOTS + DEFINE_FEATURE_DOWNLOADER_IMPL_SLOTS signals: /* Used internally by the stop() method */ diff --git a/src/providers/wfs/oapif/qgsoapifutils.h b/src/providers/wfs/oapif/qgsoapifutils.h index bf375953a8d3..f1a904dc23dd 100644 --- a/src/providers/wfs/oapif/qgsoapifutils.h +++ b/src/providers/wfs/oapif/qgsoapifutils.h @@ -38,7 +38,7 @@ class QgsOAPIFJson qint64 length = -1; }; - //! Parses the "link" property of jParet + //! Parses the "link" property of jParent static std::vector parseLinks( const json &jParent ); //! Find among links the one that matches rel, by using an optional list of preferable types. diff --git a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h index fc7f2806370a..81f9a173ed57 100644 --- a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h +++ b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h @@ -208,7 +208,7 @@ class QgsFeatureDownloaderImpl } while(0) // Sorry for ugliness. Due to QgsFeatureDownloaderImpl that cannot derive from QObject -#define DEFINE_FEATURE_DOWLOADER_IMPL_SLOTS \ +#define DEFINE_FEATURE_DOWNLOADER_IMPL_SLOTS \ protected: \ void emitDoStop() override { emit doStop(); } \ void setStopFlag() { QgsFeatureDownloaderImpl::setStopFlag(); } \ diff --git a/src/providers/wfs/qgsbasenetworkrequest.cpp b/src/providers/wfs/qgsbasenetworkrequest.cpp index f33d74cc3eac..e85149acee87 100644 --- a/src/providers/wfs/qgsbasenetworkrequest.cpp +++ b/src/providers/wfs/qgsbasenetworkrequest.cpp @@ -19,6 +19,7 @@ #include "qgslogger.h" #include "qgsmessagelog.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgssettings.h" #include "qgsvariantutils.h" diff --git a/src/providers/wfs/qgsbasenetworkrequest.h b/src/providers/wfs/qgsbasenetworkrequest.h index dd33441e5b67..7e5c69cdd1a0 100644 --- a/src/providers/wfs/qgsbasenetworkrequest.h +++ b/src/providers/wfs/qgsbasenetworkrequest.h @@ -142,7 +142,7 @@ class QgsBaseNetworkRequest : public QObject */ virtual QString errorMessageWithReason( const QString &reason ) = 0; - //! Returns experiation delay in second + //! Returns expiration delay in second virtual int defaultExpirationInSec() { return 0; } private: diff --git a/src/providers/wfs/qgswfscapabilities.cpp b/src/providers/wfs/qgswfscapabilities.cpp index b47d3c61a443..91f0a9be1724 100644 --- a/src/providers/wfs/qgswfscapabilities.cpp +++ b/src/providers/wfs/qgswfscapabilities.cpp @@ -293,41 +293,41 @@ void QgsWfsCapabilities::capabilitiesReplyFinished() QDomElement operationsMetadataElem = doc.firstChildElement( QStringLiteral( "OperationsMetadata" ) ); if ( !operationsMetadataElem.isNull() ) { - QDomNodeList contraintList = operationsMetadataElem.elementsByTagName( QStringLiteral( "Constraint" ) ); - for ( int i = 0; i < contraintList.size(); ++i ) + QDomNodeList constraintList = operationsMetadataElem.elementsByTagName( QStringLiteral( "Constraint" ) ); + for ( int i = 0; i < constraintList.size(); ++i ) { - QDomElement contraint = contraintList.at( i ).toElement(); - if ( contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "DefaultMaxFeatures" ) /* WFS 1.1 */ ) + QDomElement constraint = constraintList.at( i ).toElement(); + if ( constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "DefaultMaxFeatures" ) /* WFS 1.1 */ ) { - QDomElement value = contraint.firstChildElement( QStringLiteral( "Value" ) ); + QDomElement value = constraint.firstChildElement( QStringLiteral( "Value" ) ); if ( !value.isNull() ) { mCaps.maxFeatures = value.text().toInt(); QgsDebugMsgLevel( QStringLiteral( "maxFeatures: %1" ).arg( mCaps.maxFeatures ), 2 ); } } - else if ( contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "CountDefault" ) /* WFS 2.0 (e.g. MapServer) */ ) + else if ( constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "CountDefault" ) /* WFS 2.0 (e.g. MapServer) */ ) { - QDomElement value = contraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); + QDomElement value = constraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); if ( !value.isNull() ) { mCaps.maxFeatures = value.text().toInt(); QgsDebugMsgLevel( QStringLiteral( "maxFeatures: %1" ).arg( mCaps.maxFeatures ), 2 ); } } - else if ( contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsResultPaging" ) /* WFS 2.0 */ ) + else if ( constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsResultPaging" ) /* WFS 2.0 */ ) { - QDomElement value = contraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); + QDomElement value = constraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); if ( !value.isNull() && value.text() == QLatin1String( "TRUE" ) ) { mCaps.supportsPaging = true; QgsDebugMsgLevel( QStringLiteral( "Supports paging" ), 2 ); } } - else if ( contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsStandardJoins" ) || - contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsSpatialJoins" ) /* WFS 2.0 */ ) + else if ( constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsStandardJoins" ) || + constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "ImplementsSpatialJoins" ) /* WFS 2.0 */ ) { - QDomElement value = contraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); + QDomElement value = constraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); if ( !value.isNull() && value.text() == QLatin1String( "TRUE" ) ) { mCaps.supportsJoins = true; @@ -365,13 +365,13 @@ void QgsWfsCapabilities::capabilitiesReplyFinished() if ( name == QLatin1String( "GetFeature" ) ) { - QDomNodeList operationContraintList = operation.elementsByTagName( QStringLiteral( "Constraint" ) ); - for ( int j = 0; j < operationContraintList.size(); ++j ) + QDomNodeList operationConstraintList = operation.elementsByTagName( QStringLiteral( "Constraint" ) ); + for ( int j = 0; j < operationConstraintList.size(); ++j ) { - QDomElement contraint = operationContraintList.at( j ).toElement(); - if ( contraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "CountDefault" ) ) + QDomElement constraint = operationConstraintList.at( j ).toElement(); + if ( constraint.attribute( QStringLiteral( "name" ) ) == QLatin1String( "CountDefault" ) ) { - QDomElement value = contraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); + QDomElement value = constraint.firstChildElement( QStringLiteral( "DefaultValue" ) ); if ( !value.isNull() ) { mCaps.maxFeatures = value.text().toInt(); diff --git a/src/providers/wfs/qgswfsfeatureiterator.h b/src/providers/wfs/qgswfsfeatureiterator.h index 644b5158c0a0..f6191a2f5624 100644 --- a/src/providers/wfs/qgswfsfeatureiterator.h +++ b/src/providers/wfs/qgswfsfeatureiterator.h @@ -68,7 +68,7 @@ class QgsWFSFeatureDownloaderImpl final: public QgsWfsRequest, public QgsFeature { Q_OBJECT - DEFINE_FEATURE_DOWLOADER_IMPL_SLOTS + DEFINE_FEATURE_DOWNLOADER_IMPL_SLOTS signals: /* Used internally by the stop() method */ diff --git a/src/providers/wfs/qgswfsprovider.cpp b/src/providers/wfs/qgswfsprovider.cpp index 2fae077f2c8f..fb3641921e30 100644 --- a/src/providers/wfs/qgswfsprovider.cpp +++ b/src/providers/wfs/qgswfsprovider.cpp @@ -17,6 +17,7 @@ #include "qgis.h" #include "qgscplhttpfetchoverrider.h" +#include "qgssetrequestinitiator_p.h" #include "qgsfeature.h" #include "qgsfeedback.h" #include "qgsfields.h" @@ -2092,7 +2093,7 @@ bool QgsWFSProvider::readAttributesFromSchemaWithGMLAS( const QByteArray &respon // field_max_occurs (Integer) = 1 // field_category (String) = REGULAR - const auto pos_gmlPoint = qFieldXPath.indexOf( QStringLiteral( "/gml:Point," ) ); + const auto pos_gmlPoint = qFieldXPath.indexOf( QLatin1String( "/gml:Point," ) ); qFieldXPath.resize( pos_gmlPoint ); geomType = Qgis::WkbType::Unknown; } diff --git a/src/providers/wms/CMakeLists.txt b/src/providers/wms/CMakeLists.txt index 267ab85b486c..1a26656f8370 100644 --- a/src/providers/wms/CMakeLists.txt +++ b/src/providers/wms/CMakeLists.txt @@ -55,6 +55,9 @@ endif() target_link_libraries(provider_wms_a qgis_core ) + +# We use private headers from core that need this +target_compile_definitions(provider_wms_a PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") target_compile_definitions(provider_wms_a PRIVATE "-DQT_NO_FOREACH") if (WITH_GUI) @@ -90,6 +93,8 @@ else() endif() target_compile_definitions(provider_wms PRIVATE "-DQT_NO_FOREACH") + # We use private headers from core that need this + target_compile_definitions(provider_wms PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") install (TARGETS provider_wms RUNTIME DESTINATION ${QGIS_PLUGIN_DIR} diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index bb0dd0fdbcfe..298cd5dc9d0a 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -29,6 +29,7 @@ #include "qgslogger.h" #include "qgsmessagelog.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsexception.h" #include "qgstemporalutils.h" #include "qgsunittypes.h" @@ -212,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 ); @@ -1271,7 +1277,7 @@ void QgsWmsCapabilities::parseLayer( const QDomElement &element, QgsWmsLayerProp } else if ( tagName == QLatin1String( "LatLonBoundingBox" ) ) // legacy from earlier versions of WMS { - // boundingBox element can conatain comma as decimal separator and layer extent is not + // boundingBox element can contain comma as decimal separator and layer extent is not // calculated at all. Fixing by replacing comma with point. layerProperty.ex_GeographicBoundingBox = QgsRectangle( nodeElement.attribute( QStringLiteral( "minx" ) ).replace( ',', '.' ).toDouble(), @@ -1317,7 +1323,7 @@ void QgsWmsCapabilities::parseLayer( const QDomElement &element, QgsWmsLayerProp double wBLong, eBLong, sBLat, nBLat; bool wBOk, eBOk, sBOk, nBOk; - // boundingBox element can conatain comma as decimal separator and layer extent is not + // boundingBox element can contain comma as decimal separator and layer extent is not // calculated at all. Fixing by replacing comma with point. wBLong = wBoundLongitudeElem.text().replace( ',', '.' ).toDouble( &wBOk ); eBLong = eBoundLongitudeElem.text().replace( ',', '.' ).toDouble( &eBOk ); @@ -1699,7 +1705,7 @@ void QgsWmsCapabilities::parseTileSetProfile( const QDomElement &element ) else if ( tagName == QLatin1String( "BoundingBox" ) ) { QgsWmsBoundingBoxProperty boundingBoxProperty; - // boundingBox element can conatain comma as decimal separator and layer extent is not + // boundingBox element can contain comma as decimal separator and layer extent is not // calculated at all. Fixing by replacing comma with point. boundingBoxProperty.box = QgsRectangle( nodeElement.attribute( QStringLiteral( "minx" ) ).replace( ',', '.' ).toDouble(), 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 af8e12d2d6ad..48d202a7e43e 100644 --- a/src/providers/wms/qgswmsprovider.cpp +++ b/src/providers/wms/qgswmsprovider.cpp @@ -39,6 +39,7 @@ #include "qgsmessageoutput.h" #include "qgsmessagelog.h" #include "qgsnetworkaccessmanager.h" +#include "qgssetrequestinitiator_p.h" #include "qgsnetworkreplyparser.h" #include "qgstilecache.h" #include "qgsgdalutils.h" @@ -1297,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() ) @@ -4285,7 +4291,7 @@ QUrl QgsWmsProvider::getLegendGraphicFullURL( double scale, const QgsRectangle & QImage QgsWmsProvider::getLegendGraphic( double scale, bool forceRefresh, const QgsRectangle *visibleExtent ) { - // TODO manage return basing of getCapablity => avoid call if service is not available + // TODO manage return basing of getCapability => avoid call if service is not available // some services doesn't expose getLegendGraphic in capabilities but adding LegendURL in // the layer tags inside capabilities diff --git a/src/quickgui/qgsquickmapcanvasmap.cpp b/src/quickgui/qgsquickmapcanvasmap.cpp index e258c8484c52..9bf5e2cd37c0 100644 --- a/src/quickgui/qgsquickmapcanvasmap.cpp +++ b/src/quickgui/qgsquickmapcanvasmap.cpp @@ -18,16 +18,19 @@ #include #include "qgis.h" +#include "qgsannotationlayer.h" #include "qgsexpressioncontextutils.h" +#include "qgsgrouplayer.h" +#include "qgslabelingresults.h" +#include "qgsmaplayerelevationproperties.h" #include "qgsmaplayertemporalproperties.h" #include "qgsmaprenderercache.h" #include "qgsmaprendererparalleljob.h" #include "qgsmessagelog.h" #include "qgspallabeling.h" #include "qgsproject.h" -#include "qgsannotationlayer.h" +#include "qgssymbollayerutils.h" #include "qgsvectorlayer.h" -#include "qgslabelingresults.h" #include "qgsquickmapcanvasmap.h" #include "qgsquickmapsettings.h" @@ -45,6 +48,7 @@ QgsQuickMapCanvasMap::QgsQuickMapCanvasMap( QQuickItem *parent ) connect( mMapSettings.get(), &QgsQuickMapSettings::extentChanged, this, &QgsQuickMapCanvasMap::onExtentChanged ); connect( mMapSettings.get(), &QgsQuickMapSettings::layersChanged, this, &QgsQuickMapCanvasMap::onLayersChanged ); connect( mMapSettings.get(), &QgsQuickMapSettings::temporalStateChanged, this, &QgsQuickMapCanvasMap::onTemporalStateChanged ); + connect( mMapSettings.get(), &QgsQuickMapSettings::zRangeChanged, this, &QgsQuickMapCanvasMap::onzRangeChanged ); connect( this, &QgsQuickMapCanvasMap::renderStarting, this, &QgsQuickMapCanvasMap::isRenderingChanged ); connect( this, &QgsQuickMapCanvasMap::mapCanvasRefreshed, this, &QgsQuickMapCanvasMap::isRenderingChanged ); @@ -286,6 +290,14 @@ void QgsQuickMapCanvasMap::onTemporalStateChanged() refresh(); } +void QgsQuickMapCanvasMap::onzRangeChanged() +{ + clearElevationCache(); + + // And trigger a new rendering job + refresh(); +} + void QgsQuickMapCanvasMap::updateTransform() { QgsRectangle imageExtent = mImageMapSettings.visibleExtent(); @@ -502,9 +514,23 @@ void QgsQuickMapCanvasMap::clearTemporalCache() const QList layerList = mMapSettings->mapSettings().layers(); for ( QgsMapLayer *layer : layerList ) { + bool alreadyInvalidatedThisLayer = false; + if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) ) + { + if ( vl->renderer() && QgsSymbolLayerUtils::rendererFrameRate( vl->renderer() ) > -1 ) + { + // layer has an animated symbol assigned, so we have to redraw it regardless of whether + // or not it has temporal settings + mCache->invalidateCacheForLayer( layer ); + alreadyInvalidatedThisLayer = true; + // we can't shortcut and "continue" here, as we still need to check whether the layer + // will cause label invalidation using the logic below + } + } + if ( layer->temporalProperties() && layer->temporalProperties()->isActive() ) { - if ( QgsVectorLayer *vl = qobject_cast( layer ) ) + if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) ) { if ( vl->labelsEnabled() || vl->diagramsEnabled() ) invalidateLabels = true; @@ -513,7 +539,23 @@ void QgsQuickMapCanvasMap::clearTemporalCache() if ( layer->temporalProperties()->flags() & QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges ) continue; - mCache->invalidateCacheForLayer( layer ); + if ( !alreadyInvalidatedThisLayer ) + mCache->invalidateCacheForLayer( layer ); + } + else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) + { + const QList childLayerList = gl->childLayers(); + for ( QgsMapLayer *childLayer : childLayerList ) + { + if ( childLayer->temporalProperties() && childLayer->temporalProperties()->isActive() ) + { + if ( childLayer->temporalProperties()->flags() & QgsTemporalProperty::FlagDontInvalidateCachedRendersWhenRangeChanges ) + continue; + + mCache->invalidateCacheForLayer( layer ); + break; + } + } } } @@ -525,3 +567,48 @@ void QgsQuickMapCanvasMap::clearTemporalCache() } } +void QgsQuickMapCanvasMap::clearElevationCache() +{ + if ( mCache ) + { + bool invalidateLabels = false; + const QList layerList = mMapSettings->mapSettings().layers(); + for ( QgsMapLayer *layer : layerList ) + { + if ( layer->elevationProperties() && layer->elevationProperties()->hasElevation() ) + { + if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) ) + { + if ( vl->labelsEnabled() || vl->diagramsEnabled() ) + invalidateLabels = true; + } + + if ( layer->elevationProperties()->flags() & QgsMapLayerElevationProperties::FlagDontInvalidateCachedRendersWhenRangeChanges ) + continue; + + mCache->invalidateCacheForLayer( layer ); + } + else if ( QgsGroupLayer *gl = qobject_cast( layer ) ) + { + const QList childLayerList = gl->childLayers(); + for ( QgsMapLayer *childLayer : childLayerList ) + { + if ( childLayer->elevationProperties() && childLayer->elevationProperties()->hasElevation() ) + { + if ( childLayer->elevationProperties()->flags() & QgsMapLayerElevationProperties::FlagDontInvalidateCachedRendersWhenRangeChanges ) + continue; + + mCache->invalidateCacheForLayer( layer ); + break; + } + } + } + } + + if ( invalidateLabels ) + { + mCache->clearCacheImage( QStringLiteral( "_labels_" ) ); + mCache->clearCacheImage( QStringLiteral( "_preview_labels_" ) ); + } + } +} diff --git a/src/quickgui/qgsquickmapcanvasmap.h b/src/quickgui/qgsquickmapcanvasmap.h index f1399b704856..0789c5bded33 100644 --- a/src/quickgui/qgsquickmapcanvasmap.h +++ b/src/quickgui/qgsquickmapcanvasmap.h @@ -185,6 +185,7 @@ class QUICK_EXPORT QgsQuickMapCanvasMap : public QQuickItem void onExtentChanged(); void onLayersChanged(); void onTemporalStateChanged(); + void onzRangeChanged(); private: @@ -196,6 +197,7 @@ class QUICK_EXPORT QgsQuickMapCanvasMap : public QQuickItem void updateTransform(); void zoomToFullExtent(); void clearTemporalCache(); + void clearElevationCache(); std::unique_ptr mMapSettings; bool mPinching = false; diff --git a/src/quickgui/qgsquickmapsettings.cpp b/src/quickgui/qgsquickmapsettings.cpp index 8ac27722930a..2f79cafef01d 100644 --- a/src/quickgui/qgsquickmapsettings.cpp +++ b/src/quickgui/qgsquickmapsettings.cpp @@ -272,6 +272,7 @@ void QgsQuickMapSettings::onReadProject( const QDomDocument &doc ) emit outputDpiChanged(); emit layersChanged(); emit temporalStateChanged(); + emit zRangeChanged(); } double QgsQuickMapSettings::rotation() const @@ -344,3 +345,39 @@ void QgsQuickMapSettings::setTemporalEnd( const QDateTime &end ) mMapSettings.setTemporalRange( QgsDateTimeRange( range.begin(), end ) ); emit temporalStateChanged(); } + +double QgsQuickMapSettings::zRangeLower() const +{ + const QgsDoubleRange zRange = mMapSettings.zRange(); + return zRange.lower(); +} + +void QgsQuickMapSettings::setZRangeLower( const double &lower ) +{ + const QgsDoubleRange zRange = mMapSettings.zRange(); + if ( zRange.lower() == lower ) + { + return; + } + + mMapSettings.setZRange( QgsDoubleRange( lower, zRange.upper(), zRange.includeLower(), zRange.includeUpper() ) ); + emit zRangeChanged(); +} + +double QgsQuickMapSettings::zRangeUpper() const +{ + const QgsDoubleRange zRange = mMapSettings.zRange(); + return zRange.upper(); +} + +void QgsQuickMapSettings::setZRangeUpper( const double &upper ) +{ + const QgsDoubleRange zRange = mMapSettings.zRange(); + if ( zRange.upper() == upper ) + { + return; + } + + mMapSettings.setZRange( QgsDoubleRange( zRange.lower(), upper, zRange.includeLower(), zRange.includeUpper() ) ); + emit zRangeChanged(); +} diff --git a/src/quickgui/qgsquickmapsettings.h b/src/quickgui/qgsquickmapsettings.h index f51227ab21e2..78b795f956d7 100644 --- a/src/quickgui/qgsquickmapsettings.h +++ b/src/quickgui/qgsquickmapsettings.h @@ -135,6 +135,16 @@ class QUICK_EXPORT QgsQuickMapSettings : public QObject */ Q_PROPERTY( QDateTime temporalEnd READ temporalEnd WRITE setTemporalEnd NOTIFY temporalStateChanged ) + /** + * The Z range's lower value (since QGIS 3.38) + */ + Q_PROPERTY( double zRangeLower READ zRangeLower WRITE setZRangeLower NOTIFY zRangeChanged ) + + /** + * The Z range's upper value (since QGIS 3.38) + */ + Q_PROPERTY( double zRangeUpper READ zRangeUpper WRITE setZRangeUpper NOTIFY zRangeChanged ) + public: //! Create new map settings explicit QgsQuickMapSettings( QObject *parent = nullptr ); @@ -288,6 +298,18 @@ class QUICK_EXPORT QgsQuickMapSettings : public QObject //! \copydoc QgsQuickMapSettings::temporalEnd void setTemporalEnd( const QDateTime &end ); + //! \copydoc QgsQuickMapSettings::zRangeLower + double zRangeLower() const; + + //! \copydoc QgsQuickMapSettings::zRangeLower + void setZRangeLower( const double &lower ); + + //! \copydoc QgsQuickMapSettings::zRangeLower + double zRangeUpper() const; + + //! \copydoc QgsQuickMapSettings::zRangeLower + void setZRangeUpper( const double &upper ); + signals: //! \copydoc QgsQuickMapSettings::project void projectChanged(); @@ -329,6 +351,14 @@ class QUICK_EXPORT QgsQuickMapSettings : public QObject */ void temporalStateChanged(); + /** + * Emitted when the Z range has changed. + * \see zRangeLower() + * \see zRangeUpper() + * \since QGIS 3.38 + */ + void zRangeChanged(); + //! \copydoc QgsQuickMapSettings::devicePixelRatio void devicePixelRatioChanged(); 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/qgsfcgiserverrequest.cpp b/src/server/qgsfcgiserverrequest.cpp index 6d395c053a80..2af865ba660e 100644 --- a/src/server/qgsfcgiserverrequest.cpp +++ b/src/server/qgsfcgiserverrequest.cpp @@ -173,35 +173,49 @@ void QgsFcgiServerRequest::readData() if ( lengthstr ) { bool success = false; - int length = QString( lengthstr ).toInt( &success ); - // Note: REQUEST_BODY is not part of CGI standard, and it is not - // normally passed by any CGI web server and it is implemented only - // to allow unit tests to inject a request body and simulate a POST - // request - const char *request_body = getenv( "REQUEST_BODY" ); - if ( success && request_body ) + const int length = QString( lengthstr ).toInt( &success ); + if ( !success || length < 0 ) { - QString body( request_body ); - body.truncate( length ); - mData.append( body.toUtf8() ); - length = 0; + QgsMessageLog::logMessage( "fcgi: Invalid CONTENT_LENGTH", + QStringLiteral( "Server" ), Qgis::MessageLevel::Critical ); + mHasError = true; } + else + { + // Note: REQUEST_BODY is not part of CGI standard, and it is not + // normally passed by any CGI web server and it is implemented only + // to allow unit tests to inject a request body and simulate a POST + // request + const char *requestBody = getenv( "REQUEST_BODY" ); + #ifdef QGISDEBUG - qDebug() << "fcgi: reading " << lengthstr << " bytes from " << ( request_body ? "REQUEST_BODY" : "stdin" ); + qDebug() << "fcgi: reading " << lengthstr << " bytes from " << ( requestBody ? "REQUEST_BODY" : "stdin" ); #endif - if ( success ) - { - // XXX This not efficient at all !! - for ( int i = 0; i < length; ++i ) + + if ( requestBody ) { - mData.append( getchar() ); + const size_t requestBodyLength = strlen( requestBody ); + const int actualLength = static_cast( std::min( length, requestBodyLength ) ); + if ( static_cast( actualLength ) < requestBodyLength ) + { + QgsMessageLog::logMessage( "fcgi: CONTENT_LENGTH is larger than actual length of REQUEST_BODY", + QStringLiteral( "Server" ), Qgis::MessageLevel::Critical ); + mHasError = true; + } + mData = QByteArray::fromRawData( requestBody, actualLength ); + } + else + { + mData.resize( length ); + const int actualLength = static_cast( fread( mData.data(), 1, length, stdin ) ); + if ( actualLength < length ) + { + mData.resize( actualLength ); + QgsMessageLog::logMessage( "fcgi: CONTENT_LENGTH is larger than actual length of stdin", + QStringLiteral( "Server" ), Qgis::MessageLevel::Critical ); + mHasError = true; + } } - } - else - { - QgsMessageLog::logMessage( "fcgi: Failed to parse CONTENT_LENGTH", - QStringLiteral( "Server" ), Qgis::MessageLevel::Critical ); - mHasError = true; } } else diff --git a/src/server/qgsfcgiserverresponse.cpp b/src/server/qgsfcgiserverresponse.cpp index e2bc366b646b..5cf0a191ba65 100644 --- a/src/server/qgsfcgiserverresponse.cpp +++ b/src/server/qgsfcgiserverresponse.cpp @@ -25,7 +25,7 @@ #include "qgslogger.h" -#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROiD) +#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) #include #include @@ -62,7 +62,7 @@ QgsSocketMonitoringThread::QgsSocketMonitoringThread( bool *isResponseFinished, Q_ASSERT( mIsResponseFinished ); Q_ASSERT( mFeedback ); -#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROiD) +#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) if ( FCGI_stdout && FCGI_stdout->fcgx_stream && FCGI_stdout->fcgx_stream->data ) { QgsFCGXStreamData *stream = static_cast( FCGI_stdin->fcgx_stream->data ); @@ -96,7 +96,7 @@ void QgsSocketMonitoringThread::run( ) return; } -#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROiD) +#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) char c; while ( !*mIsResponseFinished ) { diff --git a/src/server/qgsfilterresponsedecorator.cpp b/src/server/qgsfilterresponsedecorator.cpp index 36a4fbb696b0..b173f680118c 100644 --- a/src/server/qgsfilterresponsedecorator.cpp +++ b/src/server/qgsfilterresponsedecorator.cpp @@ -41,6 +41,21 @@ void QgsFilterResponseDecorator::start() #endif } +void QgsFilterResponseDecorator::ready() +{ +#ifdef HAVE_SERVER_PYTHON_PLUGINS + QgsServerFiltersMap::const_iterator filtersIterator; + for ( filtersIterator = mFilters.constBegin(); filtersIterator != mFilters.constEnd(); ++filtersIterator ) + { + if ( ! filtersIterator.value()->onProjectReady() ) + { + // stop propagation + return; + } + } +#endif +} + void QgsFilterResponseDecorator::finish() { #ifdef HAVE_SERVER_PYTHON_PLUGINS @@ -76,5 +91,3 @@ void QgsFilterResponseDecorator::flush() #endif mResponse.flush(); } - - diff --git a/src/server/qgsfilterresponsedecorator.h b/src/server/qgsfilterresponsedecorator.h index c72add2f8b93..64aef4d5b846 100644 --- a/src/server/qgsfilterresponsedecorator.h +++ b/src/server/qgsfilterresponsedecorator.h @@ -47,6 +47,12 @@ class QgsFilterResponseDecorator: public QgsServerResponse */ void start() SIP_THROW( QgsServerException ) SIP_VIRTUALERRORHANDLER( server_exception_handler ); + /** + * Call filters projectReady() method + * \since QGIS 3.36 + */ + void ready() SIP_THROW( QgsServerException ) SIP_VIRTUALERRORHANDLER( server_exception_handler ); + // QgsServerResponse overrides void setHeader( const QString &key, const QString &value ) override { mResponse.setHeader( key, value ); } @@ -85,8 +91,3 @@ class QgsFilterResponseDecorator: public QgsServerResponse }; #endif - - - - - diff --git a/src/server/qgsserver.cpp b/src/server/qgsserver.cpp index 50e81ced542b..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; @@ -444,7 +446,7 @@ void QgsServer::handleRequest( QgsServerRequest &request, QgsServerResponse &res sServerInterface->setConfigFilePath( project->fileName() ); } - // Call requestReady() method (if enabled) + // Call requestReady() method (if enabled) // This may also throw exceptions if there are errors in python plugins code try { @@ -490,6 +492,10 @@ void QgsServer::handleRequest( QgsServerRequest &request, QgsServerResponse &res sServerInterface->setConfigFilePath( QString() ); } + // Call projectReady() method (if enabled) + // This may also throw exceptions if there are errors in python plugins code + responseDecorator.ready(); + // Note that at this point we still might not have set a valid project. // There are APIs that work without a project (e.g. the landing page catalog API that // lists the available projects metadata). @@ -617,4 +623,3 @@ void QgsServer::initPython() } } #endif - diff --git a/src/server/qgsserverfilter.cpp b/src/server/qgsserverfilter.cpp index 64f1acd42f9c..d6b6b91a51f3 100644 --- a/src/server/qgsserverfilter.cpp +++ b/src/server/qgsserverfilter.cpp @@ -56,6 +56,11 @@ bool QgsServerFilter::onRequestReady() return true; } +bool QgsServerFilter::onProjectReady() +{ + return true; +} + bool QgsServerFilter::onResponseComplete() { Q_NOWARN_DEPRECATED_PUSH @@ -71,5 +76,3 @@ bool QgsServerFilter::onSendResponse() Q_NOWARN_DEPRECATED_POP return true; } - - diff --git a/src/server/qgsserverfilter.h b/src/server/qgsserverfilter.h index 40bb3b46f9f8..0e39fb803c54 100644 --- a/src/server/qgsserverfilter.h +++ b/src/server/qgsserverfilter.h @@ -105,6 +105,16 @@ class SERVER_EXPORT QgsServerFilter */ virtual bool onRequestReady(); + /** + * Method called when the QgsProject instance is ready to be used to perform the request, + * just before entering the main switch for core services. + * + * \return true if the call must propagate to the subsequent filters, false otherwise + * + * \since QGIS 3.36 + */ + virtual bool onProjectReady(); + /** * Method called when the QgsRequestHandler processing has done and * the response is ready, just after the main switch for core services diff --git a/src/server/qgsserverogcapi.cpp b/src/server/qgsserverogcapi.cpp index 7f9822282c64..66ccbcfb547a 100644 --- a/src/server/qgsserverogcapi.cpp +++ b/src/server/qgsserverogcapi.cpp @@ -152,7 +152,7 @@ QString QgsServerOgcApi::contentTypeToExtension( const ContentType &ct ) return contentTypeToString( ct ).toLower(); } -QgsServerOgcApi::ContentType QgsServerOgcApi::contenTypeFromExtension( const std::string &extension ) +QgsServerOgcApi::ContentType QgsServerOgcApi::contentTypeFromExtension( const std::string &extension ) { const QString exts = QString::fromStdString( extension ); const auto constMimeTypes( QgsServerOgcApi::contentTypeMimes() ); diff --git a/src/server/qgsserverogcapi.h b/src/server/qgsserverogcapi.h index 557f1d996800..d8c712a1c681 100644 --- a/src/server/qgsserverogcapi.h +++ b/src/server/qgsserverogcapi.h @@ -172,8 +172,15 @@ class SERVER_EXPORT QgsServerOgcApi : public QgsServerApi /** * Returns the Content-Type value corresponding to \a extension. + * \deprecated Use contentTypeFromExtension() */ - static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ); + Q_DECL_DEPRECATED static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension ) SIP_DEPRECATED { return contentTypeFromExtension( extension ); } // spellok + + /** + * Returns the Content-Type value corresponding to \a extension. + * \since QGIS 3.38 + */ + static QgsServerOgcApi::ContentType contentTypeFromExtension( const std::string &extension ); /** * Returns the mime-type for the \a contentType or an empty string if not found diff --git a/src/server/qgsserverogcapihandler.cpp b/src/server/qgsserverogcapihandler.cpp index e804f8fc816a..749c71c573d6 100644 --- a/src/server/qgsserverogcapihandler.cpp +++ b/src/server/qgsserverogcapihandler.cpp @@ -369,7 +369,7 @@ void QgsServerOgcApiHandler::htmlDump( const json &data, const QgsServerApiConte // Returns a short name from content types env.add_callback( "content_type_name", 1, [ = ]( Arguments & args ) { - const QgsServerOgcApi::ContentType ct { QgsServerOgcApi::contenTypeFromExtension( args.at( 0 )->get( ) ) }; + const QgsServerOgcApi::ContentType ct { QgsServerOgcApi::contentTypeFromExtension( args.at( 0 )->get( ) ) }; return QgsServerOgcApi::contentTypeToStdString( ct ); } ); diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index a484dd1d3bab..bd7503a19296 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -461,7 +461,20 @@ namespace QgsWfs { // For WFS 1.1 we honor requested CRS and axis order - const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + // Axis is not inverted if srsName starts with EPSG + // It needs to be an EPSG urn, e.g. urn:ogc:def:crs:EPSG::4326 + // This follows geoserver convention + // See: https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html + // if the crs is defined in the parameters, use it + // otherwise: + // - geojson uses 'EPSG:4326' by default + // - other formats use the default CRS (DefaultSRS, which is the layer's CRS) + const QString requestSrsName = request.serverParameters().value( QStringLiteral( "SRSNAME" ) ); + const QString srsName + { + !requestSrsName.isEmpty() ? requestSrsName : + ( aRequest.outputFormat == QgsWfsParameters::Format::GeoJSON ? QStringLiteral( "EPSG:4326" ) : outputCrs.authid() ) + }; const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && outputCrs.hasAxisInverted() && ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; @@ -1174,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() ); } @@ -1248,7 +1276,12 @@ namespace QgsWfs if ( format == QgsWfsParameters::Format::GML3 ) { // For WFS 1.1 we honor requested CRS and axis order - const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; + // Axis is not inverted if srsName starts with EPSG + // It needs to be an EPSG urn, e.g. urn:ogc:def:crs:EPSG::4326 + // This follows geoserver convention + // See: https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html + const QString requestSrsName = request.serverParameters().value( QStringLiteral( "SRSNAME" ) ); + const QString srsName = !requestSrsName.isEmpty() ? requestSrsName : crs.authid(); const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && crs.hasAxisInverted() && ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; diff --git a/src/server/services/wfs3/qgswfs3handlers.cpp b/src/server/services/wfs3/qgswfs3handlers.cpp index 085197839c61..d9ea31cc14f9 100644 --- a/src/server/services/wfs3/qgswfs3handlers.cpp +++ b/src/server/services/wfs3/qgswfs3handlers.cpp @@ -779,7 +779,9 @@ QList QgsWfs3CollectionsItemsHandler::parameters( 10 }; limit.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool { - return value.toInt() >= 0 && value.toInt() <= maxLimit; // TODO: make this configurable! + bool ok = false; + const qlonglong longVal { value.toLongLong( &ok ) }; + return ok && longVal >= 0 && longVal <= maxLimit; } ); params.push_back( limit ); @@ -802,8 +804,9 @@ QList QgsWfs3CollectionsItemsHandler::parameters( { offset.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool { - const qlonglong longVal { value.toLongLong( ) }; - return longVal >= 0 && longVal <= mapLayer->featureCount( ); + bool ok = false; + const qlonglong longVal { value.toLongLong( &ok ) }; + return ok && longVal >= 0 && longVal <= mapLayer->featureCount( ); } ); offset.setDescription( QStringLiteral( "Offset for features to retrieve [0-%1]" ).arg( mapLayer->featureCount( ) ) ); offsetValidatorSet = true; @@ -898,8 +901,9 @@ QList QgsWfs3CollectionsItemsHandler::parameters( { offset.setCustomValidator( [ ]( const QgsServerApiContext &, QVariant & value ) -> bool { - const qlonglong longVal { value.toLongLong( ) }; - return longVal >= 0 ; + bool ok = false; + const qlonglong longVal { value.toLongLong( &ok ) }; + return ok && longVal >= 0 ; } ); } diff --git a/src/server/services/wms/qgswmsparameters.cpp b/src/server/services/wms/qgswmsparameters.cpp index ea1eb3dd5159..d019f6c493a4 100644 --- a/src/server/services/wms/qgswmsparameters.cpp +++ b/src/server/services/wms/qgswmsparameters.cpp @@ -1360,7 +1360,7 @@ namespace QgsWms QStringList QgsWmsParameters::highlightLabelString() const { - return mWmsParameters.value( QgsWmsParameter::HIGHLIGHT_LABELSTRING ).toStringList( ';' ); + return mWmsParameters.value( QgsWmsParameter::HIGHLIGHT_LABELSTRING ).toStringList( ';', false ); } QStringList QgsWmsParameters::highlightLabelSize() const @@ -2116,7 +2116,7 @@ namespace QgsWms if ( mStr.startsWith( QLatin1String( "true" ), Qt::CaseInsensitive ) || mStr.startsWith( QLatin1String( "on" ), Qt::CaseInsensitive ) || mStr.startsWith( QLatin1String( "yes" ), Qt::CaseInsensitive ) || - mStr.startsWith( QLatin1String( "1" ) ) ) + mStr.startsWith( QLatin1Char( '1' ) ) ) return true; else return false; diff --git a/src/server/services/wms/qgswmsrendercontext.cpp b/src/server/services/wms/qgswmsrendercontext.cpp index de8e976501b1..eed87e272b67 100644 --- a/src/server/services/wms/qgswmsrendercontext.cpp +++ b/src/server/services/wms/qgswmsrendercontext.cpp @@ -650,6 +650,9 @@ bool QgsWmsRenderContext::isValidWidthHeight() const bool QgsWmsRenderContext::isValidWidthHeight( int width, int height ) const { + if ( width <= 0 || height <= 0 ) + return false; + //test if maxWidth / maxHeight are set in the project or as an env variable //and WIDTH / HEIGHT parameter is in the range allowed range //WIDTH @@ -692,14 +695,15 @@ bool QgsWmsRenderContext::isValidWidthHeight( int width, int height ) const return false; } - // Sanity check from internal QImage checks (see qimage.cpp) + // Sanity check from internal QImage checks + // (see QImageData::calculateImageParameters() in qimage_p.h) // this is to report a meaningful error message in case of // image creation failure and to differentiate it from out // of memory conditions. // depth for now it cannot be anything other than 32, but I don't like // to hardcode it: I hope we will support other depths in the future. - uint depth = 32; + int depth = 32; switch ( mParameters.format() ) { case QgsWmsParameters::Format::JPG: @@ -708,12 +712,12 @@ bool QgsWmsRenderContext::isValidWidthHeight( int width, int height ) const depth = 32; } + if ( width > ( std::numeric_limits::max() - 31 ) / depth ) + return false; + const int bytes_per_line = ( ( width * depth + 31 ) >> 5 ) << 2; // bytes per scanline (must be multiple of 4) - if ( std::numeric_limits::max() / depth < static_cast( width ) - || bytes_per_line <= 0 - || height <= 0 - || std::numeric_limits::max() / static_cast( bytes_per_line ) < static_cast( height ) + if ( std::numeric_limits::max() / bytes_per_line < height || std::numeric_limits::max() / sizeof( uchar * ) < static_cast( height ) ) { return false; diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 37f12d315252..a1c774ba12bc 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -155,7 +155,7 @@ namespace QgsWms } image.reset( createImage( size ) ); - // configure painter and addapt to the context + // configure painter and adapt to the context QPainter painter( image.get() ); context.setPainter( &painter ); @@ -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(); @@ -2670,7 +2670,7 @@ namespace QgsWms featureInfoString.append( featureInfoAttributeString ); } - else if ( name == QStringLiteral( "maptip" ) ) + else if ( name == QLatin1String( "maptip" ) ) { featureInfoString.append( QStringLiteral( R"HTML( %1)HTML" ).arg( value ) ); @@ -2726,7 +2726,7 @@ namespace QgsWms featureInfoString.append( featureInfoAttributeString ); } - else if ( name == QStringLiteral( "maptip" ) ) + else if ( name == QLatin1String( "maptip" ) ) { featureInfoString.append( QStringLiteral( R"HTML( %1)HTML" ).arg( value ) ); @@ -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/editorwidgets/qgsrelationreferenceconfigdlgbase.ui b/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui index 70335b5bf60e..87c0a5980407 100644 --- a/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui +++ b/src/ui/editorwidgets/qgsrelationreferenceconfigdlgbase.ui @@ -194,29 +194,31 @@
    - - - If no limit is set, all entries are loaded. - - - Limit number of entries - - - true - - - - - - Maximum number of entries - - - - - - - - + + + + + If unchecked, all the entries are loaded. + + + Limit number of entries to + + + true + + + + + + + + 0 + 0 + + + + + @@ -262,6 +264,8 @@ mCbxMapIdentification mCbxReadOnly mCbxAllowAddFeatures + mFetchLimitCheckBox + mFetchLimit mFilterGroupBox mAvailableFieldsList mAddFilterButton @@ -273,7 +277,6 @@ - diff --git a/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui b/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui index c6d71b9dd2dd..c5ef96c14c74 100644 --- a/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui +++ b/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui @@ -6,15 +6,15 @@ 0 0 - 427 - 489 + 318 + 490 Form - + @@ -52,14 +52,14 @@ - + Number of columns - + @@ -82,34 +82,41 @@ + + + + Order by value + + + - + Allow multiple selections - + Allow NULL value - + Use completer - - + + - Order by value + Only match from the beginning of the string @@ -123,8 +130,15 @@ - - + + + + + 1 + 0 + + + @@ -139,14 +153,34 @@ - + + + + Group column + + + mGroupColumn + + + + + + + + + + Display group name + + + + - Description column + Description - + @@ -178,11 +212,14 @@ mLayerName mKeyColumn mValueColumn - mAllowNull + mGroupColumn + mDescriptionExpression mOrderByValue + mAllowNull mAllowMulti - mNofColumns mUseCompleter + mCompleterMatchFromStart + mNofColumns mEditExpression mFilterExpression diff --git a/src/ui/layout/qgslayoutlegendwidgetbase.ui b/src/ui/layout/qgslayoutlegendwidgetbase.ui index d810ddbeca78..e73ad7a162da 100644 --- a/src/ui/layout/qgslayoutlegendwidgetbase.ui +++ b/src/ui/layout/qgslayoutlegendwidgetbase.ui @@ -227,6 +227,52 @@ 3 + + + + + 0 + 0 + + + + Expand all + + + + + + + :/images/themes/default/mActionExpandTree.svg:/images/themes/default/mActionExpandTree.svg + + + + 20 + 20 + + + + + + + + Collapse all + + + + + + + :/images/themes/default/mActionCollapseTree.svg:/images/themes/default/mActionCollapseTree.svg + + + + 20 + 20 + + + + @@ -1434,6 +1480,8 @@ mCheckBoxAutoUpdate mUpdateAllPushButton mItemTreeView + mExpandAllToolButton + mCollapseAllToolButton mMoveDownToolButton mMoveUpToolButton mAddGroupToolButton diff --git a/src/ui/layout/qgslayoutmapwidgetbase.ui b/src/ui/layout/qgslayoutmapwidgetbase.ui index 5a8598f45ece..5a667e478743 100644 --- a/src/ui/layout/qgslayoutmapwidgetbase.ui +++ b/src/ui/layout/qgslayoutmapwidgetbase.ui @@ -88,9 +88,9 @@ 0 - 0 + -123 548 - 1336 + 1478 @@ -425,6 +425,105 @@ + + + + Elevation Range + + + true + + + false + + + true + + + composeritem + + + + + + Lower + + + false + + + mStartDateTime + + + + + + + + + + + + + + Upper + + + false + + + mEndDateTime + + + + + + + + + + + + + + + 0 + 0 + + + + 4 + + + -9999999999.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + 4 + + + -9999999999.000000000000000 + + + 9999999999.000000000000000 + + + + + + @@ -1090,6 +1189,11 @@ mXMaxDDBtn mYMaxLineEdit mYMaxDDBtn + mElevationRangeCheckBox + mZLowerSpin + mZLowerDDBtn + mZUpperSpin + mZUpperDDBtn mTemporalCheckBox mStartDateTime mStartDateTimeDDBtn diff --git a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui index 0d2f420a2a95..5dbb914e0da0 100644 --- a/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui +++ b/src/ui/mesh/qgsmeshelevationpropertieswidgetbase.ui @@ -6,14 +6,14 @@ 0 0 - 435 - 407 + 515 + 424 Raster Elevation Properties - + 0 @@ -26,83 +26,189 @@ 0 - - - - Qt::StrongFocus + + + + Configuration - - Elevation Surface + + + + + + + + + + 0 + 0 + - - false + + QFrame::NoFrame - - vectorgeneral + + 1 - - - - - 6 - - - -99999999999.000000000000000 - - - 99999999999.000000000000000 - - - - - - - Offset - - - - - - - Scale - - - - - - - 6 - - - 0.000000000000000 - - - 99999999999.000000000000000 - - - 1.000000000000000 - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Elevation scaling and offset can be used to manually correct elevation values from the layer.</span></p><p>The scale is applied to the mesh values before adding the offset.</p></body></html> - - - true - - - - + + + + 0 + 0 + + + + + + + Scale + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The elevation will be taken from the mesh vertices.</span></p><p>Elevation scaling and offset can be used to manually correct elevation values from the layer. The scale is applied to the raster values before adding the offset.</p></body></html> + + + true + + + + + + + 6 + + + -99999999999.000000000000000 + + + 99999999999.000000000000000 + + + + + + + Offset + + + + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + + + 0 + 0 + + + + false + + + + + + Lower + + + + + + + + + + Upper + + + + + + + Limits + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The mesh layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has a single fixed elevation, or a range (slice) of elevation values. If a range is specified, mesh values will be extruded over this range.</p></body></html> + + + true + + + + + - + Profile Chart Appearance + + + @@ -110,11 +216,8 @@ - - - - + 0 @@ -122,7 +225,7 @@ - 1 + 0 @@ -138,6 +241,13 @@ 0 + + + + Line style + + + @@ -151,26 +261,6 @@ - - - - Line style - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -187,13 +277,6 @@ 0 - - - - Fill style - - - @@ -201,16 +284,10 @@ - - - - - 0 - 0 - - + + - + Fill style @@ -227,18 +304,18 @@ - - - - Qt::Vertical + + + + + 0 + 0 + - - - 20 - 40 - + + - + @@ -247,7 +324,7 @@ - + Qt::Vertical @@ -273,12 +350,13 @@ QToolButton
    qgssymbolbutton.h
    + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    - - mElevationGroupBox - mScaleZSpinBox - mOffsetZSpinBox - diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index cf28107a4bfb..287aefb431fd 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -6,7 +6,7 @@ 0 0 - 1277 + 1368 506
    @@ -16,8 +16,8 @@ 0 0 - 1277 - 20 + 1368 + 27 @@ -142,6 +142,13 @@ + + + Data Filtering + + + + @@ -149,6 +156,7 @@ + @@ -3365,9 +3373,23 @@ Shows placeholders for labels which could not be placed, e.g. due to overlaps wi Get Involved + + + + :/images/themes/default/mesh/Elevation.svg + :/images/themes/default/mesh/Elevation.svg:/images/themes/default/mesh/Elevation.svg + + + Elevation Controller + + + Elevation Controller Panel + + + diff --git a/src/ui/qgsdxfexportdialogbase.ui b/src/ui/qgsdxfexportdialogbase.ui index 173669bc6df9..2a3acbc211d8 100644 --- a/src/ui/qgsdxfexportdialogbase.ui +++ b/src/ui/qgsdxfexportdialogbase.ui @@ -6,7 +6,7 @@ 0 0 - 698 + 716 680 @@ -60,22 +60,22 @@
    - - + + - + Qt::StrongFocus - + true - + Qt::StrongFocus @@ -85,7 +85,7 @@ - + @@ -105,29 +105,30 @@ - Select All + Select All Layers - + - Deselect All + Deselect All Layers - - - - Qt::Horizontal + + + + Select Data DefinedBlocks - - - 40 - 20 - + + + + + + Deselect Data DefinedBlocks - +
    @@ -203,6 +204,13 @@ + + + + Use only selected features + + + @@ -219,9 +227,10 @@ - QgsScaleWidget + QgsFileWidget QWidget -
    qgsscalewidget.h
    +
    qgsfilewidget.h
    + 1
    QgsProjectionSelectionWidget @@ -230,15 +239,9 @@ 1 - QgsLayerTreeView - QTreeView -
    qgslayertreeview.h
    -
    - - QgsFileWidget + QgsScaleWidget QWidget -
    qgsfilewidget.h
    - 1 +
    qgsscalewidget.h
    diff --git a/src/ui/qgsexpressionpreviewbase.ui b/src/ui/qgsexpressionpreviewbase.ui index f6bcc946a568..637a09056f2e 100644 --- a/src/ui/qgsexpressionpreviewbase.ui +++ b/src/ui/qgsexpressionpreviewbase.ui @@ -7,7 +7,7 @@ 0 0 400 - 67 + 50 @@ -65,27 +65,112 @@
    - - - - - - 0 - 0 - + + + + 0 + 0 + + + + 1 + + + + + 0 + 0 + + + + + 0 + + + 0 - - Select the feature to use for the output preview + + 0 - - Feature + + 0 - - - - - - + + + + + 0 + 0 + + + + Select the feature to use for the output preview + + + Feature + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Result + + + + + + + + + + ... + + + + :/images/themes/default/mActionArrowLeft.svg:/images/themes/default/mActionArrowLeft.svg + + + + + + + ... + + + + :/images/themes/default/mActionArrowRight.svg:/images/themes/default/mActionArrowRight.svg + + + + + + @@ -95,7 +180,15 @@ QComboBox
    qgsfeaturepickerwidget.h
    + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    - + + + diff --git a/src/ui/qgsmapcanvasdockwidgetbase.ui b/src/ui/qgsmapcanvasdockwidgetbase.ui index 1fa47f5b1c78..ca561443fde2 100644 --- a/src/ui/qgsmapcanvasdockwidgetbase.ui +++ b/src/ui/qgsmapcanvasdockwidgetbase.ui @@ -13,42 +13,42 @@ Map Canvas - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 16 - 16 - - - - false - - - - - - - - - - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16 + 16 + + + + false + + + + + + + + + + @@ -140,6 +140,19 @@ Show Labels + + + + :/images/themes/default/mesh/Elevation.svg + :/images/themes/default/mesh/Elevation.svg:/images/themes/default/mesh/Elevation.svg + + + Show Elevation Controller + + + Show elevation filtering control + + diff --git a/src/ui/qgsorganizetablecolumnsdialog.ui b/src/ui/qgsorganizetablecolumnsdialog.ui index 74ad8bca7d3f..03a7e0dd38c7 100644 --- a/src/ui/qgsorganizetablecolumnsdialog.ui +++ b/src/ui/qgsorganizetablecolumnsdialog.ui @@ -43,7 +43,10 @@ - Deselect All + Hide All + + + Hides all the fields and actions in the table @@ -60,7 +63,10 @@ - Select All + Show All + + + Displays all the fields and actions in the table @@ -69,6 +75,9 @@ Toggle Selection + + Toggles visibility of the selected fields and actions + diff --git a/src/ui/qgsowssourceselectbase.ui b/src/ui/qgsowssourceselectbase.ui index 58f0fc968b33..56720b3de8f1 100644 --- a/src/ui/qgsowssourceselectbase.ui +++ b/src/ui/qgsowssourceselectbase.ui @@ -322,57 +322,6 @@ - - - - Options - - - - - - - - - Tile size - - - mTileWidthLineEdit - - - - - - - - - - - - - - - - Layer name - - - mLayerNameLineEdit - - - - - - - Feature limit for GetFeatureInfo - - - mFeatureCountLineEdit - - - - - - @@ -597,10 +546,6 @@ Always network: always load from network and do not check if the cache has a val mTimeComboBox mChangeCRSButton mFormatComboBox - mLayerNameLineEdit - mTileWidthLineEdit - mTileHeightLineEdit - mFeatureCountLineEdit mCacheComboBox mLayerUpButton mLayerDownButton diff --git a/src/ui/qgsprojectelevationsettingswidgetbase.ui b/src/ui/qgsprojectelevationsettingswidgetbase.ui index ce71fcdd2242..d6891cf5ef41 100644 --- a/src/ui/qgsprojectelevationsettingswidgetbase.ui +++ b/src/ui/qgsprojectelevationsettingswidgetbase.ui @@ -13,7 +13,7 @@ Project Elevation Settings - + 0 @@ -46,7 +46,7 @@ - + 0 @@ -81,19 +81,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -215,6 +202,101 @@ + + + + Elevation Range + + + false + + + false + + + true + + + composeritem + + + + + + + 0 + 0 + + + + Maximum elevation of interest + + + 4 + + + -9999999999.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + Minimum elevation of interest + + + 4 + + + -9999999999.000000000000000 + + + 9999999999.000000000000000 + + + + + + + Upper + + + false + + + + + + + Lower + + + false + + + + + + + When set, these heights define the upper and lower elevation limits for the area of interest in this project. + + + true + + + + + + @@ -250,7 +332,25 @@
    qgsmessagebar.h
    1 + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    + + mComboTerrainType + mFlatHeightSpinBox + mDemOffsetSpinBox + mComboDemLayer + mDemScaleSpinBox + mMeshOffsetSpinBox + mComboMeshLayer + mMeshScaleSpinBox + mElevationLowerSpin + mElevationUpperSpin + diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index 60e5c3096287..338dfaa63445 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -2306,322 +2306,168 @@ WMS capabilities - - - - - - Maximum features for Atlas print requests - - - - - - - 9999999 - - - 1 - - - - - - - + + - CRS restrictions - - - true - - - false + Map and Legend Options - + false - + true - - - - - - - - Remove selected CRS - - - - :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg - - - - - - - Fetch all CRS's from layers - - - Used - - + + + + + + + Width + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + + + + + + + Height + + + + + + + Maximum image size for GetMap and GetLegendGraphic requests + + + + - - - - Add new CRS - - - - :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg - - + + + + + + Quality for JPEG images ( 10 : smaller image - 100 : best quality ) + + + + + + + 10 + + + 100 + + + 5 + + + 90 + + + + - - - - Qt::Horizontal - - - - 40 - 20 - - - + + + + + + When using tiles set this to the size of the larger symbols to avoid cut symbols at tile boundaries. This works by drawing features that are outside the tile extent. + + + Tile buffer in pixels + + + + + + + 1024 + + + + - - - - - - - - - When using tiles set this to the size of the larger symbols to avoid cut symbols at tile boundaries. This works by drawing features that are outside the tile extent. - - - Tile buffer in pixels - - - - - - - 1024 - - - - - - - - - Use attribute form settings for GetFeatureInfo response - - - - - - - Excl&ude layouts - - - true - - - false - - - false - - - true - - - - + + + + QLayout::SetFixedSize + + + + + Default map units per mm in legend + + + + - - - - Add layout to exclude - + + - - - - - :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + Add layer groups in GetLegendGraphic - - + + + + + + Maximum features for Atlas print requests + + + + + + + 9999999 + + + 1 + + + + + + + - Remove selected layout + GetCapabilities response would skip name attribute for group items while leaving title attribute as is, thus showing the group but making it impossible to include the whole group (rather than it's elements) in subsequent GetMap request LAYERS parameter. - - - - - :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + Skip name attribute for groups - - - - Qt::Horizontal - - - - 0 - 20 - - - - - - - - Use layer ids as names - - - - - - - - - Width - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 6 - 20 - - - - - - - - - - - - - - Height - - - - - - - Maximum image size for GetMap and GetLegendGraphic requests - - - - - - - - - QLayout::SetFixedSize - - - - - Default map units per mm in legend - - - - - - - - - - - Advertised URL - - - - - - - - - - - - - - Quality for JPEG images ( 10 : smaller image - 100 : best quality ) - - - - - - - 10 - - - 100 - - - 5 - - - 90 - - - - - - - - - - - GetFeatureInfo geometry precision (decimal places) - - - - - - - 1 - - - 17 - - - 8 - - - - - @@ -2754,11 +2600,85 @@ - - - - Add geometry to feature response + + + + Layer and Feature options + + + false + + true + + + + + + Use attribute form settings for GetFeatureInfo response + + + + + + + Use layer ids as names + + + + + + + + + GetFeatureInfo geometry precision (decimal places) + + + + + + + 1 + + + 17 + + + 8 + + + + + + + + + Segmentize feature info geometry + + + + + + + Add geometry to feature response + + + + + + + + + Advertised URL + + + + + + + + + @@ -2826,10 +2746,10 @@ - - + + - Ad&vertised extent + Excl&ude layouts true @@ -2837,41 +2757,151 @@ false - + false - + true - - - + + + + + + + + Add layout to exclude + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + Remove selected layout + + + + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + - - - - Segmentize feature info geometry + + + + CRS restrictions - - - - - - Add layer groups in GetLegendGraphic + + true + + + false + + false + + + true + + + + + + + + + Remove selected CRS + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Fetch all CRS's from layers + + + Used + + + + + + + Add new CRS + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - - GetCapabilities response would skip name attribute for group items while leaving title attribute as is, thus showing the group but making it impossible to include the whole group (rather than it's elements) in subsequent GetMap request LAYERS parameter. + + + + Ad&vertised extent - - Skip name attribute for groups + + true + + + false + + + false + + + true + + + + + @@ -3627,14 +3657,14 @@ mSegmentizeFeatureInfoGeometryCheckBox mWMSPrecisionSpinBox mWMSUrlLineEdit + mAddLayerGroupsLegendGraphicCheckBox + mSkipNameForGroupCheckBox mMaxWidthLineEdit mMaxHeightLineEdit mWMSImageQualitySpinBox mWMSMaxAtlasFeaturesSpinBox - mWMSTileBufferSpinBox - twWmtsLayers - twWmtsGrids mWMTSMinScaleSpinBox + mWMSTileBufferSpinBox mWMTSUrlLineEdit twWFSLayers pbnWFSLayersSelectAll @@ -3649,6 +3679,9 @@ mStartDateTimeEdit mEndDateTimeEdit mCalculateFromLayerButton + mCoordinateCrs + twWmtsGrids + twWmtsLayers diff --git a/src/ui/qgssensorthingssourceselectbase.ui b/src/ui/qgssensorthingssourceselectbase.ui index c2f34a97b379..c9338ebb4bb9 100644 --- a/src/ui/qgssensorthingssourceselectbase.ui +++ b/src/ui/qgssensorthingssourceselectbase.ui @@ -6,12 +6,12 @@ 0 0 - 564 - 321 + 678 + 501 - + Qt::Horizontal @@ -21,29 +21,103 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - Connection Details + + + QFrame::NoFrame - - - - - + + true + + + + + 0 + 0 + 658 + 320 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Connection Details + + + + + + + + + + + + Layer Settings + + + + + + + + + + + + + 0 + 3 + + + + Filter + + + + + + Query Builder + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 0 + 21 + + + + + + + + + + + + @@ -130,20 +204,28 @@ - - - - Layer Settings - - - - - - - - + + + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    + + QgsScrollArea + QScrollArea +
    qgsscrollarea.h
    + 1 +
    + + QgsCodeEditor + QWidget +
    qgscodeeditor.h
    + 1 +
    +
    cmbConnections btnNew diff --git a/src/ui/qgssensorthingssourcewidgetbase.ui b/src/ui/qgssensorthingssourcewidgetbase.ui index 6bdd976bf655..e9d8f047c10f 100644 --- a/src/ui/qgssensorthingssourcewidgetbase.ui +++ b/src/ui/qgssensorthingssourcewidgetbase.ui @@ -7,13 +7,13 @@ 0 0 537 - 91 + 134 QgsSensorThingsSourceWidgetBase - + 0 @@ -26,12 +26,8 @@ 0 - - - - Geometry type - - + + @@ -40,26 +36,6 @@ - - - - Page size - - - - - - - - - - - - - 9999999 - - - @@ -87,6 +63,58 @@ + + + + 9999999 + + + + + + + Qt::StrongFocus + + + + + + + Page size + + + + + + + Geometry type + + + + + + + Extent limit + + + + + + + + + + Feature limit + + + + + + + 999999999 + + + @@ -96,6 +124,14 @@
    qgsspinbox.h
    + + mComboEntityType + mComboGeometryType + mRetrieveTypesButton + mSpinPageSize + mSpinFeatureLimit + mExtentLimitFrame + diff --git a/src/ui/qgssensorthingssubseteditorbase.ui b/src/ui/qgssensorthingssubseteditorbase.ui new file mode 100644 index 000000000000..04ce00c1682c --- /dev/null +++ b/src/ui/qgssensorthingssubseteditorbase.ui @@ -0,0 +1,252 @@ + + + QgsSensorThingsSubsetEditorBase + + + + 0 + 0 + 816 + 740 + + + + OGC SensorThings Data Filter + + + + + + Datasource + + + + + + + + 0 + 0 + + + + Fields + + + + 11 + + + 11 + + + 11 + + + 11 + + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">List of fields in this vector file</p></body></html> + + + + + + + + + + Operators + + + + 11 + + + 11 + + + 11 + + + 11 + + + + + or + + + + + + + gt + + + + + + + le + + + + + + + and + + + + + + + eq + + + + + + + ne + + + + + + + not + + + + + + + Comparisons + + + + + + + add + + + + + + + Logical operators + + + + + + + ge + + + + + + + lt + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Arithmetic + + + + + + + sub + + + + + + + mul + + + + + + + div + + + + + + + mod + + + + + + + + + + + 0 + 0 + + + + OGC SensorThings Filter Expression + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset + + + false + + + + + + + + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    +
    + + +
    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/qgsrasterelevationpropertieswidgetbase.ui b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui index a216d701c5d1..68b2edda12a4 100644 --- a/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterelevationpropertieswidgetbase.ui @@ -6,14 +6,20 @@ 0 0 - 417 - 382 + 537 + 575 + + + 0 + 0 + + Raster Elevation Properties - + 0 @@ -26,95 +32,377 @@ 0 - - - - Qt::StrongFocus + + + + Qt::Vertical - - Represents Elevation Surface + + + 20 + 40 + - - true + + + + + + + 0 + 0 + - - vectorgeneral + + QFrame::NoFrame - - - - - Scale - - - - - - - 6 - - - 0.000000000000000 - - - 99999999999.000000000000000 - - - 1.000000000000000 - - - - - - - 6 - - - -99999999999.000000000000000 - - - 99999999999.000000000000000 - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Elevation scaling and offset can be used to manually correct elevation values from the layer.</span></p><p>The scale is applied to the raster values before adding the offset.</p></body></html> - - - true - - - - - - - Offset - - - - - - - Band - - - - - - - + + 4 + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + The layer does not contain any elevation related data. + + + false + + + + + + + + + 0 + 0 + + + + + + + Scale + + + + + + + Offset + + + + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The pixel values in the layer represent an elevation surface, such as a Digital Elevation Model (DEM).</span></p><p>Elevation scaling and offset can be used to manually correct elevation values from the layer. The scale is applied to the raster values before adding the offset.</p></body></html> + + + true + + + + + + + 6 + + + -99999999999.000000000000000 + + + 99999999999.000000000000000 + + + + + + + + + + Band + + + + + + + + + 0 + 0 + + + + false + + + + + + Lower + + + + + + + + + + Upper + + + + + + + Limits + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + 4 + + + -9999999998.000000000000000 + + + 9999999999.000000000000000 + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">The raster layer (or selected raster band) is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has a single fixed elevation, or a range (slice) of elevation values. If a range is specified, pixels will be extruded over this range.</p></body></html> + + + true + + + + + + + + + 0 + 0 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + QToolButton::MenuButtonPopup + + + false + + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with a fixed elevation range.</span></p><p>This mode can be used when a layer has elevation data exposed through different raster bands.</p></body></html> + + + true + + + + + + + + + 0 + 0 + + + + + + + Lower + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with an elevation range, calculated via expressions.</span></p><p>This mode can be used when a layer has elevation data exposed through different raster bands.</p></body></html> + + + true + + + + + + + Upper + + + + + + + + - - + + + + + Profile Chart Appearance - + - + 0 @@ -122,7 +410,7 @@ - 1 + 0 @@ -158,19 +446,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -187,23 +462,16 @@ 0 - - - - 6 - - - -99999.000000000000000 - - - 99999.000000000000000 + + + + + 0 + 0 + - - - - - Fill style + @@ -214,31 +482,25 @@ - - - - - 0 - 0 - - + + - + Fill style - - - - Qt::Vertical + + + + 6 + + + -99999.000000000000000 - - - 20 - 40 - + + 99999.000000000000000 - + @@ -257,22 +519,22 @@
    - - - - Qt::Vertical - - - - 20 - 40 - + + + + Configuration - + + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    QgsDoubleSpinBox QDoubleSpinBox @@ -288,12 +550,15 @@ QComboBox
    qgsrasterbandcombobox.h
    + + QgsFieldExpressionWidget + QWidget +
    qgsfieldexpressionwidget.h
    + 1 +
    - - mElevationGroupBox - mScaleZSpinBox - mOffsetZSpinBox - - + + + diff --git a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui index 0a3273bf6037..fed537432da6 100644 --- a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui @@ -7,7 +7,7 @@ 0 0 577 - 413 + 678 @@ -49,10 +49,10 @@ 0 0 577 - 663 + 678 - + 0 @@ -65,11 +65,20 @@ 0 + + + true + + + 0 + 0 + + QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;} background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;} @@ -80,148 +89,194 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti true - - - + + + - Redraw layer only + Configuration - - - - Delegates temporal handling to the data provider - - - Automatic - - - true - - + + - - - - false - - - QFrame::NoFrame - - - QFrame::Raised - - - 0 + + + + 3 - - - 20 + + + + 0 + 0 + - - 0 + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + <p><b>The layer will be rendered whenever the map's temporal range overlaps the range defined below.</b></p> + + + true + + + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + Start date + + + + + + + End date + + + + + + + + + 0 + 0 + - - 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">The layer's data provider will automatically handle temporal settings for the layer.</span></p></body></html> + + + true + + + + + + + + + 0 + 0 + - - - - Start date - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - - End date - - - - - - - - - - Fixed time range (only show this layer if animation time is within this range) - - - false - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 20 - - - 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">The layer will automatically be redrawn whenever the temporal range is changed, but no time based filtering will be applied to the layer.</span></p><p>This configuration is useful when the layer has renderer settings which vary based on the temporal range. For instance, when it is using time-dependent data-defined renderer expressions.</p></body></html> + + + true + + + + + + + + + 0 + 0 + - - 0 - - - - - <html><head/><body><p><span style=" font-weight:600;">The layer will automatically be redrawn whenever the temporal range is changed, but no time based filtering will be applied to the layer.</span></p><p>This configuration is useful when the layer has renderer settings which vary based on the temporal range. For instance, when it is using time-dependent data-defined renderer expressions.</p></body></html> - - - true - - - - + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Each band in the raster layer is associated with a fixed time range.</span></p><p>This mode can be used when a layer has temporal data exposed through different raster bands.</p></body></html> + + + true + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + QToolButton::MenuButtonPopup + + + false + + + + + + + - - - @@ -240,15 +295,19 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti QDateTimeEdit
    qgsdatetimeedit.h
    + + QgsStackedWidget + QStackedWidget +
    qgsstackedwidget.h
    + 1 +
    scrollArea mTemporalGroupBox - mModeAutomaticRadio - mModeFixedRangeRadio - mStartTemporalDateTimeEdit - mEndTemporalDateTimeEdit - + + + diff --git a/src/ui/sensor/widget_serialportsensor.ui b/src/ui/sensor/widget_serialportsensor.ui index 9e6f11d6a524..a97ab3c90df3 100644 --- a/src/ui/sensor/widget_serialportsensor.ui +++ b/src/ui/sensor/widget_serialportsensor.ui @@ -41,6 +41,9 @@ Choose a serial port for the connection + + true + @@ -63,6 +66,55 @@ + + + + Data frame delimiter + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Choose a character to act as the data frame delimiter + + + + + + + false + + + + 80 + 16777215 + + + + + + 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/code_layout/acceptable_missing_doc.py b/tests/code_layout/acceptable_missing_doc.py index d177f70f7b7a..5b026a010e2e 100644 --- a/tests/code_layout/acceptable_missing_doc.py +++ b/tests/code_layout/acceptable_missing_doc.py @@ -89,7 +89,7 @@ "QgsSimpleFillSymbolLayerWidget": ["setColor(const QColor &color)", "setStrokeColor(const QColor &color)"], "pal::Pal": ["FnIsCanceled)(void *ctx)"], "QgsColorSwatchDelegate": ["QgsColorSwatchDelegate(QWidget *parent=nullptr)"], - "QgsMeshDatasetGroupTreeItemDelagate": ["QgsMeshDatasetGroupTreeItemDelagate(QObject *parent=nullptr)"], + "QgsMeshDatasetGroupTreeItemDelegate": ["QgsMeshDatasetGroupTreeItemDelegate(QObject *parent=nullptr)"], "QgsAspectFilter": ["QgsAspectFilter(const QString &inputFile, const QString &outputFile, const QString &outputFormat)"], "QgsRasterMatrix": ["asinus()", "setData(int cols, int rows, double *data, double nodataValue)", "power(const QgsRasterMatrix &other)", "number() const", "multiply(const QgsRasterMatrix &other)", "greaterThan(const QgsRasterMatrix &other)", "equal(const QgsRasterMatrix &other)", "greaterEqual(const QgsRasterMatrix &other)", "lesserEqual(const QgsRasterMatrix &other)", "nColumns() const", "logicalOr(const QgsRasterMatrix &other)", "QgsRasterMatrix(const QgsRasterMatrix &m)", "log10()", "tangens()", "divide(const QgsRasterMatrix &other)", "logicalAnd(const QgsRasterMatrix &other)", "sinus()", "squareRoot()", "changeSign()", "nRows() const", "cosinus()", "atangens()", "acosinus()", "lesserThan(const QgsRasterMatrix &other)", "OneArgOperator", "TwoArgOperator", "setNodataValue(double d)", "notEqual(const QgsRasterMatrix &other)", "nodataValue() const", "log()"], "QgsVectorFileWriter::Option": ["Option(const QString &docString, QgsVectorFileWriter::OptionType type)"], @@ -1221,7 +1221,7 @@ "QgsTextEditWidgetFactory", "pal::Pal", "QgsAuthSslErrorsDialog", - "QgsMeshDatasetGroupTreeItemDelagate", + "QgsMeshDatasetGroupTreeItemDelegate", "QgsSVGFillSymbolLayerWidget", "QgsAspectFilter", "QgsRasterMatrix", diff --git a/tests/src/3d/testqgs3drendering.cpp b/tests/src/3d/testqgs3drendering.cpp index 6335cf9f90b4..67d8294467f0 100644 --- a/tests/src/3d/testqgs3drendering.cpp +++ b/tests/src/3d/testqgs3drendering.cpp @@ -100,7 +100,7 @@ class TestQgs3DRendering : public QgsTest void test3DSceneExporter(); private: - QImage convertDepthImageToGray16Image( const QImage &depthImage ); + QImage convertDepthImageToGrayscaleImage( const QImage &depthImage ); void do3DSceneExport( int zoomLevelsCount, int expectedObjectCount, int maxFaceCount, Qgs3DMapScene *scene, QgsPolygon3DSymbol *symbol3d, QgsVectorLayer *layerPoly, QgsOffscreen3DEngine *engine ); @@ -135,33 +135,39 @@ class QgsCameraController4Test : public QgsCameraController QgsCameraPose *cameraPose() { return &mCameraPose; } }; -QImage TestQgs3DRendering::convertDepthImageToGray16Image( const QImage &depthImage ) +QImage TestQgs3DRendering::convertDepthImageToGrayscaleImage( const QImage &depthImage ) { - QImage grayImage( depthImage.width(), depthImage.height(), QImage::Format_Grayscale16 ); + const int width = depthImage.width(); + const int height = depthImage.height(); + QImage grayImage( width, height, QImage::Format_ARGB32 ); // compute image min/max depth values double minV = 9999999.0; double maxV = -9999999.0; - for ( int x = 0; x < grayImage.width(); x++ ) + for ( int y = 0; y < height; ++y ) { - for ( int y = 0; y < grayImage.height(); y++ ) + const QRgb *depthImageScanline = reinterpret_cast< const QRgb * >( depthImage.scanLine( y ) ); + for ( int x = 0; x < width; ++x ) { - double d = Qgs3DUtils::decodeDepth( depthImage.pixel( x, y ) ); - if ( d > maxV ) maxV = d; - else if ( d < minV ) minV = d; + const double d = Qgs3DUtils::decodeDepth( depthImageScanline[x] ); + if ( d > maxV ) + maxV = d; + if ( d < minV ) + minV = d; } } // transform depth value to gray value - double factor = 65635.0 / ( maxV - minV ); - for ( int x = 0; x < grayImage.width(); x++ ) + const double factor = ( maxV > minV ) ? 255.0 / ( maxV - minV ) : 1.0; + for ( int y = 0; y < height; ++y ) { - for ( int y = 0; y < grayImage.height(); y++ ) + const QRgb *depthImageScanline = reinterpret_cast< const QRgb * >( depthImage.scanLine( y ) ); + QRgb *grayImageScanline = reinterpret_cast< QRgb * >( grayImage.scanLine( y ) ); + for ( int x = 0; x < width; ++x ) { - double d = Qgs3DUtils::decodeDepth( depthImage.pixel( x, y ) ); - unsigned short v = ( unsigned short )( factor * ( d - minV ) ); - QRgba64 col = QRgba64::fromRgba64( v, v, v, ( quint16 )65635 ); - grayImage.setPixelColor( x, y, QColor( col ) ); + const double d = Qgs3DUtils::decodeDepth( depthImageScanline[x] ); + unsigned short v = static_cast< unsigned short >( factor * ( d - minV ) ); + grayImageScanline[x] = qRgb( v, v, v ); } } @@ -1537,8 +1543,8 @@ void TestQgs3DRendering::testDepthBuffer() // retrieve 3D depth image QImage depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); - QImage grayImage = convertDepthImageToGray16Image( depthImage ); - QGSVERIFYIMAGECHECK( "depth_retrieve_image", "depth_retrieve_image", grayImage, QString(), 550, QSize( 0, 0 ), 2 ); + QImage grayImage = convertDepthImageToGrayscaleImage( depthImage ); + QGSVERIFYIMAGECHECK( "depth_retrieve_image", "depth_retrieve_image", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); // =========== TEST WHEEL ZOOM QVector3D startPos = scene->cameraController()->camera()->position(); @@ -1556,8 +1562,8 @@ void TestQgs3DRendering::testDepthBuffer() QCOMPARE( testCam->cameraBefore()->viewCenter(), testCam->cameraPose()->centerPoint().toVector3D() ); depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); - grayImage = convertDepthImageToGray16Image( depthImage ); - QGSVERIFYIMAGECHECK( "depth_wheel_action_1", "depth_wheel_action_1", grayImage, QString(), 550, QSize( 0, 0 ), 2 ); + grayImage = convertDepthImageToGrayscaleImage( depthImage ); + QGSVERIFYIMAGECHECK( "depth_wheel_action_1", "depth_wheel_action_1", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); @@ -1575,8 +1581,8 @@ void TestQgs3DRendering::testDepthBuffer() QCOMPARE( testCam->cameraBefore()->viewCenter(), testCam->cameraPose()->centerPoint().toVector3D() ); depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); - grayImage = convertDepthImageToGray16Image( depthImage ); - QGSVERIFYIMAGECHECK( "depth_wheel_action_2", "depth_wheel_action_2", grayImage, QString(), 550, QSize( 0, 0 ), 2 ); + grayImage = convertDepthImageToGrayscaleImage( depthImage ); + QGSVERIFYIMAGECHECK( "depth_wheel_action_2", "depth_wheel_action_2", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); @@ -1594,8 +1600,8 @@ void TestQgs3DRendering::testDepthBuffer() QCOMPARE( testCam->cameraBefore()->viewCenter(), testCam->cameraPose()->centerPoint().toVector3D() ); depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); - grayImage = convertDepthImageToGray16Image( depthImage ); - QGSVERIFYIMAGECHECK( "depth_wheel_action_3", "depth_wheel_action_3", grayImage, QString(), 550, QSize( 0, 0 ), 2 ); + grayImage = convertDepthImageToGrayscaleImage( depthImage ); + QGSVERIFYIMAGECHECK( "depth_wheel_action_3", "depth_wheel_action_3", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); @@ -1613,8 +1619,8 @@ void TestQgs3DRendering::testDepthBuffer() QCOMPARE( testCam->cameraBefore()->viewCenter(), testCam->cameraPose()->centerPoint().toVector3D() ); depthImage = Qgs3DUtils::captureSceneDepthBuffer( engine, scene ); - grayImage = convertDepthImageToGray16Image( depthImage ); - QGSVERIFYIMAGECHECK( "depth_wheel_action_4", "depth_wheel_action_4", grayImage, QString(), 550, QSize( 0, 0 ), 2 ); + grayImage = convertDepthImageToGrayscaleImage( depthImage ); + QGSVERIFYIMAGECHECK( "depth_wheel_action_4", "depth_wheel_action_4", grayImage, QString(), 5, QSize( 0, 0 ), 2 ); scene->cameraController()->depthBufferCaptured( depthImage ); @@ -1736,7 +1742,7 @@ void TestQgs3DRendering::test3DSceneExporter() const QgsRectangle fullExtent = layerPoly->extent(); // =========== create polygon 3D renderer - QgsPolygon3DSymbol *symbol3d = new QgsPolygon3DSymbol; + std::unique_ptr< QgsPolygon3DSymbol > symbol3d = std::make_unique< QgsPolygon3DSymbol >(); symbol3d->setExtrusionHeight( 10.f ); QgsPhongMaterialSettings materialSettings; @@ -1770,15 +1776,15 @@ void TestQgs3DRendering::test3DSceneExporter() engine.setRootEntity( scene ); // =========== check with 1 big tile ==> 1 exported object - do3DSceneExport( 1, 1, 165, scene, symbol3d, layerPoly, &engine ); + do3DSceneExport( 1, 1, 165, scene, symbol3d.get(), layerPoly, &engine ); // =========== check with 4 tiles ==> 3 exported objects - do3DSceneExport( 2, 1, 165, scene, symbol3d, layerPoly, &engine ); + do3DSceneExport( 2, 1, 165, scene, symbol3d.get(), layerPoly, &engine ); // =========== check with 9 tiles ==> 3 exported objects - do3DSceneExport( 3, 3, 165, scene, symbol3d, layerPoly, &engine ); + do3DSceneExport( 3, 3, 165, scene, symbol3d.get(), layerPoly, &engine ); // =========== check with 16 tiles ==> 3 exported objects - do3DSceneExport( 4, 3, 165, scene, symbol3d, layerPoly, &engine ); + do3DSceneExport( 4, 3, 165, scene, symbol3d.get(), layerPoly, &engine ); // =========== check with 25 tiles ==> 3 exported objects - do3DSceneExport( 5, 3, 165, scene, symbol3d, layerPoly, &engine ); + do3DSceneExport( 5, 3, 165, scene, symbol3d.get(), layerPoly, &engine ); delete scene; mapSettings.setLayers( {} ); diff --git a/tests/src/CMakeLists.txt b/tests/src/CMakeLists.txt index 867e7195315a..a653575ad4dd 100644 --- a/tests/src/CMakeLists.txt +++ b/tests/src/CMakeLists.txt @@ -27,6 +27,7 @@ if (ENABLE_TESTS) endif() set_tests_properties(${TESTNAME} PROPERTIES FIXTURES_REQUIRED SOURCETREE) + target_compile_definitions(${TESTNAME} PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") endmacro() diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 090ef9344b50..4dfbd740774d 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -11022,6 +11022,7 @@ void TestQgsProcessing::parameterDxfLayers() QVERIFY( !def->checkValueIsAcceptable( "" ) ); QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); QVERIFY( def->checkValueIsAcceptable( QVariant::fromValue( vectorLayer ) ) ); + QVERIFY( def->checkValueIsAcceptable( QStringLiteral( "PointLayer" ), &context ) ); // should also be OK QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp" ) ); @@ -11051,6 +11052,32 @@ void TestQgsProcessing::parameterDxfLayers() layerList[0] = layerMap; QVERIFY( def->checkValueIsAcceptable( layerList, &context ) ); + // checkValueIsAcceptable on non-spatial layers + QgsVectorLayer *nonSpatialLayer = new QgsVectorLayer( QStringLiteral( "None" ), + QStringLiteral( "NonSpatialLayer" ), + QStringLiteral( "memory" ) ); + project.addMapLayer( nonSpatialLayer ); + + QVERIFY( !def->checkValueIsAcceptable( QVariant::fromValue( nonSpatialLayer ) ) ); + QVariantList wrongLayerList; + wrongLayerList.append( QVariant::fromValue( nonSpatialLayer ) ); + QVERIFY( !def->checkValueIsAcceptable( wrongLayerList ) ); + + QVERIFY( !def->checkValueIsAcceptable( QStringLiteral( "NonSpatialLayer" ), &context ) ); + + QStringList stringList = { QStringLiteral( "PointLayer" ) }; + QVERIFY( def->checkValueIsAcceptable( stringList ) ); + stringList << QStringLiteral( "NonSpatialLayer" ); + QVERIFY( !def->checkValueIsAcceptable( stringList, &context ) ); + + QVariantMap wrongLayerMap; + wrongLayerMap["layer"] = "NonSpatialLayer"; + wrongLayerMap["attributeIndex"] = -1; + QVariantList wrongLayerMapList; + wrongLayerMapList.append( wrongLayerMap ); + QVERIFY( !def->checkValueIsAcceptable( wrongLayerMapList, &context ) ); + + // Check values const QString valueAsPythonString = def->valueAsPythonString( layerList, context ); QCOMPARE( valueAsPythonString, QStringLiteral( "[{'layer': '%1','attributeIndex': -1}]" ).arg( vectorLayer->source() ) ); QCOMPARE( QString::fromStdString( QgsJsonUtils::jsonFromVariant( def->valueAsJsonObject( layerList, context ) ).dump() ), diff --git a/tests/src/app/testqgsapplocatorfilters.cpp b/tests/src/app/testqgsapplocatorfilters.cpp index 919a73962098..952dee23e275 100644 --- a/tests/src/app/testqgsapplocatorfilters.cpp +++ b/tests/src/app/testqgsapplocatorfilters.cpp @@ -81,7 +81,7 @@ void TestQgsAppLocatorFilters::testCalculator() // valid expression QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "1+2" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); - QCOMPARE( results.at( 0 ).getUserData().toInt(), 3 ); + QCOMPARE( results.at( 0 ).userData().toInt(), 3 ); // trigger result filter.triggerResult( results.at( 0 ) ); @@ -106,12 +106,12 @@ void TestQgsAppLocatorFilters::testLayers() results = gatherResults( &filter, QStringLiteral( "aa" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), l1->id() ); + QCOMPARE( results.at( 0 ).userData().toString(), l1->id() ); results = gatherResults( &filter, QStringLiteral( "A" ), QgsLocatorContext() ); QCOMPARE( results.count(), 2 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), l1->id() ); - QCOMPARE( results.at( 1 ).getUserData().toString(), l2->id() ); + QCOMPARE( results.at( 0 ).userData().toString(), l1->id() ); + QCOMPARE( results.at( 1 ).userData().toString(), l2->id() ); results = gatherResults( &filter, QString(), QgsLocatorContext() ); QCOMPARE( results.count(), 0 ); @@ -120,9 +120,9 @@ void TestQgsAppLocatorFilters::testLayers() context.usingPrefix = true; results = gatherResults( &filter, QString(), context ); QCOMPARE( results.count(), 3 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), l1->id() ); - QCOMPARE( results.at( 1 ).getUserData().toString(), l2->id() ); - QCOMPARE( results.at( 2 ).getUserData().toString(), l3->id() ); + QCOMPARE( results.at( 0 ).userData().toString(), l1->id() ); + QCOMPARE( results.at( 1 ).userData().toString(), l2->id() ); + QCOMPARE( results.at( 2 ).userData().toString(), l3->id() ); } void TestQgsAppLocatorFilters::testLayouts() @@ -144,12 +144,12 @@ void TestQgsAppLocatorFilters::testLayouts() results = gatherResults( &filter, QStringLiteral( "aa" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), pl1->name() ); + QCOMPARE( results.at( 0 ).userData().toString(), pl1->name() ); results = gatherResults( &filter, QStringLiteral( "A" ), QgsLocatorContext() ); QCOMPARE( results.count(), 2 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), pl1->name() ); - QCOMPARE( results.at( 1 ).getUserData().toString(), pl2->name() ); + QCOMPARE( results.at( 0 ).userData().toString(), pl1->name() ); + QCOMPARE( results.at( 1 ).userData().toString(), pl2->name() ); results = gatherResults( &filter, QString(), QgsLocatorContext() ); QCOMPARE( results.count(), 0 ); @@ -158,9 +158,9 @@ void TestQgsAppLocatorFilters::testLayouts() context.usingPrefix = true; results = gatherResults( &filter, QString(), context ); QCOMPARE( results.count(), 3 ); - QCOMPARE( results.at( 0 ).getUserData().toString(), pl1->name() ); - QCOMPARE( results.at( 1 ).getUserData().toString(), pl2->name() ); - QCOMPARE( results.at( 2 ).getUserData().toString(), pl3->name() ); + QCOMPARE( results.at( 0 ).userData().toString(), pl1->name() ); + QCOMPARE( results.at( 1 ).userData().toString(), pl2->name() ); + QCOMPARE( results.at( 2 ).userData().toString(), pl3->name() ); } void TestQgsAppLocatorFilters::testSearchActiveLayer() @@ -364,69 +364,69 @@ void TestQgsAppLocatorFilters::testGoto() QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "4 5" ), QgsLocatorContext() ); QCOMPARE( results.count(), 2 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 4 5 (Map CRS, )" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 4, 5 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 4, 5 ) ); QCOMPARE( results.at( 1 ).displayString, QObject::tr( "Go to 4°N 5°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 1 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 5, 4 ) ); + QCOMPARE( results.at( 1 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 5, 4 ) ); // locale-specific goto results = gatherResults( &filter, QStringLiteral( "1,234.56 789.012" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 1,234.56 789.012 (Map CRS, )" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1234.56, 789.012 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1234.56, 789.012 ) ); // decimal degree with suffixes results = gatherResults( &filter, QStringLiteral( "12.345N, 67.890W" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 12.345°N -67.89°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( -67.890, 12.345 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( -67.890, 12.345 ) ); results = gatherResults( &filter, QStringLiteral( "12.345 e, 67.890 s" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to -67.89°N 12.345°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 12.345, -67.890 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 12.345, -67.890 ) ); // degree/minuste/second coordinates goto // easting northing results = gatherResults( &filter, QStringLiteral( "40deg 1' 0\" E 11deg 55' 0\" S" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to -11.91666667°N 40.01666667°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 40.0166666667, -11.9166666667 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 40.0166666667, -11.9166666667 ) ); // northing easting results = gatherResults( &filter, QStringLiteral( "14°49′48″N 01°48′45″E" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 14.83°N 1.8125°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1.8125, 14.83 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1.8125, 14.83 ) ); // northing, esting (comma separated) results = gatherResults( &filter, QStringLiteral( "14°49′48″N, 01°48′45″E" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 14.83°N 1.8125°E (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1.8125, 14.83 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 1.8125, 14.83 ) ); // OSM/Leaflet/OpenLayers results = gatherResults( &filter, QStringLiteral( "https://www.openstreetmap.org/#map=15/44.5546/6.4936" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 44.5546°N 6.4936°E at scale 1:22569 (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 6.4936, 44.5546 ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 6.4936, 44.5546 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 ); // Google Maps results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@44.5546,6.4936,15.25z" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 44.5546°N 6.4936°E at scale 1:22569 (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 6.4936, 44.5546 ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 6.4936, 44.5546 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "scale" )].toDouble(), 22569.0 ); results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@7.8750,81.0149,574195m/data=!3m1!1e3" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 81.0149, 7.8750 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 81.0149, 7.8750 ) ); results = gatherResults( &filter, QStringLiteral( "https://www.google.com/maps/@27.7132,85.3288,3a,75y,278.89h,90t/data=!3m8!1e1!3m6!1sAF1QipMrXuXozGc9x9bxx5uPl_3ys4H-rNVqMLr6EYLA!2e10!3e11!6shttps:%2F%2Flh5.googleusercontent.com%2Fp%2FAF1QipMrXuXozGc9x9bxx5uPl_3ys4H-rNVqMLr6EYLA%3Dw203-h100-k-no-pi2.869903-ya293.58762-ro-1.9255565-fo100!7i3840!8i1920" ), QgsLocatorContext() ); QCOMPARE( results.count(), 1 ); QCOMPARE( results.at( 0 ).displayString, QObject::tr( "Go to 27.7132°N 85.3288°E at scale 1:282 (EPSG:4326 - WGS 84)" ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 85.3288, 27.7132 ) ); - QCOMPARE( results.at( 0 ).getUserData().toMap()[QStringLiteral( "scale" )].toDouble(), 282.0 ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "point" )].value(), QgsPointXY( 85.3288, 27.7132 ) ); + QCOMPARE( results.at( 0 ).userData().toMap()[QStringLiteral( "scale" )].toDouble(), 282.0 ); } QGSTEST_MAIN( TestQgsAppLocatorFilters ) diff --git a/tests/src/app/testqgslayerpropertiesdialogs.cpp b/tests/src/app/testqgslayerpropertiesdialogs.cpp index 7846e06aa1f5..33783d841014 100644 --- a/tests/src/app/testqgslayerpropertiesdialogs.cpp +++ b/tests/src/app/testqgslayerpropertiesdialogs.cpp @@ -122,6 +122,63 @@ class TestQgsLayerPropertiesDialogs : public QgsTest dialog.accept(); } + void testChangeVectorSubset() + { + // start with a point layer + const QString pointFileName = mTestDataDir + "points.shp"; + const QFileInfo pointFileInfo( pointFileName ); + std::unique_ptr< QgsVectorLayer > vl = std::make_unique< QgsVectorLayer >( pointFileInfo.filePath(), + pointFileInfo.completeBaseName(), QStringLiteral( "ogr" ) ); + QVERIFY( vl->isValid() ); + vl->setSubsetString( QStringLiteral( "\"class\"='Biplane'" ) ); + QCOMPARE( vl->subsetString(), QStringLiteral( "\"class\"='Biplane'" ) ); + + // no change to filter + QgsMapCanvas canvas; + QgsMessageBar messageBar; + { + QgsVectorLayerProperties dialog( &canvas, &messageBar, vl.get() ); + dialog.show(); + dialog.accept(); + } + + QCOMPARE( vl->subsetString(), QStringLiteral( "\"class\"='Biplane'" ) ); + + // change the filter to a line layer: + { + QgsVectorLayerProperties dialog( &canvas, &messageBar, vl.get() ); + dialog.txtSubsetSQL->setText( QStringLiteral( "\"class\"='B52'" ) ); + dialog.show(); + dialog.accept(); + } + QCOMPARE( vl->subsetString(), QStringLiteral( "\"class\"='B52'" ) ); + + // try with BOTH a filter change and the source widget present, to check interaction of the two + { + QgsVectorLayerProperties dialog( &canvas, &messageBar, vl.get() ); + DummySourceWidget *sourceWidget = new DummySourceWidget( &dialog ); + sourceWidget->newSource = mTestDataDir + "points.shp"; + dialog.mSourceWidget = sourceWidget; + dialog.show(); + dialog.txtSubsetSQL->setText( QStringLiteral( "\"class\"='Biplane'" ) ); + dialog.accept(); + } + QCOMPARE( vl->source(), mTestDataDir + "points.shp|subset=\"class\"='Biplane'" ); + QCOMPARE( vl->subsetString(), QStringLiteral( "\"class\"='Biplane'" ) ); + + // try with BOTH a filter change AND a source change + { + QgsVectorLayerProperties dialog( &canvas, &messageBar, vl.get() ); + DummySourceWidget *sourceWidget = new DummySourceWidget( &dialog ); + sourceWidget->newSource = mTestDataDir + "lines.shp"; + dialog.mSourceWidget = sourceWidget; + dialog.show(); + dialog.txtSubsetSQL->setText( QStringLiteral( "\"Name\" = 'Highway'" ) ); + dialog.accept(); + } + QCOMPARE( vl->source(), mTestDataDir + "lines.shp|subset=\"Name\" = 'Highway'" ); + QCOMPARE( vl->subsetString(), QStringLiteral( "\"Name\" = 'Highway'" ) ); + } void testChangeVectorDataSource() { diff --git a/tests/src/app/testqgsmaptoolidentifyaction.cpp b/tests/src/app/testqgsmaptoolidentifyaction.cpp index 7c3679d26d32..dcdbc4706407 100644 --- a/tests/src/app/testqgsmaptoolidentifyaction.cpp +++ b/tests/src/app/testqgsmaptoolidentifyaction.cpp @@ -226,11 +226,12 @@ void TestQgsMapToolIdentifyAction::closestPoint() s.setValue( QStringLiteral( "/qgis/measure/keepbaseunit" ), true ); //create a temporary layer - std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineStringZM?crs=epsg:3111&field=pk:int&field=col1:double" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineStringZM?crs=epsg:3111&field=pk:int&field=col1:double&field=url:string" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); QVERIFY( tempLayer->isValid() ); QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); f1.setAttribute( QStringLiteral( "pk" ), 1 ); - f1.setAttribute( QStringLiteral( "col1" ), 0.0 ); + f1.setAttribute( QStringLiteral( "col1" ), 3.3 ); + f1.setAttribute( QStringLiteral( "url" ), QStringLiteral( "home: http://qgis.org" ) ); QgsPolylineXY line3111; line3111 << QgsPointXY( 2484588, 2425722 ) << QgsPointXY( 2482767, 2398853 ); const QgsGeometry line3111G = QgsGeometry::fromWkt( QStringLiteral( "LineStringZM( 2484588 2425722 11 31, 2484588 2398853 15 37)" ) ) ; @@ -248,6 +249,7 @@ void TestQgsMapToolIdentifyAction::closestPoint() QgsPointXY mapPoint = canvas->getCoordinateTransform()->transform( 2484587, 2399800 ); std::unique_ptr< QgsMapToolIdentifyAction > action( new QgsMapToolIdentifyAction( canvas ) ); + QgsIdentifyResultsDialog *dlg = action->resultsDialog(); //check that closest point attributes are present QList result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.get() ); @@ -256,6 +258,44 @@ void TestQgsMapToolIdentifyAction::closestPoint() QCOMPARE( result.at( 0 ).mDerivedAttributes[tr( "Closest Y" )], QStringLiteral( "2399800.000" ) ); QCOMPARE( result.at( 0 ).mDerivedAttributes[tr( "Interpolated M" )].left( 4 ), QStringLiteral( "36.7" ) ); QCOMPARE( result.at( 0 ).mDerivedAttributes[tr( "Interpolated Z" )].left( 4 ), QStringLiteral( "14.8" ) ); + dlg->addFeature( result.at( 0 ) ); + + QTreeWidgetItem *layerItem = dlg->layerItem( tempLayer.get() ); + QVERIFY( layerItem ); + QTreeWidgetItem *featureItem = layerItem->child( 0 ); + QTreeWidgetItem *derivedItem = featureItem->child( 0 ); + QTreeWidgetItem *closestXItem = nullptr; + for ( int row = 0; row < derivedItem->childCount(); ++row ) + { + if ( derivedItem->child( row )->text( 0 ) == tr( "Closest X" ) ) + { + closestXItem = derivedItem->child( row ); + break; + } + } + QVERIFY( closestXItem ); + QCOMPARE( closestXItem->text( 1 ), QStringLiteral( "2484588.000" ) ); + QCOMPARE( dlg->retrieveAttribute( closestXItem ).toString(), QStringLiteral( "2484588.000" ) ); + + QTreeWidgetItem *col1Item = nullptr; + QTreeWidgetItem *urlAttributeItem = nullptr; + for ( int row = 0; row < featureItem->childCount(); ++row ) + { + if ( featureItem->child( row )->text( 0 ) == tr( "col1" ) ) + { + col1Item = featureItem->child( row ); + } + else if ( featureItem->child( row )->text( 0 ) == tr( "url" ) ) + { + urlAttributeItem = featureItem->child( row ); + } + } + QVERIFY( col1Item ); + QCOMPARE( col1Item->text( 1 ), QStringLiteral( "3.30000" ) ); + QCOMPARE( dlg->retrieveAttribute( col1Item ).toString(), QStringLiteral( "3.30000" ) ); + QVERIFY( urlAttributeItem ); + // urlAttributeItem has a delegate widget, but we should still be able to retrieve the raw field value from it + QCOMPARE( dlg->retrieveAttribute( urlAttributeItem ).toString(), QStringLiteral( "home: http://qgis.org" ) ); // polygons //create a temporary layer diff --git a/tests/src/app/testqgsmaptoolsplitfeatures.cpp b/tests/src/app/testqgsmaptoolsplitfeatures.cpp index 1ca8deb5219e..bc1bf33bb6ba 100644 --- a/tests/src/app/testqgsmaptoolsplitfeatures.cpp +++ b/tests/src/app/testqgsmaptoolsplitfeatures.cpp @@ -35,17 +35,21 @@ class TestQgsMapToolSplitFeatures : public QObject private slots: void initTestCase(); void cleanupTestCase(); + void cleanup(); void testNoFeaturesSplit(); void testSplitPolygon(); void testSplitPolygonTopologicalEditing(); void testSplitSelectedLines(); void testSplitSomeOfSelectedLines(); + // see https://github.com/qgis/QGIS/issues/29270 + void testSplitPolygonSnapToSegment(); private: QPoint mapToPoint( double x, double y ); QgisApp *mQgisApp = nullptr; QgsMapCanvas *mCanvas = nullptr; + TestQgsMapToolUtils *mUtils = nullptr; QgsVectorLayer *mMultiLineStringLayer = nullptr; QgsVectorLayer *mPolygonLayer = nullptr; QgsVectorLayer *mMultiPolygonLayer = nullptr; @@ -72,31 +76,26 @@ void TestQgsMapToolSplitFeatures::initTestCase() mMultiLineStringLayer->startEditing(); lineF1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "MultiLineString ((0 0, 10 0))" ) ) ); lineF2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "MultiLineString ((0 5, 10 5),(10 5, 15 5))" ) ) ); - mMultiLineStringLayer->addFeature( lineF1 ); - mMultiLineStringLayer->addFeature( lineF2 ); + mMultiLineStringLayer->dataProvider()->addFeatures( QgsFeatureList() << lineF1 << lineF2 ); mPolygonLayer = new QgsVectorLayer( QStringLiteral( "PolygonZ?crs=EPSG:3946" ), QStringLiteral( "layer polygon" ), QStringLiteral( "memory" ) ); QVERIFY( mPolygonLayer->isValid() ); mPolygonLayer->startEditing(); polygonF1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "PolygonZ ((0 5 10, 0 10 20, 10 10 30, 10 5 20, 0 5 10))" ) ) ); - polygonF2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 10 5 30, 10 0 20, 0 0 10))" ) ) ); - mPolygonLayer->addFeature( polygonF1 ); - mPolygonLayer->addFeature( polygonF2 ); + polygonF2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 10 5 30, 0 0 10))" ) ) ); + mPolygonLayer->dataProvider()->addFeatures( QgsFeatureList() << polygonF1 << polygonF2 ); mMultiPolygonLayer = new QgsVectorLayer( QStringLiteral( "MultiPolygon?crs=EPSG:3946" ), QStringLiteral( "layer multipolygon" ), QStringLiteral( "memory" ) ); QVERIFY( mMultiPolygonLayer->isValid() ); mMultiPolygonLayer->startEditing(); multipolygonF1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "MultiPolygon (((0 5, 0 10, 10 10, 10 5, 0 5)),((0 0, 0 4, 10 4, 10 0, 0 0)))" ) ) ); - mMultiPolygonLayer->addFeature( multipolygonF1 ); + mMultiPolygonLayer->dataProvider()->addFeature( multipolygonF1 ); mCanvas->setFrameStyle( QFrame::NoFrame ); mCanvas->resize( 50, 50 ); mCanvas->setExtent( QgsRectangle( 0, 0, 10, 10 ) ); mCanvas->show(); // to make the canvas resize mCanvas->hide(); - // Disable flaky tests on windows... - // QCOMPARE( mCanvas->mapSettings().outputSize(), QSize( 50, 50 ) ); - // QCOMPARE( mCanvas->mapSettings().visibleExtent(), QgsRectangle( 0, 0, 10, 10 ) ); QgsProject::instance()->addMapLayers( QList() << mMultiLineStringLayer << mPolygonLayer @@ -107,6 +106,9 @@ void TestQgsMapToolSplitFeatures::initTestCase() << mPolygonLayer << mMultiPolygonLayer ); + QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; + mCanvas->setMapTool( mapTool ); + mUtils = new TestQgsMapToolUtils( mapTool ); } void TestQgsMapToolSplitFeatures::cleanupTestCase() @@ -114,6 +116,17 @@ void TestQgsMapToolSplitFeatures::cleanupTestCase() QgsApplication::exitQgis(); } +// runs after each test +void TestQgsMapToolSplitFeatures::cleanup() +{ + mMultiLineStringLayer->undoStack()->setIndex( 0 ); + mMultiLineStringLayer->removeSelection(); + mPolygonLayer->undoStack()->setIndex( 0 ); + mPolygonLayer->removeSelection(); + mMultiPolygonLayer->undoStack()->setIndex( 0 ); + mMultiPolygonLayer->removeSelection(); +} + QPoint TestQgsMapToolSplitFeatures::mapToPoint( double x, double y ) { @@ -125,209 +138,123 @@ QPoint TestQgsMapToolSplitFeatures::mapToPoint( double x, double y ) void TestQgsMapToolSplitFeatures::testNoFeaturesSplit() { mCanvas->setCurrentLayer( mMultiLineStringLayer ); - QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; - mCanvas->setMapTool( mapTool ); - std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 7 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 8 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 8 ), - Qt::RightButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - - QVERIFY( mMultiLineStringLayer->featureCount() == 2 ); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 2 ); + mUtils->mouseClick( 4, 7, Qt::LeftButton ); + mUtils->mouseClick( 4, 8, Qt::LeftButton ); + mUtils->mouseClick( 4, 8, Qt::RightButton ); + + QCOMPARE( mMultiLineStringLayer->featureCount(), 2 ); + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 0 ); } void TestQgsMapToolSplitFeatures::testSplitPolygon() { - QgsProject::instance()->setTopologicalEditing( false ); mCanvas->setCurrentLayer( mPolygonLayer ); - QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; - mCanvas->setMapTool( mapTool ); - std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 11 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 3 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 3 ), - Qt::RightButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - QVERIFY( mPolygonLayer->undoStack()->index() == 3 ); - QVERIFY( mPolygonLayer->featureCount() == 3 ); - QCOMPARE( mPolygonLayer->getFeature( polygonF1.id() ).geometry().asWkt(), QStringLiteral( "PolygonZ ((4 10 24, 4 5 14, 0 5 10, 0 10 20, 4 10 24))" ) ); - QCOMPARE( mPolygonLayer->getFeature( polygonF2.id() ).geometry().asWkt(), QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 10 5 30, 10 0 20, 0 0 10))" ) ); + mUtils->mouseClick( 4, 11, Qt::LeftButton ); + mUtils->mouseClick( 4, 3, Qt::LeftButton ); + mUtils->mouseClick( 4, 3, Qt::RightButton ); - // no change to other layers - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 2 ); - QVERIFY( mMultiPolygonLayer->undoStack()->index() == 1 ); + QCOMPARE( mPolygonLayer->undoStack()->index(), 1 ); + QCOMPARE( mPolygonLayer->featureCount(), 3 ); + QCOMPARE( mPolygonLayer->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "PolygonZ ((4 10 24, 4 5 14, 0 5 10, 0 10 20, 4 10 24))" ) ); + QCOMPARE( mPolygonLayer->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 10 5 30, 0 0 10))" ) ); - // undo changes - mPolygonLayer->undoStack()->undo(); - QVERIFY( mPolygonLayer->undoStack()->index() == 2 ); + // no change to other layers + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 0 ); + QCOMPARE( mMultiPolygonLayer->undoStack()->index(), 0 ); } void TestQgsMapToolSplitFeatures::testSplitPolygonTopologicalEditing() { QgsProject::instance()->setTopologicalEditing( true ); mCanvas->setCurrentLayer( mPolygonLayer ); - QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; - mCanvas->setMapTool( mapTool ); - std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 11 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 3 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 4, 3 ), - Qt::RightButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - QVERIFY( mPolygonLayer->undoStack()->index() == 3 ); - QVERIFY( mPolygonLayer->featureCount() == 3 ); - QCOMPARE( mPolygonLayer->getFeature( polygonF1.id() ).geometry().asWkt(), QStringLiteral( "PolygonZ ((4 10 24, 4 5 14, 0 5 10, 0 10 20, 4 10 24))" ) ); - QCOMPARE( mPolygonLayer->getFeature( polygonF2.id() ).geometry().asWkt(), QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 4 5 14, 10 5 30, 10 0 20, 0 0 10))" ) ); - - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 3 ); - QCOMPARE( mMultiLineStringLayer->getFeature( lineF2.id() ).geometry().asWkt(), QStringLiteral( "MultiLineString ((0 5, 4 5, 10 5),(10 5, 15 5))" ) ); - QVERIFY( mMultiPolygonLayer->undoStack()->index() == 2 ); - QCOMPARE( mMultiPolygonLayer->getFeature( multipolygonF1.id() ).geometry().asWkt(), QStringLiteral( "MultiPolygon (((0 5, 0 10, 4 10, 10 10, 10 5, 4 5, 0 5)),((0 0, 0 4, 10 4, 10 0, 0 0)))" ) ); - - // undo changes - mPolygonLayer->undoStack()->undo(); - QVERIFY( mPolygonLayer->undoStack()->index() == 2 ); - mMultiLineStringLayer->undoStack()->undo(); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 2 ); - mMultiPolygonLayer->undoStack()->undo(); - QVERIFY( mMultiPolygonLayer->undoStack()->index() == 1 ); + mUtils->mouseClick( 4, 11, Qt::LeftButton ); + mUtils->mouseClick( 4, 3, Qt::LeftButton ); + mUtils->mouseClick( 4, 3, Qt::RightButton ); + + QCOMPARE( mPolygonLayer->undoStack()->index(), 1 ); + QCOMPARE( mPolygonLayer->featureCount(), 3 ); + QCOMPARE( mPolygonLayer->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "PolygonZ ((4 10 24, 4 5 14, 0 5 10, 0 10 20, 4 10 24))" ) ); + QCOMPARE( mPolygonLayer->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "PolygonZ ((0 0 10, 0 5 20, 4 5 14, 10 5 30, 0 0 10))" ) ); + + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 1 ); + QCOMPARE( mMultiLineStringLayer->getFeature( 2 ).geometry().asWkt(), QStringLiteral( "MultiLineString ((0 5, 4 5, 10 5),(10 5, 15 5))" ) ); + QCOMPARE( mMultiPolygonLayer->undoStack()->index(), 1 ); + QCOMPARE( mMultiPolygonLayer->getFeature( 1 ).geometry().asWkt(), QStringLiteral( "MultiPolygon (((0 5, 0 10, 4 10, 10 10, 10 5, 4 5, 0 5)),((0 0, 0 4, 10 4, 10 0, 0 0)))" ) ); + + QgsProject::instance()->setTopologicalEditing( false ); } void TestQgsMapToolSplitFeatures::testSplitSelectedLines() { - mMultiLineStringLayer->select( lineF1.id() ); + mMultiLineStringLayer->select( 1 ); mCanvas->setCurrentLayer( mMultiLineStringLayer ); - QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; - mCanvas->setMapTool( mapTool ); - - std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, 6 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, -1 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, -1 ), - Qt::RightButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); + mUtils->mouseClick( 5, 6, Qt::LeftButton ); + mUtils->mouseClick( 5, -1, Qt::LeftButton ); + mUtils->mouseClick( 5, -1, Qt::RightButton ); // only the selected feature should be split - QVERIFY( mMultiLineStringLayer->featureCount() == 3 ); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 3 ); - - // undo changes - mMultiLineStringLayer->undoStack()->undo(); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 2 ); - mMultiLineStringLayer->removeSelection(); + QCOMPARE( mMultiLineStringLayer->featureCount(), 3 ); + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 1 ); } void TestQgsMapToolSplitFeatures::testSplitSomeOfSelectedLines() { mMultiLineStringLayer->selectAll(); mCanvas->setCurrentLayer( mMultiLineStringLayer ); - QgsMapToolSplitFeatures *mapTool = new QgsMapToolSplitFeatures( mCanvas ) ; - mCanvas->setMapTool( mapTool ); - std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, 1 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, -1 ), - Qt::LeftButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); - - event.reset( new QgsMapMouseEvent( - mCanvas, - QEvent::MouseButtonRelease, - mapToPoint( 5, -1 ), - Qt::RightButton - ) ); - mapTool->cadCanvasReleaseEvent( event.get() ); + mUtils->mouseClick( 5, 1, Qt::LeftButton ); + mUtils->mouseClick( 5, -1, Qt::LeftButton ); + mUtils->mouseClick( 5, -1, Qt::RightButton ); // intersecting selected feature should be split - QVERIFY( mMultiLineStringLayer->featureCount() == 3 ); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 3 ); + QCOMPARE( mMultiLineStringLayer->featureCount(), 3 ); + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 1 ); +} - // undo changes - mMultiLineStringLayer->undoStack()->undo(); - QVERIFY( mMultiLineStringLayer->undoStack()->index() == 2 ); - mMultiLineStringLayer->removeSelection(); +void TestQgsMapToolSplitFeatures::testSplitPolygonSnapToSegment() +{ + QgsProject::instance()->setTopologicalEditing( false ); + QgsSnappingConfig oldCfg = mCanvas->snappingUtils()->config(); + QgsSnappingConfig cfg( oldCfg ); + cfg.setEnabled( true ); + cfg.setTolerance( 20 ); + cfg.setUnits( Qgis::MapToolUnit::Pixels ); + cfg.setMode( Qgis::SnappingMode::ActiveLayer ); + cfg.setTypeFlag( Qgis::SnappingType::Segment ); + mCanvas->snappingUtils()->setConfig( cfg ); + mCanvas->snappingUtils()->locatorForLayer( mPolygonLayer )->init(); + + mCanvas->setCurrentLayer( mPolygonLayer ); + + mUtils->mouseClick( 1, 0.6, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 1, 4.9, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 2, 1.1, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 2, 4.9, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 3, 1.6, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 3, 4.9, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 4, 2.1, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 4, 4.9, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 5, 2.6, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 5, 4.9, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 6, 3.1, Qt::LeftButton, Qt::KeyboardModifiers(), true ); + mUtils->mouseClick( 6, 3.1, Qt::RightButton, Qt::KeyboardModifiers(), true ); + + // Split line should split the triangle into 11 triangles + QCOMPARE( mPolygonLayer->undoStack()->index(), 1 ); + QCOMPARE( mPolygonLayer->featureCount(), 12 ); + + // No change to the other feature in the layer + QCOMPARE( mPolygonLayer->getFeature( 1 ).geometry().asWkt( 2 ), QStringLiteral( "PolygonZ ((0 5 10, 0 10 20, 10 10 30, 10 5 20, 0 5 10))" ) ); + + // No change to other layers + QCOMPARE( mMultiLineStringLayer->undoStack()->index(), 0 ); + QCOMPARE( mMultiPolygonLayer->undoStack()->index(), 0 ); + + mCanvas->snappingUtils()->setConfig( oldCfg ); } QGSTEST_MAIN( TestQgsMapToolSplitFeatures ) diff --git a/tests/src/core/testqgscoordinatereferencesystem.cpp b/tests/src/core/testqgscoordinatereferencesystem.cpp index db52bc8338a3..0f7adc55c849 100644 --- a/tests/src/core/testqgscoordinatereferencesystem.cpp +++ b/tests/src/core/testqgscoordinatereferencesystem.cpp @@ -49,7 +49,10 @@ class TestQgsCoordinateReferenceSystem: public QObject void projectedCrs(); void geocentricCrs(); void geographic3d(); + void toHorizontal(); + void toVertical(); void coordinateEpoch(); + void createCompound(); void saveAsUserCrs(); void createFromId(); void fromEpsgId(); @@ -277,6 +280,14 @@ void TestQgsCoordinateReferenceSystem::compoundCrs() crs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5500" ) ); QVERIFY( crs.isValid() ); QCOMPARE( crs.type(), Qgis::CrsType::Compound ); + QVERIFY( crs.isGeographic() ); + QCOMPARE( crs.mapUnits(), Qgis::DistanceUnit::Degrees ); + + crs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:9388" ) ); + QVERIFY( crs.isValid() ); + QCOMPARE( crs.type(), Qgis::CrsType::Compound ); + QVERIFY( !crs.isGeographic() ); + QCOMPARE( crs.mapUnits(), Qgis::DistanceUnit::Meters ); } void TestQgsCoordinateReferenceSystem::verticalCrs() @@ -289,6 +300,13 @@ void TestQgsCoordinateReferenceSystem::verticalCrs() QVERIFY( crs.isValid() ); QCOMPARE( crs.type(), Qgis::CrsType::Vertical ); + QCOMPARE( crs.mapUnits(), Qgis::DistanceUnit::Meters ); + + crs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:6358" ) ); + QVERIFY( crs.isValid() ); + + QCOMPARE( crs.type(), Qgis::CrsType::Vertical ); + QCOMPARE( crs.mapUnits(), Qgis::DistanceUnit::Feet ); } void TestQgsCoordinateReferenceSystem::projectedCrs() @@ -315,6 +333,34 @@ void TestQgsCoordinateReferenceSystem::geographic3d() QCOMPARE( crs.type(), Qgis::CrsType::Geographic3d ); } +void TestQgsCoordinateReferenceSystem::toHorizontal() +{ + // invalid + QVERIFY( !QgsCoordinateReferenceSystem().horizontalCrs().isValid() ); + // vertical only + QVERIFY( !QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ).horizontalCrs().isValid() ); + // compound + QCOMPARE( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5500" ) ).horizontalCrs().authid(), QStringLiteral( "EPSG:4759" ) ); + // already horizontal + QCOMPARE( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ).horizontalCrs().authid(), QStringLiteral( "EPSG:3111" ) ); + // geographic 3d + QCOMPARE( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ).horizontalCrs().authid(), QStringLiteral( "EPSG:4979" ) ); +} + +void TestQgsCoordinateReferenceSystem::toVertical() +{ + // invalid + QVERIFY( !QgsCoordinateReferenceSystem().verticalCrs().isValid() ); + // horizontal only + QVERIFY( !QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ).verticalCrs().isValid() ); + // compound + QCOMPARE( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5500" ) ).verticalCrs().authid(), QStringLiteral( "EPSG:5703" ) ); + // already vertical + QCOMPARE( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ).verticalCrs().authid(), QStringLiteral( "EPSG:5703" ) ); + // geographic 3d + QVERIFY( !QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ).verticalCrs().isValid() ); +} + void TestQgsCoordinateReferenceSystem::coordinateEpoch() { QgsCoordinateReferenceSystem crs( QStringLiteral( "EPSG:4326" ) ); @@ -334,6 +380,27 @@ void TestQgsCoordinateReferenceSystem::coordinateEpoch() QVERIFY( crs2.projObject() ); } +void TestQgsCoordinateReferenceSystem::createCompound() +{ + //horizontal invalid / vertical invalid + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem(), QgsCoordinateReferenceSystem() ).isValid() ); + // horizontal valid / vertical invalid + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ), QgsCoordinateReferenceSystem() ).isValid() ); + // horizontal invalid / vertical valid + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ) ).isValid() ); + // horizontal valid / vertical valid + const QgsCoordinateReferenceSystem compound = QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ) ); + QVERIFY( compound.isValid() ); + QCOMPARE( compound.description(), QStringLiteral( "unnamed" ) ); + QCOMPARE( compound.type(), Qgis::CrsType::Compound ); + // horizontal / vertical flipped + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).isValid() ); + // horizontal valid / not vertical + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3113" ) ) ).isValid() ); + // horizontal already a compound + QVERIFY( !QgsCoordinateReferenceSystem::createCompoundCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5500" ) ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:5703" ) ) ).isValid() ); +} + void TestQgsCoordinateReferenceSystem::createFromId() { QgsCoordinateReferenceSystem myCrs; diff --git a/tests/src/core/testqgsdxfexport.cpp b/tests/src/core/testqgsdxfexport.cpp index 84deb5691f03..e1d438b2253b 100644 --- a/tests/src/core/testqgsdxfexport.cpp +++ b/tests/src/core/testqgsdxfexport.cpp @@ -32,7 +32,9 @@ #include "qgsmarkersymbol.h" #include "qgsmarkersymbollayer.h" #include "qgslinesymbol.h" +#include "qgssymbollayerutils.h" +#include #include #include @@ -52,6 +54,7 @@ class TestQgsDxfExport : public QObject void cleanup();// will be called after every testfunction. void testPoints(); void testPointsDataDefinedSizeAngle(); + void testPointsDataDefinedSizeSymbol(); void testLines(); void testPolygons(); void testMultiSurface(); @@ -73,12 +76,18 @@ class TestQgsDxfExport : public QObject void testTransform(); void testDataDefinedPoints(); void testExtent(); + void testSelectedPoints(); + void testSelectedLines(); + void testSelectedPolygons(); + void testMultipleLayersWithSelection(); + void testExtentWithSelection(); private: QgsVectorLayer *mPointLayer = nullptr; QgsVectorLayer *mPointLayerNoSymbols = nullptr; QgsVectorLayer *mPointLayerGeometryGenerator = nullptr; QgsVectorLayer *mPointLayerDataDefinedSizeAngle = nullptr; + QgsVectorLayer *mPointLayerDataDefinedSizeSymbol = nullptr; QgsVectorLayer *mLineLayer = nullptr; QgsVectorLayer *mPolygonLayer = nullptr; @@ -149,6 +158,24 @@ void TestQgsDxfExport::init() mPointLayerDataDefinedSizeAngle->setRenderer( new QgsSingleSymbolRenderer( markerDataDefinedSymbol ) ); QgsProject::instance()->addMapLayer( mPointLayerDataDefinedSizeAngle ); + // Point layer with data-defined size and data defined svg symbol + mPointLayerDataDefinedSizeSymbol = new QgsVectorLayer( filename, QStringLiteral( "points" ), QStringLiteral( "ogr" ) ); + QVERIFY( mPointLayerDataDefinedSizeSymbol ); + QgsSvgMarkerSymbolLayer *svgSymbolLayer = new QgsSvgMarkerSymbolLayer( QStringLiteral( "symbol.svg" ) ); + QgsPropertyCollection ddProperties; + ddProperties.setProperty( QgsSymbolLayer::Property::Size, QgsProperty::fromExpression( "Importance / 10.0" ) ); + const QString planeSvgPath = QgsSymbolLayerUtils::svgSymbolNameToPath( QStringLiteral( "/gpsicons/plane.svg" ), QgsPathResolver() ); + const QString planeOrangeSvgPath = QgsSymbolLayerUtils::svgSymbolNameToPath( QStringLiteral( "/gpsicons/plane_orange.svg" ), QgsPathResolver() ); + 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 ) ); + svgSymbolLayer->setDataDefinedProperties( ddProperties ); + QgsSymbolLayerList ddSymbolLayerList; + ddSymbolLayerList << svgSymbolLayer; + QgsMarkerSymbol *markerSvgDataDefinedSymbol = new QgsMarkerSymbol( ddSymbolLayerList ); + mPointLayerDataDefinedSizeSymbol->setRenderer( new QgsSingleSymbolRenderer( markerSvgDataDefinedSymbol ) ); + QgsProject::instance()->addMapLayer( mPointLayerDataDefinedSizeSymbol ); + filename = QStringLiteral( TEST_DATA_DIR ) + "/lines.shp"; mLineLayer = new QgsVectorLayer( filename, QStringLiteral( "lines" ), QStringLiteral( "ogr" ) ); QVERIFY( mLineLayer->isValid() ); @@ -225,6 +252,33 @@ void TestQgsDxfExport::testPointsDataDefinedSizeAngle() QVERIFY( fileContainsText( file, QStringLiteral( "symbolLayer0" ) ) ); } +void TestQgsDxfExport::testPointsDataDefinedSizeSymbol() +{ + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayerDataDefinedSizeSymbol, -1, true, -1 ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPointLayerDataDefinedSizeAngle->extent() ); + mapSettings.setLayers( QList() << mPointLayerDataDefinedSizeAngle ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayerDataDefinedSizeAngle->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 2000000 ); + d.setSymbologyExport( Qgis::FeatureSymbologyExport::PerSymbolLayer ); + + QByteArray dxfByteArray; + QBuffer dxfBuffer( &dxfByteArray ); + dxfBuffer.open( QIODevice::WriteOnly ); + QCOMPARE( d.writeToFile( &dxfBuffer, QStringLiteral( "ISO-8859-1" ) ), QgsDxfExport::ExportResult::Success ); + dxfBuffer.close(); + + QString dxfString = QString::fromLatin1( dxfByteArray ); + QVERIFY( dxfString.contains( QStringLiteral( "symbolLayer0class" ) ) ); +} + void TestQgsDxfExport::testLines() { QgsDxfExport d; @@ -1447,6 +1501,274 @@ void TestQgsDxfExport::testExtent() QCOMPARE( fileContainsText( file2, "polygons", &debugInfo ), false ); } +void TestQgsDxfExport::testSelectedPoints() +{ + mPointLayer->selectByExpression( QStringLiteral( "Class = 'Jet'" ) ); + QVERIFY( mPointLayer->selectedFeatureCount() > 0 ); + + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPointLayer->extent() ); + mapSettings.setLayers( QList() << mPointLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setFlags( QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file = getTempFileName( "selected_points_dxf_only_selected" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + QVERIFY( !fileContainsText( file, QStringLiteral( "nan.0" ) ) ); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->selectedFeatureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + + // There's a selection, but now we want to export all features + d.setFlags( d.flags() & ~QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file2 = getTempFileName( "selected_point_dxf_not_only_selected" ); + QFile dxfFile2( file2 ); + QCOMPARE( d.writeToFile( &dxfFile2, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile2.close(); + + QVERIFY( !fileContainsText( file2, QStringLiteral( "nan.0" ) ) ); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file2, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mPointLayer->featureCount() ); + QVERIFY( mPointLayer->selectedFeatureCount() > 0 ); + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + + mPointLayer->removeSelection(); +} + +void TestQgsDxfExport::testSelectedLines() +{ + mLineLayer->selectByExpression( QStringLiteral( "Name = 'Highway'" ) ); + QVERIFY( mLineLayer->selectedFeatureCount() > 0 ); + + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mLineLayer ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mLineLayer->extent() ); + mapSettings.setLayers( QList() << mLineLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mLineLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setFlags( QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file = getTempFileName( "selected_lines_dxf_only_selected" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mLineLayer->selectedFeatureCount() ); + QCOMPARE( result->wkbType(), Qgis::WkbType::LineString ); + + // There's a selection, but now we want to export all features + d.setFlags( d.flags() & ~QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file2 = getTempFileName( "selected_lines_dxf_not_only_selected" ); + QFile dxfFile2( file2 ); + QCOMPARE( d.writeToFile( &dxfFile2, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile2.close(); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file2, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), mLineLayer->featureCount() ); + QVERIFY( mLineLayer->selectedFeatureCount() > 0 ); + QCOMPARE( result->wkbType(), Qgis::WkbType::LineString ); + + mLineLayer->removeSelection(); +} + +void TestQgsDxfExport::testSelectedPolygons() +{ + mPolygonLayer->selectByExpression( QStringLiteral( "Name = 'Lake'" ) ); + QVERIFY( mPolygonLayer->selectedFeatureCount() > 0 ); + + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPolygonLayer ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPolygonLayer->extent() ); + mapSettings.setLayers( QList() << mPolygonLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPolygonLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setFlags( QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file = getTempFileName( "selected_polygons_dxf_only_selected" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), 8L ); + QCOMPARE( result->wkbType(), Qgis::WkbType::LineString ); + + // There's a selection, but now we want to export all features + d.setFlags( d.flags() & ~QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file2 = getTempFileName( "selected_polygons_dxf_not_only_selected" ); + QFile dxfFile2( file2 ); + QCOMPARE( d.writeToFile( &dxfFile2, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile2.close(); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file2, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), 12L ); + QVERIFY( mPolygonLayer->selectedFeatureCount() > 0 ); + QCOMPARE( result->wkbType(), Qgis::WkbType::LineString ); + + mPolygonLayer->removeSelection(); +} + +void TestQgsDxfExport::testMultipleLayersWithSelection() +{ + mPointLayer->selectByExpression( QStringLiteral( "Class = 'Jet'" ) ); + QVERIFY( mPointLayer->selectedFeatureCount() > 0 ); + mLineLayer->selectByExpression( QStringLiteral( "Name = 'Highway'" ) ); + QVERIFY( mLineLayer->selectedFeatureCount() > 0 ); + + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer ) << QgsDxfExport::DxfLayer( mLineLayer ) ); + + QgsRectangle extent; + extent = mPointLayer->extent(); + extent.combineExtentWith( mLineLayer->extent() ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( extent ); + mapSettings.setLayers( QList() << mPointLayer << mLineLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setFlags( QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file = getTempFileName( "sel_points_lines_dxf_only_sel" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + QVERIFY( !fileContainsText( file, QStringLiteral( "nan.0" ) ) ); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QStringList subLayers = result->dataProvider()->subLayers(); + QCOMPARE( subLayers.count(), 2 ); + QStringList subLayer1 = { QStringLiteral( "0" ), + QStringLiteral( "entities" ), + QStringLiteral( "8" ), + QStringLiteral( "Point" ) + }; + QStringList subLayer2 = { QStringLiteral( "0" ), + QStringLiteral( "entities" ), + QStringLiteral( "2" ), + QStringLiteral( "LineString" ) + }; + QVERIFY( subLayers.constFirst().startsWith( subLayer1.join( QgsDataProvider::sublayerSeparator() ) ) ); + QVERIFY( subLayers.constLast().startsWith( subLayer2.join( QgsDataProvider::sublayerSeparator() ) ) ); + + // There's a selection, but now we want to export all features + d.setFlags( d.flags() & ~QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file2 = getTempFileName( "sel_points_lines_dxf_not_only_sel" ); + QFile dxfFile2( file2 ); + QCOMPARE( d.writeToFile( &dxfFile2, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile2.close(); + + // reload and compare + result = std::make_unique< QgsVectorLayer >( file2, "dxf" ); + QVERIFY( result->isValid() ); + subLayers = result->dataProvider()->subLayers(); + QCOMPARE( subLayers.count(), 2 ); + subLayer1 = QStringList{ QStringLiteral( "0" ), + QStringLiteral( "entities" ), + QStringLiteral( "%1" ).arg( mPointLayer->featureCount() ), + QStringLiteral( "Point" ) + }; + subLayer2 = QStringList{ QStringLiteral( "0" ), + QStringLiteral( "entities" ), + QStringLiteral( "%1" ).arg( mLineLayer->featureCount() ), + QStringLiteral( "LineString" ) + }; + QVERIFY( subLayers.constFirst().startsWith( subLayer1.join( QgsDataProvider::sublayerSeparator() ) ) ); + QVERIFY( subLayers.constLast().startsWith( subLayer2.join( QgsDataProvider::sublayerSeparator() ) ) ); + QVERIFY( mPointLayer->selectedFeatureCount() > 0 ); + QVERIFY( mLineLayer->selectedFeatureCount() > 0 ); + + mPointLayer->removeSelection(); + mLineLayer->removeSelection(); +} + +void TestQgsDxfExport::testExtentWithSelection() +{ + mPointLayer->selectByExpression( QStringLiteral( "Class = 'Jet'" ) ); + QVERIFY( mPointLayer->selectedFeatureCount() > 0 ); + + QgsDxfExport d; + d.addLayers( QList< QgsDxfExport::DxfLayer >() << QgsDxfExport::DxfLayer( mPointLayer ) ); + + QgsMapSettings mapSettings; + const QSize size( 640, 480 ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( mPointLayer->extent() ); + mapSettings.setLayers( QList() << mPointLayer ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setDestinationCrs( mPointLayer->crs() ); + + d.setMapSettings( mapSettings ); + d.setSymbologyScale( 1000 ); + d.setExtent( QgsRectangle( -109.0, 25.0, -86.0, 37.0 ) ); + d.setFlags( QgsDxfExport::FlagOnlySelectedFeatures ); + + const QString file = getTempFileName( "point_extent_dxf_with_selection" ); + QFile dxfFile( file ); + QCOMPARE( d.writeToFile( &dxfFile, QStringLiteral( "CP1252" ) ), QgsDxfExport::ExportResult::Success ); + dxfFile.close(); + + // reload and compare + std::unique_ptr< QgsVectorLayer > result = std::make_unique< QgsVectorLayer >( file, "dxf" ); + QVERIFY( result->isValid() ); + QCOMPARE( result->featureCount(), 3L ); // 4 in extent, 8 selected, 17 in total + QCOMPARE( result->wkbType(), Qgis::WkbType::Point ); + mPointLayer->removeSelection(); +} + bool TestQgsDxfExport::fileContainsText( const QString &path, const QString &text, QString *debugInfo ) const { QStringList debugLines; diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index d36f0a93e278..314c82da324e 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -79,6 +79,7 @@ class TestQgsLayoutMap : public QgsTest void testLayeredExportLabelsByLayer(); void testTemporal(); void testLabelResults(); + void testZRange(); private: QgsRasterLayer *mRasterLayer = nullptr; @@ -2058,5 +2059,36 @@ void TestQgsLayoutMap::testLabelResults() } +void TestQgsLayoutMap::testZRange() +{ + QgsLayout l( QgsProject::instance( ) ); + std::unique_ptr< QgsLayoutItemMap > map = std::make_unique< QgsLayoutItemMap >( &l ); + + QgsMapSettings settings = map->mapSettings( map->extent(), QSize( 512, 512 ), 72, false ); + QVERIFY( settings.zRange().isInfinite() ); + QVERIFY( !settings.expressionContext().variable( QStringLiteral( "map_z_range_lower" ) ).isValid() ); + QVERIFY( !settings.expressionContext().variable( QStringLiteral( "map_z_range_upper" ) ).isValid() ); + + map->setZRangeEnabled( true ); + map->setZRange( QgsDoubleRange( 30, 150 ) ); + map->refresh(); + + settings = map->mapSettings( map->extent(), QSize( 512, 512 ), 72, false ); + QCOMPARE( settings.zRange().lower(), 30.0 ); + QCOMPARE( settings.zRange().upper(), 150.0 ); + + QCOMPARE( settings.expressionContext().variable( QStringLiteral( "map_z_range_lower" ) ).toDouble(), 30.0 ); + QCOMPARE( settings.expressionContext().variable( QStringLiteral( "map_z_range_upper" ) ).toDouble(), 150.0 ); + + map->dataDefinedProperties().setProperty( QgsLayoutObject::DataDefinedProperty::MapZRangeLower, QgsProperty::fromExpression( QStringLiteral( "15+2" ) ) ); + map->dataDefinedProperties().setProperty( QgsLayoutObject::DataDefinedProperty::MapZRangeUpper, QgsProperty::fromExpression( QStringLiteral( "15+32" ) ) ); + map->refreshDataDefinedProperty( QgsLayoutObject::DataDefinedProperty::MapZRangeLower ); + map->refreshDataDefinedProperty( QgsLayoutObject::DataDefinedProperty::MapZRangeUpper ); + + settings = map->mapSettings( map->extent(), QSize( 512, 512 ), 72, false ); + QCOMPARE( settings.zRange().lower(), 17.0 ); + QCOMPARE( settings.zRange().upper(), 47.0 ); +} + QGSTEST_MAIN( TestQgsLayoutMap ) #include "testqgslayoutmap.moc" diff --git a/tests/src/core/testqgsmapsettings.cpp b/tests/src/core/testqgsmapsettings.cpp index 8f7ba8d21466..09db2e3397bb 100644 --- a/tests/src/core/testqgsmapsettings.cpp +++ b/tests/src/core/testqgsmapsettings.cpp @@ -582,6 +582,23 @@ void TestQgsMapSettings::testExpressionContext() QCOMPARE( r.toString(), QStringLiteral( "EPSG:7030" ) ); + e = QgsExpression( QStringLiteral( "@map_z_range_lower" ) ); + r = e.evaluate( &c ); + QVERIFY( !r.isValid() ); + e = QgsExpression( QStringLiteral( "@map_z_range_upper" ) ); + r = e.evaluate( &c ); + QVERIFY( !r.isValid() ); + + ms.setZRange( QgsDoubleRange( 0.5, 100.5 ) ); + c = QgsExpressionContext(); + c << QgsExpressionContextUtils::mapSettingsScope( ms ); + e = QgsExpression( QStringLiteral( "@map_z_range_lower" ) ); + r = e.evaluate( &c ); + QCOMPARE( r.toDouble(), 0.5 ); + e = QgsExpression( QStringLiteral( "@map_z_range_upper" ) ); + r = e.evaluate( &c ); + QCOMPARE( r.toDouble(), 100.5 ); + e = QgsExpression( QStringLiteral( "@map_start_time" ) ); r = e.evaluate( &c ); QVERIFY( !r.isValid() ); diff --git a/tests/src/core/testqgsmapsettingsutils.cpp b/tests/src/core/testqgsmapsettingsutils.cpp index ea7adfca52bf..dbd091f48c57 100644 --- a/tests/src/core/testqgsmapsettingsutils.cpp +++ b/tests/src/core/testqgsmapsettingsutils.cpp @@ -66,6 +66,16 @@ void TestQgsMapSettingsUtils::createWorldFileContent() mMapSettings.setRotation( 145 ); QCOMPARE( QgsMapSettingsUtils::worldFileContent( mMapSettings ), QString( "-0.81915204428899191\r\n0.57357643635104594\r\n0.57357643635104594\r\n0.81915204428899191\r\n0.5\r\n0.49999999999999994\r\n" ) ); + + mMapSettings.setRotation( 0 ); + mMapSettings.setDevicePixelRatio( 2.0 ); + QgsMapSettingsUtils::worldFileParameters( mMapSettings, a, b, c, d, e, f ); + QCOMPARE( a, 0.5 ); + QCOMPARE( b, 0.0 ); + QCOMPARE( c, 0.5 ); + QCOMPARE( d, 0.0 ); + QCOMPARE( e, -0.5 ); + QCOMPARE( f, 0.5 ); } void TestQgsMapSettingsUtils::containsAdvancedEffects() diff --git a/tests/src/core/testqgsnmeaconnection.cpp b/tests/src/core/testqgsnmeaconnection.cpp index 7618f7d97ba2..64feb5a916cf 100644 --- a/tests/src/core/testqgsnmeaconnection.cpp +++ b/tests/src/core/testqgsnmeaconnection.cpp @@ -50,6 +50,14 @@ class ReplayNmeaConnection : public QgsNmeaConnection return spy.constLast().at( 0 ).value< QgsGpsInformation >(); } + void pushString( const QString &string ) + { + const qint64 pos = mBuffer->pos(); + mBuffer->write( string.toLocal8Bit().constData() ); + mBuffer->seek( pos ); + parseData(); + } + private: QBuffer *mBuffer = nullptr; @@ -74,6 +82,7 @@ class TestQgsNmeaConnection : public QgsTest void testConstellation(); void testPosition(); void testComponent(); + void testIncompleteMessage(); }; @@ -468,5 +477,31 @@ void TestQgsNmeaConnection::testComponent() QCOMPARE( info.componentValue( Qgis::GpsInformationComponent::Bearing ).toDouble(), 2 ); } +void TestQgsNmeaConnection::testIncompleteMessage() +{ + ReplayNmeaConnection connection; + QSignalSpy stateChangedSpy( &connection, &QgsNmeaConnection::stateChanged ); + + QCOMPARE( connection.status(), QgsGpsConnection::Status::Connected ); + + // start with an incomplete message + connection.pushString( QStringLiteral( "$GPGGA," ) ); + // status should be "data received", we don't have the full sentence yet + QCOMPARE( connection.status(), QgsGpsConnection::Status::DataReceived ); + // should be no stateChanged signal yet, we are still waiting on more data + QCOMPARE( stateChangedSpy.size(), 0 ); + + connection.pushString( QStringLiteral( "084112.185,6900.0,N,01800.0,E,1,04,1.4,35.0,M,29.4,M,,0000*63\r\n" ) ); + // got a full sentence now, status should be "data received" + QCOMPARE( connection.status(), QgsGpsConnection::Status::GPSDataReceived ); + QCOMPARE( stateChangedSpy.size(), 1 ); + const QgsGpsInformation info = stateChangedSpy.at( 0 ).at( 0 ).value< QgsGpsInformation >(); + + QCOMPARE( info.componentValue( Qgis::GpsInformationComponent::Location ).value< QgsPointXY >(), QgsPointXY( 18, 69 ) ); + QCOMPARE( info.componentValue( Qgis::GpsInformationComponent::Altitude ).toDouble(), 35 ); + QCOMPARE( info.componentValue( Qgis::GpsInformationComponent::GroundSpeed ).toDouble(), 0 ); + QCOMPARE( info.componentValue( Qgis::GpsInformationComponent::Bearing ).toDouble(), 0 ); +} + QGSTEST_MAIN( TestQgsNmeaConnection ) #include "testqgsnmeaconnection.moc" diff --git a/tests/src/core/testqgsrasterlayer.cpp b/tests/src/core/testqgsrasterlayer.cpp index aa0b504d85b2..42b50b241438 100644 --- a/tests/src/core/testqgsrasterlayer.cpp +++ b/tests/src/core/testqgsrasterlayer.cpp @@ -657,19 +657,15 @@ void TestQgsRasterLayer::transparency() qDebug( "contrastEnhancement.minimumValue = %.17g", renderer->contrastEnhancement()->minimumValue() ); qDebug( "contrastEnhancement.maximumValue = %.17g", renderer->contrastEnhancement()->maximumValue() ); - QList myTransparentSingleValuePixelList; + QVector myTransparentSingleValuePixelList; QgsRasterTransparency *rasterTransparency = new QgsRasterTransparency(); - QgsRasterTransparency::TransparentSingleValuePixel myTransparentPixel; - myTransparentPixel.min = -2.5840000772112106e+38; - myTransparentPixel.max = -1.0879999684602689e+38; - myTransparentPixel.percentTransparent = 50; - myTransparentSingleValuePixelList.append( myTransparentPixel ); - - myTransparentPixel.min = 1.359999960575336e+37; - myTransparentPixel.max = 9.520000231087593e+37; - myTransparentPixel.percentTransparent = 70; - myTransparentSingleValuePixelList.append( myTransparentPixel ); + myTransparentSingleValuePixelList.append( + QgsRasterTransparency::TransparentSingleValuePixel( -2.5840000772112106e+38, -1.0879999684602689e+38, 0.5 ) + ); + myTransparentSingleValuePixelList.append( + QgsRasterTransparency::TransparentSingleValuePixel( 1.359999960575336e+37, 9.520000231087593e+37, 0.3 ) + ); rasterTransparency->setTransparentSingleValuePixelList( myTransparentSingleValuePixelList ); diff --git a/tests/src/core/testqgsvaluerelationfieldformatter.cpp b/tests/src/core/testqgsvaluerelationfieldformatter.cpp index a3d89992fd52..deb2e1d69858 100644 --- a/tests/src/core/testqgsvaluerelationfieldformatter.cpp +++ b/tests/src/core/testqgsvaluerelationfieldformatter.cpp @@ -38,6 +38,7 @@ class TestQgsValueRelationFieldFormatter: public QObject void cleanup(); // will be called after every testfunction. void testDependencies(); void testSortValueNull(); + void testGroup(); private: std::unique_ptr mLayer1; @@ -164,5 +165,20 @@ void TestQgsValueRelationFieldFormatter::testSortValueNull() QCOMPARE( value, QVariant( QString( "iron" ) ) ); } +void TestQgsValueRelationFieldFormatter::testGroup() +{ + const QgsValueRelationFieldFormatter formatter; + QVariantMap config; + config.insert( QStringLiteral( "Layer" ), mLayer2->id() ); + config.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) ); + config.insert( QStringLiteral( "Value" ), QStringLiteral( "raccord" ) ); + config.insert( QStringLiteral( "Group" ), QStringLiteral( "material" ) ); + + QgsValueRelationFieldFormatter::ValueRelationCache cache = formatter.createCache( config ); + QVERIFY( !cache.isEmpty() ); + QCOMPARE( cache.at( 0 ).group, QVariant( QStringLiteral( "iron" ) ) ); + QCOMPARE( cache.at( cache.size() - 1 ).group, QVariant( QStringLiteral( "steel" ) ) ); +} + QGSTEST_MAIN( TestQgsValueRelationFieldFormatter ) #include "testqgsvaluerelationfieldformatter.moc" diff --git a/tests/src/geometry_checker/testqgsgeometrychecks.cpp b/tests/src/geometry_checker/testqgsgeometrychecks.cpp index 18486041642e..3ac984bc4080 100644 --- a/tests/src/geometry_checker/testqgsgeometrychecks.cpp +++ b/tests/src/geometry_checker/testqgsgeometrychecks.cpp @@ -1125,7 +1125,7 @@ void TestQgsGeometryChecks::testSelfIntersectionCheck() // make sure the other part of the ring isn't present in this feature. We may have OTHER parts in this feature though, depending on the GDAL version! for ( int i = 1; i < collectionResult->numGeometries(); ++i ) { - QVERIFY( qgsgeometry_cast< const QgsPolygon * >( collectionResult->geometryN( i ) )->exteriorRing()->asWkt( 2 ) != QStringLiteral( "LineString (1.24 -0.05, 1.45 0.1, 1.26 0.09, 1.24 -0.05)" ) ); + QVERIFY( qgsgeometry_cast< const QgsPolygon * >( collectionResult->geometryN( i ) )->exteriorRing()->asWkt( 2 ) != QLatin1String( "LineString (1.24 -0.05, 1.45 0.1, 1.26 0.09, 1.24 -0.05)" ) ); } testContext.second[errs3[0]->layerId()]->getFeature( nextId, f ); QCOMPARE( qgsgeometry_cast< const QgsPolygon * >( f.geometry().constGet() )->exteriorRing()->asWkt( 2 ), QStringLiteral( "LineString (1.24 -0.05, 1.45 0.1, 1.26 0.09, 1.24 -0.05)" ) ); 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/gui/testqgsquerybuilder.cpp b/tests/src/gui/testqgsquerybuilder.cpp index a55a8f2f3021..6e873b73fa1b 100644 --- a/tests/src/gui/testqgsquerybuilder.cpp +++ b/tests/src/gui/testqgsquerybuilder.cpp @@ -101,13 +101,13 @@ void TestQgsQueryBuilder::testFillValues() QgsQueryBuilder queryBuilder( &vl ); - queryBuilder.fillValues( 0, 10 ); + queryBuilder.fillValues( "intarray", 10 ); QCOMPARE( getModelItemDisplayStrings( queryBuilder.mModelValues ), QStringList() << "1" << "2, 3" << "4, 5, 6" << "NULL" ); - queryBuilder.fillValues( 1, 10 ); + queryBuilder.fillValues( "strarray", 10 ); QCOMPARE( getModelItemDisplayStrings( queryBuilder.mModelValues ), QStringList() << "NULL" << "testA" << "testB, testC" ); - queryBuilder.fillValues( 2, 10 ); + queryBuilder.fillValues( "intf", 10 ); QCOMPARE( getModelItemDisplayStrings( queryBuilder.mModelValues ), QStringList() << "0" << "42" << "NULL" ); } diff --git a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp index fc5e1bf7c5dc..c4a16968cc80 100644 --- a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp +++ b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp @@ -62,6 +62,7 @@ class TestQgsValueRelationWidgetWrapper : public QObject void testRegressionGH42003(); void testAllowMultiColumns(); void testAllowMultiAndCompleter(); + void testGroup(); }; void TestQgsValueRelationWidgetWrapper::initTestCase() @@ -1817,5 +1818,88 @@ void TestQgsValueRelationWidgetWrapper::testAllowMultiAndCompleter() QCOMPARE( w_favoriteauthors.mTableWidget->item( 1, 2 )->text(), QStringLiteral( "Ken Follett" ) ); QCOMPARE( w_favoriteauthors.mTableWidget->item( 1, 2 )->data( Qt::UserRole ).toString(), QStringLiteral( "6" ) ); } + +void TestQgsValueRelationWidgetWrapper::testGroup() +{ + // create a vector layer + QgsVectorLayer vl1( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) ); + QgsVectorLayer vl2( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( &vl1, false, false ); + QgsProject::instance()->addMapLayer( &vl2, false, false ); + + // insert some features + QgsFeature f1( vl1.fields() ); + f1.setAttribute( QStringLiteral( "pk" ), 1 ); + f1.setAttribute( QStringLiteral( "province" ), 123 ); + f1.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Some Place By The River" ) ); + f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" ) ) ); + QVERIFY( f1.isValid() ); + QgsFeature f2( vl1.fields() ); + f2.setAttribute( QStringLiteral( "pk" ), 2 ); + f2.setAttribute( QStringLiteral( "province" ), 245 ); + f2.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Dreamland By The Clouds" ) ); + f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 1 0, 1 1, 2 1, 2 0, 1 0 ))" ) ) ); + QVERIFY( f2.isValid() ); + QVERIFY( vl1.dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) ); + + QgsFeature f3( vl2.fields() ); + f3.setAttribute( QStringLiteral( "fk_province" ), 123 ); + f3.setAttribute( QStringLiteral( "fk_municipality" ), 1 ); + f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 0.5 0.5)" ) ) ); + QVERIFY( f3.isValid() ); + QVERIFY( f3.geometry().isGeosValid() ); + QVERIFY( vl2.dataProvider()->addFeature( f3 ) ); + + // Test display-less grouping with combobox + QgsValueRelationWidgetWrapper w( &vl2, vl2.fields().indexOf( QLatin1String( "fk_municipality" ) ), nullptr, nullptr ); + QVariantMap cfg; + cfg.insert( QStringLiteral( "Layer" ), vl1.id() ); + cfg.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) ); + cfg.insert( QStringLiteral( "Value" ), QStringLiteral( "municipality" ) ); + cfg.insert( QStringLiteral( "Group" ), QStringLiteral( "province" ) ); + cfg.insert( QStringLiteral( "DisplayGroupName" ), false ); + cfg.insert( QStringLiteral( "AllowMulti" ), false ); + cfg.insert( QStringLiteral( "AllowNull" ), false ); + cfg.insert( QStringLiteral( "OrderByValue" ), true ); + w.setConfig( cfg ); + w.widget(); + w.setEnabled( true ); + + QComboBox *comboBox = qobject_cast( w.widget() ); + QCOMPARE( comboBox->isEnabled(), true ); + QCOMPARE( comboBox->model()->rowCount(), 3 ); // 2 items + 1 separator + + // Test display grouping with combobox + QgsValueRelationWidgetWrapper w2( &vl2, vl2.fields().indexOf( QLatin1String( "fk_municipality" ) ), nullptr, nullptr ); + cfg.insert( QStringLiteral( "DisplayGroupName" ), true ); + w2.setConfig( cfg ); + w2.widget(); + w2.setEnabled( true ); + + comboBox = qobject_cast( w2.widget() ); + QCOMPARE( comboBox->isEnabled(), true ); + QCOMPARE( comboBox->model()->rowCount(), 5 ); // 2 items + 2 group names + 1 separator + + // Test display-less grouping with multi-selection table + QgsValueRelationWidgetWrapper w3( &vl2, vl2.fields().indexOf( QLatin1String( "fk_municipality" ) ), nullptr, nullptr ); + cfg.insert( QStringLiteral( "DisplayGroupName" ), false ); + cfg.insert( QStringLiteral( "AllowMulti" ), true ); + w3.setConfig( cfg ); + w3.widget(); + w3.setEnabled( true ); + + QCOMPARE( w3.mTableWidget->rowCount(), 3 ); // 2 items + 1 separator + + // Test display grouping with multi-selection table + QgsValueRelationWidgetWrapper w4( &vl2, vl2.fields().indexOf( QLatin1String( "fk_municipality" ) ), nullptr, nullptr ); + cfg.insert( QStringLiteral( "DisplayGroupName" ), true ); + cfg.insert( QStringLiteral( "AllowMulti" ), true ); + w4.setConfig( cfg ); + w4.widget(); + w4.setEnabled( true ); + + QCOMPARE( w4.mTableWidget->rowCount(), 4 ); // 2 items + 2 group names +} + QGSTEST_MAIN( TestQgsValueRelationWidgetWrapper ) #include "testqgsvaluerelationwidgetwrapper.moc" diff --git a/tests/src/providers/CMakeLists.txt b/tests/src/providers/CMakeLists.txt index 3504e817ac3f..f8871a827811 100644 --- a/tests/src/providers/CMakeLists.txt +++ b/tests/src/providers/CMakeLists.txt @@ -85,6 +85,7 @@ if(UNIX AND NOT ANDROID AND CMAKE_BUILD_TYPE MATCHES Debug) add_executable(test_provider_wcs ${WCSTEST_SRCS} ) target_compile_features(test_provider_wcs PRIVATE cxx_std_17) + target_compile_definitions(test_provider_wcs PRIVATE "CMAKE_SOURCE_DIR=\"${CMAKE_SOURCE_DIR}\"") include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 5e05df4dbf0a..72140d293c02 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -63,10 +63,12 @@ ADD_PYTHON_TEST(PyQgsDateTimeStatisticalSummary test_qgsdatetimestatisticalsumma ADD_PYTHON_TEST(PyQgsDatumTransform test_qgsdatumtransforms.py) ADD_PYTHON_TEST(PyQgsDelimitedTextProvider test_qgsdelimitedtextprovider.py) ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py) +ADD_PYTHON_TEST(PyQgsElevationControllerWidget test_qgselevationcontrollerwidget.py) ADD_PYTHON_TEST(PyQgsEllipsoidUtils test_qgsellipsoidutils.py) ADD_PYTHON_TEST(PyQgsEmbeddedSymbolRenderer test_qgsembeddedsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsExifTools test_qgsexiftools.py) ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py) +ADD_PYTHON_TEST(PyQgsExpressionPreviewWidget test_qgsexpressionpreviewwidget.py) ADD_PYTHON_TEST(PyQgsExternalStorageWebDav test_qgsexternalstorage_webdav.py) ADD_PYTHON_TEST(PyQgsExternalStorageAwsS3 test_qgsexternalstorage_awss3.py) ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py) @@ -104,6 +106,7 @@ ADD_PYTHON_TEST(PyQgsGraduatedSymbolRenderer test_qgsgraduatedsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsGraph test_qgsgraph.py) ADD_PYTHON_TEST(PyQgsGroupLayer test_qgsgrouplayer.py) ADD_PYTHON_TEST(PyQgsHashLineSymbolLayer test_qgshashlinesymbollayer.py) +ADD_PYTHON_TEST(PyQgsHillshadeRenderer test_qgshillshaderenderer.py) ADD_PYTHON_TEST(PyQgsImageCache test_qgsimagecache.py) ADD_PYTHON_TEST(PyQgsInterpolatedLineSymbolLayer test_qgsinterpolatedlinesymbollayers.py) ADD_PYTHON_TEST(PyQgsInterval test_qgsinterval.py) @@ -174,6 +177,7 @@ ADD_PYTHON_TEST(PyQgsMarkerLineSymbolLayer test_qgsmarkerlinesymbollayer.py) ADD_PYTHON_TEST(PyQgsMatrix4x4 test_qgsmatrix4x4.py) ADD_PYTHON_TEST(PyQgsMergedFeatureRenderer test_qgsmergedfeaturerenderer.py) ADD_PYTHON_TEST(PyQgsMeshLayerElevationProperties test_qgsmeshlayerelevationproperties.py) +ADD_PYTHON_TEST(PyQgsMeshLayerRenderer test_qgsmeshlayerrenderer.py) ADD_PYTHON_TEST(PyQgsMessageLog test_qgsmessagelog.py) ADD_PYTHON_TEST(PyQgsMetadataBase test_qgsmetadatabase.py) ADD_PYTHON_TEST(PyQgsMetadataUtils test_qgsmetadatautils.py) @@ -199,6 +203,7 @@ ADD_PYTHON_TEST(PyQgsOptional test_qgsoptional.py) ADD_PYTHON_TEST(PyQgsOrientedBox3D test_qgsorientedbox3d.py) ADD_PYTHON_TEST(PyQgsOwsConnection test_qgsowsconnection.py) ADD_PYTHON_TEST(PyQgsPainting test_qgspainting.py) +ADD_PYTHON_TEST(PyQgsPalettedRasterRenderer test_qgspalettedrasterrenderer.py) ADD_PYTHON_TEST(PyQgsPalLabelingBase test_qgspallabeling_base.py) ADD_PYTHON_TEST(PyQgsPalLabelingCanvas test_qgspallabeling_canvas.py) ADD_PYTHON_TEST(PyQgsPalLabelingLayout test_qgspallabeling_layout.py) @@ -250,18 +255,23 @@ ADD_PYTHON_TEST(PyQgsProviderSublayerModel test_qgsprovidersublayermodel.py) ADD_PYTHON_TEST(TestQgsRandomMarkerSymbolLayer test_qgsrandommarkersymbollayer.py) ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py) ADD_PYTHON_TEST(PyQgsRasterAttributeTable test_qgsrasterattributetable.py) +ADD_PYTHON_TEST(PyQgsRasterContourRenderer test_qgsrastercontourrenderer.py) ADD_PYTHON_TEST(PyQgsRasterFileWriter test_qgsrasterfilewriter.py) ADD_PYTHON_TEST(PyQgsRasterFileWriterTask test_qgsrasterfilewritertask.py) ADD_PYTHON_TEST(PyQgsRasterLayer test_qgsrasterlayer.py) ADD_PYTHON_TEST(PyQgsRasterLayerElevationProperties test_qgsrasterlayerelevationproperties.py) 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) ADD_PYTHON_TEST(PyQgsRasterRange test_qgsrasterrange.py) +ADD_PYTHON_TEST(PyQgsRasterRendererRegistry test_qgsrasterrendererregistry.py) ADD_PYTHON_TEST(PyQgsRasterRendererUtils test_qgsrasterrendererutils.py) ADD_PYTHON_TEST(PyQgsRasterResampler test_qgsrasterresampler.py) +ADD_PYTHON_TEST(PyQgsRasterTransparency test_qgsrastertransparency.py) ADD_PYTHON_TEST(PyQgsRecentCoordinateReferenceSystemsModel test_qgsrecentcoordinatereferencesystemsmodel.py) ADD_PYTHON_TEST(PyQgsRectangle test_qgsrectangle.py) ADD_PYTHON_TEST(PyQgsReferencedGeometry test_qgsreferencedgeometry.py) @@ -277,6 +287,9 @@ ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py) ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py) ADD_PYTHON_TEST(PyQgsScaleCalculator test_qgsscalecalculator.py) +ADD_PYTHON_TEST(PyQgsSingleBandColorDataRenderer test_qgssinglebandcolordatarenderer.py) +ADD_PYTHON_TEST(PyQgsSingleBandGrayRenderer test_qgssinglebandgrayrenderer.py) +ADD_PYTHON_TEST(PyQgsSingleBandPseudoColorRenderer test_qgssinglebandpseudocolorrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsSphere test_qgssphere.py) @@ -372,6 +385,10 @@ ADD_PYTHON_TEST(PyQgsVtpk test_qgsvtpk.py) if (NOT WIN32) ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py) + if (WITH_QTSERIALPORT) + ADD_PYTHON_TEST(PyQgsSerialPortSensor test_qgsserialportsensor.py) + endif() + # Add optional tests which depend on certain cmake options if (WITH_SERVER) ADD_PYTHON_TEST(PyQgsPalLabelingServer test_qgspallabeling_server.py) @@ -452,6 +469,7 @@ if (WITH_GUI) ADD_PYTHON_TEST(PyQgsNewVectorTableDialog test_qgsnewvectortabledialog.py) ADD_PYTHON_TEST(PyQgsNumericFormatGui test_qgsnumericformatgui.py) ADD_PYTHON_TEST(PyQgsOpacityWidget test_qgsopacitywidget.py) + ADD_PYTHON_TEST(PyQgsOverlayWidgetLayout test_qgsoverlaywidgetlayout.py) ADD_PYTHON_TEST(PyQgsPanelWidget test_qgspanelwidget.py) ADD_PYTHON_TEST(PyQgsPanelWidgetStack test_qgspanelwidgetstack.py) ADD_PYTHON_TEST(PyQgsPointCloudAttributeComboBox test_qgspointcloudattributecombobox.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.py b/tests/src/python/test_provider_ogr.py index e3e8c7a740e0..517fc53359fa 100644 --- a/tests/src/python/test_provider_ogr.py +++ b/tests/src/python/test_provider_ogr.py @@ -1092,7 +1092,6 @@ def testBoolFieldEvaluation(self): self.assertEqual(vl.fields().at(0).name(), 'bool') self.assertEqual(vl.fields().at(0).type(), QVariant.Bool) self.assertEqual([f[0] for f in vl.getFeatures()], [True, False, NULL]) - self.assertEqual([f[0].__class__.__name__ for f in vl.getFeatures()], ['bool', 'bool', 'QVariant']) def testReloadDataAndFeatureCount(self): @@ -1155,8 +1154,7 @@ def testSpatialiteDefaultValues(self): # Test default values dp = vl.dataProvider() - # FIXME: should it be None? - self.assertTrue(dp.defaultValue(0).isNull()) + self.assertEqual(dp.defaultValue(0), NULL) self.assertIsNone(dp.defaultValue(1)) # FIXME: This fails because there is no backend-side evaluation in this provider # self.assertTrue(dp.defaultValue(2).startswith(now.strftime('%Y-%m-%d'))) @@ -1404,7 +1402,16 @@ def testHTTPRequestsOverrider(self): # Asked when ogr provider try to open. See QgsOgrProvider::QgsOgrProvider#453 open( OpenModeForceReadOnly ); handler.add('GET', '/collections/foo', 200, {'Content-Type': 'application/json'}, '{ "id": "foo" }') - if int(gdal.VersionInfo('VERSION_NUM')) < GDAL_COMPUTE_VERSION(3, 9, 0): + # 3.8.3 not necessarily the minimum version + if int(gdal.VersionInfo('VERSION_NUM')) >= GDAL_COMPUTE_VERSION(3, 8, 3): + handler.add('GET', '/', 200, {'Content-Type': 'application/json'}, '{ "id": "foo" }') + handler.add('GET', '/api', 200, {'Content-Type': 'application/json'}, '{ "id": "foo" }') + + handler.add('GET', '/collections/foo/items?limit=20', 200, {'Content-Type': 'application/geo+json'}, + '{ "type": "FeatureCollection", "features": [] }') + handler.add('GET', '/collections/foo/items?limit=1000', 200, {'Content-Type': 'application/geo+json'}, + '{ "type": "FeatureCollection", "features": [] }') + else: # See QgsOgrProvider::open#4012 mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, options, mLayerName, errCause, true ); handler.add('GET', '/collections/foo/items?limit=10', 200, {'Content-Type': 'application/geo+json'}, '{ "type": "FeatureCollection", "features": [] }') @@ -1417,17 +1424,6 @@ def testHTTPRequestsOverrider(self): handler.add('GET', '/collections/foo/items?limit=10', 200, {'Content-Type': 'application/geo+json'}, '{ "type": "FeatureCollection", "features": [] }') - else: - handler.add('GET', '/', 200, {'Content-Type': 'application/json'}, '{ "id": "foo" }') - handler.add('GET', '/api', 200, {'Content-Type': 'application/json'}, '{ "id": "foo" }') - - handler.add('GET', '/collections/foo/items?limit=20', 200, {'Content-Type': 'application/geo+json'}, - '{ "type": "FeatureCollection", "features": [] }') - handler.add('GET', '/collections/foo/items?limit=1000', 200, {'Content-Type': 'application/geo+json'}, - '{ "type": "FeatureCollection", "features": [] }') - handler.add('GET', '/collections/foo/items?limit=1000', 200, {'Content-Type': 'application/geo+json'}, - '{ "type": "FeatureCollection", "features": [] }') - with mockedwebserver.install_http_handler(handler): vl = QgsVectorLayer("OAPIF:http://127.0.0.1:%d/collections/foo" % port, 'test', 'ogr') self.assertTrue(vl.isValid()) diff --git a/tests/src/python/test_provider_ogr_gpkg.py b/tests/src/python/test_provider_ogr_gpkg.py index 3519116d86ca..94b21bb54acb 100644 --- a/tests/src/python/test_provider_ogr_gpkg.py +++ b/tests/src/python/test_provider_ogr_gpkg.py @@ -2938,6 +2938,71 @@ def testExtent(self): self.assertAlmostEqual(vl.extent3D().zMaximum(), 75.0, places=3) del vl + def testQueryLayers(self): + """Test issue GH #56345""" + + temp_dir = QTemporaryDir() + temp_path = temp_dir.path() + filename = os.path.join(temp_path, "test.gpkg") + ds = ogr.GetDriverByName("GPKG").CreateDataSource(filename) + lyr = ds.CreateLayer("points", geom_type=ogr.wkbPoint) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(1 2)')) + lyr.CreateFeature(f) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(3 4)')) + lyr.CreateFeature(f) + f = None + + vl = QgsVectorLayer(filename + '|layername=points') + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 2) + + vl = QgsVectorLayer(filename) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 2) + + # Add lines layer + lyr = ds.CreateLayer("lines", geom_type=ogr.wkbLineString) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('LINESTRING(1 2, 3 4)')) + lyr.CreateFeature(f) + f = None + ds = None + + vl = QgsVectorLayer(filename + '|layername=lines') + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Line) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 1) + + vl = QgsVectorLayer(filename) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.geometryType(), Qgis.GeometryType.Point) + self.assertEqual(vl.featureCount(), 2) + + # Set subset string to SELECT * FROM lines + self.assertTrue(vl.setSubsetString('SELECT * FROM lines WHERE fid > 0')) + self.assertTrue(vl.isValid()) + self.assertIn('|subset=SELECT * FROM lines WHERE fid > 0', vl.dataProvider().dataSourceUri()) + f = next(vl.getFeatures()) + self.assertEqual(f.geometry().type(), Qgis.GeometryType.Line) + self.assertEqual(vl.featureCount(), 1) + # This fails because the vector layer doesn't know about the new geometry type + # self.assertEqual(vl.geometryType(), Qgis.GeometryType.Line) + + self.assertTrue(vl.setSubsetString('')) + self.assertTrue(vl.isValid()) + self.assertIn("layername=lines", vl.dataProvider().dataSourceUri()) + # This fails because the vector layer doesn't know about the new geometry type + # self.assertEqual(vl.geometryType(), Qgis.GeometryType.Line) + f = next(vl.getFeatures()) + self.assertEqual(f.geometry().type(), Qgis.GeometryType.Line) + self.assertEqual(f.geometry().asWkt().upper(), 'LINESTRING (1 2, 3 4)') + self.assertEqual(vl.allFeatureIds(), [1]) + # This fails on CI but only on the "5, ALL_BUT_PROVIDERS" workflow + # for some reason that I cannto reproduce locally, featureCount returns 2 + # self.assertEqual(vl.featureCount(), 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_provider_postgres.py b/tests/src/python/test_provider_postgres.py index 94550973aad5..05cbabf6c1dc 100644 --- a/tests/src/python/test_provider_postgres.py +++ b/tests/src/python/test_provider_postgres.py @@ -2707,6 +2707,40 @@ def testDefaultValuesAndClauses(self): self.assertEqual(feature.attribute(4), 123) self.assertEqual(feature.attribute(5), 'My default') + def testNoDefaultValueClauseForPKWithNoDefaultValue(self): + """Test issue GH #54058""" + + self.execSQLCommand( + 'ALTER TABLE IF EXISTS qgis_test."gh_54058" DROP CONSTRAINT IF EXISTS pk_gh_54058;') + self.execSQLCommand( + 'DROP TABLE IF EXISTS qgis_test."gh_54058" CASCADE;') + self.execSQLCommand( + 'CREATE TABLE qgis_test."gh_54058" ( "T_Id" integer NOT NULL, name text );') + self.execSQLCommand( + 'ALTER TABLE qgis_test."gh_54058" ADD CONSTRAINT pk_gh_54058 PRIMARY KEY ("T_Id");') + + vl = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'id\' table="qgis_test"."gh_54058" () sql=', 'gh_54058', 'postgres') + self.assertTrue(vl.isValid()) + + dp = vl.dataProvider() + self.assertEqual(dp.defaultValueClause(0), '') # Not nextVal(NULL) anymore! + + def testNoDefaultValueClauseForUniqueNotNullFieldWithNoDefaultValue(self): + """Test issue GH #54058b""" + + self.execSQLCommand( + 'DROP TABLE IF EXISTS qgis_test."gh_54058b" CASCADE;') + self.execSQLCommand( + 'CREATE TABLE qgis_test."gh_54058b" ( "T_Id" integer NOT NULL, name varchar(8) UNIQUE, codigo integer NOT NULL UNIQUE);') + + vl = QgsVectorLayer(self.dbconn + ' sslmode=disable key=\'id\' table="qgis_test"."gh_54058b" () sql=', 'gh_54058b', 'postgres') + self.assertTrue(vl.isValid()) + + dp = vl.dataProvider() + self.assertEqual(dp.defaultValueClause(0), '') # The issue didn't occur here + self.assertEqual(dp.defaultValueClause(1), '') # The issue didn't occur here + self.assertEqual(dp.defaultValueClause(2), '') # Not nextVal(NULL) anymore! + def testEncodeDecodeUri(self): """Test PG encode/decode URI""" diff --git a/tests/src/python/test_provider_sensorthings.py b/tests/src/python/test_provider_sensorthings.py index f2efc0eb277d..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:] @@ -79,20 +80,29 @@ def tearDownClass(cls): super().tearDownClass() def test_filter_for_wkb_type(self): + """ + Test constructing a valid filter string which will return only + features with a desired WKB type + """ + self.assertEqual( + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.Point), + "location/type eq 'Point' or location/geometry/type eq 'Point'" + ) self.assertEqual( - QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.Point), "location/type eq 'Point'" + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.PointZ), + "location/type eq 'Point' or location/geometry/type eq 'Point'" ) self.assertEqual( - QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.PointZ), "location/type eq 'Point'" + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.FeatureOfInterest, Qgis.WkbType.Polygon), + "feature/type eq 'Polygon' or feature/geometry/type eq 'Polygon'" ) self.assertEqual( - QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.FeatureOfInterest, Qgis.WkbType.Polygon), "feature/type eq 'Polygon'" + QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.LineString), + "location/type eq 'LineString' or location/geometry/type eq 'LineString'" ) - # TODO -- there is NO documentation on what the type must be for line filtering, - # and I can't find any public servers with line geometries to test with! - # Find some way to confirm if this is 'Line' or 'LineString' or ... self.assertEqual( - QgsSensorThingsUtils.filterForWkbType(Qgis.SensorThingsEntity.Location, Qgis.WkbType.LineString), "location/type eq 'LineString'" + 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): @@ -131,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( @@ -169,8 +183,37 @@ 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): + """ + Test constructing valid filter strings for features which intersect + an extent + """ + self.assertFalse(QgsSensorThingsUtils.filterForExtent('', QgsRectangle())) + self.assertFalse(QgsSensorThingsUtils.filterForExtent('test', QgsRectangle())) + self.assertFalse(QgsSensorThingsUtils.filterForExtent('', QgsRectangle(1, 2, 3, 4))) + self.assertEqual(QgsSensorThingsUtils.filterForExtent('test', QgsRectangle(1, 2, 3, 4)), + "geo.intersects(test, geography'POLYGON((1 2, 3 2, 3 4, 1 4, 1 2))')") + + def test_combine_filters(self): + """ + Test combining multiple filter strings into one + """ + self.assertFalse(QgsSensorThingsUtils.combineFilters([])) + self.assertFalse(QgsSensorThingsUtils.combineFilters([''])) + self.assertEqual(QgsSensorThingsUtils.combineFilters(['', 'a eq 1']), 'a eq 1') + self.assertEqual(QgsSensorThingsUtils.combineFilters(['a eq 1', 'b eq 2']), '(a eq 1) and (b eq 2)') + self.assertEqual(QgsSensorThingsUtils.combineFilters(['a eq 1', '', 'b eq 2', 'c eq 3']), + '(a eq 1) and (b eq 2) and (c eq 3)') def test_invalid_layer(self): + """ + Test construction of layers using bad URLs + """ vl = QgsVectorLayer( "url='http://fake.com/fake_qgis_http_endpoint'", "test", "sensorthings" ) @@ -183,6 +226,10 @@ def test_invalid_layer(self): ) def test_layer_invalid_json(self): + """ + Test that connecting to services which return non-parsable JSON + are cleanly handled (i.e. no crashes!) + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -198,6 +245,9 @@ def test_layer_invalid_json(self): self.assertIn("parse error", vl.dataProvider().error().summary()) def test_layer(self): + """ + Test construction of a basic layer using a valid SensorThings endpoint + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -251,7 +301,7 @@ def test_layer(self): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -265,6 +315,8 @@ def test_layer(self): self.assertTrue(vl.isValid()) self.assertEqual(vl.storageType(), "OGC SensorThings API") self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + # pessimistic "worst case" extent should be used + self.assertEqual(vl.extent(), QgsRectangle(-180, -90, 180, 90)) self.assertEqual(vl.featureCount(), 4962) self.assertIn("Entity TypeLocation", vl.htmlMetadata()) self.assertIn(f'href="http://{endpoint}/Locations"', vl.htmlMetadata()) @@ -297,6 +349,9 @@ def test_layer(self): self.assertEqual(vl.wkbType(), Qgis.WkbType.MultiPolygonZ) def test_thing(self): + """ + Test a layer retrieving 'Thing' entities from a service + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -402,13 +457,14 @@ def test_thing(self): "sensorthings", ) self.assertTrue(vl.isValid()) + # basic layer properties tests self.assertEqual(vl.storageType(), "OGC SensorThings API") self.assertEqual(vl.wkbType(), Qgis.WkbType.NoGeometry) + self.assertTrue(vl.extent().isNull()) self.assertEqual(vl.featureCount(), 3) self.assertFalse(vl.crs().isValid()) self.assertIn("Entity TypeThing", vl.htmlMetadata()) self.assertIn(f'href="http://{endpoint}/Things"', vl.htmlMetadata()) - self.assertEqual( [f.name() for f in vl.fields()], [ @@ -430,6 +486,7 @@ def test_thing(self): ], ) + # 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"]) @@ -449,6 +506,9 @@ def test_thing(self): ) def test_location(self): + """ + Test a layer retrieving 'Location' entities from a service + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -470,14 +530,14 @@ def test_location(self): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -494,8 +554,8 @@ def test_location(self): "location": { "type": "Point", "coordinates": [ - 11.623373, - 52.132017 + 11.6, + 52.1 ] }, "properties": { @@ -513,8 +573,8 @@ def test_location(self): "location": { "type": "Point", "coordinates": [ - 12.623373, - 53.132017 + 12.6, + 53.1 ] }, "properties": { @@ -525,7 +585,7 @@ def test_location(self): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -533,7 +593,7 @@ def test_location(self): ) with open( - sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -550,8 +610,8 @@ def test_location(self): "location": { "type": "Point", "coordinates": [ - 13.623373, - 55.132017 + 13.6, + 55.1 ] }, "properties": { @@ -573,8 +633,11 @@ def test_location(self): "sensorthings", ) self.assertTrue(vl.isValid()) + # basic layer properties tests self.assertEqual(vl.storageType(), "OGC SensorThings API") self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + # pessimistic "worst case" extent should initially be used + self.assertEqual(vl.extent(), QgsRectangle(-180, -90, 180, 90)) self.assertEqual(vl.featureCount(), 3) self.assertEqual(vl.crs().authid(), "EPSG:4326") self.assertIn("Entity TypeLocation", vl.htmlMetadata()) @@ -601,6 +664,7 @@ def test_location(self): ], ) + # 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"]) @@ -625,7 +689,217 @@ def test_location(self): ["Point (11.6 52.1)", "Point (12.6 53.1)", "Point (13.6 55.1)"], ) + # all features fetched, accurate extent should be returned + self.assertEqual(vl.extent(), QgsRectangle(11.6, 52.1, 13.6, 55.1)) + + def test_location_formalism(self): + """ + Test https://github.com/qgis/QGIS/issues/56732 + """ + 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": "Locations", + "url": "endpoint/Locations" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 11.6, + 52.1 + ] + } + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/Locations(2)", + "@iot.id": 2, + "name": "Location 2", + "description": "Desc 2", + "encodingType": "application/geo+json", + "location": { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 12.6, + 53.1 + ] + } + }, + "properties": { + "owner": "owner 2" + }, + "Things@iot.navigationLink": "endpoint/Locations(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/Locations(3)", + "@iot.id": 3, + "name": "Location 3", + "description": "Desc 3", + "encodingType": "application/geo+json", + "location": { + "type": "feature", + "geometry": { + "type": "Point", + "coordinates": [ + 13.6, + 55.1 + ] + } + }, + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/Locations(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' type=PointZ pageSize=2 entity='Location'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + # pessimistic "worst case" extent should initially be used + self.assertEqual(vl.extent(), QgsRectangle(-180, -90, 180, 90)) + self.assertEqual(vl.featureCount(), 3) + self.assertEqual(vl.crs().authid(), "EPSG:4326") + self.assertIn("Entity TypeLocation", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Locations"', + vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "properties", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, + ], + ) + + 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"][-13:] for f in features], + ["/Locations(1)", "/Locations(2)", "/Locations(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1", "Location 2", "Location 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (11.6 52.1)", "Point (12.6 53.1)", + "Point (13.6 55.1)"], + ) + + # all features fetched, accurate extent should be returned + self.assertEqual(vl.extent(), + QgsRectangle(11.6, 52.1, 13.6, 55.1)) + def test_filter_rect(self): + """ + Test retrieving features using feature requests with filter + rectangles set + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -647,14 +921,14 @@ def test_filter_rect(self): ) with open( - sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -702,7 +976,7 @@ def test_filter_rect(self): } ], - "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'" + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -710,7 +984,7 @@ def test_filter_rect(self): ) with open( - sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point'"), + sanitize(endpoint, "/Locations?$top=2&$skip=2&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -745,7 +1019,8 @@ def test_filter_rect(self): ) with open( - sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))') and location/type eq 'Point'"), + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))'))"), "wt", encoding="utf8", ) as f: @@ -798,7 +1073,8 @@ def test_filter_rect(self): ) with open( - sanitize(endpoint, "/Locations?$top=2&$count=false&$filter=geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))') and location/type eq 'Point'"), + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((10 0, 20 0, 20 80, 10 80, 10 0))'))"), "wt", encoding="utf8", ) as f: @@ -838,6 +1114,7 @@ def test_filter_rect(self): "sensorthings", ) self.assertTrue(vl.isValid()) + # basic layer properties tests self.assertEqual(vl.storageType(), "OGC SensorThings API") self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) self.assertEqual(vl.featureCount(), 3) @@ -847,6 +1124,7 @@ def test_filter_rect(self): self.assertIn(f'href="http://{endpoint}/Locations"', vl.htmlMetadata()) + # test retrieving subset of features from a filter rect only request = QgsFeatureRequest() request.setFilterRect( QgsRectangle(1, 0, 10, 80) @@ -878,6 +1156,7 @@ def test_filter_rect(self): "Point (3.6 55.1)"], ) + # test retrieving a different subset with a different extent request = QgsFeatureRequest() request.setFilterRect( QgsRectangle(10, 0, 20, 80) @@ -907,6 +1186,7 @@ def test_filter_rect(self): ["Point (12.6 53.1)"], ) + # a filter rect which covers all features request = QgsFeatureRequest() request.setFilterRect( QgsRectangle(0, 0, 20, 80) @@ -919,7 +1199,10 @@ def test_filter_rect(self): ["/Locations(1)", "/Locations(3)", "/Locations(2)"], ) - def test_historical_location(self): + def test_extent_limit(self): + """ + Test a layer with a hardcoded extent limit set at the provider level + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -929,8 +1212,8 @@ def test_historical_location(self): { "value": [ { - "name": "HistoricalLocations", - "url": "endpoint/HistoricalLocations" + "name": "Locations", + "url": "endpoint/Locations" } ], "serverSettings": { @@ -941,14 +1224,16 @@ def test_historical_location(self): ) with open( - sanitize(endpoint, "/HistoricalLocations?$top=0&$count=true"), + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))'))"), "wt", encoding="utf8", ) as f: - f.write("""{"@iot.count":3,"value":[]}""") + f.write("""{"@iot.count":2,"value":[]}""") with open( - sanitize(endpoint, "/HistoricalLocations?$top=2&$count=false"), + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 10 0, 10 80, 1 80, 1 0))'))"), "wt", encoding="utf8", ) as f: @@ -957,43 +1242,1088 @@ def test_historical_location(self): { "value": [ { - "@iot.selfLink": "endpoint/HistoricalLocations(1)", + "@iot.selfLink": "endpoint/Locations(1)", "@iot.id": 1, - "time": "2020-03-20T16:35:23.383586Z", - "Things@iot.navigationLink": "endpoint/HistoricalLocations(1)/Things", - "Locations@iot.navigationLink": "endpoint/HistoricalLocations(1)/Locations" + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" }, { - "@iot.selfLink": "endpoint/HistoricalLocations(2)", - "@iot.id": 2, - "time": "2021-03-20T16:35:23.383586Z", - "Things@iot.navigationLink": "endpoint/HistoricalLocations(2)/Things", - "Locations@iot.navigationLink": "endpoint/HistoricalLocations(2)/Locations" - - } - ], - "@iot.nextLink": "endpoint/HistoricalLocations?$top=2&$skip=2" + "@iot.selfLink": "endpoint/Locations(3)", + "@iot.id": 3, + "name": "Location 3", + "description": "Desc 3", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 3.623373, + 55.132017 + ] + }, + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/Locations(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(3)/HistoricalLocations" + } + ] } """.replace( "endpoint", "http://" + endpoint ) ) - with open( - sanitize(endpoint, "/HistoricalLocations?$top=2&$skip=2"), - "wt", - encoding="utf8", + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 3 0, 3 50, 1 50, 1 0))'))"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + } + ] + }""".replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' bbox='1,0,10,80' type=PointZ pageSize=2 entity='Location'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + self.assertEqual(vl.featureCount(), 2) + # should use the hardcoded extent limit as the initial guess, not global extents + self.assertEqual(vl.extent(), QgsRectangle(1, 0, 10, 80)) + self.assertEqual(vl.crs().authid(), "EPSG:4326") + self.assertIn("Entity TypeLocation", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Locations"', + vl.htmlMetadata()) + + # test retrieving a subset of the features from the layer, + # using a filter rect which only covers a part of the hardcoded + # provider's extent + request = QgsFeatureRequest() + request.setFilterRect( + QgsRectangle(1, 0, 3, 50) + ) + + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ["1"]) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (1.6 52.1)"], + ) + + # test retrieving all features from layer -- the hardcoded + # provider level extent filter should still apply + request = QgsFeatureRequest() + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ["1", "3"]) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)", "/Locations(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1", "Location 3"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1", "Desc 3"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, + {"owner": "owner 3"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (1.6 52.1)", + "Point (3.6 55.1)"], + ) + + # should have accurate layer extent now + self.assertEqual(vl.extent(), QgsRectangle(1.62337299999999995, 52.13201699999999761, 3.62337299999999995, + 55.13201699999999761)) + + def test_subset_string(self): + """ + Test a layer with a hardcoded user-defined filter string + at the provider level + """ + 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": "Locations", + "url": "endpoint/Locations" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":2,"value":[]}""") + + with open( + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (name eq 'Location 1')"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":1,"value":[]}""") + + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 3 0, 3 50, 1 50, 1 0))')) and (name eq 'Location 1')"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + } + ] +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 3 0, 3 50, 1 50, 1 0))'))"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + } + ] + }""".replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' type=PointZ pageSize=2 entity='Location'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + self.assertEqual(vl.featureCount(), 2) + + vl.setSubsetString("name eq 'Location 1'") + self.assertEqual(vl.subsetString(), "name eq 'Location 1'") + self.assertEqual(vl.source(), f" type=PointZ entity='Location' pageSize='2' url='http://{endpoint}' sql=name eq 'Location 1'") + self.assertEqual(vl.featureCount(), 1) + + self.assertEqual(vl.crs().authid(), "EPSG:4326") + self.assertIn("Entity TypeLocation", + vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Locations"', + vl.htmlMetadata()) + + # test retrieving a subset of features, using a request which + # must be combined with the layer's subset filter + request = QgsFeatureRequest() + request.setFilterRect( + QgsRectangle(1, 0, 3, 50) + ) + + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ["1"]) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (1.6 52.1)"], + ) + + # test retrieving all features from layer, only a subset + # which matches the layer's subset string should still be + # returned + request = QgsFeatureRequest() + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ["1"]) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (1.6 52.1)"], + ) + + # should have accurate layer extent now + self.assertEqual(vl.extent(), QgsRectangle(1.62337299999999995, + 52.13201699999999761, + 1.62337299999999995, + 52.13201699999999761)) + + def test_feature_limit(self): + """ + Test a layer with a hardcoded maximum number of features to retrieve + from the service + """ + 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": "Locations", + "url": "endpoint/Locations" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=location/type eq 'Point' or location/geometry/type eq 'Point'"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, + "/Locations?$top=0&$count=true&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (name eq 'Location 1')"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":1,"value":[]}""") + + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((1 0, 3 0, 3 50, 1 50, 1 0))'))"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + } + ] + }""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, + "/Locations?$top=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((0 0, 100 0, 100 150, 0 150, 0 0))'))"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Locations(1)", + "@iot.id": 1, + "name": "Location 1", + "description": "Desc 1", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 1.623373, + 52.132017 + ] + }, + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Locations(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/Locations(2)", + "@iot.id": 2, + "name": "Location 2", + "description": "Desc 2", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 81, + 52 + ] + }, + "properties": { + "owner": "owner 2" + }, + "Things@iot.navigationLink": "endpoint/Locations(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(2)/HistoricalLocations" + } + ], + "@iot.nextLink": "endpoint/Locations?$top=2&$skip=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((0 0, 100 0, 100 150, 0 150, 0 0))'))" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + # Note -- top param here should be replaced by "top=1", NOT be the "top=2" parameter from the previous page's iot.nextLink url! + with open( + sanitize(endpoint, + "/Locations?$top=1&$skip=2&$count=false&$filter=(location/type eq 'Point' or location/geometry/type eq 'Point') and (geo.intersects(location, geography'POLYGON((0 0, 100 0, 100 150, 0 150, 0 0))'))"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Locations(3)", + "@iot.id": 3, + "name": "Location 3", + "description": "Desc 3", + "encodingType": "application/geo+json", + "location": { + "type": "Point", + "coordinates": [ + 82, + 53 + ] + }, + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/Locations(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Locations(3)/HistoricalLocations" + } + ], + "@iot.nextLink": "endpoint/Locations?$top=1&$skip=3" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' type=PointZ pageSize=2 featureLimit=3 entity='Location'", + "test", + "sensorthings", + ) + self.assertTrue(vl.isValid()) + # basic layer properties tests + self.assertEqual(vl.storageType(), "OGC SensorThings API") + self.assertEqual(vl.wkbType(), Qgis.WkbType.PointZ) + self.assertEqual(vl.featureCount(), 3) + + # test retrieving a subset of the 3 features by using + # a request with a filter rect only matching one of the features + request = QgsFeatureRequest() + request.setFilterRect( + QgsRectangle(1, 0, 3, 50) + ) + + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ["1"]) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Location 1"], + ) + self.assertEqual( + [f["description"] for f in features], + ["Desc 1"] + ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}], + ) + + self.assertEqual( + [f.geometry().asWkt(1) for f in features], + ["Point (1.6 52.1)"], + ) + + # test retrieving all features from layer using a filter rect + # which matches all features -- this is actually testing that + # the provider is correctly constructing a url with the right + # skip/limit values (if it isn't, then we'll get no features + # back since the dummy endpoint address used above won't match) + request = QgsFeatureRequest() + request.setFilterRect( + QgsRectangle(0, 0, 100, 150) + ) + features = list(vl.getFeatures(request)) + self.assertEqual([f["id"] for f in features], ['1', '2', '3']) + self.assertEqual( + [f["selfLink"][-13:] for f in features], + ["/Locations(1)", "/Locations(2)", "/Locations(3)"], + ) + + # should have accurate layer extent now + self.assertEqual(vl.extent(), QgsRectangle(1.62337299999999995, + 52, + 82, + 53)) + + def test_historical_location(self): + """ + Test a layer retrieving 'Historical Location' entities from a service + """ + 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": "HistoricalLocations", + "url": "endpoint/HistoricalLocations" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/HistoricalLocations?$top=0&$count=true"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/HistoricalLocations?$top=2&$count=false"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/HistoricalLocations(1)", + "@iot.id": 1, + "time": "2020-03-20T16:35:23.383586Z", + "Things@iot.navigationLink": "endpoint/HistoricalLocations(1)/Things", + "Locations@iot.navigationLink": "endpoint/HistoricalLocations(1)/Locations" + }, + { + "@iot.selfLink": "endpoint/HistoricalLocations(2)", + "@iot.id": 2, + "time": "2021-03-20T16:35:23.383586Z", + "Things@iot.navigationLink": "endpoint/HistoricalLocations(2)/Things", + "Locations@iot.navigationLink": "endpoint/HistoricalLocations(2)/Locations" + + } + ], + "@iot.nextLink": "endpoint/HistoricalLocations?$top=2&$skip=2" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/HistoricalLocations?$top=2&$skip=2"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/HistoricalLocations(3)", + "@iot.id": 3, + "time": "2022-03-20T16:35:23.383586Z", + "Things@iot.navigationLink": "endpoint/HistoricalLocations(3)/Things", + "Locations@iot.navigationLink": "endpoint/HistoricalLocations(3)/Locations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' type=PointZ pageSize=2 entity='HistoricalLocation'", + "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 TypeHistoricalLocation", vl.htmlMetadata() + ) + self.assertIn( + f'href="http://{endpoint}/HistoricalLocations"', vl.htmlMetadata() + ) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "time", + ], + ) + self.assertEqual( + [f.type() for f in vl.fields()], + [ + QVariant.String, + QVariant.String, + 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"][-23:] for f in features], + [ + "/HistoricalLocations(1)", + "/HistoricalLocations(2)", + "/HistoricalLocations(3)", + ], + ) + self.assertEqual( + [f["time"] for f in features], + [ + QDateTime(QDate(2020, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), + QDateTime(QDate(2021, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), + QDateTime(QDate(2022, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), + ], + ) + + def test_datastream(self): + """ + Test a layer retrieving 'Datastream' entities from a service + """ + 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": "Datastreams", + "url": "endpoint/Datastreams" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/Datastreams?$top=0&$count=true"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/Datastreams?$top=2&$count=false"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Datastreams(1)", + "@iot.id": 1, + "name": "Datastream 1", + "description": "Desc 1", + "unitOfMeasurement": { + "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", + "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/Datastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/Datastreams(2)", + "@iot.id": 2, + "name": "Datastream 2", + "description": "Desc 2", + "unitOfMeasurement": { + "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", + "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/Datastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/Datastreams?$top=2&$skip=2" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/Datastreams?$top=2&$skip=2"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ + { + "value": [ + { + "@iot.selfLink": "endpoint/Datastreams(3)", + "@iot.id": 3, + "name": "Datastream 3", + "description": "Desc 3", + "unitOfMeasurement": { + "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", + "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/Datastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" + } + ] + } + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + vl = QgsVectorLayer( + f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Datastream'", + "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 TypeDatastream", vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Datastreams"', vl.htmlMetadata()) + + self.assertEqual( + [f.name() for f in vl.fields()], + [ + "id", + "selfLink", + "name", + "description", + "unitOfMeasurement", + "observationType", + "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.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"][-15:] for f in features], + ["/Datastreams(1)", "/Datastreams(2)", "/Datastreams(3)"], + ) + self.assertEqual( + [f["name"] for f in features], + ["Datastream 1", "Datastream 2", "Datastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["unitOfMeasurement"] 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["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_sensor(self): + """ + Test a layer retrieving 'Sensor' entities from a service + """ + 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": "Sensors", + "url": "endpoint/Sensors" + } + ], + "serverSettings": { + } +}""".replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/Sensors?$top=0&$count=true"), + "wt", + encoding="utf8", + ) as f: + f.write("""{"@iot.count":3,"value":[]}""") + + with open( + sanitize(endpoint, "/Sensors?$top=2&$count=false"), + "wt", + encoding="utf8", + ) as f: + f.write( + """ +{ + "value": [ + { + "@iot.selfLink": "endpoint/Sensors(1)", + "@iot.id": 1, + "name": "Datastream 1", + "description": "Desc 1", + "encodingType": "application/pdf", + "metadata": "http://www.a.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + "properties": { + "owner": "owner 1" + }, + "Things@iot.navigationLink": "endpoint/Datastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" + }, + { + "@iot.selfLink": "endpoint/Sensors(2)", + "@iot.id": 2, + "name": "Datastream 2", + "description": "Desc 2", + "encodingType": "application/pdf", + "metadata": "http://www.b.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + "properties": { + "owner": "owner 2" + }, + "Things@iot.navigationLink": "endpoint/Datastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" + + } + ], + "@iot.nextLink": "endpoint/Sensors?$top=2&$skip=2" +} + """.replace( + "endpoint", "http://" + endpoint + ) + ) + + with open( + sanitize(endpoint, "/Sensors?$top=2&$skip=2"), + "wt", + encoding="utf8", ) as f: f.write( """ { "value": [ { - "@iot.selfLink": "endpoint/HistoricalLocations(3)", + "@iot.selfLink": "endpoint/Sensors(3)", "@iot.id": 3, - "time": "2022-03-20T16:35:23.383586Z", - "Things@iot.navigationLink": "endpoint/HistoricalLocations(3)/Things", - "Locations@iot.navigationLink": "endpoint/HistoricalLocations(3)/Locations" + "name": "Datastream 3", + "description": "Desc 3", + "encodingType": "application/pdf", + "metadata": "http://www.c.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + "properties": { + "owner": "owner 3" + }, + "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" } ] } @@ -1003,28 +2333,28 @@ def test_historical_location(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' type=PointZ pageSize=2 entity='HistoricalLocation'", + f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Sensor'", "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 TypeHistoricalLocation", vl.htmlMetadata() - ) - self.assertIn( - f'href="http://{endpoint}/HistoricalLocations"', vl.htmlMetadata() - ) + self.assertIn("Entity TypeSensor", vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Sensors"', vl.htmlMetadata()) self.assertEqual( [f.name() for f in vl.fields()], [ "id", "selfLink", - "time", + "name", + "description", + "metadata", + "properties", ], ) self.assertEqual( @@ -1032,31 +2362,45 @@ def test_historical_location(self): [ QVariant.String, QVariant.String, - QVariant.DateTime, + QVariant.String, + QVariant.String, + QVariant.String, + QVariant.Map, ], ) + # 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"][-23:] for f in features], - [ - "/HistoricalLocations(1)", - "/HistoricalLocations(2)", - "/HistoricalLocations(3)", - ], + [f["selfLink"][-11:] for f in features], + ["/Sensors(1)", "/Sensors(2)", "/Sensors(3)"], ) self.assertEqual( - [f["time"] for f in features], + [f["name"] for f in features], + ["Datastream 1", "Datastream 2", "Datastream 3"], + ) + self.assertEqual( + [f["description"] for f in features], ["Desc 1", "Desc 2", "Desc 3"] + ) + self.assertEqual( + [f["metadata"] for f in features], [ - QDateTime(QDate(2020, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), - QDateTime(QDate(2021, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), - QDateTime(QDate(2022, 3, 20), QTime(16, 35, 23, 384), Qt.TimeSpec(1)), + "http://www.a.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + "http://www.b.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + "http://www.c.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", ], ) + self.assertEqual( + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, {"owner": "owner 3"}], + ) - def test_datastream(self): + def test_observed_property(self): + """ + Test a layer retrieving 'Observed Property' entities from a service + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -1066,8 +2410,8 @@ def test_datastream(self): { "value": [ { - "name": "Datastreams", - "url": "endpoint/Datastreams" + "name": "ObservedProperties", + "url": "endpoint/ObservedProperties" } ], "serverSettings": { @@ -1078,14 +2422,14 @@ def test_datastream(self): ) with open( - sanitize(endpoint, "/Datastreams?$top=0&$count=true"), + sanitize(endpoint, "/ObservedProperties?$top=0&$count=true"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Datastreams?$top=2&$count=false"), + sanitize(endpoint, "/ObservedProperties?$top=2&$count=false"), "wt", encoding="utf8", ) as f: @@ -1094,18 +2438,11 @@ def test_datastream(self): { "value": [ { - "@iot.selfLink": "endpoint/Datastreams(1)", + "@iot.selfLink": "endpoint/ObservedProperties(1)", "@iot.id": 1, "name": "Datastream 1", "description": "Desc 1", - "unitOfMeasurement": { - "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", - "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-12T04:00:00Z", - "resultTime": "2017-12-31T23:30:00Z/2017-12-31T23:31:00Z", + "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/1", "properties": { "owner": "owner 1" }, @@ -1113,18 +2450,11 @@ def test_datastream(self): "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" }, { - "@iot.selfLink": "endpoint/Datastreams(2)", + "@iot.selfLink": "endpoint/ObservedProperties(2)", "@iot.id": 2, "name": "Datastream 2", + "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/2", "description": "Desc 2", - "unitOfMeasurement": { - "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", - "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" }, @@ -1133,7 +2463,7 @@ def test_datastream(self): } ], - "@iot.nextLink": "endpoint/Datastreams?$top=2&$skip=2" + "@iot.nextLink": "endpoint/ObservedProperties?$top=2&$skip=2" } """.replace( "endpoint", "http://" + endpoint @@ -1141,7 +2471,7 @@ def test_datastream(self): ) with open( - sanitize(endpoint, "/Datastreams?$top=2&$skip=2"), + sanitize(endpoint, "/ObservedProperties?$top=2&$skip=2"), "wt", encoding="utf8", ) as f: @@ -1150,18 +2480,11 @@ def test_datastream(self): { "value": [ { - "@iot.selfLink": "endpoint/Datastreams(3)", + "@iot.selfLink": "endpoint/ObservedProperties(3)", "@iot.id": 3, "name": "Datastream 3", "description": "Desc 3", - "unitOfMeasurement": { - "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", - "phenomenonTime": "2020-12-31T23:00:00Z/2021-01-12T04:00:00Z", - "resultTime": "2020-12-31T23:30:00Z/2020-12-31T23:31:00Z", + "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/3", "properties": { "owner": "owner 3" }, @@ -1176,17 +2499,22 @@ def test_datastream(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Datastream'", + f"url='http://{endpoint}' type=PointZ pageSize=2 entity='ObservedProperty'", "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 TypeDatastream", vl.htmlMetadata()) - self.assertIn(f'href="http://{endpoint}/Datastreams"', vl.htmlMetadata()) + self.assertIn( + "Entity TypeObservedProperty", vl.htmlMetadata() + ) + self.assertIn( + f'href="http://{endpoint}/ObservedProperties"', vl.htmlMetadata() + ) self.assertEqual( [f.name() for f in vl.fields()], @@ -1194,14 +2522,9 @@ def test_datastream(self): "id", "selfLink", "name", + "definition", "description", - "unitOfMeasurement", - "observationType", "properties", - "phenomenonTimeStart", - "phenomenonTimeEnd", - "resultTimeStart", - "resultTimeEnd", ], ) self.assertEqual( @@ -1211,22 +2534,22 @@ def test_datastream(self): QVariant.String, QVariant.String, QVariant.String, - QVariant.Map, QVariant.String, 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"][-15:] for f in features], - ["/Datastreams(1)", "/Datastreams(2)", "/Datastreams(3)"], + [f["selfLink"][-22:] for f in features], + [ + "/ObservedProperties(1)", + "/ObservedProperties(2)", + "/ObservedProperties(3)", + ], ) self.assertEqual( [f["name"] for f in features], @@ -1236,63 +2559,11 @@ def test_datastream(self): [f["description"] for f in features], ["Desc 1", "Desc 2", "Desc 3"] ) self.assertEqual( - [f["unitOfMeasurement"] 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["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], + [f["definition"] 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)), + "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/1", + "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/2", + "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/3", ], ) self.assertEqual( @@ -1300,7 +2571,10 @@ def test_datastream(self): [{"owner": "owner 1"}, {"owner": "owner 2"}, {"owner": "owner 3"}], ) - def test_sensor(self): + def test_observation(self): + """ + Test a layer retrieving 'Observation' entities from a service + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -1310,8 +2584,8 @@ def test_sensor(self): { "value": [ { - "name": "Sensors", - "url": "endpoint/Sensors" + "name": "Observations", + "url": "endpoint/Observations" } ], "serverSettings": { @@ -1322,14 +2596,14 @@ def test_sensor(self): ) with open( - sanitize(endpoint, "/Sensors?$top=0&$count=true"), + sanitize(endpoint, "/Observations?$top=0&$count=true"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Sensors?$top=2&$count=false"), + sanitize(endpoint, "/Observations?$top=2&$count=false"), "wt", encoding="utf8", ) as f: @@ -1338,34 +2612,38 @@ def test_sensor(self): { "value": [ { - "@iot.selfLink": "endpoint/Sensors(1)", + "@iot.selfLink": "endpoint/Observations(1)", "@iot.id": 1, - "name": "Datastream 1", - "description": "Desc 1", - "encodingType": "application/pdf", - "metadata": "http://www.a.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", - "properties": { - "owner": "owner 1" + "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-01T00:00:00Z", + "result": 12.5962142944, + "resultTime": "2017-12-31T23:00:30Z", + "resultQuality": "good", + "validTime": "2017-12-31T23:00:00Z/2018-12-31T00:00:00Z", + "parameters":{ + "a":1, + "b":2 }, "Things@iot.navigationLink": "endpoint/Datastreams(1)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" }, { - "@iot.selfLink": "endpoint/Sensors(2)", + "@iot.selfLink": "endpoint/Observations(2)", "@iot.id": 2, - "name": "Datastream 2", - "description": "Desc 2", - "encodingType": "application/pdf", - "metadata": "http://www.b.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", - "properties": { - "owner": "owner 2" + "phenomenonTime": "2018-01-01T00:00:00Z/2018-01-01T01:00:00Z", + "result": 7.7946872711, + "resultTime": "2018-01-01T00:30:00Z", + "validTime": "2018-12-31T23:00:00Z/2019-12-31T00:00:00Z", + "resultQuality": ["good", "fair"], + "parameters":{ + "a":3, + "b":4 }, "Things@iot.navigationLink": "endpoint/Datastreams(2)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" } ], - "@iot.nextLink": "endpoint/Sensors?$top=2&$skip=2" + "@iot.nextLink": "endpoint/Observations?$top=2&$skip=2" } """.replace( "endpoint", "http://" + endpoint @@ -1373,7 +2651,7 @@ def test_sensor(self): ) with open( - sanitize(endpoint, "/Sensors?$top=2&$skip=2"), + sanitize(endpoint, "/Observations?$top=2&$skip=2"), "wt", encoding="utf8", ) as f: @@ -1382,15 +2660,12 @@ def test_sensor(self): { "value": [ { - "@iot.selfLink": "endpoint/Sensors(3)", + "@iot.selfLink": "endpoint/Observations(3)", "@iot.id": 3, - "name": "Datastream 3", - "description": "Desc 3", - "encodingType": "application/pdf", - "metadata": "http://www.c.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", - "properties": { - "owner": "owner 3" - }, + "phenomenonTime": "2018-01-01T02:00:00Z/2018-01-01T02:30:00Z", + "result": 4.1779522896, + "resultTime": "2018-01-01T02:30:00Z", + "validTime": "2019-12-31T23:00:00Z/2020-12-31T00:00:00Z", "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" } @@ -1402,27 +2677,32 @@ def test_sensor(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Sensor'", + f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Observation'", "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 TypeSensor", vl.htmlMetadata()) - self.assertIn(f'href="http://{endpoint}/Sensors"', vl.htmlMetadata()) + self.assertIn("Entity TypeObservation", vl.htmlMetadata()) + self.assertIn(f'href="http://{endpoint}/Observations"', vl.htmlMetadata()) self.assertEqual( [f.name() for f in vl.fields()], [ "id", "selfLink", - "name", - "description", - "metadata", - "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "result", + "resultTime", + "resultQuality", + "validTimeStart", + "validTimeEnd", + "parameters", ], ) self.assertEqual( @@ -1430,41 +2710,83 @@ def test_sensor(self): [ QVariant.String, QVariant.String, + QVariant.DateTime, + QVariant.DateTime, QVariant.String, - QVariant.String, - QVariant.String, + QVariant.DateTime, + QVariant.StringList, + QVariant.DateTime, + QVariant.DateTime, QVariant.Map, ], ) + # 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"][-11:] for f in features], - ["/Sensors(1)", "/Sensors(2)", "/Sensors(3)"], + [f["selfLink"][-16:] for f in features], + ["/Observations(1)", "/Observations(2)", "/Observations(3)"], ) self.assertEqual( - [f["name"] for f in features], - ["Datastream 1", "Datastream 2", "Datastream 3"], + [f["phenomenonTimeStart"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(2, 0, 0, 0), Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["phenomenonTimeEnd"] for f in features], + [ + QDateTime(QDate(2018, 1, 1), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(1, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(2, 30, 0, 0), Qt.TimeSpec(1)), + ], + ) + # TODO -- these should be doubles + self.assertEqual( + [f["result"] for f in features], + ["12.5962", "7.79469", "4.17795"], + ) + self.assertEqual( + [f["resultTime"] for f in features], + [ + QDateTime(QDate(2017, 12, 31), QTime(23, 0, 30, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(0, 30, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2018, 1, 1), QTime(2, 30, 0, 0), Qt.TimeSpec(1)), + ], + ) + self.assertEqual( + [f["resultQuality"] for f in features], + [["good"], ["good", "fair"], None], ) self.assertEqual( - [f["description"] for f in features], ["Desc 1", "Desc 2", "Desc 3"] + [f["validTimeStart"] 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(2019, 12, 31), QTime(23, 0, 0, 0), Qt.TimeSpec(1)), + ], ) self.assertEqual( - [f["metadata"] for f in features], + [f["validTimeEnd"] for f in features], [ - "http://www.a.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", - "http://www.b.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", - "http://www.c.at/fileadmin/site/umweltthemen/luft/PM_Aequivalenz_Dokumentation.pdf", + QDateTime(QDate(2018, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2019, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), + QDateTime(QDate(2020, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), ], ) self.assertEqual( - [f["properties"] for f in features], - [{"owner": "owner 1"}, {"owner": "owner 2"}, {"owner": "owner 3"}], + [f["parameters"] for f in features], + [{"a": 1, "b": 2}, {"a": 3, "b": 4}, None], ) - def test_observed_property(self): + def test_feature_of_interest(self): + """ + Test a layer retrieving 'Features of Interest' entities from a service + """ with tempfile.TemporaryDirectory() as temp_dir: base_path = temp_dir.replace("\\", "/") endpoint = base_path + "/fake_qgis_http_endpoint" @@ -1474,8 +2796,8 @@ def test_observed_property(self): { "value": [ { - "name": "ObservedProperties", - "url": "endpoint/ObservedProperties" + "name": "FeaturesOfInterest", + "url": "endpoint/FeaturesOfInterest" } ], "serverSettings": { @@ -1486,14 +2808,14 @@ def test_observed_property(self): ) with open( - sanitize(endpoint, "/ObservedProperties?$top=0&$count=true"), + sanitize(endpoint, "/FeaturesOfInterest?$top=0&$count=true&$filter=feature/type eq 'Point' or feature/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/ObservedProperties?$top=2&$count=false"), + sanitize(endpoint, "/FeaturesOfInterest?$top=2&$count=false&$filter=feature/type eq 'Point' or feature/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -1502,32 +2824,51 @@ def test_observed_property(self): { "value": [ { - "@iot.selfLink": "endpoint/ObservedProperties(1)", + "@iot.selfLink": "endpoint/FeaturesOfInterest(1)", "@iot.id": 1, - "name": "Datastream 1", - "description": "Desc 1", - "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/1", + "description": "Air quality sample SAM.09.LAA.822.7.1", + "encodingType": "application/geo+json", + "feature": { + "type": "Point", + "coordinates": [ + 16.3929202777778, + 48.1610363888889 + ] + }, + "name": "SAM.09.LAA.822.7.1", "properties": { - "owner": "owner 1" + "localId": "SAM.09.LAA.822.7.1", + "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", + "namespace": "AT.0008.20.AQ", + "owner": "http://luft.umweltbundesamt.at" }, "Things@iot.navigationLink": "endpoint/Datastreams(1)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" }, { - "@iot.selfLink": "endpoint/ObservedProperties(2)", + "@iot.selfLink": "endpoint/FeaturesOfInterest(2)", "@iot.id": 2, - "name": "Datastream 2", - "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/2", - "description": "Desc 2", + "encodingType": "application/geo+json", + "feature": { + "type": "Point", + "coordinates": [ + 16.5256138888889, + 48.1620694444444 + ] + }, + "name": "SAM.09.LOB.823.7.1", "properties": { - "owner": "owner 2" + "localId": "SAM.09.LOB.823.7.1", + "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", + "namespace": "AT.0008.20.AQ", + "owner": "http://luft.umweltbundesamt.at" }, "Things@iot.navigationLink": "endpoint/Datastreams(2)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" } ], - "@iot.nextLink": "endpoint/ObservedProperties?$top=2&$skip=2" + "@iot.nextLink": "endpoint/FeaturesOfInterest?$top=2&$skip=2&$filter=feature/type eq 'Point' or feature/geometry/type eq 'Point'" } """.replace( "endpoint", "http://" + endpoint @@ -1535,7 +2876,7 @@ def test_observed_property(self): ) with open( - sanitize(endpoint, "/ObservedProperties?$top=2&$skip=2"), + sanitize(endpoint, "/FeaturesOfInterest?$top=2&$skip=2&$filter=feature/type eq 'Point' or feature/geometry/type eq 'Point'"), "wt", encoding="utf8", ) as f: @@ -1544,15 +2885,25 @@ def test_observed_property(self): { "value": [ { - "@iot.selfLink": "endpoint/ObservedProperties(3)", + "@iot.selfLink": "endpoint/FeaturesOfInterest(3)", "@iot.id": 3, - "name": "Datastream 3", - "description": "Desc 3", - "definition": "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/3", - "properties": { - "owner": "owner 3" - }, - "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", +"description": "Air quality sample SAM.09.LOB.824.1.1", + "encodingType": "application/geo+json", + "feature": { + "type": "Point", + "coordinates": [ + 16.5256138888889, + 48.1620694444444 + ] + }, + "name": "SAM.09.LOB.824.1.1", + "properties": { + "localId": "SAM.09.LOB.824.1.1", + "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", + "namespace": "AT.0008.20.AQ", + "owner": "http://luft.umweltbundesamt.at" + }, + "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" } ] @@ -1563,20 +2914,21 @@ def test_observed_property(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' type=PointZ pageSize=2 entity='ObservedProperty'", + f"url='http://{endpoint}' pageSize=2 type=PointZ entity='FeatureOfInterest'", "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.wkbType(), Qgis.WkbType.PointZ) self.assertEqual(vl.featureCount(), 3) - self.assertFalse(vl.crs().isValid()) + self.assertEqual(vl.crs().authid(), "EPSG:4326") self.assertIn( - "Entity TypeObservedProperty", vl.htmlMetadata() + "Entity TypeFeatureOfInterest", vl.htmlMetadata() ) self.assertIn( - f'href="http://{endpoint}/ObservedProperties"', vl.htmlMetadata() + f'href="http://{endpoint}/FeaturesOfInterest"', vl.htmlMetadata() ) self.assertEqual( @@ -1585,7 +2937,6 @@ def test_observed_property(self): "id", "selfLink", "name", - "definition", "description", "properties", ], @@ -1597,43 +2948,48 @@ def test_observed_property(self): QVariant.String, QVariant.String, QVariant.String, - QVariant.String, QVariant.Map, ], ) + # 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"][-22:] for f in features], - [ - "/ObservedProperties(1)", - "/ObservedProperties(2)", - "/ObservedProperties(3)", - ], + ["/FeaturesOfInterest(1)", "/FeaturesOfInterest(2)", "/FeaturesOfInterest(3)"], ) self.assertEqual( [f["name"] for f in features], - ["Datastream 1", "Datastream 2", "Datastream 3"], + ['SAM.09.LAA.822.7.1', 'SAM.09.LOB.823.7.1', 'SAM.09.LOB.824.1.1'], ) self.assertEqual( - [f["description"] for f in features], ["Desc 1", "Desc 2", "Desc 3"] + [f["description"] for f in features], + ['Air quality sample SAM.09.LAA.822.7.1', None, 'Air quality sample SAM.09.LOB.824.1.1'] ) self.assertEqual( - [f["definition"] for f in features], - [ - "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/1", - "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/2", - "http://dd.eionet.europa.eu/vocabulary/aq/pollutant/3", - ], + [f["properties"] for f in features], + [{'localId': 'SAM.09.LAA.822.7.1', + 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', + 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}, + {'localId': 'SAM.09.LOB.823.7.1', + 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', + 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}, + {'localId': 'SAM.09.LOB.824.1.1', + 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', + 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}], ) + self.assertEqual( - [f["properties"] for f in features], - [{"owner": "owner 1"}, {"owner": "owner 2"}, {"owner": "owner 3"}], + [f.geometry().asWkt(1) for f in features], + ['Point (16.4 48.2)', 'Point (16.5 48.2)', 'Point (16.5 48.2)'], ) - def test_observation(self): + 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" @@ -1643,8 +2999,8 @@ def test_observation(self): { "value": [ { - "name": "Observations", - "url": "endpoint/Observations" + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" } ], "serverSettings": { @@ -1655,14 +3011,14 @@ def test_observation(self): ) with open( - sanitize(endpoint, "/Observations?$top=0&$count=true"), + sanitize(endpoint, "/MultiDatastreams?$top=0&$count=true"), "wt", encoding="utf8", ) as f: f.write("""{"@iot.count":3,"value":[]}""") with open( - sanitize(endpoint, "/Observations?$top=2&$count=false"), + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false"), "wt", encoding="utf8", ) as f: @@ -1671,38 +3027,51 @@ def test_observation(self): { "value": [ { - "@iot.selfLink": "endpoint/Observations(1)", + "@iot.selfLink": "endpoint/MultiDatastreams(1)", "@iot.id": 1, - "phenomenonTime": "2017-12-31T23:00:00Z/2018-01-01T00:00:00Z", - "result": 12.5962142944, - "resultTime": "2017-12-31T23:00:30Z", - "resultQuality": "good", - "validTime": "2017-12-31T23:00:00Z/2018-12-31T00:00:00Z", - "parameters":{ - "a":1, - "b":2 + "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/Datastreams(1)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" + "Things@iot.navigationLink": "endpoint/MultiDatastreams(1)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(1)/HistoricalLocations" }, { - "@iot.selfLink": "endpoint/Observations(2)", + "@iot.selfLink": "endpoint/MultiDatastreams(2)", "@iot.id": 2, - "phenomenonTime": "2018-01-01T00:00:00Z/2018-01-01T01:00:00Z", - "result": 7.7946872711, - "resultTime": "2018-01-01T00:30:00Z", - "validTime": "2018-12-31T23:00:00Z/2019-12-31T00:00:00Z", - "resultQuality": ["good", "fair"], - "parameters":{ - "a":3, - "b":4 + "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/Datastreams(2)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" + "Things@iot.navigationLink": "endpoint/MultiDatastreams(2)/Things", + "HistoricalLocations@iot.navigationLink": "endpoint/MultiDatastreams(2)/HistoricalLocations" } ], - "@iot.nextLink": "endpoint/Observations?$top=2&$skip=2" + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2" } """.replace( "endpoint", "http://" + endpoint @@ -1710,7 +3079,7 @@ def test_observation(self): ) with open( - sanitize(endpoint, "/Observations?$top=2&$skip=2"), + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2"), "wt", encoding="utf8", ) as f: @@ -1719,14 +3088,24 @@ def test_observation(self): { "value": [ { - "@iot.selfLink": "endpoint/Observations(3)", + "@iot.selfLink": "endpoint/MultiDatastreams(3)", "@iot.id": 3, - "phenomenonTime": "2018-01-01T02:00:00Z/2018-01-01T02:30:00Z", - "result": 4.1779522896, - "resultTime": "2018-01-01T02:30:00Z", - "validTime": "2019-12-31T23:00:00Z/2020-12-31T00:00:00Z", - "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" + "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" } ] } @@ -1736,31 +3115,36 @@ def test_observation(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=PointZ entity='Observation'", + 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 TypeObservation", vl.htmlMetadata()) - self.assertIn(f'href="http://{endpoint}/Observations"', vl.htmlMetadata()) + 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", - "result", - "resultTime", - "resultQuality", - "validTimeStart", - "validTimeEnd", - "parameters", + "resultTimeStart", + "resultTimeEnd", ], ) self.assertEqual( @@ -1768,79 +3152,125 @@ def test_observation(self): [ QVariant.String, QVariant.String, - QVariant.DateTime, - QVariant.DateTime, QVariant.String, - QVariant.DateTime, + QVariant.String, + QVariant.Map, + QVariant.String, QVariant.StringList, + QVariant.Map, + QVariant.DateTime, + QVariant.DateTime, QVariant.DateTime, QVariant.DateTime, - QVariant.Map, ], ) + # 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"][-16:] for f in features], - ["/Observations(1)", "/Observations(2)", "/Observations(3)"], + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], ) self.assertEqual( - [f["phenomenonTimeStart"] for f in features], + [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], [ - QDateTime(QDate(2017, 12, 31), QTime(23, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(2, 0, 0, 0), Qt.TimeSpec(1)), + [{ + "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["phenomenonTimeEnd"] for f in features], + [f["observationType"] for f in features], [ - QDateTime(QDate(2018, 1, 1), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(1, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(2, 30, 0, 0), Qt.TimeSpec(1)), + "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", ], ) - # TODO -- these should be doubles self.assertEqual( - [f["result"] for f in features], - ["12.5962", "7.79469", "4.17795"], + [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["resultTime"] for f in features], + [f["phenomenonTimeStart"] for f in features], [ - QDateTime(QDate(2017, 12, 31), QTime(23, 0, 30, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(0, 30, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2018, 1, 1), QTime(2, 30, 0, 0), Qt.TimeSpec(1)), + 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["resultQuality"] for f in features], - [["good"], ["good", "fair"], None], + [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["validTimeStart"] for f in features], + [f["resultTimeStart"] 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(2019, 12, 31), QTime(23, 0, 0, 0), Qt.TimeSpec(1)), + 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["validTimeEnd"] for f in features], + [f["resultTimeEnd"] for f in features], [ - QDateTime(QDate(2018, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2019, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), - QDateTime(QDate(2020, 12, 31), QTime(0, 0, 0, 0), Qt.TimeSpec(1)), + 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["parameters"] for f in features], - [{"a": 1, "b": 2}, {"a": 3, "b": 4}, None], + [f["properties"] for f in features], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], ) - def test_feature_of_interest(self): + 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" @@ -1850,8 +3280,8 @@ def test_feature_of_interest(self): { "value": [ { - "name": "FeaturesOfInterest", - "url": "endpoint/FeaturesOfInterest" + "name": "MultiDatastreams", + "url": "endpoint/MultiDatastreams" } ], "serverSettings": { @@ -1862,14 +3292,14 @@ def test_feature_of_interest(self): ) with open( - sanitize(endpoint, "/FeaturesOfInterest?$top=0&$count=true&$filter=feature/type eq 'Point'"), + 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, "/FeaturesOfInterest?$top=2&$count=false&$filter=feature/type eq 'Point'"), + sanitize(endpoint, "/MultiDatastreams?$top=2&$count=false&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -1878,51 +3308,67 @@ def test_feature_of_interest(self): { "value": [ { - "@iot.selfLink": "endpoint/FeaturesOfInterest(1)", + "@iot.selfLink": "endpoint/MultiDatastreams(1)", "@iot.id": 1, - "description": "Air quality sample SAM.09.LAA.822.7.1", - "encodingType": "application/geo+json", - "feature": { - "type": "Point", - "coordinates": [ - 16.3929202777778, - 48.1610363888889 - ] - }, - "name": "SAM.09.LAA.822.7.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": { - "localId": "SAM.09.LAA.822.7.1", - "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", - "namespace": "AT.0008.20.AQ", - "owner": "http://luft.umweltbundesamt.at" + "owner": "owner 1" }, - "Things@iot.navigationLink": "endpoint/Datastreams(1)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(1)/HistoricalLocations" + "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/FeaturesOfInterest(2)", + "@iot.selfLink": "endpoint/MultiDatastreams(2)", "@iot.id": 2, - "encodingType": "application/geo+json", - "feature": { - "type": "Point", - "coordinates": [ - 16.5256138888889, - 48.1620694444444 - ] - }, - "name": "SAM.09.LOB.823.7.1", + "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": { - "localId": "SAM.09.LOB.823.7.1", - "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", - "namespace": "AT.0008.20.AQ", - "owner": "http://luft.umweltbundesamt.at" + "owner": "owner 2" }, - "Things@iot.navigationLink": "endpoint/Datastreams(2)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(2)/HistoricalLocations" + "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/FeaturesOfInterest?$top=2&$skip=2&$filter=feature/type eq 'Point'" + "@iot.nextLink": "endpoint/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'" } """.replace( "endpoint", "http://" + endpoint @@ -1930,7 +3376,7 @@ def test_feature_of_interest(self): ) with open( - sanitize(endpoint, "/FeaturesOfInterest?$top=2&$skip=2&$filter=feature/type eq 'Point'"), + sanitize(endpoint, "/MultiDatastreams?$top=2&$skip=2&$filter=observedArea/type eq 'Polygon' or observedArea/geometry/type eq 'Polygon'"), "wt", encoding="utf8", ) as f: @@ -1939,26 +3385,32 @@ def test_feature_of_interest(self): { "value": [ { - "@iot.selfLink": "endpoint/FeaturesOfInterest(3)", + "@iot.selfLink": "endpoint/MultiDatastreams(3)", "@iot.id": 3, -"description": "Air quality sample SAM.09.LOB.824.1.1", - "encodingType": "application/geo+json", - "feature": { - "type": "Point", - "coordinates": [ - 16.5256138888889, - 48.1620694444444 - ] - }, - "name": "SAM.09.LOB.824.1.1", - "properties": { - "localId": "SAM.09.LOB.824.1.1", - "metadata": "http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample", - "namespace": "AT.0008.20.AQ", - "owner": "http://luft.umweltbundesamt.at" - }, - "Things@iot.navigationLink": "endpoint/Datastreams(3)/Things", - "HistoricalLocations@iot.navigationLink": "endpoint/Datastreams(3)/HistoricalLocations" + "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" } ] } @@ -1968,21 +3420,20 @@ def test_feature_of_interest(self): ) vl = QgsVectorLayer( - f"url='http://{endpoint}' pageSize=2 type=PointZ entity='FeatureOfInterest'", + 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.PointZ) + self.assertEqual(vl.wkbType(), Qgis.WkbType.MultiPolygonZ) self.assertEqual(vl.featureCount(), 3) - self.assertEqual(vl.crs().authid(), "EPSG:4326") - self.assertIn( - "Entity TypeFeatureOfInterest", vl.htmlMetadata() - ) - self.assertIn( - f'href="http://{endpoint}/FeaturesOfInterest"', vl.htmlMetadata() - ) + 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()], @@ -1991,7 +3442,14 @@ def test_feature_of_interest(self): "selfLink", "name", "description", + "unitOfMeasurements", + "observationType", + "multiObservationDataTypes", "properties", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "resultTimeStart", + "resultTimeEnd", ], ) self.assertEqual( @@ -2002,32 +3460,122 @@ def test_feature_of_interest(self): 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"][-22:] for f in features], - ["/FeaturesOfInterest(1)", "/FeaturesOfInterest(2)", "/FeaturesOfInterest(3)"], + [f["selfLink"][-20:] for f in features], + ["/MultiDatastreams(1)", "/MultiDatastreams(2)", "/MultiDatastreams(3)"], ) self.assertEqual( [f["name"] for f in features], - ['SAM.09.LAA.822.7.1', 'SAM.09.LOB.823.7.1', 'SAM.09.LOB.824.1.1'], + ["MultiDatastream 1", "MultiDatastream 2", "MultiDatastream 3"], ) self.assertEqual( [f["description"] for f in features], - ['Air quality sample SAM.09.LAA.822.7.1', None, 'Air quality sample SAM.09.LOB.824.1.1'] + ["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], - [{'localId': 'SAM.09.LAA.822.7.1', 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}, {'localId': 'SAM.09.LOB.823.7.1', 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}, {'localId': 'SAM.09.LOB.824.1.1', 'metadata': 'http://luft.umweltbundesamt.at/inspire/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=aqd:AQD_Sample', 'namespace': 'AT.0008.20.AQ', 'owner': 'http://luft.umweltbundesamt.at'}], + [{"owner": "owner 1"}, {"owner": "owner 2"}, + {"owner": "owner 3"}], ) - self.assertEqual( - [f.geometry().asWkt(1) for f in features], - ['Point (16.4 48.2)', 'Point (16.5 48.2)', 'Point (16.5 48.2)'], + [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): @@ -2085,6 +3633,45 @@ def testDecodeUri(self): }, ) + uri = "url='https://sometest.com/api' bbox='1,2,3,4' type=MultiPolygonZ authcfg='abc' entity='Location'" + parts = QgsProviderRegistry.instance().decodeUri("sensorthings", uri) + self.assertEqual( + parts, + { + "url": "https://sometest.com/api", + "entity": "Location", + "geometryType": "polygon", + "authcfg": "abc", + "bounds": QgsRectangle(1, 2, 3, 4) + }, + ) + + uri = "url='https://sometest.com/api' type=MultiPolygonZ authcfg='abc' entity='Location' sql=name eq 'test'" + parts = QgsProviderRegistry.instance().decodeUri("sensorthings", uri) + self.assertEqual( + parts, + { + "url": "https://sometest.com/api", + "entity": "Location", + "geometryType": "polygon", + "authcfg": "abc", + "sql": "name eq 'test'" + }, + ) + + uri = "url='https://sometest.com/api' type=MultiPolygonZ authcfg='abc' featureLimit='50' entity='Location'" + parts = QgsProviderRegistry.instance().decodeUri("sensorthings", uri) + self.assertEqual( + parts, + { + "url": "https://sometest.com/api", + "entity": "Location", + "geometryType": "polygon", + "authcfg": "abc", + "featureLimit": 50 + }, + ) + def testEncodeUri(self): """ Test encoding a SensorThings uri @@ -2138,6 +3725,45 @@ def testEncodeUri(self): "authcfg=aaaaa type=MultiPolygonZ entity='Location' url='http://blah.com'", ) + parts = { + "url": "http://blah.com", + "authcfg": "aaaaa", + "entity": "location", + "geometryType": "polygon", + "bounds": QgsRectangle(1, 2, 3, 4) + } + uri = QgsProviderRegistry.instance().encodeUri("sensorthings", parts) + self.assertEqual( + uri, + "authcfg=aaaaa type=MultiPolygonZ bbox='1,2,3,4' entity='Location' url='http://blah.com'", + ) + + parts = { + "url": "http://blah.com", + "authcfg": "aaaaa", + "entity": "location", + "geometryType": "polygon", + "sql": "name eq 'test'" + } + uri = QgsProviderRegistry.instance().encodeUri("sensorthings", parts) + self.assertEqual( + uri, + "authcfg=aaaaa type=MultiPolygonZ entity='Location' url='http://blah.com' sql=name eq 'test'", + ) + + parts = { + "url": "http://blah.com", + "authcfg": "aaaaa", + "entity": "location", + "geometryType": "polygon", + "featureLimit": 50 + } + uri = QgsProviderRegistry.instance().encodeUri("sensorthings", parts) + self.assertEqual( + uri, + "authcfg=aaaaa type=MultiPolygonZ entity='Location' featureLimit='50' url='http://blah.com'", + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/src/python/test_provider_spatialite.py b/tests/src/python/test_provider_spatialite.py index c00e91dedebe..6a41635fb82b 100644 --- a/tests/src/python/test_provider_spatialite.py +++ b/tests/src/python/test_provider_spatialite.py @@ -1561,9 +1561,8 @@ def testSpatialiteDefaultValues(self): # Test default values dp = vl.dataProvider() - # FIXME: should it be None? - self.assertTrue(dp.defaultValue(0).isNull()) - self.assertIsNone(dp.defaultValue(1)) + self.assertEqual(dp.defaultValue(0), NULL) + self.assertEqual(dp.defaultValue(1), NULL) # FIXME: This fails because there is no backend-side evaluation in this provider # self.assertTrue(dp.defaultValue(2).startswith(now.strftime('%Y-%m-%d'))) self.assertTrue(dp.defaultValue( diff --git a/tests/src/python/test_python_repr.py b/tests/src/python/test_python_repr.py index f5cee3d3d908..ee7145eec3d5 100644 --- a/tests/src/python/test_python_repr.py +++ b/tests/src/python/test_python_repr.py @@ -176,6 +176,8 @@ def testQgsPolygonRepr(self): def testQgsRectangleRepr(self): r = QgsRectangle(1, 2, 3, 4) self.assertEqual(r.__repr__(), '') + r = QgsRectangle() + self.assertEqual(r.__repr__(), '') def testQgsReferencedRectangleRepr(self): r = QgsReferencedRectangle(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('EPSG:4326')) diff --git a/tests/src/python/test_qgsarcgisrestutils.py b/tests/src/python/test_qgsarcgisrestutils.py index 33a75a51874b..58254bd4b092 100644 --- a/tests/src/python/test_qgsarcgisrestutils.py +++ b/tests/src/python/test_qgsarcgisrestutils.py @@ -320,7 +320,7 @@ def test_feature_to_json(self): # with special characters - attributes[0] = 'aaa" , . - ; : ä ö ü è é à ? + &' + attributes[0] = 'aaa" \' , . - ; : ä ö ü è é à ? + & \\ /' test_feature.setAttributes(attributes) res = QgsArcGisRestUtils.featureToJson(test_feature, context, flags=QgsArcGisRestUtils.FeatureToJsonFlags(QgsArcGisRestUtils.FeatureToJsonFlag.IncludeNonObjectIdAttributes)) self.assertEqual(res, {'attributes': {'a_boolean_field': True, @@ -328,7 +328,7 @@ def test_feature_to_json(self): 'a_date_field': 1646352000000, 'a_double_field': 5.5, 'a_int_field': 5, - 'a_string_field': 'aaa%22%20%2C%20.%20-%20%3B%20%3A%20%C3%A4%20%C3%B6%20%C3%BC%20%C3%A8%20%C3%A9%20%C3%A0%20%3F%20%2B%20%26', + 'a_string_field': """aaa%5C%22%20'%20%2C%20.%20-%20%3B%20%3A%20%C3%A4%20%C3%B6%20%C3%BC%20%C3%A8%20%C3%A9%20%C3%A0%20%3F%20%2B%20%26%20%5C%5C%20%2F""", 'a_null_value': None}}) def test_field_to_json(self): diff --git a/tests/src/python/test_qgsbookmarkmodel.py b/tests/src/python/test_qgsbookmarkmodel.py index 66379bdd62ec..f712b36b9992 100644 --- a/tests/src/python/test_qgsbookmarkmodel.py +++ b/tests/src/python/test_qgsbookmarkmodel.py @@ -63,8 +63,8 @@ def testBookmarkModel(self): self.assertFalse(model.setData(model.index(-1, 0), 4, Qt.ItemDataRole.EditRole)) self.assertFalse(model.setData(model.index(0, 0), 4, Qt.ItemDataRole.EditRole)) - self.assertFalse(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertFalse(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable)) + self.assertFalse(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertFalse(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable) # add some bookmarks b = QgsBookmark() @@ -143,15 +143,15 @@ def testBookmarkModel(self): self.assertEqual(app_manager.bookmarks()[0].rotation(), -1.2) self.assertFalse(model.setData(model.index(2, 0), 4, Qt.ItemDataRole.EditRole)) - self.assertTrue(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertTrue(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertTrue(int(model.flags(model.index(0, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) - self.assertTrue(int(model.flags(model.index(1, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) - self.assertTrue(int(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertTrue(int(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertFalse(int(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertFalse(int(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertFalse(int(model.flags(model.index(2, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) + self.assertTrue(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertTrue(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertTrue(model.flags(model.index(0, 8)) & Qt.ItemFlag.ItemIsUserCheckable) + self.assertTrue(model.flags(model.index(1, 8)) & Qt.ItemFlag.ItemIsUserCheckable) + self.assertTrue(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertTrue(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertFalse(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertFalse(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertFalse(model.flags(model.index(2, 8)) & Qt.ItemFlag.ItemIsUserCheckable) # add bookmark to project manager b3 = QgsBookmark() @@ -224,18 +224,18 @@ def testBookmarkModel(self): self.assertEqual(project_manager.bookmarks()[0].rotation(), 361) self.assertFalse(model.setData(model.index(3, 0), 4, Qt.ItemDataRole.EditRole)) - self.assertTrue(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertTrue(int(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertTrue(int(model.flags(model.index(0, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) - self.assertTrue(int(model.flags(model.index(1, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) - self.assertTrue(int(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertTrue(int(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertTrue(int(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertTrue(int(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertTrue(int(model.flags(model.index(2, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) - self.assertFalse(int(model.flags(model.index(3, 0)) & Qt.ItemFlag.ItemIsEnabled)) - self.assertFalse(int(model.flags(model.index(3, 0)) & Qt.ItemFlag.ItemIsEditable)) - self.assertFalse(int(model.flags(model.index(3, 8)) & Qt.ItemFlag.ItemIsUserCheckable)) + self.assertTrue(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertTrue(model.flags(model.index(0, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertTrue(model.flags(model.index(0, 8)) & Qt.ItemFlag.ItemIsUserCheckable) + self.assertTrue(model.flags(model.index(1, 8)) & Qt.ItemFlag.ItemIsUserCheckable) + self.assertTrue(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertTrue(model.flags(model.index(1, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertTrue(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertTrue(model.flags(model.index(2, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertTrue(model.flags(model.index(2, 8)) & Qt.ItemFlag.ItemIsUserCheckable) + self.assertFalse(model.flags(model.index(3, 0)) & Qt.ItemFlag.ItemIsEnabled) + self.assertFalse(model.flags(model.index(3, 0)) & Qt.ItemFlag.ItemIsEditable) + self.assertFalse(model.flags(model.index(3, 8)) & Qt.ItemFlag.ItemIsUserCheckable) # try transferring bookmark from app->project self.assertTrue(model.setData(model.index(1, 8), Qt.CheckState.Checked, Qt.ItemDataRole.CheckStateRole)) diff --git a/tests/src/python/test_qgscoordinatereferencesystemutils.py b/tests/src/python/test_qgscoordinatereferencesystemutils.py index a7062d2f63ea..845bea61a96a 100644 --- a/tests/src/python/test_qgscoordinatereferencesystemutils.py +++ b/tests/src/python/test_qgscoordinatereferencesystemutils.py @@ -30,6 +30,16 @@ def test_axis_order(self): self.assertEqual(QgsCoordinateReferenceSystemUtils.defaultCoordinateOrderForCrs(QgsCoordinateReferenceSystem()), Qgis.CoordinateOrder.XY) self.assertEqual(QgsCoordinateReferenceSystemUtils.defaultCoordinateOrderForCrs(QgsCoordinateReferenceSystem('EPSG:3111')), Qgis.CoordinateOrder.XY) self.assertEqual(QgsCoordinateReferenceSystemUtils.defaultCoordinateOrderForCrs(QgsCoordinateReferenceSystem('EPSG:4326')), Qgis.CoordinateOrder.YX) + # compound crs + self.assertEqual( + QgsCoordinateReferenceSystemUtils.defaultCoordinateOrderForCrs( + QgsCoordinateReferenceSystem('EPSG:5500')), + Qgis.CoordinateOrder.YX) + # vertical crs, should be no error here and just return the default + self.assertEqual( + QgsCoordinateReferenceSystemUtils.defaultCoordinateOrderForCrs( + QgsCoordinateReferenceSystem('EPSG:5703')), + Qgis.CoordinateOrder.XY) def test_axis_direction_to_abbreviation(self): """ diff --git a/tests/src/python/test_qgselevationcontrollerwidget.py b/tests/src/python/test_qgselevationcontrollerwidget.py new file mode 100644 index 000000000000..8c8fe3a2dc10 --- /dev/null +++ b/tests/src/python/test_qgselevationcontrollerwidget.py @@ -0,0 +1,169 @@ +"""QGIS Unit tests for QgsElevationControllerWidget + +.. 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. +""" +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtTest import QSignalSpy +from qgis.core import ( + QgsDoubleRange, + QgsProject +) +from qgis.gui import QgsElevationControllerWidget +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsElevationControllerWidget(QgisTestCase): + + def testRange(self): + w = QgsElevationControllerWidget() + spy = QSignalSpy(w.rangeChanged) + w.setRangeLimits(QgsDoubleRange(100.5, 1000)) + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(100.5, 1000)) + self.assertEqual(len(spy), 1) + + # ensure that range is losslessly maintained if the user doesn't + # move the slider + w.setRange(QgsDoubleRange(130.3, 920.6)) + self.assertEqual(len(spy), 2) + self.assertEqual(w.range(), + QgsDoubleRange(130.3, 920.6)) + self.assertEqual(spy[-1][0], + QgsDoubleRange(130.3, 920.6)) + # no change = no signal + w.setRange(QgsDoubleRange(130.3, 920.6)) + self.assertEqual(len(spy), 2) + # tiny change, not enough to be within widget precision, should still + # raise signal + w.setRange(QgsDoubleRange(130.300001, 920.6)) + self.assertEqual(len(spy), 3) + self.assertEqual(spy[-1][0], + QgsDoubleRange(130.300001, 920.6)) + + # change visible limits to something which fits the old range + # make sure this is lossless + w.setRangeLimits(QgsDoubleRange(50, 1050)) + self.assertEqual(w.range(), + QgsDoubleRange(130.300001, 920.6)) + self.assertEqual(len(spy), 3) + + # change visible limits to something which fits only part of the old range + w.setRangeLimits(QgsDoubleRange(160, 1050)) + self.assertEqual(w.range(), + QgsDoubleRange(160.0, 920.6)) + self.assertEqual(len(spy), 4) + self.assertEqual(spy[-1][0], + QgsDoubleRange(160.0, 920.6)) + + w.setRangeLimits(QgsDoubleRange(120, 917.5)) + self.assertEqual(w.range(), + QgsDoubleRange(160.0, 917.5)) + self.assertEqual(len(spy), 5) + self.assertEqual(spy[-1][0], QgsDoubleRange(160.0, 917.5)) + + w.setRangeLimits(QgsDoubleRange(171, 815.5)) + self.assertEqual(w.range(), + QgsDoubleRange(171, 815.5)) + self.assertEqual(len(spy), 6) + self.assertEqual(spy[-1][0], + QgsDoubleRange(171, 815.5)) + + # infinite range => should be ignored + w.setRangeLimits(QgsDoubleRange()) + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(171, 815.5)) + self.assertEqual(w.range(), + QgsDoubleRange(171, 815.5)) + self.assertEqual(len(spy), 6) + + def test_slider_interaction(self): + """ + Simulate user interaction with slider + """ + w = QgsElevationControllerWidget() + spy = QSignalSpy(w.rangeChanged) + w.setRangeLimits(QgsDoubleRange(100.5, 1000)) + self.assertEqual(w.rangeLimits(), QgsDoubleRange(100.5, 1000)) + self.assertEqual(len(spy), 1) + w.setRange(QgsDoubleRange(130.3, 920.6)) + self.assertEqual(len(spy), 2) + self.assertEqual(w.range(), QgsDoubleRange(130.3, 920.6)) + self.assertEqual(spy[-1][0], QgsDoubleRange(130.3, 920.6)) + + slider_range = w.slider().maximum() - w.slider().minimum() + # slider should have a decent integer precision: + self.assertGreaterEqual(slider_range, 500) + + w.slider().setRange(int(w.slider().minimum() + slider_range * 0.4), + int(w.slider().minimum() + slider_range * 0.7)) + self.assertEqual(len(spy), 3) + self.assertAlmostEqual(spy[-1][0].lower(), 459.644, 3) + self.assertAlmostEqual(spy[-1][0].upper(), 729.495, 3) + self.assertAlmostEqual(w.range().lower(), 459.644, 3) + self.assertAlmostEqual(w.range().upper(), 729.495, 3) + + def testFixedRangeSize(self): + """ + Test that fixed range size is correctly handled + """ + w = QgsElevationControllerWidget() + w.setRangeLimits(QgsDoubleRange(100.5, 1000)) + w.setFixedRangeSize(10.0001) + self.assertEqual(w.fixedRangeSize(), 10.0001) + w.setRange(QgsDoubleRange(130.3, 920.6)) + self.assertAlmostEqual(w.range().upper() - w.range().lower(), 10.0001, 6) + + w.slider().setLowerValue(50) + self.assertAlmostEqual(w.range().upper() - w.range().lower(), 10.0001, 6) + + def test_project_interaction(self): + """ + Test interaction of widget with project + """ + elevation_properties = QgsProject.instance().elevationProperties() + elevation_properties.setElevationRange(QgsDoubleRange(50, 160)) + w = QgsElevationControllerWidget() + spy = QSignalSpy(w.rangeChanged) + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(50, 160) + ) + # initially selected range should be full range + self.assertEqual(w.range(), + QgsDoubleRange(50, 160) + ) + + # change range limits for project + elevation_properties.setElevationRange(QgsDoubleRange(80, 130)) + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(80, 130) + ) + self.assertEqual(w.range(), QgsDoubleRange(80, 130)) + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], QgsDoubleRange(80, 130)) + + # expand out range from current value + elevation_properties.setElevationRange(QgsDoubleRange(40, 190)) + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(40, 190) + ) + # selected range should be unchanged + self.assertEqual(len(spy), 1) + self.assertEqual(w.range(), QgsDoubleRange(80, 130)) + + # a project with no elevation range + elevation_properties.setElevationRange(QgsDoubleRange()) + w = QgsElevationControllerWidget() + # ensure some initial range is set, even if we are just guessing! + self.assertEqual(w.rangeLimits(), + QgsDoubleRange(0, 100) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsexpressionpreviewwidget.py b/tests/src/python/test_qgsexpressionpreviewwidget.py new file mode 100644 index 000000000000..3f737dbdf1d6 --- /dev/null +++ b/tests/src/python/test_qgsexpressionpreviewwidget.py @@ -0,0 +1,67 @@ +"""QGIS Unit tests for QgsExpressionPreviewWidget + +.. 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. +""" + +from qgis.PyQt.QtWidgets import QToolButton +from qgis.gui import QgsExpressionPreviewWidget +from qgis.core import ( + QgsExpressionContext, + QgsExpressionContextScope +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsExpressionPreviewWidget(QgisTestCase): + + def test_custom_mode(self): + """ + Test using a custom preview generator with the widget + """ + def make_context(value): + res = QgsExpressionContext() + scope = QgsExpressionContextScope() + scope.setVariable('test', value) + scope.setVariable('test2', value * 2) + res.appendScope(scope) + return res + + w = QgsExpressionPreviewWidget() + w.setCustomPreviewGenerator('Band', + [['Band 1', 1], ['Band 2', 2], ['Band 3', 3]], + make_context) + w.setExpressionText("@test * 5") + self.assertEqual(w.currentPreviewText(), '5') + w.setExpressionText("@test2 * 5") + self.assertEqual(w.currentPreviewText(), '10') + + next_button = w.findChild(QToolButton, 'mCustomButtonNext') + prev_button = w.findChild(QToolButton, 'mCustomButtonPrev') + self.assertFalse(prev_button.isEnabled()) + self.assertTrue(next_button.isEnabled()) + next_button.click() + self.assertEqual(w.currentPreviewText(), '20') + self.assertTrue(prev_button.isEnabled()) + self.assertTrue(next_button.isEnabled()) + next_button.click() + self.assertEqual(w.currentPreviewText(), '30') + self.assertTrue(prev_button.isEnabled()) + self.assertFalse(next_button.isEnabled()) + prev_button.click() + self.assertEqual(w.currentPreviewText(), '20') + self.assertTrue(prev_button.isEnabled()) + self.assertTrue(next_button.isEnabled()) + prev_button.click() + self.assertEqual(w.currentPreviewText(), '10') + self.assertFalse(prev_button.isEnabled()) + self.assertTrue(next_button.isEnabled()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsfeatureiterator.py b/tests/src/python/test_qgsfeatureiterator.py index 46954104ce2f..6c81906e5003 100644 --- a/tests/src/python/test_qgsfeatureiterator.py +++ b/tests/src/python/test_qgsfeatureiterator.py @@ -115,6 +115,29 @@ def addFeatures(self, vl): feat['Staff'] = 2 vl.addFeature(feat) + def test_VectorLayerEditing(self): + ogr_layer = QgsVectorLayer(os.path.join(TEST_DATA_DIR, 'points.shp'), 'Points', 'ogr') + self.assertTrue(ogr_layer.isValid()) + + request = QgsFeatureRequest() + iterator = ogr_layer.getFeatures(request) + self.assertTrue(iterator.isValid()) + + self.assertTrue(ogr_layer.startEditing()) + iterator = ogr_layer.getFeatures(request) + self.assertTrue(iterator.isValid()) + + memory_layer = QgsVectorLayer("Point?field=x:string&field=y:integer&field=z:integer", "layer", "memory") + self.assertTrue(memory_layer.isValid()) + + request = QgsFeatureRequest() + iterator = memory_layer.getFeatures(request) + self.assertTrue(iterator.isValid()) + + self.assertTrue(memory_layer.startEditing()) + iterator = memory_layer.getFeatures(request) + self.assertTrue(iterator.isValid()) + def test_ExpressionFieldNested(self): myShpFile = os.path.join(TEST_DATA_DIR, 'points.shp') layer = QgsVectorLayer(myShpFile, 'Points', 'ogr') diff --git a/tests/src/python/test_qgsfieldmodel.py b/tests/src/python/test_qgsfieldmodel.py index 6cfd5f6048b0..ed5a6786f2ce 100644 --- a/tests/src/python/test_qgsfieldmodel.py +++ b/tests/src/python/test_qgsfieldmodel.py @@ -348,6 +348,14 @@ def testJoinedFieldIsEditableRole(self): self.assertEqual(proxy_m.rowCount(), 1) self.assertEqual(proxy_m.data(proxy_m.index(0, 0)), 'id_a') + proxy_m.setFilters(QgsFieldProxyModel.Filter.AllTypes | QgsFieldProxyModel.Filter.OriginProvider) + proxy_m.sourceFieldModel().setLayer(layer) + self.assertEqual(proxy_m.rowCount(), 1) + self.assertEqual(proxy_m.data(proxy_m.index(0, 0)), 'id_a') + proxy_m.sourceFieldModel().setLayer(layer3) + self.assertEqual(proxy_m.rowCount(), 1) + self.assertEqual(proxy_m.data(proxy_m.index(0, 0)), 'id_a') + def testFieldIsWidgetEditableRole(self): l, m = create_model() self.assertTrue(m.data(m.indexFromName('fldtxt'), QgsFieldModel.FieldRoles.FieldIsWidgetEditable)) diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index ed6a9cccdfe4..975dc6810df6 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -6298,6 +6298,10 @@ def testIsGeosValid(self): 'MultiPolygon (((159865.14786298031685874 6768656.31838363595306873, 159858.97975336571107619 6769211.44824895076453686, 160486.07089751763851382 6769211.44824895076453686, 160481.95882444124436006 6768658.37442017439752817, 160163.27316101978067309 6768658.37442017439752817, 160222.89822062765597366 6769116.87056819349527359, 160132.43261294672265649 6769120.98264127038419247, 160163.27316101978067309 6768658.37442017439752817, 159865.14786298031685874 6768656.31838363595306873)))', False, True, 'Ring self-intersection'], ['Polygon((0 3, 3 0, 3 3, 0 0, 0 3))', False, False, 'Self-intersection'], + ['LineString(0 0)', False, False, 'LineString has less than 2 points and is not empty.'], + ['LineString Empty', True, True, ''], + ['MultiLineString((0 0))', False, False, 'QGIS geometry cannot be converted to a GEOS geometry'], + ['MultiLineString Empty', True, True, ''], ] for t in tests: # run each check 2 times to allow for testing of cached value @@ -6305,7 +6309,7 @@ def testIsGeosValid(self): for i in range(2): res = g1.isGeosValid() self.assertEqual(res, t[1], - f"mismatch for {t[0]}, expected:\n{t[1]}\nGot:\n{res}\n") + f"mismatch for {t[0]}, iter {i}, expected:\n{t[1]}\nGot:\n{res}\n") if not res: self.assertEqual(g1.lastError(), t[3], t[0]) for i in range(2): diff --git a/tests/src/python/test_qgshillshaderenderer.py b/tests/src/python/test_qgshillshaderenderer.py new file mode 100644 index 000000000000..2998cc69876f --- /dev/null +++ b/tests/src/python/test_qgshillshaderenderer.py @@ -0,0 +1,70 @@ +"""QGIS Unit tests for QgsHillshadeRenderer + +From build dir, run: +ctest -R PyQgsHillshadeRenderer -V + +.. 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.core import ( + QgsHillshadeRenderer, + QgsRasterLayer, +) +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 TestQgsHillshadeRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsHillshadeRenderer(layer.dataProvider(), + 1, 90, 45) + + self.assertEqual(renderer.azimuth(), 90) + self.assertEqual(renderer.altitude(), 45) + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_hillshade_invalid_layer(self): + """ + Test hillshade raster render band with a broken layer path + """ + renderer = QgsHillshadeRenderer(None, + 1, 90, 45) + + self.assertEqual(renderer.azimuth(), 90) + self.assertEqual(renderer.altitude(), 45) + self.assertEqual(renderer.inputBand(), 1) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsjsonutils.py b/tests/src/python/test_qgsjsonutils.py index 6b14d88df317..30915b0c3a6b 100644 --- a/tests/src/python/test_qgsjsonutils.py +++ b/tests/src/python/test_qgsjsonutils.py @@ -9,7 +9,7 @@ __date__ = '3/05/2016' __copyright__ = 'Copyright 2016, The QGIS Project' -from qgis.PyQt.QtCore import QLocale, Qt, QTextCodec, QVariant +from qgis.PyQt.QtCore import QT_VERSION_STR, QLocale, Qt, QVariant from qgis.core import ( NULL, QgsCoordinateReferenceSystem, @@ -31,7 +31,10 @@ from qgis.testing import start_app, QgisTestCase start_app() -codec = QTextCodec.codecForName("System") + +if int(QT_VERSION_STR.split('.')[0]) < 6: + from qgis.PyQt.QtCore import QTextCodec + codec = QTextCodec.codecForName("System") class TestQgsJsonUtils(QgisTestCase): @@ -42,17 +45,25 @@ def testStringToFeatureList(self): fields.append(QgsField("name", QVariant.String)) # empty string - features = QgsJsonUtils.stringToFeatureList("", fields, codec) + if int(QT_VERSION_STR.split('.')[0]) >= 6: + features = QgsJsonUtils.stringToFeatureList("", fields) + else: + features = QgsJsonUtils.stringToFeatureList("", fields, codec) self.assertEqual(features, []) # bad string - features = QgsJsonUtils.stringToFeatureList("asdasdas", fields, codec) + if int(QT_VERSION_STR.split('.')[0]) >= 6: + features = QgsJsonUtils.stringToFeatureList("asdasdas", fields) + else: + features = QgsJsonUtils.stringToFeatureList("asdasdas", fields, codec) self.assertEqual(features, []) # geojson string with 1 feature - features = QgsJsonUtils.stringToFeatureList( - '{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands"}}', - fields, codec) + s = '{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands"}}' + if int(QT_VERSION_STR.split('.')[0]) >= 6: + features = QgsJsonUtils.stringToFeatureList(s, fields) + else: + features = QgsJsonUtils.stringToFeatureList(s, fields, codec) self.assertEqual(len(features), 1) self.assertFalse(features[0].geometry().isNull()) self.assertEqual(features[0].geometry().wkbType(), QgsWkbTypes.Type.Point) @@ -62,9 +73,11 @@ def testStringToFeatureList(self): self.assertEqual(features[0]['name'], "Dinagat Islands") # geojson string with 2 features - features = QgsJsonUtils.stringToFeatureList( - '{ "type": "FeatureCollection","features":[{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands"}}, {\n"type": "Feature","geometry": {"type": "Point","coordinates": [110, 20]},"properties": {"name": "Henry Gale Island"}}]}', - fields, codec) + s = '{ "type": "FeatureCollection","features":[{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands"}}, {\n"type": "Feature","geometry": {"type": "Point","coordinates": [110, 20]},"properties": {"name": "Henry Gale Island"}}]}' + if int(QT_VERSION_STR.split('.')[0]) >= 6: + features = QgsJsonUtils.stringToFeatureList(s, fields) + else: + features = QgsJsonUtils.stringToFeatureList(s, fields, codec) self.assertEqual(len(features), 2) self.assertFalse(features[0].geometry().isNull()) self.assertEqual(features[0].geometry().wkbType(), QgsWkbTypes.Type.Point) @@ -121,17 +134,25 @@ def testStringToFields(self): """test retrieving fields from GeoJSON strings""" # empty string - fields = QgsJsonUtils.stringToFields("", codec) + if int(QT_VERSION_STR.split('.')[0]) >= 6: + fields = QgsJsonUtils.stringToFields("") + else: + fields = QgsJsonUtils.stringToFields("", codec) self.assertEqual(fields.count(), 0) # bad string - fields = QgsJsonUtils.stringToFields("asdasdas", codec) + if int(QT_VERSION_STR.split('.')[0]) >= 6: + fields = QgsJsonUtils.stringToFields("asdasdas") + else: + fields = QgsJsonUtils.stringToFields("asdasdas", codec) self.assertEqual(fields.count(), 0) # geojson string - fields = QgsJsonUtils.stringToFields( - '{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands","height":5.5}}', - codec) + s = '{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands","height":5.5}}' + if int(QT_VERSION_STR.split('.')[0]) >= 6: + fields = QgsJsonUtils.stringToFields(s) + else: + fields = QgsJsonUtils.stringToFields(s, codec) self.assertEqual(fields.count(), 2) self.assertEqual(fields[0].name(), "name") self.assertEqual(fields[0].type(), QVariant.String) @@ -139,9 +160,11 @@ def testStringToFields(self): self.assertEqual(fields[1].type(), QVariant.Double) # geojson string with 2 features - fields = QgsJsonUtils.stringToFields( - '{ "type": "FeatureCollection","features":[{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands","height":5.5}}, {\n"type": "Feature","geometry": {"type": "Point","coordinates": [110, 20]},"properties": {"name": "Henry Gale Island","height":6.5}}]}', - codec) + s = '{ "type": "FeatureCollection","features":[{\n"type": "Feature","geometry": {"type": "Point","coordinates": [125, 10]},"properties": {"name": "Dinagat Islands","height":5.5}}, {\n"type": "Feature","geometry": {"type": "Point","coordinates": [110, 20]},"properties": {"name": "Henry Gale Island","height":6.5}}]}' + if int(QT_VERSION_STR.split('.')[0]) >= 6: + fields = QgsJsonUtils.stringToFields(s) + else: + fields = QgsJsonUtils.stringToFields(s, codec) self.assertEqual(fields.count(), 2) self.assertEqual(fields[0].name(), "name") self.assertEqual(fields[0].type(), QVariant.String) diff --git a/tests/src/python/test_qgslayertreeview.py b/tests/src/python/test_qgslayertreeview.py index 39e4b595f9f6..8d02dc56d710 100644 --- a/tests/src/python/test_qgslayertreeview.py +++ b/tests/src/python/test_qgslayertreeview.py @@ -626,6 +626,34 @@ def testProxyModel(self): self.assertEqual(proxy_items, ['layer2']) + # test valid layer filtering + broken_layer = QgsVectorLayer("xxxx", "broken", "ogr") + self.assertFalse(broken_layer.isValid()) + self.project.addMapLayers([broken_layer]) + + proxy_model.setFilterText(None) + + proxy_items = [] + for r in range(proxy_model.rowCount()): + proxy_items.append(proxy_model.data(proxy_model.index(r, 0))) + self.assertEqual(proxy_items, ['broken', 'layer1', 'layer2']) + + proxy_model.setHideValidLayers(True) + + proxy_items = [] + for r in range(proxy_model.rowCount()): + proxy_items.append(proxy_model.data(proxy_model.index(r, 0))) + self.assertEqual(proxy_items, ['broken']) + + proxy_model.setHideValidLayers(False) + + proxy_items = [] + for r in range(proxy_model.rowCount()): + proxy_items.append(proxy_model.data(proxy_model.index(r, 0))) + self.assertEqual(proxy_items, ['broken', 'layer1', 'layer2']) + + self.project.removeMapLayer(broken_layer) + def testProxyModelCurrentIndex(self): """Test a crash spotted out while developing the proxy model""" diff --git a/tests/src/python/test_qgslayoutelevationprofile.py b/tests/src/python/test_qgslayoutelevationprofile.py index d0e6a5d86d31..0465eeafef41 100644 --- a/tests/src/python/test_qgslayoutelevationprofile.py +++ b/tests/src/python/test_qgslayoutelevationprofile.py @@ -795,6 +795,82 @@ def test_draw_zero_label_interval(self): 'zero_label_interval', layout )) + def test_draw_map_units_tolerance(self): + """ + Test rendering the layout profile item using symbols with map unit sizes + """ + vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') + vl.setCrs(QgsCoordinateReferenceSystem()) + self.assertTrue(vl.isValid()) + + for line in [ + 'LineStringZ (321829.48893365426920354 129991.38697145861806348 1, 321847.89668515208177269 129996.63588572069420479 1, 321848.97131609614007175 129979.22330882755341008 1, 321830.31725845142500475 129978.07136809575604275 1, 321829.48893365426920354 129991.38697145861806348 1)', + 'LineStringZ (321920.00953056826256216 129924.58260190498549491 2, 321924.65299345907988027 129908.43546159457764588 2, 321904.78543491888558492 129903.99811821122420952 2, 321900.80605239619035274 129931.39860145389684476 2, 321904.84799937985371798 129931.71552911199978553 2, 321908.93646715773502365 129912.90030360443051904 2, 321914.20495146053144708 129913.67693978428724222 2, 321911.30165811872575432 129923.01272751353099011 2, 321920.00953056826256216 129924.58260190498549491 2)', + 'LineStringZ (321923.10517279652412981 129919.61521573827485554 3, 321922.23537852568551898 129928.3598982143739704 3, 321928.60423935484141111 129934.22530528216157109 3, 321929.39881197665818036 129923.29054521876969375 3, 321930.55804549407912418 129916.53248518184409477 3, 321923.10517279652412981 129919.61521573827485554 3)', + 'LineStringZ (321990.47451346553862095 129909.63588680300745182 4, 321995.04325810901354998 129891.84052284323843196 4, 321989.66826330573530868 129890.5092018858413212 4, 321990.78512359503656626 129886.49917887404444627 4, 321987.37291929306229576 129885.64982962771318853 4, 321985.2254804756375961 129893.81317058412241749 4, 321987.63158903241856024 129894.41078495365218259 4, 321984.34022761805681512 129907.57450046355370432 4, 321990.47451346553862095 129909.63588680300745182 4)', + 'LineStringZ (322103.03910495212767273 129795.91051736124791205 5, 322108.25568856322206557 129804.76113295342656784 5, 322113.29666162584908307 129803.9285887333098799 5, 322117.78645010641776025 129794.48194090687320568 5, 322103.03910495212767273 129795.91051736124791205 5)']: + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt(line)) + self.assertTrue(vl.dataProvider().addFeature(f)) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute) + line_symbol = QgsLineSymbol.createSimple({'color': '#ff00ff', 'width': '0.8'}) + line_symbol.setWidthUnit(Qgis.RenderUnit.MapUnits) + vl.elevationProperties().setProfileLineSymbol(line_symbol) + vl.elevationProperties().setRespectLayerSymbology(False) + + p = QgsProject() + p.addMapLayer(vl) + layout = QgsLayout(p) + layout.initializeDefaults() + + profile_item = QgsLayoutItemElevationProfile(layout) + layout.addLayoutItem(profile_item) + profile_item.attemptSetSceneRect(QRectF(10, 10, 180, 180)) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (321897.18831187387695536 129916.86947759155009408, 321942.11597351566888392 129924.94403429214435164)') + + profile_item.setProfileCurve(curve) + profile_item.setCrs(QgsCoordinateReferenceSystem()) + + profile_item.plot().setXMaximum(curve.length()) + profile_item.plot().setYMaximum(14) + + profile_item.plot().xAxis().setGridIntervalMajor(10) + profile_item.plot().xAxis().setGridIntervalMinor(5) + profile_item.plot().xAxis().setGridMajorSymbol(QgsLineSymbol.createSimple({'color': '#ffaaff', 'width': 2})) + profile_item.plot().xAxis().setGridMinorSymbol( + QgsLineSymbol.createSimple({'color': '#ffffaa', 'width': 2})) + + format = QgsTextFormat() + format.setFont(QgsFontUtils.getStandardTestFont("Bold")) + format.setSize(20) + format.setNamedStyle("Bold") + format.setColor(QColor(0, 0, 0)) + profile_item.plot().xAxis().setTextFormat(format) + profile_item.plot().xAxis().setLabelInterval(20) + + profile_item.plot().yAxis().setGridIntervalMajor(10) + profile_item.plot().yAxis().setGridIntervalMinor(5) + profile_item.plot().yAxis().setGridMajorSymbol(QgsLineSymbol.createSimple({'color': '#ffffaa', 'width': 2})) + profile_item.plot().yAxis().setGridMinorSymbol( + QgsLineSymbol.createSimple({'color': '#aaffaa', 'width': 2})) + + profile_item.plot().yAxis().setTextFormat(format) + profile_item.plot().yAxis().setLabelInterval(10) + + profile_item.plot().setChartBorderSymbol( + QgsFillSymbol.createSimple({'style': 'no', 'color': '#aaffaa', 'width_border': 2})) + + profile_item.setTolerance(1) + profile_item.setLayers([vl]) + + self.assertTrue(self.render_layout_check( + 'vector_layer_map_units_tolerance', layout + )) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsmeshlayerelevationproperties.py b/tests/src/python/test_qgsmeshlayerelevationproperties.py index 214f06079aac..0243a429a00d 100644 --- a/tests/src/python/test_qgsmeshlayerelevationproperties.py +++ b/tests/src/python/test_qgsmeshlayerelevationproperties.py @@ -16,6 +16,7 @@ QgsLineSymbol, QgsMeshLayerElevationProperties, QgsReadWriteContext, + QgsDoubleRange ) import unittest from qgis.testing import start_app, QgisTestCase @@ -27,9 +28,13 @@ class TestQgsMeshLayerElevationProperties(QgisTestCase): def testBasic(self): props = QgsMeshLayerElevationProperties(None) + self.assertEqual(props.mode(), + Qgis.MeshElevationMode.FromVertices) + self.assertEqual(props.zScale(), 1) self.assertEqual(props.zOffset(), 0) self.assertTrue(props.hasElevation()) + self.assertTrue(props.fixedRange().isInfinite()) self.assertIsInstance(props.profileLineSymbol(), QgsLineSymbol) self.assertIsInstance(props.profileFillSymbol(), QgsFillSymbol) self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line) @@ -58,6 +63,8 @@ def testBasic(self): props2 = QgsMeshLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FromVertices) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') @@ -66,6 +73,8 @@ def testBasic(self): self.assertEqual(props2.elevationLimit(), 909) props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FromVertices) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') @@ -73,6 +82,67 @@ def testBasic(self): self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) + def test_basic_fixed_range(self): + """ + Basic tests for the class using the FixedElevationRange mode + """ + props = QgsMeshLayerElevationProperties(None) + self.assertTrue(props.fixedRange().isInfinite()) + + props.setMode(Qgis.MeshElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(103.1, 106.8)) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRange(), QgsDoubleRange(103.1, 106.8)) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 106.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.MeshElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + # include lower, exclude upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + + # exclude lower, include upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsMeshLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsmeshlayerrenderer.py b/tests/src/python/test_qgsmeshlayerrenderer.py new file mode 100644 index 000000000000..b8e44a14afe1 --- /dev/null +++ b/tests/src/python/test_qgsmeshlayerrenderer.py @@ -0,0 +1,86 @@ +"""QGIS Unit tests for QgsMeshLayerRenderer. + +.. 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 +import unittest + +from qgis.PyQt.QtCore import QSize +from qgis.core import ( + Qgis, + QgsDoubleRange, + QgsMapSettings, + QgsMeshLayer +) +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() + + +class TestQgsMeshLayerLabeling(QgisTestCase): + + @classmethod + def control_path_prefix(cls): + return "mesh" + + def test_render_fixed_elevation_range_with_z_range_filter(self): + """ + Test rendering a mesh with a fixed elevation range when + map settings has a z range filtrer + """ + mesh_layer = QgsMeshLayer( + os.path.join(unitTestDataPath(), 'mesh', 'quad_flower.2dm'), + 'mdal', 'mdal') + self.assertTrue(mesh_layer.isValid()) + + # set layer as elevation enabled + mesh_layer.elevationProperties().setMode( + Qgis.MeshElevationMode.FixedElevationRange + ) + mesh_layer.elevationProperties().setFixedRange( + QgsDoubleRange(33, 38) + ) + + map_settings = QgsMapSettings() + map_settings.setOutputSize(QSize(400, 400)) + map_settings.setOutputDpi(96) + map_settings.setDestinationCrs(mesh_layer.crs()) + map_settings.setExtent(mesh_layer.extent()) + map_settings.setLayers([mesh_layer]) + + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, fixed elevation range layer', + 'elevation_no_filter', + map_settings) + ) + + # map settings range includes layer's range + map_settings.setZRange(QgsDoubleRange(30, 35)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings includes layers fixed range', + 'fixed_elevation_range_included', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layers fixed range', + 'fixed_elevation_range_excluded', + map_settings) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsoverlaywidgetlayout.py b/tests/src/python/test_qgsoverlaywidgetlayout.py new file mode 100644 index 000000000000..287fb73c335d --- /dev/null +++ b/tests/src/python/test_qgsoverlaywidgetlayout.py @@ -0,0 +1,211 @@ +"""QGIS Unit tests for QgsOverlayWidgetLayout + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Nyall Dawson' +__date__ = '24/1/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' + +import unittest + +from qgis.PyQt.QtCore import Qt, QPoint +from qgis.PyQt.QtWidgets import QWidget +from qgis.gui import ( + QgsOverlayWidgetLayout +) +from qgis.testing import start_app, QgisTestCase + +app = start_app() + + +class TestQgsOverlayWidgetLayout(QgisTestCase): + + def testLayout(self): + parent = QWidget() + parent.setFixedSize(600, 400) + + layout = QgsOverlayWidgetLayout() + layout.setContentsMargins( + 5, 6, 7, 8 + ) + parent.setLayout(layout) + parent.show() + self.assertEqual(parent.rect().right(), 599) + self.assertEqual(parent.rect().bottom(), 399) + + child_left_1 = QWidget() + child_left_1.setFixedWidth(30) + layout.addWidget(child_left_1, Qt.Edge.LeftEdge) + child_left_1.show() + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().topLeft()), + QPoint(5, 6)) + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().bottomRight()), + QPoint(34, 390)) + + child_left_2 = QWidget() + child_left_2.setFixedWidth(40) + layout.addWidget(child_left_2, Qt.Edge.LeftEdge) + child_left_2.show() + + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().topLeft()), + QPoint(5, 6)) + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().bottomRight()), + QPoint(34, 390)) + + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().topLeft()), + QPoint(35, 6)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().bottomRight()), + QPoint(74, 390)) + + layout.setHorizontalSpacing(12) + child_right_1 = QWidget() + child_right_1.setFixedWidth(40) + layout.addWidget(child_right_1, Qt.Edge.RightEdge) + child_right_1.show() + child_right_2 = QWidget() + child_right_2.setFixedWidth(80) + layout.addWidget(child_right_2, Qt.Edge.RightEdge) + child_right_2.show() + + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().topLeft()), + QPoint(5, 6)) + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().bottomRight()), + QPoint(34, 390)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().topLeft()), + QPoint(47, 6)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().bottomRight()), + QPoint(86, 390)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().topLeft()), + QPoint(552, 6)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().bottomRight()), + QPoint(591, 390)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().topLeft()), + QPoint(460, 6)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().bottomRight()), + QPoint(539, 390)) + + layout.setVerticalSpacing(13) + child_top_1 = QWidget() + child_top_1.setFixedHeight(20) + layout.addWidget(child_top_1, Qt.Edge.TopEdge) + child_top_1.show() + child_top_2 = QWidget() + child_top_2.setFixedHeight(30) + layout.addWidget(child_top_2, Qt.Edge.TopEdge) + child_top_2.show() + + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().topLeft()), + QPoint(5, 6)) + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().bottomRight()), + QPoint(34, 390)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().topLeft()), + QPoint(47, 6)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().bottomRight()), + QPoint(86, 390)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().topLeft()), + QPoint(552, 6)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().bottomRight()), + QPoint(591, 390)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().topLeft()), + QPoint(460, 6)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().bottomRight()), + QPoint(539, 390)) + self.assertEqual( + child_top_1.mapToParent(child_top_1.rect().topLeft()), + QPoint(99, 6)) + self.assertEqual( + child_top_1.mapToParent(child_top_1.rect().bottomRight()), + QPoint(447, 25)) + self.assertEqual( + child_top_2.mapToParent(child_top_2.rect().topLeft()), + QPoint(99, 39)) + self.assertEqual( + child_top_2.mapToParent(child_top_2.rect().bottomRight()), + QPoint(447, 68)) + + child_bottom_1 = QWidget() + child_bottom_1.setFixedHeight(20) + layout.addWidget(child_bottom_1, Qt.Edge.BottomEdge) + child_bottom_1.show() + child_bottom_2 = QWidget() + child_bottom_2.setFixedHeight(30) + layout.addWidget(child_bottom_2, Qt.Edge.BottomEdge) + child_bottom_2.show() + + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().topLeft()), + QPoint(5, 6)) + self.assertEqual( + child_left_1.mapToParent(child_left_1.rect().bottomRight()), + QPoint(34, 390)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().topLeft()), + QPoint(47, 6)) + self.assertEqual( + child_left_2.mapToParent(child_left_2.rect().bottomRight()), + QPoint(86, 390)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().topLeft()), + QPoint(552, 6)) + self.assertEqual( + child_right_1.mapToParent(child_right_1.rect().bottomRight()), + QPoint(591, 390)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().topLeft()), + QPoint(460, 6)) + self.assertEqual( + child_right_2.mapToParent(child_right_2.rect().bottomRight()), + QPoint(539, 390)) + self.assertEqual( + child_top_1.mapToParent(child_top_1.rect().topLeft()), + QPoint(99, 6)) + self.assertEqual( + child_top_1.mapToParent(child_top_1.rect().bottomRight()), + QPoint(447, 25)) + self.assertEqual( + child_top_2.mapToParent(child_top_2.rect().topLeft()), + QPoint(99, 39)) + self.assertEqual( + child_top_2.mapToParent(child_top_2.rect().bottomRight()), + QPoint(447, 68)) + self.assertEqual( + child_bottom_1.mapToParent(child_bottom_1.rect().topLeft()), + QPoint(99, 371)) + self.assertEqual( + child_bottom_1.mapToParent(child_bottom_1.rect().bottomRight()), + QPoint(447, 390)) + self.assertEqual( + child_bottom_2.mapToParent(child_bottom_2.rect().topLeft()), + QPoint(99, 328)) + self.assertEqual( + child_bottom_2.mapToParent(child_bottom_2.rect().bottomRight()), + QPoint(447, 357)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgspalettedrasterrenderer.py b/tests/src/python/test_qgspalettedrasterrenderer.py new file mode 100644 index 000000000000..eec66491e060 --- /dev/null +++ b/tests/src/python/test_qgspalettedrasterrenderer.py @@ -0,0 +1,506 @@ +"""QGIS Unit tests for QgsPalettedRasterRenderer + +From build dir, run: +ctest -R PyQgsPalettedRasterRenderer -V + +.. 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 + +import numpy as np +from osgeo import gdal +from qgis.PyQt.QtCore import QFileInfo, QTemporaryDir +from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsColorRampShader, + QgsGradientColorRamp, + QgsLimitedRandomColorRamp, + QgsMapSettings, + QgsPalettedRasterRenderer, + QgsRasterLayer +) +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 TestQgsPalettedRasterRenderer(QgisTestCase): + + def testPaletted(self): + """ test paletted raster renderer with raster with color table""" + path = os.path.join(unitTestDataPath('raster'), + 'with_color_table.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, + [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) + + self.assertEqual(renderer.nColors(), 2) + self.assertEqual(renderer.usesBands(), [1]) + self.assertEqual(renderer.inputBand(), 1) + + # test labels + self.assertEqual(renderer.label(1), 'class 2') + self.assertEqual(renderer.label(3), 'class 1') + self.assertFalse(renderer.label(101)) + + # test legend symbology - should be sorted by value + legend = renderer.legendSymbologyItems() + self.assertEqual(legend[0][0], 'class 2') + self.assertEqual(legend[1][0], 'class 1') + self.assertEqual(legend[0][1].name(), '#00ff00') + self.assertEqual(legend[1][1].name(), '#ff0000') + + # test retrieving classes + classes = renderer.classes() + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'class 1') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + + # test set label + # bad index + renderer.setLabel(1212, 'bad') + renderer.setLabel(3, 'new class') + self.assertEqual(renderer.label(3), 'new class') + + # color ramp + r = QgsLimitedRandomColorRamp(5) + renderer.setSourceColorRamp(r) + self.assertEqual(renderer.sourceColorRamp().type(), 'random') + self.assertEqual(renderer.sourceColorRamp().count(), 5) + + # clone + new_renderer = renderer.clone() + classes = new_renderer.classes() + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'new class') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + self.assertEqual(new_renderer.sourceColorRamp().type(), 'random') + self.assertEqual(new_renderer.sourceColorRamp().count(), 5) + + # write to xml and read + doc = QDomDocument('testdoc') + elem = doc.createElement('qgis') + renderer.writeXml(doc, elem) + restored = QgsPalettedRasterRenderer.create(elem.firstChild().toElement(), layer.dataProvider()) + self.assertTrue(restored) + self.assertEqual(restored.usesBands(), [1]) + classes = restored.classes() + self.assertTrue(classes) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[0].label, 'class 2') + self.assertEqual(classes[1].label, 'new class') + self.assertEqual(classes[0].color.name(), '#00ff00') + self.assertEqual(classes[1].color.name(), '#ff0000') + self.assertEqual(restored.sourceColorRamp().type(), 'random') + self.assertEqual(restored.sourceColorRamp().count(), 5) + + # render test + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer', + 'paletted_renderer', + ms) + ) + + def testPalettedBandInvalidLayer(self): + """ test paletted raster render band with a broken layer path""" + renderer = QgsPalettedRasterRenderer(None, 2, + [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) + + self.assertEqual(renderer.inputBand(), 2) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + def testPalettedBand(self): + """ test paletted raster render band""" + path = os.path.join(unitTestDataPath(), + 'landsat_4326.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 2, + [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) + + self.assertEqual(renderer.inputBand(), 2) + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 2) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 2) + self.assertTrue(renderer.setInputBand(1)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer_band2', + 'paletted_renderer_band2', + ms) + ) + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 3, + [QgsPalettedRasterRenderer.Class(120, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(123, QColor(255, 0, 0), 'class 1'), + QgsPalettedRasterRenderer.Class(124, QColor(0, 0, 255), 'class 1')]) + + layer.setRenderer(renderer) + ms = QgsMapSettings() + ms.setLayers([layer]) + ms.setExtent(layer.extent()) + + self.assertTrue( + self.render_map_settings_check( + 'paletted_renderer_band3', + 'paletted_renderer_band3', + ms) + ) + + def testPalettedColorTableToClassData(self): + entries = [QgsColorRampShader.ColorRampItem(5, QColor(255, 0, 0), 'item1'), + QgsColorRampShader.ColorRampItem(3, QColor(0, 255, 0), 'item2'), + QgsColorRampShader.ColorRampItem(6, QColor(0, 0, 255), 'item3'), + ] + classes = QgsPalettedRasterRenderer.colorTableToClassData(entries) + self.assertEqual(classes[0].value, 5) + self.assertEqual(classes[1].value, 3) + self.assertEqual(classes[2].value, 6) + self.assertEqual(classes[0].label, 'item1') + self.assertEqual(classes[1].label, 'item2') + self.assertEqual(classes[2].label, 'item3') + self.assertEqual(classes[0].color.name(), '#ff0000') + self.assertEqual(classes[1].color.name(), '#00ff00') + self.assertEqual(classes[2].color.name(), '#0000ff') + + # test #13263 + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(1)) + self.assertEqual(len(classes), 4) + classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(15)) + self.assertEqual(len(classes), 256) + + def testLoadPalettedColorDataFromString(self): + """ + Test interpreting a bunch of color data format strings + """ + esri_clr_format = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' + esri_clr_format_win = '1 255 255 0\r\n2 64 0 128\r\n3 255 32 32\r\n4 0 255 0\r\n5 0 0 255' + esri_clr_format_tab = '1\t255\t255\t0\n2\t64\t0\t128\n3\t255\t32\t32\n4\t0\t255\t0\n5\t0\t0\t255' + esri_clr_spaces = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' + gdal_clr_comma = '1,255,255,0\n2,64,0,128\n3,255,32,32\n4,0,255,0\n5,0,0,255' + gdal_clr_colon = '1:255:255:0\n2:64:0:128\n3:255:32:32\n4:0:255:0\n5:0:0:255' + for f in [esri_clr_format, + esri_clr_format_win, + esri_clr_format_tab, + esri_clr_spaces, + gdal_clr_comma, + gdal_clr_colon]: + classes = QgsPalettedRasterRenderer.classDataFromString(f) + self.assertEqual(len(classes), 5) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#ffff00') + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#400080') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#ff2020') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#00ff00') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + + grass_named_colors = '0 white\n1 yellow\n3 black\n6 blue\n9 magenta\n11 aqua\n13 grey\n14 gray\n15 orange\n19 brown\n21 purple\n22 violet\n24 indigo\n90 green\n180 cyan\n270 red\n' + classes = QgsPalettedRasterRenderer.classDataFromString(grass_named_colors) + self.assertEqual(len(classes), 16) + self.assertEqual(classes[0].value, 0) + self.assertEqual(classes[0].color.name(), '#ffffff') + self.assertEqual(classes[1].value, 1) + self.assertEqual(classes[1].color.name(), '#ffff00') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#000000') + self.assertEqual(classes[3].value, 6) + self.assertEqual(classes[3].color.name(), '#0000ff') + self.assertEqual(classes[4].value, 9) + self.assertEqual(classes[4].color.name(), '#ff00ff') + self.assertEqual(classes[5].value, 11) + self.assertEqual(classes[5].color.name(), '#00ffff') + self.assertEqual(classes[6].value, 13) + self.assertEqual(classes[6].color.name(), '#808080') + self.assertEqual(classes[7].value, 14) + self.assertEqual(classes[7].color.name(), '#808080') + self.assertEqual(classes[8].value, 15) + self.assertEqual(classes[8].color.name(), '#ffa500') + self.assertEqual(classes[9].value, 19) + self.assertEqual(classes[9].color.name(), '#a52a2a') + self.assertEqual(classes[10].value, 21) + self.assertEqual(classes[10].color.name(), '#800080') + self.assertEqual(classes[11].value, 22) + self.assertEqual(classes[11].color.name(), '#ee82ee') + self.assertEqual(classes[12].value, 24) + self.assertEqual(classes[12].color.name(), '#4b0082') + self.assertEqual(classes[13].value, 90) + self.assertEqual(classes[13].color.name(), '#008000') + self.assertEqual(classes[14].value, 180) + self.assertEqual(classes[14].color.name(), '#00ffff') + self.assertEqual(classes[15].value, 270) + self.assertEqual(classes[15].color.name(), '#ff0000') + + gdal_alpha = '1:255:255:0:0\n2:64:0:128:50\n3:255:32:32:122\n4:0:255:0:200\n5:0:0:255:255' + classes = QgsPalettedRasterRenderer.classDataFromString(gdal_alpha) + self.assertEqual(len(classes), 5) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#ffff00') + self.assertEqual(classes[0].color.alpha(), 0) + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#400080') + self.assertEqual(classes[1].color.alpha(), 50) + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#ff2020') + self.assertEqual(classes[2].color.alpha(), 122) + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#00ff00') + self.assertEqual(classes[3].color.alpha(), 200) + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + self.assertEqual(classes[4].color.alpha(), 255) + + # qgis style, with labels + qgis_style = '3 255 0 0 255 class 1\n4 0 255 0 200 class 2' + classes = QgsPalettedRasterRenderer.classDataFromString(qgis_style) + self.assertEqual(len(classes), 2) + self.assertEqual(classes[0].value, 3) + self.assertEqual(classes[0].color.name(), '#ff0000') + self.assertEqual(classes[0].color.alpha(), 255) + self.assertEqual(classes[0].label, 'class 1') + self.assertEqual(classes[1].value, 4) + self.assertEqual(classes[1].color.name(), '#00ff00') + self.assertEqual(classes[1].color.alpha(), 200) + self.assertEqual(classes[1].label, 'class 2') + + # some bad inputs + bad = '' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = '\n\n\n' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = 'x x x x' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 0) + bad = '1 255 0 0\n2 255 255\n3 255 0 255' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 2) + bad = '1 255 a 0' + classes = QgsPalettedRasterRenderer.classDataFromString(bad) + self.assertEqual(len(classes), 1) + + def testLoadPalettedClassDataFromFile(self): + # bad file + classes = QgsPalettedRasterRenderer.classDataFromFile('ajdhjashjkdh kjahjkdhk') + self.assertEqual(len(classes), 0) + + # good file! + path = os.path.join(unitTestDataPath('raster'), + 'test.clr') + classes = QgsPalettedRasterRenderer.classDataFromFile(path) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].color.name(), '#000000') + self.assertEqual(classes[0].color.alpha(), 255) + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].color.name(), '#c8c8c8') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].color.name(), '#006e00') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].color.name(), '#6e4100') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].color.name(), '#0000ff') + self.assertEqual(classes[4].color.alpha(), 255) + self.assertEqual(classes[5].value, 6) + self.assertEqual(classes[5].color.name(), '#0059ff') + self.assertEqual(classes[6].value, 7) + self.assertEqual(classes[6].color.name(), '#00aeff') + self.assertEqual(classes[7].value, 8) + self.assertEqual(classes[7].color.name(), '#00fff6') + self.assertEqual(classes[8].value, 9) + self.assertEqual(classes[8].color.name(), '#eeff00') + self.assertEqual(classes[9].value, 10) + self.assertEqual(classes[9].color.name(), '#ffb600') + + def testPalettedClassDataToString(self): + classes = [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] + self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), + '1 0 255 0 255 class 2\n3 255 0 0 255 class 1') + # must be sorted by value to work OK in ArcMap + classes = [QgsPalettedRasterRenderer.Class(4, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] + self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), + '3 255 0 0 255 class 1\n4 0 255 0 255 class 2') + + def testPalettedClassDataFromLayer(self): + # no layer + classes = QgsPalettedRasterRenderer.classDataFromRaster(None, 1) + self.assertFalse(classes) + + # 10 class layer + path = os.path.join(unitTestDataPath('raster'), + 'with_color_table.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer10 = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].value, 1) + self.assertEqual(classes[0].label, '1') + self.assertEqual(classes[1].value, 2) + self.assertEqual(classes[1].label, '2') + self.assertEqual(classes[2].value, 3) + self.assertEqual(classes[2].label, '3') + self.assertEqual(classes[3].value, 4) + self.assertEqual(classes[3].label, '4') + self.assertEqual(classes[4].value, 5) + self.assertEqual(classes[4].label, '5') + self.assertEqual(classes[5].value, 6) + self.assertEqual(classes[5].label, '6') + self.assertEqual(classes[6].value, 7) + self.assertEqual(classes[6].label, '7') + self.assertEqual(classes[7].value, 8) + self.assertEqual(classes[7].label, '8') + self.assertEqual(classes[8].value, 9) + self.assertEqual(classes[8].label, '9') + self.assertEqual(classes[9].value, 10) + self.assertEqual(classes[9].label, '10') + + # bad band + self.assertFalse(QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 10101010)) + + # with ramp + r = QgsGradientColorRamp(QColor(200, 0, 0, 100), QColor(0, 200, 0, 200)) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1, r) + self.assertEqual(len(classes), 10) + self.assertEqual(classes[0].color.name(), '#c80000') + self.assertEqual(classes[1].color.name(), '#b21600') + self.assertEqual(classes[2].color.name(), '#9c2c00') + self.assertIn(classes[3].color.name(), ('#854200', '#854300')) + self.assertEqual(classes[4].color.name(), '#6f5900') + self.assertEqual(classes[5].color.name(), '#596f00') + self.assertIn(classes[6].color.name(), ('#428500', '#438500')) + self.assertEqual(classes[7].color.name(), '#2c9c00') + self.assertEqual(classes[8].color.name(), '#16b200') + self.assertEqual(classes[9].color.name(), '#00c800') + + # 30 class layer + path = os.path.join(unitTestDataPath('raster'), + 'unique_1.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer10 = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) + self.assertEqual(len(classes), 30) + expected = [11, 21, 22, 24, 31, 82, 2002, 2004, 2014, 2019, 2027, 2029, 2030, 2080, 2081, 2082, 2088, 2092, + 2097, 2098, 2099, 2105, 2108, 2110, 2114, 2118, 2126, 2152, 2184, 2220] + self.assertEqual([c.value for c in classes], expected) + + # bad layer + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) + self.assertFalse(classes) + + def testPalettedRendererWithNegativeColorValue(self): + """ test paletted raster renderer with negative values in color table""" + + path = os.path.join(unitTestDataPath('raster'), + 'hub13263.vrt') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, + [QgsPalettedRasterRenderer.Class(-1, QColor(0, 255, 0), 'class 2'), + QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) + + self.assertEqual(renderer.nColors(), 2) + self.assertEqual(renderer.usesBands(), [1]) + + def testPalettedRendererWithFloats(self): + """Tests for https://github.com/qgis/QGIS/issues/39058""" + + tempdir = QTemporaryDir() + temppath = os.path.join(tempdir.path(), 'paletted.tif') + + # Create a float raster with unique values up to 65536 + one extra row + driver = gdal.GetDriverByName('GTiff') + outRaster = driver.Create(temppath, 256, 256 + 1, 1, gdal.GDT_Float32) + outband = outRaster.GetRasterBand(1) + data = [] + for r in range(256 + 1): + data.append(list(range(r * 256, (r + 1) * 256))) + npdata = np.array(data, np.float32) + outband.WriteArray(npdata) + outband.FlushCache() + outRaster.FlushCache() + del outRaster + + layer = QgsRasterLayer(temppath, 'paletted') + self.assertTrue(layer.isValid()) + self.assertEqual(layer.dataProvider().dataType(1), Qgis.DataType.Float32) + classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) + # Check max classes count, hardcoded in QGIS renderer + self.assertEqual(len(classes), 65536) + class_values = [] + for c in classes: + class_values.append(c.value) + self.assertEqual(sorted(class_values), list(range(65536))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsprojectelevationproperties.py b/tests/src/python/test_qgsprojectelevationproperties.py index 35c98c738ae8..7cb9845fc855 100644 --- a/tests/src/python/test_qgsprojectelevationproperties.py +++ b/tests/src/python/test_qgsprojectelevationproperties.py @@ -13,6 +13,7 @@ from qgis.PyQt.QtCore import QTemporaryDir from qgis.PyQt.QtXml import QDomDocument +from qgis.PyQt.QtTest import QSignalSpy from qgis.core import ( QgsFlatTerrainProvider, QgsMeshTerrainProvider, @@ -21,6 +22,7 @@ QgsRasterDemTerrainProvider, QgsRasterLayer, QgsReadWriteContext, + QgsDoubleRange, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -35,6 +37,7 @@ class TestQgsProjectElevationProperties(QgisTestCase): def testBasic(self): props = QgsProjectElevationProperties(None) self.assertIsInstance(props.terrainProvider(), QgsFlatTerrainProvider) + self.assertTrue(props.elevationRange().isInfinite()) provider = QgsRasterDemTerrainProvider() provider.setOffset(5) @@ -43,6 +46,16 @@ def testBasic(self): self.assertIsInstance(props.terrainProvider(), QgsRasterDemTerrainProvider) + range_changed_spy = QSignalSpy(props.elevationRangeChanged) + props.setElevationRange(QgsDoubleRange(34.2, 78.6)) + self.assertEqual(props.elevationRange(), QgsDoubleRange(34.2, 78.6)) + self.assertEqual(len(range_changed_spy), 1) + self.assertEqual(range_changed_spy[-1][0], QgsDoubleRange(34.2, 78.6)) + + # no signal if not changed + props.setElevationRange(QgsDoubleRange(34.2, 78.6)) + self.assertEqual(len(range_changed_spy), 1) + doc = QDomDocument("testdoc") elem = props.writeXml(doc, QgsReadWriteContext()) @@ -51,6 +64,7 @@ def testBasic(self): self.assertIsInstance(props2.terrainProvider(), QgsRasterDemTerrainProvider) self.assertEqual(props2.terrainProvider().offset(), 5) self.assertEqual(props2.terrainProvider().scale(), 3) + self.assertEqual(props2.elevationRange(), QgsDoubleRange(34.2, 78.6)) mesh_provider = QgsMeshTerrainProvider() mesh_provider.setOffset(2) diff --git a/tests/src/python/test_qgsprojectionselectionwidgets.py b/tests/src/python/test_qgsprojectionselectionwidgets.py index c9c2e27668ce..1f801146fc27 100644 --- a/tests/src/python/test_qgsprojectionselectionwidgets.py +++ b/tests/src/python/test_qgsprojectionselectionwidgets.py @@ -190,6 +190,23 @@ def testRecent(self): self.assertEqual(cb.itemText(3), 'Layer CRS: EPSG:3111 - GDA94 / Vicgrid') self.assertEqual(cb.itemText(4), 'EPSG:28356 - GDA94 / MGA zone 56') + spy = QSignalSpy(w.crsChanged) + cb.setCurrentIndex(1) + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], QgsCoordinateReferenceSystem('EPSG:3113')) + cb.setCurrentIndex(0) + self.assertEqual(len(spy), 2) + self.assertEqual(spy[-1][0], QgsCoordinateReferenceSystem('EPSG:4326')) + cb.setCurrentIndex(2) + self.assertEqual(len(spy), 3) + self.assertEqual(spy[-1][0], QgsCoordinateReferenceSystem('EPSG:4326')) + cb.setCurrentIndex(3) + self.assertEqual(len(spy), 4) + self.assertEqual(spy[-1][0], QgsCoordinateReferenceSystem('EPSG:3111')) + cb.setCurrentIndex(4) + self.assertEqual(len(spy), 5) + self.assertEqual(spy[-1][0], QgsCoordinateReferenceSystem('EPSG:28356')) + def testFilters(self): registry = QgsApplication.coordinateReferenceSystemRegistry() registry.clearRecent() diff --git a/tests/src/python/test_qgsrange.py b/tests/src/python/test_qgsrange.py index ed59039bce15..f1cef0877539 100644 --- a/tests/src/python/test_qgsrange.py +++ b/tests/src/python/test_qgsrange.py @@ -10,7 +10,7 @@ __copyright__ = 'Copyright 2017, The QGIS Project' from qgis.PyQt.QtCore import QDate -from qgis.core import QgsDateRange, QgsDoubleRange, QgsIntRange +from qgis.core import QgsDateRange, QgsDoubleRange, QgsIntRange, Qgis from qgis.testing import unittest @@ -22,12 +22,42 @@ def testGetters(self): self.assertEqual(range.upper(), 11) self.assertTrue(range.includeLower()) self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) range = QgsIntRange(-1, 3, False, False) self.assertEqual(range.lower(), -1) self.assertEqual(range.upper(), 3) self.assertFalse(range.includeLower()) self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeLowerExcludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeLowerExcludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeLowerIncludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeLowerIncludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) def testIsInfinite(self): range = QgsIntRange() @@ -207,12 +237,42 @@ def testGetters(self): self.assertEqual(range.upper(), 11) self.assertTrue(range.includeLower()) self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) range = QgsDoubleRange(-1.0, 3.0, False, False) self.assertEqual(range.lower(), -1) self.assertEqual(range.upper(), 3) self.assertFalse(range.includeLower()) self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeBoth) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.IncludeLowerExcludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertTrue(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.IncludeLowerExcludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeLowerIncludeUpper) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertTrue(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeLowerIncludeUpper) + + range = QgsIntRange(-1, 3, Qgis.RangeLimits.ExcludeBoth) + self.assertEqual(range.lower(), -1) + self.assertEqual(range.upper(), 3) + self.assertFalse(range.includeLower()) + self.assertFalse(range.includeUpper()) + self.assertEqual(range.rangeLimits(), Qgis.RangeLimits.ExcludeBoth) def testEquality(self): self.assertEqual(QgsDoubleRange(1, 10), QgsDoubleRange(1, 10)) diff --git a/tests/src/python/test_qgsrangeslider.py b/tests/src/python/test_qgsrangeslider.py index 20cab71d3bef..105147e74697 100644 --- a/tests/src/python/test_qgsrangeslider.py +++ b/tests/src/python/test_qgsrangeslider.py @@ -41,6 +41,10 @@ def testSettersGetters(self): w.setPageStep(5) self.assertEqual(w.pageStep(), 5) + self.assertEqual(w.fixedRangeSize(), -1) + w.setFixedRangeSize(5) + self.assertEqual(w.fixedRangeSize(), 5) + def testLimits(self): w = QgsRangeSlider() spy = QSignalSpy(w.rangeLimitsChanged) @@ -268,6 +272,56 @@ def testChangeLimitsOutsideValue(self): self.assertEqual(len(spy), 6) self.assertEqual(spy[-1], [3, 7]) + def test_fixed_range_width(self): + """ + Test interactions with fixed range widths + """ + w = QgsRangeSlider() + w.setRangeLimits(0, 100) + w.setFixedRangeSize(10) + self.assertEqual(w.upperValue() - w.lowerValue(), 10) + + w.setUpperValue(70) + self.assertEqual(w.upperValue(), 70) + self.assertEqual(w.lowerValue(), 60) + + w.setLowerValue(5) + self.assertEqual(w.upperValue(), 15) + self.assertEqual(w.lowerValue(), 5) + + # try to force value outside range + w.setUpperValue(5) + self.assertEqual(w.upperValue(), 10) + self.assertEqual(w.lowerValue(), 0) + + w.setLowerValue(95) + self.assertEqual(w.upperValue(), 100) + self.assertEqual(w.lowerValue(), 90) + + w.setRange(0, 5) + self.assertEqual(w.upperValue(), 10) + self.assertEqual(w.lowerValue(), 0) + + w.setRange(95, 100) + self.assertEqual(w.upperValue(), 100) + self.assertEqual(w.lowerValue(), 90) + + # with zero width fixed range + w.setFixedRangeSize(0) + self.assertEqual(w.upperValue() - w.lowerValue(), 0) + + w.setUpperValue(70) + self.assertEqual(w.upperValue(), 70) + self.assertEqual(w.lowerValue(), 70) + + w.setLowerValue(5) + self.assertEqual(w.upperValue(), 5) + self.assertEqual(w.lowerValue(), 5) + + w.setRange(0, 5) + self.assertEqual(w.upperValue(), 0) + self.assertEqual(w.lowerValue(), 0) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrastercontourrenderer.py b/tests/src/python/test_qgsrastercontourrenderer.py new file mode 100644 index 000000000000..a5e7f0441fcf --- /dev/null +++ b/tests/src/python/test_qgsrastercontourrenderer.py @@ -0,0 +1,64 @@ +"""QGIS Unit tests for QgsRasterContourRenderer + +From build dir, run: +ctest -R PyQgsRasterContourRenderer -V + +.. 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.core import ( + QgsRasterContourRenderer, + QgsRasterLayer, +) +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 TestQgsRasterContourRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsRasterContourRenderer(layer.dataProvider()) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_contour_invalid_layer(self): + """ + Test contour raster render band with a broken layer path + """ + renderer = QgsRasterContourRenderer(None) + + self.assertEqual(renderer.inputBand(), 1) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrasterlayer.py b/tests/src/python/test_qgsrasterlayer.py index 6c52760cfc12..66fe966741ae 100644 --- a/tests/src/python/test_qgsrasterlayer.py +++ b/tests/src/python/test_qgsrasterlayer.py @@ -456,141 +456,6 @@ def testQgsRasterMinMaxOrigin(self): mmoUnserialized.readXml(parentElem) self.assertEqual(mmo, mmoUnserialized) - def testPaletted(self): - """ test paletted raster renderer with raster with color table""" - path = os.path.join(unitTestDataPath('raster'), - 'with_color_table.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, - [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) - - self.assertEqual(renderer.nColors(), 2) - self.assertEqual(renderer.usesBands(), [1]) - - # test labels - self.assertEqual(renderer.label(1), 'class 2') - self.assertEqual(renderer.label(3), 'class 1') - self.assertFalse(renderer.label(101)) - - # test legend symbology - should be sorted by value - legend = renderer.legendSymbologyItems() - self.assertEqual(legend[0][0], 'class 2') - self.assertEqual(legend[1][0], 'class 1') - self.assertEqual(legend[0][1].name(), '#00ff00') - self.assertEqual(legend[1][1].name(), '#ff0000') - - # test retrieving classes - classes = renderer.classes() - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'class 1') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - - # test set label - # bad index - renderer.setLabel(1212, 'bad') - renderer.setLabel(3, 'new class') - self.assertEqual(renderer.label(3), 'new class') - - # color ramp - r = QgsLimitedRandomColorRamp(5) - renderer.setSourceColorRamp(r) - self.assertEqual(renderer.sourceColorRamp().type(), 'random') - self.assertEqual(renderer.sourceColorRamp().count(), 5) - - # clone - new_renderer = renderer.clone() - classes = new_renderer.classes() - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'new class') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - self.assertEqual(new_renderer.sourceColorRamp().type(), 'random') - self.assertEqual(new_renderer.sourceColorRamp().count(), 5) - - # write to xml and read - doc = QDomDocument('testdoc') - elem = doc.createElement('qgis') - renderer.writeXml(doc, elem) - restored = QgsPalettedRasterRenderer.create(elem.firstChild().toElement(), layer.dataProvider()) - self.assertTrue(restored) - self.assertEqual(restored.usesBands(), [1]) - classes = restored.classes() - self.assertTrue(classes) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[0].label, 'class 2') - self.assertEqual(classes[1].label, 'new class') - self.assertEqual(classes[0].color.name(), '#00ff00') - self.assertEqual(classes[1].color.name(), '#ff0000') - self.assertEqual(restored.sourceColorRamp().type(), 'random') - self.assertEqual(restored.sourceColorRamp().count(), 5) - - # render test - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer', - 'paletted_renderer', - ms) - ) - - def testPalettedBand(self): - """ test paletted raster render band""" - path = os.path.join(unitTestDataPath(), - 'landsat_4326.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 2, - [QgsPalettedRasterRenderer.Class(137, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(138, QColor(255, 0, 0), 'class 1'), - QgsPalettedRasterRenderer.Class(139, QColor(0, 0, 255), 'class 1')]) - - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer_band2', - 'paletted_renderer_band2', - ms) - ) - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 3, - [QgsPalettedRasterRenderer.Class(120, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(123, QColor(255, 0, 0), 'class 1'), - QgsPalettedRasterRenderer.Class(124, QColor(0, 0, 255), 'class 1')]) - - layer.setRenderer(renderer) - ms = QgsMapSettings() - ms.setLayers([layer]) - ms.setExtent(layer.extent()) - - self.assertTrue( - self.render_map_settings_check( - 'paletted_renderer_band3', - 'paletted_renderer_band3', - ms) - ) - def testBrightnessContrastGamma(self): """ test raster brightness/contrast/gamma filter""" path = os.path.join(unitTestDataPath(), @@ -731,312 +596,6 @@ def testInvertSemiOpaqueColors(self): ms) ) - def testPalettedColorTableToClassData(self): - entries = [QgsColorRampShader.ColorRampItem(5, QColor(255, 0, 0), 'item1'), - QgsColorRampShader.ColorRampItem(3, QColor(0, 255, 0), 'item2'), - QgsColorRampShader.ColorRampItem(6, QColor(0, 0, 255), 'item3'), - ] - classes = QgsPalettedRasterRenderer.colorTableToClassData(entries) - self.assertEqual(classes[0].value, 5) - self.assertEqual(classes[1].value, 3) - self.assertEqual(classes[2].value, 6) - self.assertEqual(classes[0].label, 'item1') - self.assertEqual(classes[1].label, 'item2') - self.assertEqual(classes[2].label, 'item3') - self.assertEqual(classes[0].color.name(), '#ff0000') - self.assertEqual(classes[1].color.name(), '#00ff00') - self.assertEqual(classes[2].color.name(), '#0000ff') - - # test #13263 - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(1)) - self.assertEqual(len(classes), 4) - classes = QgsPalettedRasterRenderer.colorTableToClassData(layer.dataProvider().colorTable(15)) - self.assertEqual(len(classes), 256) - - def testLoadPalettedColorDataFromString(self): - """ - Test interpreting a bunch of color data format strings - """ - esri_clr_format = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' - esri_clr_format_win = '1 255 255 0\r\n2 64 0 128\r\n3 255 32 32\r\n4 0 255 0\r\n5 0 0 255' - esri_clr_format_tab = '1\t255\t255\t0\n2\t64\t0\t128\n3\t255\t32\t32\n4\t0\t255\t0\n5\t0\t0\t255' - esri_clr_spaces = '1 255 255 0\n2 64 0 128\n3 255 32 32\n4 0 255 0\n5 0 0 255' - gdal_clr_comma = '1,255,255,0\n2,64,0,128\n3,255,32,32\n4,0,255,0\n5,0,0,255' - gdal_clr_colon = '1:255:255:0\n2:64:0:128\n3:255:32:32\n4:0:255:0\n5:0:0:255' - for f in [esri_clr_format, - esri_clr_format_win, - esri_clr_format_tab, - esri_clr_spaces, - gdal_clr_comma, - gdal_clr_colon]: - classes = QgsPalettedRasterRenderer.classDataFromString(f) - self.assertEqual(len(classes), 5) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#ffff00') - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#400080') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#ff2020') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#00ff00') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - - grass_named_colors = '0 white\n1 yellow\n3 black\n6 blue\n9 magenta\n11 aqua\n13 grey\n14 gray\n15 orange\n19 brown\n21 purple\n22 violet\n24 indigo\n90 green\n180 cyan\n270 red\n' - classes = QgsPalettedRasterRenderer.classDataFromString(grass_named_colors) - self.assertEqual(len(classes), 16) - self.assertEqual(classes[0].value, 0) - self.assertEqual(classes[0].color.name(), '#ffffff') - self.assertEqual(classes[1].value, 1) - self.assertEqual(classes[1].color.name(), '#ffff00') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#000000') - self.assertEqual(classes[3].value, 6) - self.assertEqual(classes[3].color.name(), '#0000ff') - self.assertEqual(classes[4].value, 9) - self.assertEqual(classes[4].color.name(), '#ff00ff') - self.assertEqual(classes[5].value, 11) - self.assertEqual(classes[5].color.name(), '#00ffff') - self.assertEqual(classes[6].value, 13) - self.assertEqual(classes[6].color.name(), '#808080') - self.assertEqual(classes[7].value, 14) - self.assertEqual(classes[7].color.name(), '#808080') - self.assertEqual(classes[8].value, 15) - self.assertEqual(classes[8].color.name(), '#ffa500') - self.assertEqual(classes[9].value, 19) - self.assertEqual(classes[9].color.name(), '#a52a2a') - self.assertEqual(classes[10].value, 21) - self.assertEqual(classes[10].color.name(), '#800080') - self.assertEqual(classes[11].value, 22) - self.assertEqual(classes[11].color.name(), '#ee82ee') - self.assertEqual(classes[12].value, 24) - self.assertEqual(classes[12].color.name(), '#4b0082') - self.assertEqual(classes[13].value, 90) - self.assertEqual(classes[13].color.name(), '#008000') - self.assertEqual(classes[14].value, 180) - self.assertEqual(classes[14].color.name(), '#00ffff') - self.assertEqual(classes[15].value, 270) - self.assertEqual(classes[15].color.name(), '#ff0000') - - gdal_alpha = '1:255:255:0:0\n2:64:0:128:50\n3:255:32:32:122\n4:0:255:0:200\n5:0:0:255:255' - classes = QgsPalettedRasterRenderer.classDataFromString(gdal_alpha) - self.assertEqual(len(classes), 5) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#ffff00') - self.assertEqual(classes[0].color.alpha(), 0) - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#400080') - self.assertEqual(classes[1].color.alpha(), 50) - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#ff2020') - self.assertEqual(classes[2].color.alpha(), 122) - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#00ff00') - self.assertEqual(classes[3].color.alpha(), 200) - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - self.assertEqual(classes[4].color.alpha(), 255) - - # qgis style, with labels - qgis_style = '3 255 0 0 255 class 1\n4 0 255 0 200 class 2' - classes = QgsPalettedRasterRenderer.classDataFromString(qgis_style) - self.assertEqual(len(classes), 2) - self.assertEqual(classes[0].value, 3) - self.assertEqual(classes[0].color.name(), '#ff0000') - self.assertEqual(classes[0].color.alpha(), 255) - self.assertEqual(classes[0].label, 'class 1') - self.assertEqual(classes[1].value, 4) - self.assertEqual(classes[1].color.name(), '#00ff00') - self.assertEqual(classes[1].color.alpha(), 200) - self.assertEqual(classes[1].label, 'class 2') - - # some bad inputs - bad = '' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = '\n\n\n' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = 'x x x x' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 0) - bad = '1 255 0 0\n2 255 255\n3 255 0 255' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 2) - bad = '1 255 a 0' - classes = QgsPalettedRasterRenderer.classDataFromString(bad) - self.assertEqual(len(classes), 1) - - def testLoadPalettedClassDataFromFile(self): - # bad file - classes = QgsPalettedRasterRenderer.classDataFromFile('ajdhjashjkdh kjahjkdhk') - self.assertEqual(len(classes), 0) - - # good file! - path = os.path.join(unitTestDataPath('raster'), - 'test.clr') - classes = QgsPalettedRasterRenderer.classDataFromFile(path) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].color.name(), '#000000') - self.assertEqual(classes[0].color.alpha(), 255) - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].color.name(), '#c8c8c8') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].color.name(), '#006e00') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].color.name(), '#6e4100') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].color.name(), '#0000ff') - self.assertEqual(classes[4].color.alpha(), 255) - self.assertEqual(classes[5].value, 6) - self.assertEqual(classes[5].color.name(), '#0059ff') - self.assertEqual(classes[6].value, 7) - self.assertEqual(classes[6].color.name(), '#00aeff') - self.assertEqual(classes[7].value, 8) - self.assertEqual(classes[7].color.name(), '#00fff6') - self.assertEqual(classes[8].value, 9) - self.assertEqual(classes[8].color.name(), '#eeff00') - self.assertEqual(classes[9].value, 10) - self.assertEqual(classes[9].color.name(), '#ffb600') - - def testPalettedClassDataToString(self): - classes = [QgsPalettedRasterRenderer.Class(1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] - self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), - '1 0 255 0 255 class 2\n3 255 0 0 255 class 1') - # must be sorted by value to work OK in ArcMap - classes = [QgsPalettedRasterRenderer.Class(4, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')] - self.assertEqual(QgsPalettedRasterRenderer.classDataToString(classes), - '3 255 0 0 255 class 1\n4 0 255 0 255 class 2') - - def testPalettedClassDataFromLayer(self): - # no layer - classes = QgsPalettedRasterRenderer.classDataFromRaster(None, 1) - self.assertFalse(classes) - - # 10 class layer - path = os.path.join(unitTestDataPath('raster'), - 'with_color_table.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer10 = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].value, 1) - self.assertEqual(classes[0].label, '1') - self.assertEqual(classes[1].value, 2) - self.assertEqual(classes[1].label, '2') - self.assertEqual(classes[2].value, 3) - self.assertEqual(classes[2].label, '3') - self.assertEqual(classes[3].value, 4) - self.assertEqual(classes[3].label, '4') - self.assertEqual(classes[4].value, 5) - self.assertEqual(classes[4].label, '5') - self.assertEqual(classes[5].value, 6) - self.assertEqual(classes[5].label, '6') - self.assertEqual(classes[6].value, 7) - self.assertEqual(classes[6].label, '7') - self.assertEqual(classes[7].value, 8) - self.assertEqual(classes[7].label, '8') - self.assertEqual(classes[8].value, 9) - self.assertEqual(classes[8].label, '9') - self.assertEqual(classes[9].value, 10) - self.assertEqual(classes[9].label, '10') - - # bad band - self.assertFalse(QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 10101010)) - - # with ramp - r = QgsGradientColorRamp(QColor(200, 0, 0, 100), QColor(0, 200, 0, 200)) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1, r) - self.assertEqual(len(classes), 10) - self.assertEqual(classes[0].color.name(), '#c80000') - self.assertEqual(classes[1].color.name(), '#b21600') - self.assertEqual(classes[2].color.name(), '#9c2c00') - self.assertIn(classes[3].color.name(), ('#854200', '#854300')) - self.assertEqual(classes[4].color.name(), '#6f5900') - self.assertEqual(classes[5].color.name(), '#596f00') - self.assertIn(classes[6].color.name(), ('#428500', '#438500')) - self.assertEqual(classes[7].color.name(), '#2c9c00') - self.assertEqual(classes[8].color.name(), '#16b200') - self.assertEqual(classes[9].color.name(), '#00c800') - - # 30 class layer - path = os.path.join(unitTestDataPath('raster'), - 'unique_1.tif') - info = QFileInfo(path) - base_name = info.baseName() - layer10 = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer10.dataProvider(), 1) - self.assertEqual(len(classes), 30) - expected = [11, 21, 22, 24, 31, 82, 2002, 2004, 2014, 2019, 2027, 2029, 2030, 2080, 2081, 2082, 2088, 2092, - 2097, 2098, 2099, 2105, 2108, 2110, 2114, 2118, 2126, 2152, 2184, 2220] - self.assertEqual([c.value for c in classes], expected) - - # bad layer - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) - self.assertFalse(classes) - - def testPalettedRendererWithNegativeColorValue(self): - """ test paletted raster renderer with negative values in color table""" - - path = os.path.join(unitTestDataPath('raster'), - 'hub13263.vrt') - info = QFileInfo(path) - base_name = info.baseName() - layer = QgsRasterLayer(path, base_name) - self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') - - renderer = QgsPalettedRasterRenderer(layer.dataProvider(), 1, - [QgsPalettedRasterRenderer.Class(-1, QColor(0, 255, 0), 'class 2'), - QgsPalettedRasterRenderer.Class(3, QColor(255, 0, 0), 'class 1')]) - - self.assertEqual(renderer.nColors(), 2) - self.assertEqual(renderer.usesBands(), [1]) - - def testPalettedRendererWithFloats(self): - """Tests for https://github.com/qgis/QGIS/issues/39058""" - - tempdir = QTemporaryDir() - temppath = os.path.join(tempdir.path(), 'paletted.tif') - - # Create a float raster with unique values up to 65536 + one extra row - driver = gdal.GetDriverByName('GTiff') - outRaster = driver.Create(temppath, 256, 256 + 1, 1, gdal.GDT_Float32) - outband = outRaster.GetRasterBand(1) - data = [] - for r in range(256 + 1): - data.append(list(range(r * 256, (r + 1) * 256))) - npdata = np.array(data, np.float32) - outband.WriteArray(npdata) - outband.FlushCache() - outRaster.FlushCache() - del outRaster - - layer = QgsRasterLayer(temppath, 'paletted') - self.assertTrue(layer.isValid()) - self.assertEqual(layer.dataProvider().dataType(1), Qgis.DataType.Float32) - classes = QgsPalettedRasterRenderer.classDataFromRaster(layer.dataProvider(), 1) - # Check max classes count, hardcoded in QGIS renderer - self.assertEqual(len(classes), 65536) - class_values = [] - for c in classes: - class_values.append(c.value) - self.assertEqual(sorted(class_values), list(range(65536))) - def testClone(self): myPath = os.path.join(unitTestDataPath('raster'), 'band1_float32_noct_epsg4326.tif') diff --git a/tests/src/python/test_qgsrasterlayerelevationproperties.py b/tests/src/python/test_qgsrasterlayerelevationproperties.py index f5b46d2a4d96..bbae91f795d4 100644 --- a/tests/src/python/test_qgsrasterlayerelevationproperties.py +++ b/tests/src/python/test_qgsrasterlayerelevationproperties.py @@ -9,7 +9,9 @@ __date__ = '09/11/2020' __copyright__ = 'Copyright 2020, The QGIS Project' +import math import os +import unittest from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( @@ -18,9 +20,10 @@ QgsLineSymbol, QgsRasterLayerElevationProperties, QgsReadWriteContext, - QgsRasterLayer + QgsRasterLayer, + QgsDoubleRange, + QgsProperty ) -import unittest from qgis.testing import start_app, QgisTestCase from utilities import unitTestDataPath @@ -30,16 +33,23 @@ class TestQgsRasterLayerElevationProperties(QgisTestCase): - def testBasic(self): + def test_basic_elevation_surface(self): + """ + Basic tests for the class using the RepresentsElevationSurface mode + """ props = QgsRasterLayerElevationProperties(None) + self.assertEqual(props.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props.zScale(), 1) self.assertEqual(props.zOffset(), 0) self.assertFalse(props.isEnabled()) self.assertFalse(props.hasElevation()) self.assertEqual(props.bandNumber(), 1) + self.assertTrue(props.fixedRange().isInfinite()) self.assertIsInstance(props.profileLineSymbol(), QgsLineSymbol) self.assertIsInstance(props.profileFillSymbol(), QgsFillSymbol) - self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.Line) + self.assertEqual(props.profileSymbology(), + Qgis.ProfileSurfaceSymbology.Line) props.setZOffset(0.5) props.setZScale(2) @@ -52,10 +62,12 @@ def testBasic(self): self.assertTrue(props.isEnabled()) self.assertEqual(props.bandNumber(), 2) self.assertTrue(props.hasElevation()) - self.assertEqual(props.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props.elevationLimit(), 909) - sym = QgsLineSymbol.createSimple({'outline_color': '#ff4433', 'outline_width': 0.5}) + sym = QgsLineSymbol.createSimple( + {'outline_color': '#ff4433', 'outline_width': 0.5}) props.setProfileLineSymbol(sym) self.assertEqual(props.profileLineSymbol().color().name(), '#ff4433') @@ -69,25 +81,250 @@ def testBasic(self): props2 = QgsRasterLayerElevationProperties(None) props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) self.assertEqual(props2.bandNumber(), 2) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') self.assertEqual(props2.profileFillSymbol().color().name(), '#ff44ff') - self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props2.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.RepresentsElevationSurface) self.assertEqual(props2.zScale(), 2) self.assertEqual(props2.zOffset(), 0.5) self.assertTrue(props2.isEnabled()) self.assertEqual(props2.bandNumber(), 2) self.assertEqual(props2.profileLineSymbol().color().name(), '#ff4433') self.assertEqual(props2.profileFillSymbol().color().name(), '#ff44ff') - self.assertEqual(props2.profileSymbology(), Qgis.ProfileSurfaceSymbology.FillBelow) + self.assertEqual(props2.profileSymbology(), + Qgis.ProfileSurfaceSymbology.FillBelow) self.assertEqual(props2.elevationLimit(), 909) + def test_basic_fixed_range(self): + """ + Basic tests for the class using the FixedElevationRange mode + """ + props = QgsRasterLayerElevationProperties(None) + self.assertTrue(props.fixedRange().isInfinite()) + + props.setMode(Qgis.RasterElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(103.1, 106.8)) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRange(), QgsDoubleRange(103.1, 106.8)) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 106.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8)) + + # include lower, exclude upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)) + + # exclude lower, include upper + props.setFixedRange(QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRange(), QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)) + + def test_basic_fixed_range_per_band(self): + """ + Basic tests for the class using the FixedRangePerBand mode + """ + props = QgsRasterLayerElevationProperties(None) + self.assertFalse(props.fixedRangePerBand()) + + props.setMode(Qgis.RasterElevationMode.FixedRangePerBand) + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + self.assertEqual(props.calculateZRange(None), + QgsDoubleRange(103.1, 126.8)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 104.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(114.8, 124.8))) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(128.8, 134.8))) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(1, 2)), -1) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(103, 104)), 1) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(104, 108)), 2) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(112, 112)), 2) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(112, 118)), 3) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(118, 218)), 3) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(212, 218)), -1) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedRangePerBand) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedRangePerBand) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8), + 2: QgsDoubleRange(106.8, 116.8), + 3: QgsDoubleRange(116.8, 126.8)}) + + # include lower, exclude upper + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8, + includeLower=True, + includeUpper=False)}) + + # exclude lower, include upper + props.setFixedRangePerBand({1: QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), {1: QgsDoubleRange(103.1, 106.8, + includeLower=False, + includeUpper=True)}) + + def test_basic_dynamic_range_per_band(self): + """ + Basic tests for the class using the DynamicRangePerBand mode + """ + raster_layer = QgsRasterLayer(os.path.join(unitTestDataPath(), 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + props = QgsRasterLayerElevationProperties(None) + + props.setMode(Qgis.RasterElevationMode.DynamicRangePerBand) + props.dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation, + QgsProperty.fromExpression("@band*2")) + props.dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation, + QgsProperty.fromExpression("@band*2 + 1")) + # fixed ranges should not be affected by scale/offset + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation).asExpression(), + '@band*2') + self.assertEqual(props.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation).asExpression(), + '@band*2 + 1') + + # layer is required to calculated z range + self.assertEqual(props.calculateZRange(None), QgsDoubleRange()) + self.assertEqual(props.calculateZRange(raster_layer), QgsDoubleRange(2, 19)) + + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8))) + self.assertTrue(props.isVisibleInZRange(QgsDoubleRange(3.1, 6.8), raster_layer)) + self.assertFalse( + props.isVisibleInZRange(QgsDoubleRange(1.1, 1.9), raster_layer)) + self.assertFalse(props.isVisibleInZRange(QgsDoubleRange(104.8, 114.8))) + + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(1, 2)), -1) + self.assertEqual( + props.bandForElevationRange(None, QgsDoubleRange(3.1, 6.8)), -1) + self.assertEqual( + props.bandForElevationRange(raster_layer, QgsDoubleRange(3.1, 6.8)), 3) + self.assertEqual( + props.bandForElevationRange(raster_layer, QgsDoubleRange(13.1, 16.8)), 8) + self.assertEqual( + props.bandForElevationRange(raster_layer, QgsDoubleRange(113.1, 116.8)), -1) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.DynamicRangePerBand) + self.assertEqual(props2.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation).asExpression(), + '@band*2') + self.assertEqual(props2.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation).asExpression(), + '@band*2 + 1') + + props2 = props.clone() + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.DynamicRangePerBand) + self.assertEqual( + props2.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation).asExpression(), + '@band*2') + + self.assertEqual(props2.dataDefinedProperties().property( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation).asExpression(), + '@band*2 + 1') + def test_looks_like_dem(self): layer = QgsRasterLayer( os.path.join(unitTestDataPath(), 'landsat.tif'), 'i am not a dem') @@ -99,7 +336,9 @@ def test_looks_like_dem(self): # layer data type doesn't look like a dem layer = QgsRasterLayer( - os.path.join(unitTestDataPath(), 'raster/band1_byte_ct_epsg4326.tif'), 'i am not a dem') + os.path.join(unitTestDataPath(), + 'raster/band1_byte_ct_epsg4326.tif'), + 'i am not a dem') self.assertTrue(layer.isValid()) self.assertFalse( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) @@ -109,7 +348,8 @@ def test_looks_like_dem(self): self.assertTrue(layer.isValid()) # not like a dem, the layer name doesn't hint this to - self.assertFalse(QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) + self.assertFalse( + QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) layer.setName('i am a DEM') self.assertTrue( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) @@ -121,6 +361,86 @@ def test_looks_like_dem(self): self.assertTrue( QgsRasterLayerElevationProperties.layerLooksLikeDem(layer)) + def test_elevation_range_for_pixel_value(self): + """ + Test transforming pixel values to elevation ranges + """ + raster_layer = QgsRasterLayer(os.path.join(unitTestDataPath(), 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + props = QgsRasterLayerElevationProperties(raster_layer) + + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=3), + QgsDoubleRange()) + props.setEnabled(True) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=3), + QgsDoubleRange(3, 3)) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=math.nan), + QgsDoubleRange()) + + # check that band number is respected + props.setBandNumber(2) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=3), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=2, pixelValue=3), + QgsDoubleRange(3, 3)) + + # check that offset/scale is respected + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=2, pixelValue=3), + QgsDoubleRange(6.5, 6.5)) + + # with fixed range mode + props.setMode(Qgis.RasterElevationMode.FixedElevationRange) + props.setFixedRange(QgsDoubleRange(11, 15)) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=3), + QgsDoubleRange(11, 15)) + + # with fixed range per band mode + props.setMode(Qgis.RasterElevationMode.FixedRangePerBand) + props.setFixedRangePerBand({1: QgsDoubleRange(11, 15), + 2: QgsDoubleRange(16, 25)}) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=3), + QgsDoubleRange(11, 15)) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=2, pixelValue=3), + QgsDoubleRange(16, 25)) + + # with dynamic range per band mode + props.setMode(Qgis.RasterElevationMode.DynamicRangePerBand) + props.dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation, + QgsProperty.fromExpression("@band*2")) + props.dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation, + QgsProperty.fromExpression("@band*2 + 1")) + self.assertEqual( + props.elevationRangeForPixelValue(layer=None, band=1, pixelValue=math.nan), + QgsDoubleRange()) + self.assertEqual( + props.elevationRangeForPixelValue(layer=raster_layer, band=1, pixelValue=3), + QgsDoubleRange(2, 3)) + self.assertEqual( + props.elevationRangeForPixelValue(layer=raster_layer, band=2, pixelValue=3), + QgsDoubleRange(4, 5)) + self.assertEqual( + props.elevationRangeForPixelValue(layer=raster_layer, band=100, pixelValue=3), + QgsDoubleRange()) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterlayerrenderer.py b/tests/src/python/test_qgsrasterlayerrenderer.py index 97af105176b2..727a55affca5 100644 --- a/tests/src/python/test_qgsrasterlayerrenderer.py +++ b/tests/src/python/test_qgsrasterlayerrenderer.py @@ -11,14 +11,26 @@ import os -from qgis.PyQt.QtCore import QSize +from qgis.PyQt.QtCore import ( + QSize, + QDate, + QTime, + QDateTime +) from qgis.core import ( + Qgis, QgsCoordinateReferenceSystem, QgsGeometry, QgsMapClippingRegion, QgsMapSettings, QgsRasterLayer, QgsRectangle, + QgsDoubleRange, + QgsSingleBandGrayRenderer, + QgsContrastEnhancement, + QgsRasterLayerElevationProperties, + QgsProperty, + QgsDateTimeRange ) import unittest from qgis.testing import start_app, QgisTestCase @@ -63,6 +75,448 @@ def testRenderWithPainterClipRegions(self): mapsettings) ) + def test_render_dem_with_z_range_filter(self): + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif')) + self.assertTrue(raster_layer.isValid()) + # start with no elevation settings on layer + self.assertFalse(raster_layer.elevationProperties().hasElevation()) + + 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]) + map_settings.setZRange(QgsDoubleRange(100, 130)) + + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings, not elevation enabled layer', + 'dem_no_filter', + map_settings) + ) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + # no filter on map settings + map_settings.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, elevation enabled layer', + 'dem_no_filter', + map_settings) + ) + + # filter on map settings, elevation enabled layer => should be filtered + map_settings.setZRange(QgsDoubleRange(100, 130)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings, elevation enabled layer', + 'dem_filter', + map_settings) + ) + + # with offset and scaling + raster_layer.elevationProperties().setZOffset(50) + raster_layer.elevationProperties().setZScale(0.75) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings, elevation enabled layer with offset and scale', + 'dem_filter_offset_and_scale', + map_settings) + ) + + def test_render_fixed_elevation_range_with_z_range_filter(self): + """ + Test rendering a raster with a fixed elevation range when + map settings has a z range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif')) + self.assertTrue(raster_layer.isValid()) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedElevationRange + ) + raster_layer.elevationProperties().setFixedRange( + QgsDoubleRange(33, 38) + ) + + 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.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, fixed elevation range layer', + 'dem_no_filter', + map_settings) + ) + + # map settings range includes layer's range + map_settings.setZRange(QgsDoubleRange(30, 35)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings includes layers fixed range', + 'fixed_elevation_range_included', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layers fixed range', + 'fixed_elevation_range_excluded', + map_settings) + ) + + def test_render_fixed_range_per_band_with_z_range_filter(self): + """ + Test rendering a raster with a fixed range per band when + map settings has a z range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 3) + contrast = QgsContrastEnhancement() + contrast.setMinimumValue(70) + contrast.setMaximumValue(125) + renderer.setContrastEnhancement(contrast) + raster_layer.setRenderer(renderer) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.FixedRangePerBand + ) + raster_layer.elevationProperties().setFixedRangePerBand( + {3: QgsDoubleRange(33, 38), + 4: QgsDoubleRange(35, 40), + 5: QgsDoubleRange(40, 48)} + ) + + 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.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, elevation range per band', + 'elevation_range_per_band_no_filter', + map_settings) + ) + + # map settings range matches band 3 only + map_settings.setZRange(QgsDoubleRange(30, 34)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 only', + 'elevation_range_per_band_match_3', + map_settings) + ) + + # map settings range matches band 3 and 4, should pick the highest (4) + map_settings.setZRange(QgsDoubleRange(36, 38.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 and 4', + 'elevation_range_per_band_match_4', + map_settings) + ) + + # map settings range matches band 5 + map_settings.setZRange(QgsDoubleRange(46, 58.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 5', + 'elevation_range_per_band_match_5', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layer band ranges', + 'fixed_elevation_range_excluded', + map_settings) + ) + + def test_render_dynamic_range_per_band_with_z_range_filter(self): + """ + Test rendering a raster with a dynamic range per band when + map settings has a z range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 3) + contrast = QgsContrastEnhancement() + contrast.setMinimumValue(70) + contrast.setMaximumValue(125) + renderer.setContrastEnhancement(contrast) + raster_layer.setRenderer(renderer) + + # set layer as elevation enabled + raster_layer.elevationProperties().setEnabled(True) + raster_layer.elevationProperties().setMode( + Qgis.RasterElevationMode.DynamicRangePerBand + ) + + raster_layer.elevationProperties().dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandLowerElevation, + QgsProperty.fromExpression('case when @band=3 then 33 when @band=4 then 35 when @band=5 then 40 else null end') + ) + raster_layer.elevationProperties().dataDefinedProperties().setProperty( + QgsRasterLayerElevationProperties.Property.RasterPerBandUpperElevation, + QgsProperty.fromExpression('case when @band=3 then 38 when @band=4 then 40 when @band=5 then 48 else null end') + ) + + 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.setZRange(QgsDoubleRange()) + self.assertTrue( + self.render_map_settings_check( + 'No Z range filter on map settings, elevation range per band', + 'elevation_range_per_band_no_filter', + map_settings) + ) + + # map settings range matches band 3 only + map_settings.setZRange(QgsDoubleRange(30, 34)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 only', + 'elevation_range_per_band_match_3', + map_settings) + ) + + # map settings range matches band 3 and 4, should pick the highest (4) + map_settings.setZRange(QgsDoubleRange(36, 38.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 3 and 4', + 'elevation_range_per_band_match_4', + map_settings) + ) + + # map settings range matches band 5 + map_settings.setZRange(QgsDoubleRange(46, 58.5)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings matches band 5', + 'elevation_range_per_band_match_5', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setZRange(QgsDoubleRange(130, 135)) + self.assertTrue( + self.render_map_settings_check( + 'Z range filter on map settings outside of layer band ranges', + 'fixed_elevation_range_excluded', + map_settings) + ) + + def test_render_fixed_temporal_range_with_temporal_range_filter(self): + """ + Test rendering a raster with a fixed temporal range when + map settings has a temporal range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, '3d', 'dtm.tif')) + self.assertTrue(raster_layer.isValid()) + + # set layer as elevation enabled + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.FixedTemporalRange + ) + raster_layer.temporalProperties().setFixedTemporalRange( + QgsDateTimeRange( + QDateTime(QDate(2023, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 12, 31), + QTime(23, 59, 59)) + ) + ) + + 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 filter on map settings, fixed temporal range layer', + 'dem_no_filter', + map_settings) + ) + + # map settings range includes layer's range + map_settings.setIsTemporal(True) + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2022, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2023, 6, 30), + QTime(23, 59, 59)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings includes layers fixed range', + 'fixed_elevation_range_included', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2024, 1, 1), + QTime(0, 0, 0)), + QDateTime(QDate(2024, 6, 30), + QTime(23, 59, 59)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings outside of layers fixed range', + 'fixed_elevation_range_excluded', + map_settings) + ) + + def test_render_fixed_range_per_band_with_temporal_range_filter(self): + """ + Test rendering a raster with a fixed temporal range per band when + map settings has a temporal range filter + """ + raster_layer = QgsRasterLayer(os.path.join(TEST_DATA_DIR, 'landsat_4326.tif')) + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 3) + contrast = QgsContrastEnhancement() + contrast.setMinimumValue(70) + contrast.setMaximumValue(125) + renderer.setContrastEnhancement(contrast) + raster_layer.setRenderer(renderer) + + # set layer as temporal enabled + raster_layer.temporalProperties().setIsActive(True) + raster_layer.temporalProperties().setMode( + Qgis.RasterTemporalMode.FixedRangePerBand + ) + raster_layer.temporalProperties().setFixedRangePerBand( + { + 3: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), QTime(12, 13, 14)) + ), + 4: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)) + ), + 5: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + )} + ) + + 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, temporal range per band', + 'elevation_range_per_band_no_filter', + map_settings) + ) + + # map settings range matches band 3 only + map_settings.setIsTemporal(True) + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 6), + QTime(13, 13, 14)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings matches band 3 only', + 'elevation_range_per_band_match_3', + map_settings) + ) + + # map settings range matches band 3 and 4, should pick the latest (4) + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 5), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), + QTime(13, 13, 14)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings matches band 3 and 4', + 'elevation_range_per_band_match_4', + map_settings) + ) + + # map settings range matches band 5 + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 10), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 15), + QTime(13, 13, 14)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings matches band 5', + 'elevation_range_per_band_match_5', + map_settings) + ) + + # map settings range excludes layer's range + map_settings.setTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2024, 5, 10), + QTime(12, 13, 14)), + QDateTime(QDate(2024, 5, 15), + QTime(13, 13, 14)) + )) + self.assertTrue( + self.render_map_settings_check( + 'Temporal range filter on map settings outside of layer band ranges', + 'fixed_elevation_range_excluded', + map_settings) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterlayertemporalproperties.py b/tests/src/python/test_qgsrasterlayertemporalproperties.py new file mode 100644 index 000000000000..90cc2dfaa96c --- /dev/null +++ b/tests/src/python/test_qgsrasterlayertemporalproperties.py @@ -0,0 +1,278 @@ +"""QGIS Unit tests for QgsRasterLayerTemporalProperties + +.. 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 unittest + +from qgis.PyQt.QtCore import ( + QDate, + QTime, + QDateTime +) +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsRasterLayerTemporalProperties, + QgsReadWriteContext, + QgsDateTimeRange +) +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsRasterLayerTemporalProperties(QgisTestCase): + + def test_basic_fixed_range(self): + """ + Basic tests for the class using the FixedTemporalRange mode + """ + props = QgsRasterLayerTemporalProperties(None) + self.assertTrue(props.fixedTemporalRange().isInfinite()) + + props.setIsActive(True) + props.setMode(Qgis.RasterTemporalMode.FixedTemporalRange) + props.setFixedTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 7, 3), QTime(1, 3, 4)))) + self.assertEqual(props.fixedTemporalRange(), + QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 7, 3), QTime(1, 3, 4)))) + self.assertFalse(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2022, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2022, 7, 3), QTime(1, 3, 4))))) + self.assertTrue(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 1, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 6, 3), QTime(1, 3, 4))))) + self.assertTrue(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 6, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 9, 3), QTime(1, 3, 4))))) + self.assertFalse(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2024, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2024, 7, 3), QTime(1, 3, 4))))) + self.assertEqual(props.allTemporalRanges(None), + [QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 7, 3), QTime(1, 3, 4)))]) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerTemporalProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.mode(), + Qgis.RasterElevationMode.FixedElevationRange) + self.assertEqual(props2.fixedTemporalRange(), + QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 7, 3), QTime(1, 3, 4)))) + + def test_basic_fixed_range_per_band(self): + """ + Basic tests for the class using the FixedRangePerBand mode + """ + props = QgsRasterLayerTemporalProperties(None) + props.setIsActive(True) + self.assertFalse(props.fixedRangePerBand()) + + props.setMode(Qgis.RasterTemporalMode.FixedRangePerBand) + props.setFixedRangePerBand({ + 1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), QTime(12, 13, 14)) + ), + 2: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)) + ), + 3: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + )}) + self.assertEqual(props.fixedRangePerBand(), + { + 1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), + QTime(12, 13, 14)) + ), + 2: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)) + ), + 3: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + )}) + self.assertFalse(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 1), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)) + ))) + self.assertTrue(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 5), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)) + ))) + self.assertTrue(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 8), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + ))) + self.assertTrue(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 10), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 13), + QTime(12, 13, 14)) + ))) + self.assertFalse(props.isVisibleInTemporalRange(QgsDateTimeRange( + QDateTime(QDate(2023, 5, 12), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 14), + QTime(12, 13, 14)) + ))) + self.assertEqual(props.allTemporalRanges(None), + [QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), + QTime(12, 13, 14)) + ), + QgsDateTimeRange( + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)) + ), + QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + )]) + + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 4), + QTime(12, 13, 14)) + )), -1) + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 6), + QTime(12, 14, 14)) + )), 1) + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 3), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), + QTime(12, 14, 14)) + )), 2) + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 10), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 12), + QTime(12, 14, 14)) + )), 3) + self.assertEqual(props.bandForTemporalRange(None, QgsDateTimeRange( + QDateTime(QDate(2023, 5, 13), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 16), + QTime(12, 14, 14)) + )), -1) + + 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.FixedRangePerBand) + self.assertEqual(props2.fixedRangePerBand(), + { + 1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 6), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 8), + QTime(12, 13, 14)) + ), + 2: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 7), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)) + ), + 3: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)) + )}) + + # include lower, exclude upper + props.setFixedRangePerBand({1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)), + includeBeginning=True, + includeEnd=False)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerTemporalProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), + {1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)), + includeBeginning=True, + includeEnd=False)}) + + # exclude lower, include upper + props.setFixedRangePerBand({1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)), + includeBeginning=False, + includeEnd=True)}) + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsRasterLayerTemporalProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.fixedRangePerBand(), + {1: QgsDateTimeRange( + QDateTime(QDate(2023, 5, 9), + QTime(12, 13, 14)), + QDateTime(QDate(2023, 5, 11), + QTime(12, 13, 14)), + includeBeginning=False, + includeEnd=True)}) + + +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_qgsrasterrendererregistry.py b/tests/src/python/test_qgsrasterrendererregistry.py new file mode 100644 index 000000000000..b57ba82d8352 --- /dev/null +++ b/tests/src/python/test_qgsrasterrendererregistry.py @@ -0,0 +1,51 @@ +"""QGIS Unit tests for QgsRasterRendererRegistry + +.. 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. +""" +from qgis.core import ( + Qgis, + QgsRasterRendererRegistry +) +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 TestQgsRasterRendererRegistry(QgisTestCase): + + def test_registered(self): + """ + Test that standard renderers are registered + """ + registry = QgsRasterRendererRegistry() + self.assertIn('multibandcolor', registry.renderersList()) + self.assertIn('singlebandgray', registry.renderersList()) + self.assertIn('singlebandpseudocolor', registry.renderersList()) + self.assertIn('singlebandcolordata', registry.renderersList()) + self.assertIn('hillshade', registry.renderersList()) + self.assertIn('paletted', registry.renderersList()) + self.assertIn('contour', registry.renderersList()) + + def test_capabilities(self): + """ + Test retrieving renderer capabilities + """ + registry = QgsRasterRendererRegistry() + self.assertFalse(registry.rendererCapabilities('not a renderer')) + self.assertEqual(registry.rendererCapabilities('multibandcolor'), + Qgis.RasterRendererCapability.UsesMultipleBands) + self.assertEqual(registry.rendererCapabilities('singlebandgray'), + Qgis.RasterRendererCapabilities()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrastertransparency.py b/tests/src/python/test_qgsrastertransparency.py new file mode 100644 index 000000000000..111e62b934e4 --- /dev/null +++ b/tests/src/python/test_qgsrastertransparency.py @@ -0,0 +1,119 @@ +"""QGIS Unit tests for QgsRasterTransparency + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Nyall Dawson' +__date__ = '29/02/2024' +__copyright__ = 'Copyright 2024, The QGIS Project' + +from qgis.core import QgsRasterTransparency +from qgis.testing import TestCase, unittest + + +class TestQgsRasterTransparency(TestCase): + + def test_transparency_single_repr(self): + self.assertEqual(repr(QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3)), + '') + + def test_transparency_single_equality(self): + self.assertEqual(QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(2, 10, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(1, 11, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(1, 10, 0.4)) + + def test_transparency_single_value(self): + transparency = QgsRasterTransparency() + self.assertFalse(transparency.transparentSingleValuePixelList()) + transparency.setTransparentSingleValuePixelList([ + QgsRasterTransparency.TransparentSingleValuePixel(10, 20, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(30, 40, 0.6, includeMaximum=False), + QgsRasterTransparency.TransparentSingleValuePixel(50, 60, 0.9, includeMinimum=False) + ]) + self.assertEqual( + transparency.transparentSingleValuePixelList(), + [ + QgsRasterTransparency.TransparentSingleValuePixel(10, 20, 0.3), + QgsRasterTransparency.TransparentSingleValuePixel(30, 40, 0.6, includeMaximum=False), + QgsRasterTransparency.TransparentSingleValuePixel(50, 60, 0.9, includeMinimum=False) + ] + ) + self.assertEqual(transparency.alphaValue(0), 255) + self.assertEqual(transparency.alphaValue(10), 76) + self.assertEqual(transparency.alphaValue(15), 76) + self.assertEqual(transparency.alphaValue(20), 76) + self.assertEqual(transparency.alphaValue(25), 255) + self.assertEqual(transparency.alphaValue(30), 153) + self.assertEqual(transparency.alphaValue(35), 153) + self.assertEqual(transparency.alphaValue(40), 255) + self.assertEqual(transparency.alphaValue(45), 255) + self.assertEqual(transparency.alphaValue(50), 255) + self.assertEqual(transparency.alphaValue(55), 229) + self.assertEqual(transparency.alphaValue(60), 229) + self.assertEqual(transparency.alphaValue(61), 255) + + self.assertEqual(transparency.opacityForValue(0), 1.0) + self.assertEqual(transparency.opacityForValue(10), 0.3) + self.assertEqual(transparency.opacityForValue(15), 0.3) + self.assertEqual(transparency.opacityForValue(20), 0.3) + self.assertEqual(transparency.opacityForValue(25), 1.0) + self.assertEqual(transparency.opacityForValue(30), 0.6) + self.assertEqual(transparency.opacityForValue(35), 0.6) + self.assertEqual(transparency.opacityForValue(40), 1.0) + self.assertEqual(transparency.opacityForValue(45), 1.0) + self.assertEqual(transparency.opacityForValue(50), 1.0) + self.assertEqual(transparency.opacityForValue(55), 0.9) + self.assertEqual(transparency.opacityForValue(60), 0.9) + self.assertEqual(transparency.opacityForValue(61), 1.0) + + def test_transparency_three_repr(self): + self.assertEqual(repr(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3)), + '') + + def test_transparency_three_equality(self): + self.assertEqual(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(2, 10, 20, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(1, 11, 20, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 25, 0.3)) + self.assertNotEqual(QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(1, 10, 20, 0.4)) + + def test_transparency_three_value(self): + transparency = QgsRasterTransparency() + self.assertFalse(transparency.transparentThreeValuePixelList()) + transparency.setTransparentThreeValuePixelList([ + QgsRasterTransparency.TransparentThreeValuePixel(10, 20, 30, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(30, 40, 50, 0.6) + ]) + self.assertEqual( + transparency.transparentThreeValuePixelList(), + [ + QgsRasterTransparency.TransparentThreeValuePixel(10, 20, 30, 0.3), + QgsRasterTransparency.TransparentThreeValuePixel(30, 40, 50, 0.6) + ] + ) + self.assertEqual(transparency.alphaValue(0, 0, 0), 255) + self.assertEqual(transparency.alphaValue(10, 0, 0), 255) + self.assertEqual(transparency.alphaValue(10, 20, 0), 255) + self.assertEqual(transparency.alphaValue(10, 20, 30), 76) + self.assertEqual(transparency.alphaValue(30, 40, 50), 153) + self.assertEqual(transparency.opacityForRgbValues(0, 0, 0), 1) + self.assertEqual(transparency.opacityForRgbValues(10, 0, 0), 1) + self.assertEqual(transparency.opacityForRgbValues(10, 20, 0), 1) + self.assertEqual(transparency.opacityForRgbValues(10, 20, 30), 0.3) + self.assertEqual(transparency.opacityForRgbValues(30, 40, 50), 0.6) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsserialportsensor.py b/tests/src/python/test_qgsserialportsensor.py new file mode 100644 index 000000000000..91e4e10f4cb9 --- /dev/null +++ b/tests/src/python/test_qgsserialportsensor.py @@ -0,0 +1,100 @@ +"""QGIS Unit tests for QgsSerialPortSensor. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2023 by Mathieu Pellerin' +__date__ = '19/03/2023' +__copyright__ = 'Copyright 2023, The QGIS Project' + +import os +import posix + +from qgis.PyQt.QtCore import QCoreApplication, QEvent, QLocale, QTemporaryDir, QIODevice, QBuffer, QByteArray +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtXml import QDomDocument +from qgis.core import ( + Qgis, + QgsApplication, + QgsProject, + QgsSerialPortSensor, + QgsSensorManager +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsSerialPortSensor(QgisTestCase): + + manager = None + + @classmethod + def setUpClass(cls): + """Run before all tests""" + super().setUpClass() + QCoreApplication.setOrganizationName("QGIS_Test") + QCoreApplication.setOrganizationDomain("QGIS_TestQgsSerialPortSensor.com") + QCoreApplication.setApplicationName("QGIS_TestQgsSerialPortSensor") + QLocale.setDefault(QLocale(QLocale.Language.English)) + start_app() + + cls.manager = QgsProject.instance().sensorManager() + + def setUp(self): + """Run before each test.""" + pass + + def tearDown(self): + """Run after each test.""" + pass + + def testSerialPortSensor(self): + fd1, fd2 = posix.openpty() + fd_file = os.fdopen(fd1, "wb") + + serial_port_sensor = QgsSerialPortSensor() + serial_port_sensor.setName('serial port sensor') + serial_port_sensor.setPortName(os.ttyname(fd2)) + serial_port_sensor.setDelimiter(b'\n') + + serial_port_sensor_id = serial_port_sensor.id() + + self.manager.addSensor(serial_port_sensor) + + sensor_spy = QSignalSpy(serial_port_sensor.dataChanged) + manager_spy = QSignalSpy(self.manager.sensorDataCaptured) + + serial_port_sensor.connectSensor() + self.assertEqual(serial_port_sensor.status(), Qgis.DeviceConnectionStatus.Connected) + + QCoreApplication.processEvents() + fd_file.write(b'test 1\nfull ') + fd_file.flush() + QCoreApplication.processEvents() + + # No signal should be fired as the delimiter must be captured at least once to insure a full data frame + self.assertEqual(len(sensor_spy), 0) + self.assertEqual(len(manager_spy), 0) + + QCoreApplication.processEvents() + fd_file.write(b'test 2\n') + fd_file.flush() + QCoreApplication.processEvents() + + self.assertEqual(len(sensor_spy), 1) + self.assertEqual(len(manager_spy), 1) + self.assertEqual(serial_port_sensor.data().lastValue, QByteArray(b'full test 2')) + self.assertEqual(self.manager.sensorData('serial port sensor').lastValue, b'full test 2') + + self.manager.removeSensor(serial_port_sensor_id) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsserver.py b/tests/src/python/test_qgsserver.py index 666a2bda962c..4b6dcc9995a5 100644 --- a/tests/src/python/test_qgsserver.py +++ b/tests/src/python/test_qgsserver.py @@ -249,10 +249,16 @@ def _img_diff(self, image: str, control_image, max_diff, max_size_diff=QSize(), # TODO fix this, it's not actually testing anything..! return True + rendered_image = QImage(temp_image) + if rendered_image.format() not in (QImage.Format.Format_RGB32, + QImage.Format.Format_ARGB32, + QImage.Format.Format_ARGB32_Premultiplied): + rendered_image = rendered_image.convertToFormat(QImage.Format.Format_ARGB32) + return self.image_check( control_image, control_image, - QImage(temp_image), + rendered_image, control_image, allowed_mismatch=max_diff, control_path_prefix="qgis_server", diff --git a/tests/src/python/test_qgsserver_api.py b/tests/src/python/test_qgsserver_api.py index 3cf6d4843e19..32b72664bbbc 100644 --- a/tests/src/python/test_qgsserver_api.py +++ b/tests/src/python/test_qgsserver_api.py @@ -635,6 +635,15 @@ def test_invalid_args(self): self.assertEqual(response.body(), b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request + # Test overflowing int32 + request = QgsBufferServerRequest( + 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=' + str((1 << 32))) + response = QgsBufferServerResponse() + self.server.handleRequest(request, response, project) + self.assertEqual(response.statusCode(), 400) # Bad request + self.assertEqual(response.body(), + b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request + def test_wfs3_collection_items_limit(self): """Test WFS3 API item limits""" project = QgsProject() diff --git a/tests/src/python/test_qgsserver_plugins.py b/tests/src/python/test_qgsserver_plugins.py index 0dc109930137..f2ee1ba36086 100644 --- a/tests/src/python/test_qgsserver_plugins.py +++ b/tests/src/python/test_qgsserver_plugins.py @@ -304,6 +304,10 @@ def onRequestReady(self): request = self.serverInterface().requestHandler() return self.propagate + def onProjectReady(self): + request = self.serverInterface().requestHandler() + return self.propagate + def onSendResponse(self): request = self.serverInterface().requestHandler() request.clearBody() @@ -324,12 +328,18 @@ class Filter2(QgsServerFilter): def __init__(self, iface): super().__init__(iface) self.request_ready = False + self.project_ready = False def onRequestReady(self): request = self.serverInterface().requestHandler() self.request_ready = True return True + def onProjectReady(self): + request = self.serverInterface().requestHandler() + self.project_ready = True + return True + def onSendResponse(self): request = self.serverInterface().requestHandler() request.appendBody(b'D') @@ -340,6 +350,37 @@ def onResponseComplete(self): request.appendBody(b'E') return True + # Methods to manage propagate filter + class Filter3(QgsServerFilter): + def __init__(self, iface, propagate_filter): + super().__init__(iface) + self.propagate_filter = propagate_filter + self.step_to_stop_propagate = None + + def onRequestReady(self): + request = self.serverInterface().requestHandler() + if self.step_to_stop_propagate == 'onRequestReady': + self.propagate_filter.propagate = False + return True + + def onProjectReady(self): + request = self.serverInterface().requestHandler() + if self.step_to_stop_propagate == 'onProjectReady': + self.propagate_filter.propagate = False + return True + + def onSendResponse(self): + request = self.serverInterface().requestHandler() + if self.step_to_stop_propagate == 'onSendResponse': + self.propagate_filter.propagate = False + return True + + def onResponseComplete(self): + request = self.serverInterface().requestHandler() + if self.step_to_stop_propagate == 'onResponseComplete': + self.propagate_filter.propagate = False + return True + serverIface = self.server.serverInterface() serverIface.setFilters({}) @@ -359,14 +400,60 @@ def onResponseComplete(self): filter1.propagate = False _, body = self._execute_request_project(f'?service={service0.name()}', project=project) self.assertFalse(filter2.request_ready) + self.assertFalse(filter2.project_ready) self.assertEqual(body, b'ABC') # Test with propagation filter1.propagate = True _, body = self._execute_request_project(f'?service={service0.name()}', project=project) self.assertTrue(filter2.request_ready) + self.assertTrue(filter2.project_ready) self.assertEqual(body, b'ABDCE') + # Manage propagation + filter3 = Filter3(serverIface, filter1) + serverIface.registerFilter(filter3, 100) + + # Stop at onResponseComplete + filter1.propagate = True + filter2.request_ready = False + filter2.project_ready = False + filter3.step_to_stop_propagate = 'onResponseComplete' + _, body = self._execute_request_project(f'?service={service0.name()}', project=project) + self.assertTrue(filter2.request_ready) + self.assertTrue(filter2.project_ready) + self.assertEqual(body, b'ABDC') + + # Stop at onSendResponse + filter1.propagate = True + filter2.request_ready = False + filter2.project_ready = False + filter3.step_to_stop_propagate = 'onSendResponse' + _, body = self._execute_request_project(f'?service={service0.name()}', project=project) + self.assertTrue(filter2.request_ready) + self.assertTrue(filter2.project_ready) + self.assertEqual(body, b'ABC') + + # Stop at onProjectReady + filter1.propagate = True + filter2.request_ready = False + filter2.project_ready = False + filter3.step_to_stop_propagate = 'onProjectReady' + _, body = self._execute_request_project(f'?service={service0.name()}', project=project) + self.assertTrue(filter2.request_ready) + self.assertFalse(filter2.project_ready) + self.assertEqual(body, b'ABC') + + # Stop at onRequestReady + filter1.propagate = True + filter2.request_ready = False + filter2.project_ready = False + filter3.step_to_stop_propagate = 'onRequestReady' + _, body = self._execute_request_project(f'?service={service0.name()}', project=project) + self.assertFalse(filter2.request_ready) + self.assertFalse(filter2.project_ready) + self.assertEqual(body, b'ABC') + serverIface.setFilters({}) reg.unregisterService(service0.name(), service0.version()) diff --git a/tests/src/python/test_qgsserver_request.py b/tests/src/python/test_qgsserver_request.py index ec83cd2d3b65..a5ffe7876919 100644 --- a/tests/src/python/test_qgsserver_request.py +++ b/tests/src/python/test_qgsserver_request.py @@ -197,6 +197,48 @@ def _check_links(params, method='GET'): _check_links(params) _check_links(params, 'POST') + def test_fcgiRequestPOST_invalid_length_not_an_integer(self): + """Test post request handler with wrong CONTENT_LENGTH""" + + data = '+1' + self._set_env({ + 'SERVER_NAME': 'www.myserver.com', + 'SERVICE': 'WFS', + 'REQUEST_BODY': data, + 'CONTENT_LENGTH': "not an integer", + 'REQUEST_METHOD': 'POST', + }) + request = QgsFcgiServerRequest() + self.assertTrue(request.hasError()) + + def test_fcgiRequestPOST_invalid_length_negative(self): + """Test post request handler with wrong CONTENT_LENGTH""" + + data = '+1' + self._set_env({ + 'SERVER_NAME': 'www.myserver.com', + 'SERVICE': 'WFS', + 'REQUEST_BODY': data, + 'CONTENT_LENGTH': "-1", + 'REQUEST_METHOD': 'POST', + }) + request = QgsFcgiServerRequest() + self.assertTrue(request.hasError()) + + def test_fcgiRequestPOST_too_short_length(self): + """Test post request handler with wrong CONTENT_LENGTH""" + + data = '+1' + self._set_env({ + 'SERVER_NAME': 'www.myserver.com', + 'SERVICE': 'WFS', + 'REQUEST_BODY': data, + 'CONTENT_LENGTH': str(len(data) - 1), + 'REQUEST_METHOD': 'POST', + }) + request = QgsFcgiServerRequest() + self.assertTrue(request.hasError()) + def test_fcgiRequestBody(self): """Test request body""" data = '+1' @@ -208,6 +250,7 @@ def test_fcgiRequestBody(self): 'REQUEST_METHOD': 'POST', }) request = QgsFcgiServerRequest() + self.assertFalse(request.hasError()) response = QgsBufferServerResponse() self.server.handleRequest(request, response) self.assertEqual(request.parameter('REQUEST_BODY'), '+1') 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_qgsserver_wms_getmap.py b/tests/src/python/test_qgsserver_wms_getmap.py index 7b05351e7c4d..5763c7f98d9f 100644 --- a/tests/src/python/test_qgsserver_wms_getmap.py +++ b/tests/src/python/test_qgsserver_wms_getmap.py @@ -336,6 +336,25 @@ def test_wms_getmap_invalid_parameters(self): err = b"HEIGHT (\'FOO\') cannot be converted into int" in r self.assertTrue(err) + # height should be > 0 + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectPath), + "SERVICE": "WMS", + "VERSION": "1.1.1", + "REQUEST": "GetMap", + "LAYERS": "Country", + "STYLES": "", + "FORMAT": "image/png", + "BBOX": "-16817707,-4710778,5696513,14587125", + "HEIGHT": "-1", + "WIDTH": "1", + "CRS": "EPSG:3857" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"The requested map size is too large" in r + self.assertTrue(err) + # width should be an int qs = "?" + "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), @@ -355,6 +374,25 @@ def test_wms_getmap_invalid_parameters(self): err = b"WIDTH (\'FOO\') cannot be converted into int" in r self.assertTrue(err) + # width should be > 0 + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectPath), + "SERVICE": "WMS", + "VERSION": "1.1.1", + "REQUEST": "GetMap", + "LAYERS": "Country", + "STYLES": "", + "FORMAT": "image/png", + "BBOX": "-16817707,-4710778,5696513,14587125", + "HEIGHT": "1", + "WIDTH": "-1", + "CRS": "EPSG:3857" + }.items())]) + + r, h = self._result(self._execute_request(qs)) + err = b"The requested map size is too large" in r + self.assertTrue(err) + # bbox should be formatted like "double,double,double,double" qs = "?" + "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectPath), @@ -1327,6 +1365,31 @@ def test_wms_getmap_highlight_line(self): r, h = self._result(self._execute_request(qs)) self._img_diff_error(r, h, "WMS_GetMap_Highlight_Line") + def test_wms_getmap_highlight_empty_labels(self): + # Checks if the empty label for Eurasia is correctly handled. Otherwise the highlight point for Eurasia would be labeled as Africa + qs = "?" + "&".join(["%s=%s" % i for i in list({ + "MAP": urllib.parse.quote(self.projectPath), + "SERVICE": "WMS", + "VERSION": "1.1.1", + "REQUEST": "GetMap", + "LAYERS": "Country_Labels", + "HIGHLIGHT_GEOM": "POINT(-4000000 12215266);POINT(3271207 6832268);POINT(2360238 1035192)", + "HIGHLIGHT_LABELSTRING": "Arctic;;Africa", + "HIGHLIGHT_SYMBOL": "circle%23ff000017.5%237bdcb5128.4;circle%23ff000017.5%237bdcb5128.4;circle%23ff000017.5%237bdcb5128.4", + "HIGHLIGHT_LABELSIZE": "16;16;16", + "HIGHLIGHT_LABELCOLOR": "red;red;red", + "HIGHLIGHT_LABELBUFFERCOLOR": "white;white;white", + "HIGHLIGHT_LABELBUFFERSIZE": "1;1;1", + "STYLES": "", + "FORMAT": "image/png", + "BBOX": "-16817707,-4710778,5696513,14587125", + "HEIGHT": "500", + "WIDTH": "500", + "CRS": "EPSG:3857" + }.items())]) + r, h = self._result(self._execute_request(qs)) + self._img_diff_error(r, h, "WMS_GetMap_Highlight_Empty_Labels") + def test_wms_getmap_annotations(self): qs = "?" + "&".join(["%s=%s" % i for i in list({ "MAP": urllib.parse.quote(self.projectAnnotationPath), diff --git a/tests/src/python/test_qgssinglebandcolordatarenderer.py b/tests/src/python/test_qgssinglebandcolordatarenderer.py new file mode 100644 index 000000000000..42fd44063c00 --- /dev/null +++ b/tests/src/python/test_qgssinglebandcolordatarenderer.py @@ -0,0 +1,64 @@ +"""QGIS Unit tests for QgsSingleBandColorDataRenderer + +From build dir, run: +ctest -R PyQgsSingleBandColorDataRenderer -V + +.. 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.core import ( + QgsSingleBandColorDataRenderer, + QgsRasterLayer, +) +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 TestQgsSingleBandColorDataRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandColorDataRenderer(layer.dataProvider(), 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_singleband_invalid_layer(self): + """ + Test singleband raster render band with a broken layer path + """ + renderer = QgsSingleBandColorDataRenderer(None, 5) + + self.assertEqual(renderer.inputBand(), 5) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgssinglebandgrayrenderer.py b/tests/src/python/test_qgssinglebandgrayrenderer.py new file mode 100644 index 000000000000..e82e1335492e --- /dev/null +++ b/tests/src/python/test_qgssinglebandgrayrenderer.py @@ -0,0 +1,66 @@ +"""QGIS Unit tests for QgsSingleBandGrayRenderer. + +From build dir, run: +ctest -R PyQgsSingleBandGrayRenderer -V + +.. 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.core import ( + QgsRasterLayer, + QgsSingleBandGrayRenderer, +) +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 TestQgsSingleBandGrayRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandGrayRenderer(layer.dataProvider(), + 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_invalid_layer(self): + """ + Test gray renderer band with a broken layer path + """ + renderer = QgsSingleBandGrayRenderer(None, + 11) + + self.assertEqual(renderer.inputBand(), 11) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgssinglebandpseudocolorrenderer.py b/tests/src/python/test_qgssinglebandpseudocolorrenderer.py new file mode 100644 index 000000000000..98c7a95bbc6e --- /dev/null +++ b/tests/src/python/test_qgssinglebandpseudocolorrenderer.py @@ -0,0 +1,66 @@ +"""QGIS Unit tests for QgsSingleBandPseudoColorRenderer. + +From build dir, run: +ctest -R PyQgsSingleBandPseudoColorRenderer -V + +.. 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.core import ( + QgsRasterLayer, + QgsSingleBandPseudoColorRenderer, +) +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 TestQgsSingleBandPseudoColorRenderer(QgisTestCase): + + def test_renderer(self): + path = os.path.join(unitTestDataPath(), + 'landsat.tif') + info = QFileInfo(path) + base_name = info.baseName() + layer = QgsRasterLayer(path, base_name) + self.assertTrue(layer.isValid(), f'Raster not loaded: {path}') + + renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), + 1) + + self.assertEqual(renderer.inputBand(), 1) + + self.assertFalse(renderer.setInputBand(0)) + self.assertEqual(renderer.inputBand(), 1) + self.assertFalse(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 1) + self.assertTrue(renderer.setInputBand(2)) + self.assertEqual(renderer.inputBand(), 2) + + def test_invalid_layer(self): + """ + Test renderer band with a broken layer path + """ + renderer = QgsSingleBandPseudoColorRenderer(None, + 11) + + self.assertEqual(renderer.inputBand(), 11) + + # the renderer input is broken, we don't know what bands are valid, so all should be accepted + self.assertTrue(renderer.setInputBand(10)) + self.assertEqual(renderer.inputBand(), 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsvectorlayerprofilegenerator.py b/tests/src/python/test_qgsvectorlayerprofilegenerator.py index bc3a96b883eb..2a68466da5e4 100644 --- a/tests/src/python/test_qgsvectorlayerprofilegenerator.py +++ b/tests/src/python/test_qgsvectorlayerprofilegenerator.py @@ -10,6 +10,7 @@ __copyright__ = 'Copyright 2022, The QGIS Project' import os +import math from qgis.PyQt.QtCore import QDir from qgis.core import ( @@ -37,6 +38,7 @@ QgsRendererCategory, QgsSymbolLayer, QgsVectorLayer, + QgsWkbTypes, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -54,7 +56,7 @@ def control_path_prefix(cls): @staticmethod def round_dict(val, places): - return {round(k, places): round(val[k], places) for k in sorted(val.keys())} + return {round(k, places): round(val[k], places) for k in val.keys() if not math.isnan(val[k])} def create_transform_context(self): context = QgsCoordinateTransformContext() @@ -172,6 +174,7 @@ def testPointGenerationTerrain(self): results = generator.takeResults() self.assertFalse(results.distanceToHeightMap()) + # 15 meters tolerance req.setTolerance(15) generator = vl.createProfileGenerator(req) self.assertTrue(generator.generateProfile()) @@ -188,6 +191,23 @@ def testPointGenerationTerrain(self): self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) self.assertAlmostEqual(results.zRange().upper(), 67.25, 2) + # 70 meters tolerance + req.setTolerance(70) + generator = vl.createProfileGenerator(req) + self.assertTrue(generator.generateProfile()) + results = generator.takeResults() + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 1), + {175.6: 69.5, 31.2: 69.5, 1223.2: 56.8, 1242.5: 55.2}) + self.assertAlmostEqual(results.zRange().lower(), 55.249, 2) + self.assertAlmostEqual(results.zRange().upper(), 69.5, 2) + else: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 1), + {31.2: 67.2, 175.6: 65.8, 1242.5: 52.2}) + self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 67.25, 2) + def testPointGenerationRelative(self): """ Points layer with relative clamping @@ -502,6 +522,106 @@ def testLineGenerationTerrain(self): self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2) + def testLineGenerationTerrainTolerance(self): + vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') + self.assertTrue(vl.isValid()) + + for line in ['LineStringZ(322006 129874 12, 322008 129910 13, 322038 129909 14, 322037 129868 15)', + 'LineStringZ(322068 129900 16, 322128 129813 17)', + 'LineStringZ(321996 129914 11, 321990 129896 15)', + 'LineStringZ(321595 130176 1, 321507 130104 10)', + 'LineStringZ(321558 129930 1, 321568 130029 10, 321516 130049 5)', + 'LineStringZ(321603 129967 3, 321725 130042 9)']: + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt(line)) + self.assertTrue(vl.dataProvider().addFeature(f)) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Terrain) + vl.elevationProperties().setZScale(2.5) + vl.elevationProperties().setZOffset(10) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (-347692.88994020794052631 6632796.97473032586276531, -346576.99897185183363035 6632367.38372825458645821, -346396.02439485350623727 6632344.35087973903864622, -346374.34608158958144486 6632220.09952207934111357)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + rl = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm.tif'), 'DTM') + self.assertTrue(rl.isValid()) + terrain_provider = QgsRasterDemTerrainProvider() + terrain_provider.setLayer(rl) + terrain_provider.setScale(0.3) + terrain_provider.setOffset(-5) + req.setTerrainProvider(terrain_provider) + + req.setCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + + # very small tolerance + req.setTolerance(0.1) + generator = vl.createProfileGenerator(req) + self.assertTrue(generator.generateProfile()) + results = generator.takeResults() + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {675.09: 66.5, 675.24: 66.5, 1195.73: 49.25, 1195.74: 49.25, 1223.14: 50.0, + 1223.15: 50.0, 1271.97: 53.75, 1271.99: 53.75, 1339.33: 58.25, 1339.51: 58.25, + 1444.18: 58.25, 1444.59: 58.25}) + self.assertAlmostEqual(results.zRange().lower(), 49.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 66.5, 2) + else: + # TODO find a way to test with an older proj version + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {675.09: 66.5, 675.24: 66.5, 1195.73: 49.25, 1195.74: 49.25, 1223.14: 50.0, + 1223.15: 50.0, 1271.97: 53.75, 1271.99: 53.75, 1339.33: 58.25, 1339.51: 58.25, + 1444.18: 58.25, 1444.59: 58.25}) + self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2) + + # 1 meter tolerance + req.setTolerance(1) + generator = vl.createProfileGenerator(req) + self.assertTrue(generator.generateProfile()) + results = generator.takeResults() + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {674.43: 66.5, 675.91: 66.5, 1195.73: 49.25, 1195.91: 49.25, 1223.06: 50.0, + 1223.23: 50.0, 1271.86: 53.75, 1272.1: 53.75, 1338.5: 58.25, 1340.34: 58.25, + 1442.29: 56.75, 1446.48: 57.5}) + self.assertAlmostEqual(results.zRange().lower(), 49.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 66.5, 2) + else: + # TODO find a way to test with an older proj version + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {675.09: 66.5, 675.24: 66.5, 1195.73: 49.25, 1195.74: 49.25, 1223.14: 50.0, + 1223.15: 50.0, 1271.97: 53.75, 1271.99: 53.75, 1339.33: 58.25, 1339.51: 58.25, + 1444.18: 58.25, 1444.59: 58.25}) + self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2) + + # 15 meters tolerance + req.setTolerance(15) + generator = vl.createProfileGenerator(req) + self.assertTrue(generator.generateProfile()) + results = generator.takeResults() + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {664.1: 65.75, 686.24: 67.25, 1195.73: 49.25, 1198.44: 49.25, 1221.85: 50.0, + 1224.44: 49.25, 1270.21: 54.5, 1273.75: 53.0, 1325.61: 59.0, 1353.23: 57.5, + 1412.92: 56.0, 1475.85: 57.5}) + self.assertAlmostEqual(results.zRange().lower(), 49.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 67.25, 2) + else: + # TODO find a way to test with an older proj version + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 2), + {675.09: 66.5, 675.24: 66.5, 1195.73: 49.25, 1195.74: 49.25, 1223.14: 50.0, + 1223.15: 50.0, 1271.97: 53.75, 1271.99: 53.75, 1339.33: 58.25, 1339.51: 58.25, + 1444.18: 58.25, 1444.59: 58.25}) + self.assertAlmostEqual(results.zRange().lower(), 52.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 62.7499, 2) + def testLineGenerationRelative(self): vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') self.assertTrue(vl.isValid()) @@ -855,6 +975,78 @@ def testPolygonGenerationRelativeExtrusion(self): 'MultiPolygonZ (((-346387.6 6632223.9 67, -346384.8 6632219 67, -346384.8 6632219 74, -346387.6 6632223.9 74, -346387.6 6632223.9 67)),((-346384.8 6632219 67, -346383.5 6632216.9 67, -346383.5 6632216.9 74, -346384.8 6632219 74, -346384.8 6632219 67)))', 'MultiPolygonZ (((-346582.6 6632371.7 62.3, -346579.7 6632370.7 62.3, -346579.7 6632370.7 69.3, -346582.6 6632371.7 69.3, -346582.6 6632371.7 62.3)),((-346579.7 6632370.7 62.3, -346577 6632369.7 62.3, -346570.8 6632367.9 62.3, -346570.8 6632367.9 69.3, -346577 6632369.7 69.3, -346579.7 6632370.7 69.3, -346579.7 6632370.7 62.3)))']) + def testPolygonGenerationRelativeExtrusionTolerance(self): + vl = QgsVectorLayer('PolygonZ?crs=EPSG:27700', 'lines', 'memory') + self.assertTrue(vl.isValid()) + + for line in [ + 'PolygonZ ((321829.48893365426920354 129991.38697145861806348 1, 321847.89668515208177269 129996.63588572069420479 1, 321848.97131609614007175 129979.22330882755341008 1, 321830.31725845142500475 129978.07136809575604275 1, 321829.48893365426920354 129991.38697145861806348 1))', + 'PolygonZ ((321920.00953056826256216 129924.58260190498549491 2, 321924.65299345907988027 129908.43546159457764588 2, 321904.78543491888558492 129903.99811821122420952 2, 321900.80605239619035274 129931.39860145389684476 2, 321904.84799937985371798 129931.71552911199978553 2, 321908.93646715773502365 129912.90030360443051904 2, 321914.20495146053144708 129913.67693978428724222 2, 321911.30165811872575432 129923.01272751353099011 2, 321920.00953056826256216 129924.58260190498549491 2))', + 'PolygonZ ((321923.10517279652412981 129919.61521573827485554 3, 321922.23537852568551898 129928.3598982143739704 3, 321928.60423935484141111 129934.22530528216157109 3, 321929.39881197665818036 129923.29054521876969375 3, 321930.55804549407912418 129916.53248518184409477 3, 321923.10517279652412981 129919.61521573827485554 3))', + 'PolygonZ ((321990.47451346553862095 129909.63588680300745182 4, 321995.04325810901354998 129891.84052284323843196 4, 321989.66826330573530868 129890.5092018858413212 4, 321990.78512359503656626 129886.49917887404444627 4, 321987.37291929306229576 129885.64982962771318853 4, 321985.2254804756375961 129893.81317058412241749 4, 321987.63158903241856024 129894.41078495365218259 4, 321984.34022761805681512 129907.57450046355370432 4, 321990.47451346553862095 129909.63588680300745182 4))', + 'PolygonZ ((322103.03910495212767273 129795.91051736124791205 5, 322108.25568856322206557 129804.76113295342656784 5, 322113.29666162584908307 129803.9285887333098799 5, 322117.78645010641776025 129794.48194090687320568 5, 322103.03910495212767273 129795.91051736124791205 5))']: + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt(line)) + self.assertTrue(vl.dataProvider().addFeature(f)) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Relative) + vl.elevationProperties().setZScale(2.5) + vl.elevationProperties().setZOffset(10) + vl.elevationProperties().setExtrusionEnabled(True) + vl.elevationProperties().setExtrusionHeight(7) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (-347701.59207547508412972 6632766.96282589063048363, -346577.00878971704514697 6632369.7371364813297987, -346449.93654899462126195 6632331.81857067719101906, -346383.52035177784273401 6632216.85897350125014782)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + rl = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm.tif'), 'DTM') + self.assertTrue(rl.isValid()) + terrain_provider = QgsRasterDemTerrainProvider() + terrain_provider.setLayer(rl) + terrain_provider.setScale(0.3) + terrain_provider.setOffset(-5) + req.setTerrainProvider(terrain_provider) + + req.setCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + req.setTolerance(2.0) + + generator = vl.createProfileGenerator(req) + self.assertTrue(generator.generateProfile()) + results = generator.takeResults() + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 1), + {1041.0: 60.2, 1042.2: 60.2, 1042.9: 60.2, 1048.2: 60.2, 1050.8: 60.2, 1066.9: 60.2, + 1073.4: 60.2, 1076.2: 60.2, 1077.9: 62.0, 1079.9: 62.0, 1089.9: 62.0, 1092.2: 62.0, + 1185.4: 59.2, 1188.2: 59.2, 1192.6: 59.2, 1192.7: 59.2, 1197.9: 59.2, 1200.4: 59.2, + 1449.3: 65.5, 1450.1: 65.5, 1451.1: 65.5, 1458.1: 65.5}) + self.assertAlmostEqual(results.zRange().lower(), 59.25, 2) + self.assertAlmostEqual(results.zRange().upper(), 65.5, 2) + else: + self.assertEqual(self.round_dict(results.distanceToHeightMap(), 1), + {1041.8: 53.5, 1042.4: 53.5, 1049.5: 53.5, 1070.2: 53.5, 1073.1: 53.5, 1074.8: 53.5, + 1078.9: 56.0, 1083.9: 56.0, 1091.1: 56.0, 1186.8: 62.3, 1189.8: 62.3, 1192.7: 62.3, + 1199.2: 62.2, 1450.0: 67.0, 1455.6: 67.0, 1458.1: 67.0}) + self.assertAlmostEqual(results.zRange().lower(), 53.5, 2) + self.assertAlmostEqual(results.zRange().upper(), 74.00000, 2) + + if QgsProjUtils.projVersionMajor() >= 8: + self.assertCountEqual([g.asWkt(1) for g in results.asGeometries()], + [ + 'MultiLineStringZ ((-346696.3 6632409.8 60.2, -346688.9 6632411.2 60.2, -346687.5 6632406.6 60.2),(-346718.9 6632417.7 60.2, -346719.5 6632421.6 60.2, -346718.2 6632421.7 60.2, -346712.5 6632419.7 60.2, -346711.5 6632415.1 60.2))', + 'LineStringZ (-346684.1 6632405.4 62, -346684.6 6632409.8 62, -346673.3 6632405.9 62, -346672.4 6632401.3 62)', + 'LineStringZ (-346571.4 6632370.2 59.3, -346570.2 6632365.6 59.3, -346577.6 6632367.8 59.3, -346577.7 6632367.9 59.3, -346581.9 6632369.4 59.3, -346583.2 6632374 59.3, -346576.4 6632371.6 59.3)', + 'LineStringZ (-346381.8 6632217.9 65.5, -346385.3 6632215.9 65.5, -346388.7 6632221.9 65.5, -346387 6632224.9 65.5, -346385.8 6632224.7 65.5)']) + else: + self.assertCountEqual([g.asWkt(1) for g in results.asGeometries()], + [ + 'MultiPolygonZ (((-346684.3 6632407.6 56, -346679.6 6632406 56, -346679.6 6632406 63, -346684.3 6632407.6 63, -346684.3 6632407.6 56)),((-346679.6 6632406 56, -346672.8 6632403.6 56, -346672.8 6632403.6 63, -346679.6 6632406 63, -346679.6 6632406 56)))', + 'MultiPolygonZ (((-346718.7 6632419.8 53.5, -346712 6632417.4 53.5, -346712 6632417.4 60.5, -346718.7 6632419.8 60.5, -346718.7 6632419.8 53.5)),((-346719.3 6632420 53.5, -346718.7 6632419.8 53.5, -346718.7 6632419.8 60.5, -346719.3 6632420 60.5, -346719.3 6632420 53.5)),((-346689.7 6632409.5 53.5, -346688.2 6632409 53.5, -346688.2 6632409 60.5, -346689.7 6632409.5 60.5, -346689.7 6632409.5 53.5)),((-346692.5 6632410.5 53.5, -346689.7 6632409.5 53.5, -346689.7 6632409.5 60.5, -346692.5 6632410.5 60.5, -346692.5 6632410.5 53.5)))', + 'MultiPolygonZ (((-346387.6 6632223.9 67, -346384.8 6632219 67, -346384.8 6632219 74, -346387.6 6632223.9 74, -346387.6 6632223.9 67)),((-346384.8 6632219 67, -346383.5 6632216.9 67, -346383.5 6632216.9 74, -346384.8 6632219 74, -346384.8 6632219 67)))', + 'MultiPolygonZ (((-346582.6 6632371.7 62.3, -346579.7 6632370.7 62.3, -346579.7 6632370.7 69.3, -346582.6 6632371.7 69.3, -346582.6 6632371.7 62.3)),((-346579.7 6632370.7 62.3, -346577 6632369.7 62.3, -346570.8 6632367.9 62.3, -346570.8 6632367.9 69.3, -346577 6632369.7 69.3, -346579.7 6632370.7 69.3, -346579.7 6632370.7 62.3)))']) + def test25DPolygonGeneration(self): vl = QgsVectorLayer('PolygonZ?crs=EPSG:2056', 'lines', 'memory') self.assertTrue(vl.isValid()) @@ -1592,6 +1784,54 @@ def testRenderProfileAsSurfaceLinesWithMarkers(self): res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) self.assertTrue(self.image_check('vector_lines_as_surface_with_markers', 'vector_lines_as_surface_with_markers', res)) + def testRenderProfileAsLineWithHoledDtm(self): + rl = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm_with_holes.tif'), 'DTM') + self.assertTrue(rl.isValid()) + + rl.elevationProperties().setEnabled(True) + rl.elevationProperties().setProfileSymbology(Qgis.ProfileSurfaceSymbology.Line) + line_symbol = QgsLineSymbol.createSimple({'color': '#ff00ff', 'width': '0.8'}) + rl.elevationProperties().setProfileLineSymbol(line_symbol) + + curve = QgsLineString() + curve.fromWkt('LineString (320900 129000, 322900 129000)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + req.setCrs(QgsCoordinateReferenceSystem()) + + plot_renderer = QgsProfilePlotRenderer([rl], req) + plot_renderer.startGeneration() + plot_renderer.waitForFinished() + + res = plot_renderer.renderToImage(1600, 800, 0, curve.length(), 0, 90) + self.assertTrue(self.image_check('vector_lines_as_line_with_holed_dtm', 'vector_lines_as_line_with_holed_dtm', res)) + + def testRenderProfileAsSurfaceFillBelowWithHoledDtm(self): + rl = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm_with_holes.tif'), 'DTM') + self.assertTrue(rl.isValid()) + + rl.elevationProperties().setEnabled(True) + rl.elevationProperties().setProfileSymbology(Qgis.ProfileSurfaceSymbology.FillBelow) + fill_symbol = QgsFillSymbol.createSimple({'color': '#ff00ff', 'outline_style': 'no'}) + rl.elevationProperties().setProfileFillSymbol(fill_symbol) + line_symbol = QgsLineSymbol.createSimple({'color': '#ff00ff', 'width': '0.8'}) + rl.elevationProperties().setProfileLineSymbol(line_symbol) + + curve = QgsLineString() + curve.fromWkt('LineString (320900 129000, 322900 129000)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + req.setCrs(QgsCoordinateReferenceSystem()) + + plot_renderer = QgsProfilePlotRenderer([rl], req) + plot_renderer.startGeneration() + plot_renderer.waitForFinished() + + res = plot_renderer.renderToImage(1600, 800, 0, curve.length(), 0, 90) + self.assertTrue(self.image_check('vector_lines_as_fill_below_surface_with_holed_dtm', 'vector_lines_as_fill_below_surface_with_holed_dtm', res)) + def testRenderProfileAsSurfaceFillBelow(self): vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') vl.setCrs(QgsCoordinateReferenceSystem()) @@ -1752,6 +1992,48 @@ def testRenderProfileAsSurfaceFillAboveLimit(self): res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) self.assertTrue(self.image_check('vector_lines_as_fill_above_surface_limit', 'vector_lines_as_fill_above_surface_limit', res)) + def testRenderProfileAsSurfaceFillAboveLimitTolerance(self): + vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') + vl.setCrs(QgsCoordinateReferenceSystem()) + self.assertTrue(vl.isValid()) + + for line in [ + 'LineStringZ (321829.48893365426920354 129991.38697145861806348 1, 321847.89668515208177269 129996.63588572069420479 1, 321848.97131609614007175 129979.22330882755341008 1, 321830.31725845142500475 129978.07136809575604275 1, 321829.48893365426920354 129991.38697145861806348 1)', + 'LineStringZ (321920.00953056826256216 129924.58260190498549491 2, 321924.65299345907988027 129908.43546159457764588 2, 321904.78543491888558492 129903.99811821122420952 2, 321900.80605239619035274 129931.39860145389684476 2, 321904.84799937985371798 129931.71552911199978553 2, 321908.93646715773502365 129912.90030360443051904 2, 321914.20495146053144708 129913.67693978428724222 2, 321911.30165811872575432 129923.01272751353099011 2, 321920.00953056826256216 129924.58260190498549491 2)', + 'LineStringZ (321923.10517279652412981 129919.61521573827485554 3, 321922.23537852568551898 129928.3598982143739704 3, 321928.60423935484141111 129934.22530528216157109 3, 321929.39881197665818036 129923.29054521876969375 3, 321930.55804549407912418 129916.53248518184409477 3, 321923.10517279652412981 129919.61521573827485554 3)', + 'LineStringZ (321990.47451346553862095 129909.63588680300745182 4, 321995.04325810901354998 129891.84052284323843196 4, 321989.66826330573530868 129890.5092018858413212 4, 321990.78512359503656626 129886.49917887404444627 4, 321987.37291929306229576 129885.64982962771318853 4, 321985.2254804756375961 129893.81317058412241749 4, 321987.63158903241856024 129894.41078495365218259 4, 321984.34022761805681512 129907.57450046355370432 4, 321990.47451346553862095 129909.63588680300745182 4)', + 'LineStringZ (322103.03910495212767273 129795.91051736124791205 5, 322108.25568856322206557 129804.76113295342656784 5, 322113.29666162584908307 129803.9285887333098799 5, 322117.78645010641776025 129794.48194090687320568 5, 322103.03910495212767273 129795.91051736124791205 5)']: + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt(line)) + self.assertTrue(vl.dataProvider().addFeature(f)) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute) + vl.elevationProperties().setType(Qgis.VectorProfileType.ContinuousSurface) + vl.elevationProperties().setProfileSymbology(Qgis.ProfileSurfaceSymbology.FillAbove) + vl.elevationProperties().setElevationLimit( + 10) + fill_symbol = QgsFillSymbol.createSimple({'color': '#ff00ff', 'outline_style': 'no'}) + vl.elevationProperties().setRespectLayerSymbology(False) + vl.elevationProperties().setProfileFillSymbol(fill_symbol) + line_symbol = QgsLineSymbol.createSimple({'color': '#ff00ff', 'width': '0.8'}) + vl.elevationProperties().setProfileLineSymbol(line_symbol) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (321897.18831187387695536 129916.86947759155009408, 321942.11597351566888392 129924.94403429214435164)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + req.setCrs(QgsCoordinateReferenceSystem()) + req.setTolerance(20) + + plot_renderer = QgsProfilePlotRenderer([vl], req) + plot_renderer.startGeneration() + plot_renderer.waitForFinished() + + res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) + self.assertTrue(self.image_check('vector_lines_as_fill_above_surface_limit_tolerance', 'vector_lines_as_fill_above_surface_limit_tolerance', res)) + def testRenderProfileSymbolWithMapUnits(self): vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') vl.setCrs(QgsCoordinateReferenceSystem()) @@ -1788,6 +2070,43 @@ def testRenderProfileSymbolWithMapUnits(self): res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) self.assertTrue(self.image_check('vector_profile_map_units', 'vector_profile_map_units', res)) + def testRenderProfileSymbolWithMapUnitsTolerance(self): + vl = QgsVectorLayer('LineStringZ?crs=EPSG:27700', 'lines', 'memory') + vl.setCrs(QgsCoordinateReferenceSystem()) + self.assertTrue(vl.isValid()) + + for line in [ + 'LineStringZ (321829.48893365426920354 129991.38697145861806348 1, 321847.89668515208177269 129996.63588572069420479 1, 321848.97131609614007175 129979.22330882755341008 1, 321830.31725845142500475 129978.07136809575604275 1, 321829.48893365426920354 129991.38697145861806348 1)', + 'LineStringZ (321920.00953056826256216 129924.58260190498549491 2, 321924.65299345907988027 129908.43546159457764588 2, 321904.78543491888558492 129903.99811821122420952 2, 321900.80605239619035274 129931.39860145389684476 2, 321904.84799937985371798 129931.71552911199978553 2, 321908.93646715773502365 129912.90030360443051904 2, 321914.20495146053144708 129913.67693978428724222 2, 321911.30165811872575432 129923.01272751353099011 2, 321920.00953056826256216 129924.58260190498549491 2)', + 'LineStringZ (321923.10517279652412981 129919.61521573827485554 3, 321922.23537852568551898 129928.3598982143739704 3, 321928.60423935484141111 129934.22530528216157109 3, 321929.39881197665818036 129923.29054521876969375 3, 321930.55804549407912418 129916.53248518184409477 3, 321923.10517279652412981 129919.61521573827485554 3)', + 'LineStringZ (321990.47451346553862095 129909.63588680300745182 4, 321995.04325810901354998 129891.84052284323843196 4, 321989.66826330573530868 129890.5092018858413212 4, 321990.78512359503656626 129886.49917887404444627 4, 321987.37291929306229576 129885.64982962771318853 4, 321985.2254804756375961 129893.81317058412241749 4, 321987.63158903241856024 129894.41078495365218259 4, 321984.34022761805681512 129907.57450046355370432 4, 321990.47451346553862095 129909.63588680300745182 4)', + 'LineStringZ (322103.03910495212767273 129795.91051736124791205 5, 322108.25568856322206557 129804.76113295342656784 5, 322113.29666162584908307 129803.9285887333098799 5, 322117.78645010641776025 129794.48194090687320568 5, 322103.03910495212767273 129795.91051736124791205 5)']: + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt(line)) + self.assertTrue(vl.dataProvider().addFeature(f)) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute) + vl.elevationProperties().setRespectLayerSymbology(False) + line_symbol = QgsLineSymbol.createSimple({'color': '#ff00ff', 'width': '0.8'}) + line_symbol.setWidthUnit(Qgis.RenderUnit.MapUnits) + vl.elevationProperties().setProfileLineSymbol(line_symbol) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (321897.18831187387695536 129916.86947759155009408, 321942.11597351566888392 129924.94403429214435164)') + req = QgsProfileRequest(curve) + req.setTransformContext(self.create_transform_context()) + + req.setCrs(QgsCoordinateReferenceSystem()) + req.setTolerance(10) + + plot_renderer = QgsProfilePlotRenderer([vl], req) + plot_renderer.startGeneration() + plot_renderer.waitForFinished() + + res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) + self.assertTrue(self.image_check('vector_profile_map_units_tolerance', 'vector_profile_map_units_tolerance', res)) + def testRenderLayerSymbology(self): vl = QgsVectorLayer('PolygonZ?crs=EPSG:27700', 'lines', 'memory') vl.setCrs(QgsCoordinateReferenceSystem()) @@ -1833,6 +2152,163 @@ def testRenderLayerSymbology(self): res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14) self.assertTrue(self.image_check('vector_polygon_layer_symbology', 'vector_polygon_layer_symbology', res)) + def doCheckPoint(self, request: QgsProfileRequest, tolerance: float, layer: QgsVectorLayer, expectedFeatures): + request.setTolerance(tolerance) + + profGen = layer.createProfileGenerator(request) + self.assertIsNotNone(profGen) + self.assertTrue(profGen.generateProfile()) + results = profGen.takeResults() + features = results.asFeatures(Qgis.ProfileExportType.Features3D) + self.assertFalse(len(features) == 0) + + expected = sorted(expectedFeatures.copy()) + actual = [f.attributes['id'] for _, f in enumerate(features)] + actualUniqSorted = sorted(list(set(actual))) + + self.assertEqual(actualUniqSorted, expected) + + for k, feat in enumerate(features): + hasValidZ = False + if QgsWkbTypes.hasZ(feat.geometry.wkbType()): + for v in feat.geometry.vertices(): + if not math.isnan(v.z()): + hasValidZ = True + break + self.assertTrue(hasValidZ, "All vertice are on the ground!") + else: + self.assertTrue(hasValidZ, "Geometry should have z coordinates!") + + return results + + def doCheckLine(self, request: QgsProfileRequest, tolerance: float, layer: QgsVectorLayer, expectedFeatures, nbSubGeomPerFeature, geomType): + results = self.doCheckPoint(request, tolerance, layer, expectedFeatures) + features = results.asFeatures(Qgis.ProfileExportType.Features3D) + + actual = [f.attributes['id'] for _, f in enumerate(features)] + actualUniqSorted = sorted(list(set(actual))) + for idx, fid in enumerate(actualUniqSorted): + actual = [1 for _, f in enumerate(features) if f.attributes['id'] == fid] + self.assertEqual(len(actual), nbSubGeomPerFeature[idx]) + + for k, feat in enumerate(features): + self.assertEqual(feat.geometry.type(), geomType) + + for _, height in enumerate(results.distanceToHeightMap()): + self.assertTrue(math.isnan(height) or height > 0.0) + + return results + + def testPointGenerationFeature(self): + vl = QgsVectorLayer(os.path.join(unitTestDataPath(), '3d', 'points_with_z.shp')) + self.assertTrue(vl.isValid()) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Terrain) + vl.elevationProperties().setBinding(Qgis.AltitudeBinding.Vertex) + + dtmLayer = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm.tif')) + self.assertTrue(dtmLayer.isValid()) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (-346120 6631840, -346550 6632030, -346440 6632140, -347830 6632930)') + req = QgsProfileRequest(curve) + + terrain_provider = QgsRasterDemTerrainProvider() + terrain_provider.setLayer(dtmLayer) + + req.setTerrainProvider(terrain_provider) + req.setCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + + if Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() <= 10: + self.doCheckPoint(req, 15, vl, [5, 11, 12, 13, 14, 15, 18, 45, 46]) + elif Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() == 11: + self.doCheckPoint(req, 16, vl, [5, 11, 12, 13, 14, 15, 18, 45, 46]) + elif Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() >= 12: + self.doCheckPoint(req, 15, vl, [5, 11, 12, 13, 14, 15, 18, 45]) + + self.doCheckPoint(req, 70, vl, [0, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 18, 38, 45, 46, 48]) + + def testLineGenerationFeature(self): + vl = QgsVectorLayer(os.path.join(unitTestDataPath(), '3d', 'lines.shp')) + self.assertTrue(vl.isValid()) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Terrain) + vl.elevationProperties().setBinding(Qgis.AltitudeBinding.Vertex) + vl.elevationProperties().setExtrusionEnabled(False) + + dtmLayer = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm.tif')) + self.assertTrue(dtmLayer.isValid()) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (-346120 6631840, -346550 6632030, -346440 6632140, -347830 6632930)') + req = QgsProfileRequest(curve) + + terrain_provider = QgsRasterDemTerrainProvider() + terrain_provider.setLayer(dtmLayer) + + req.setTerrainProvider(terrain_provider) + req.setCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + + # check no tolerance + self.doCheckLine(req, 0, vl, [0, 2], [1, 5], Qgis.GeometryType.Point) + + # check increased tolerance, terrain, no extrusion + self.doCheckLine(req, 1, vl, [0, 2], [1, 5], Qgis.GeometryType.Line) + + # check increased tolerance, terrain, no extrusion + self.doCheckLine(req, 20, vl, [0, 2], [1, 3], Qgis.GeometryType.Line) + + # check increased tolerance, terrain, no extrusion + self.doCheckLine(req, 50, vl, [1, 0, 2], [1, 1, 1], Qgis.GeometryType.Line) + + # check terrain + extrusion + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Terrain) + vl.elevationProperties().setExtrusionEnabled(True) + vl.elevationProperties().setExtrusionHeight(17) + self.doCheckLine(req, 50, vl, [1, 0, 2], [1, 1, 1], Qgis.GeometryType.Polygon) + + # check no terrain + no extrusion + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute) + vl.elevationProperties().setExtrusionEnabled(False) + vl.elevationProperties().setZOffset(5.0) + self.doCheckLine(req, 50, vl, [1, 0, 2], [1, 1, 1], Qgis.GeometryType.Line) + + def testPolygonGenerationFeature(self): + vl = QgsVectorLayer(os.path.join(unitTestDataPath(), '3d', 'buildings.shp')) + self.assertTrue(vl.isValid()) + + vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Terrain) + vl.elevationProperties().setBinding(Qgis.AltitudeBinding.Vertex) + vl.elevationProperties().setExtrusionEnabled(False) + + dtmLayer = QgsRasterLayer(os.path.join(unitTestDataPath(), '3d', 'dtm.tif')) + self.assertTrue(dtmLayer.isValid()) + + curve = QgsLineString() + curve.fromWkt( + 'LineString (-346120 6631840, -346550 6632030, -346440 6632140, -347830 6632930)') + req = QgsProfileRequest(curve) + + terrain_provider = QgsRasterDemTerrainProvider() + terrain_provider.setLayer(dtmLayer) + + req.setTerrainProvider(terrain_provider) + req.setCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + + self.doCheckLine(req, 1, vl, [168, 206, 210, 284, 306, 321], [1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + + if Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() <= 10: + self.doCheckLine(req, 10, vl, [168, 172, 206, 210, 231, 267, 275, 282, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + self.doCheckLine(req, 11, vl, [168, 172, 206, 210, 231, 255, 267, 275, 282, 283, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + elif Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() == 11: + self.doCheckLine(req, 9, vl, [168, 172, 206, 210, 231, 267, 275, 282, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + self.doCheckLine(req, 10, vl, [168, 172, 206, 210, 231, 267, 275, 282, 283, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + elif Qgis.geosVersionMajor() == 3 and Qgis.geosVersionMinor() >= 12: + self.doCheckLine(req, 10, vl, [168, 172, 206, 210, 231, 267, 275, 282, 283, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + self.doCheckLine(req, 11, vl, [168, 172, 206, 210, 231, 237, 255, 267, 275, 282, 283, 284, 306, 307, 319, 321], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], Qgis.GeometryType.Line) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/3d/dtm_with_holes.tif b/tests/testdata/3d/dtm_with_holes.tif new file mode 100644 index 000000000000..fe501eb6a3d0 Binary files /dev/null and b/tests/testdata/3d/dtm_with_holes.tif differ diff --git a/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image.png b/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image.png index aa3a79afd151..93af4a7609d6 100644 Binary files a/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image.png and b/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image_mask.png b/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image_mask.png new file mode 100644 index 000000000000..4ca18899c66c Binary files /dev/null and b/tests/testdata/control_images/3d/expected_depth_retrieve_image/expected_depth_retrieve_image_mask.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1.png index aa3a79afd151..b1a3668e40ba 100644 Binary files a/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1.png and b/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1_mask.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1_mask.png new file mode 100644 index 000000000000..4ca18899c66c Binary files /dev/null and b/tests/testdata/control_images/3d/expected_depth_wheel_action_1/expected_depth_wheel_action_1_mask.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2.png index b95a1d92f0d2..24863041a355 100644 Binary files a/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2.png and b/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2_mask.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2_mask.png new file mode 100644 index 000000000000..de71ab3908c1 Binary files /dev/null and b/tests/testdata/control_images/3d/expected_depth_wheel_action_2/expected_depth_wheel_action_2_mask.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3.png index d4ab853e1b97..0af56ec8ceef 100644 Binary files a/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3.png and b/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3_mask.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3_mask.png new file mode 100644 index 000000000000..33539067f3ea Binary files /dev/null and b/tests/testdata/control_images/3d/expected_depth_wheel_action_3/expected_depth_wheel_action_3_mask.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4.png index 77ab803a0967..8424f8f0c1f7 100644 Binary files a/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4.png and b/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4.png differ diff --git a/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4_mask.png b/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4_mask.png new file mode 100644 index 000000000000..2d5603d512bc Binary files /dev/null and b/tests/testdata/control_images/3d/expected_depth_wheel_action_4/expected_depth_wheel_action_4_mask.png differ diff --git a/tests/testdata/control_images/expected_raster_transparency/expected_raster_transparency_mask.png b/tests/testdata/control_images/expected_raster_transparency/expected_raster_transparency_mask.png index e110ce4f5dcb..bf7fb854afb7 100644 Binary files a/tests/testdata/control_images/expected_raster_transparency/expected_raster_transparency_mask.png and b/tests/testdata/control_images/expected_raster_transparency/expected_raster_transparency_mask.png differ diff --git a/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance.png b/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance.png new file mode 100644 index 000000000000..1f80bd61fc2a Binary files /dev/null and b/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance.png differ diff --git a/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance_mask.png b/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance_mask.png new file mode 100644 index 000000000000..48767757b5fc Binary files /dev/null and b/tests/testdata/control_images/layout_profile/expected_vector_layer_map_units_tolerance/expected_vector_layer_map_units_tolerance_mask.png differ diff --git a/tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png b/tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png new file mode 100644 index 000000000000..b5c0414fd914 Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_elevation_no_filter/expected_elevation_no_filter.png differ diff --git a/tests/testdata/control_images/mesh/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png b/tests/testdata/control_images/mesh/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png new file mode 100644 index 000000000000..08b004aced68 Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png differ diff --git a/tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png b/tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png new file mode 100644 index 000000000000..b5c0414fd914 Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png differ diff --git a/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_above_surface_limit_tolerance/expected_vector_lines_as_fill_above_surface_limit_tolerance.png b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_above_surface_limit_tolerance/expected_vector_lines_as_fill_above_surface_limit_tolerance.png new file mode 100644 index 000000000000..0d29c7456d9d Binary files /dev/null and b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_above_surface_limit_tolerance/expected_vector_lines_as_fill_above_surface_limit_tolerance.png differ diff --git a/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_below_surface_with_holed_dtm/expected_vector_lines_as_fill_below_surface_with_holed_dtm.png b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_below_surface_with_holed_dtm/expected_vector_lines_as_fill_below_surface_with_holed_dtm.png new file mode 100644 index 000000000000..9c825d3dbb26 Binary files /dev/null and b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_fill_below_surface_with_holed_dtm/expected_vector_lines_as_fill_below_surface_with_holed_dtm.png differ diff --git a/tests/testdata/control_images/profile_chart/expected_vector_lines_as_line_with_holed_dtm/expected_vector_lines_as_line_with_holed_dtm.png b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_line_with_holed_dtm/expected_vector_lines_as_line_with_holed_dtm.png new file mode 100644 index 000000000000..73ef660b4942 Binary files /dev/null and b/tests/testdata/control_images/profile_chart/expected_vector_lines_as_line_with_holed_dtm/expected_vector_lines_as_line_with_holed_dtm.png differ diff --git a/tests/testdata/control_images/profile_chart/expected_vector_profile_map_units_tolerance/expected_vector_profile_map_units_tolerance.png b/tests/testdata/control_images/profile_chart/expected_vector_profile_map_units_tolerance/expected_vector_profile_map_units_tolerance.png new file mode 100644 index 000000000000..90634607a1b7 Binary files /dev/null and b/tests/testdata/control_images/profile_chart/expected_vector_profile_map_units_tolerance/expected_vector_profile_map_units_tolerance.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels.png new file mode 100644 index 000000000000..4b9440c7be70 Binary files /dev/null and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels_mask.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels_mask.png new file mode 100644 index 000000000000..d2528c6f0e3d Binary files /dev/null and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Highlight_Empty_Labels/WMS_GetMap_Highlight_Empty_Labels_mask.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_1bit/WMS_GetMap_Mode_1bit.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_1bit/WMS_GetMap_Mode_1bit.png index 88b9ab67b64f..2ed3b7370372 100644 Binary files a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_1bit/WMS_GetMap_Mode_1bit.png and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_1bit/WMS_GetMap_Mode_1bit.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit/WMS_GetMap_Mode_8bit.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit/WMS_GetMap_Mode_8bit.png index d4a22db05f2d..2a9156728bf3 100644 Binary files a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit/WMS_GetMap_Mode_8bit.png and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit/WMS_GetMap_Mode_8bit.png differ diff --git a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit_with_transparency/WMS_GetMap_Mode_8bit_with_transparency.png b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit_with_transparency/WMS_GetMap_Mode_8bit_with_transparency.png index ace55ce2973c..5764841a5e27 100644 Binary files a/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit_with_transparency/WMS_GetMap_Mode_8bit_with_transparency.png and b/tests/testdata/control_images/qgis_server/WMS_GetMap_Mode_8bit_with_transparency/WMS_GetMap_Mode_8bit_with_transparency.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter/expected_dem_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter/expected_dem_filter.png new file mode 100644 index 000000000000..f535f0f9486c Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter/expected_dem_filter.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter_offset_and_scale/expected_dem_filter_offset_and_scale.png b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter_offset_and_scale/expected_dem_filter_offset_and_scale.png new file mode 100644 index 000000000000..a5a8d1b51d42 Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_filter_offset_and_scale/expected_dem_filter_offset_and_scale.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_dem_no_filter/expected_dem_no_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_no_filter/expected_dem_no_filter.png new file mode 100644 index 000000000000..60cd8200a8e4 Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_dem_no_filter/expected_dem_no_filter.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png new file mode 100644 index 000000000000..00beb87ed28c Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_3/expected_elevation_range_per_band_match_3.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png new file mode 100644 index 000000000000..152c5ef8059f Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_4/expected_elevation_range_per_band_match_4.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_5/expected_elevation_range_per_band_match_5.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_5/expected_elevation_range_per_band_match_5.png new file mode 100644 index 000000000000..ae219d0d1dda Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_match_5/expected_elevation_range_per_band_match_5.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_no_filter/expected_elevation_range_per_band_no_filter.png b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_no_filter/expected_elevation_range_per_band_no_filter.png new file mode 100644 index 000000000000..00beb87ed28c Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_elevation_range_per_band_no_filter/expected_elevation_range_per_band_no_filter.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png new file mode 100644 index 000000000000..08b004aced68 Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_excluded/expected_fixed_elevation_range_excluded.png differ diff --git a/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png new file mode 100644 index 000000000000..60cd8200a8e4 Binary files /dev/null and b/tests/testdata/control_images/rasterlayerrenderer/expected_fixed_elevation_range_included/expected_fixed_elevation_range_included.png differ diff --git a/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt b/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt index ce39f88833d0..c5c1206856e6 100644 --- a/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt +++ b/tests/testdata/qgis_server/wfs_getFeature_1_1_0_featureid_0_1_1_0_srsname.txt @@ -3,21 +3,21 @@ Content-Type: text/xml; subtype=gml/3.1.1; charset=utf-8 - 44.90139484 8.20345931 - 44.90148253 8.20354699 + 8.20345931 44.90139484 + 8.20354699 44.90148253 - 44.90148253 8.20349634 - 44.90148253 8.20349634 + 8.20349634 44.90148253 + 8.20349634 44.90148253 - 44.90148253 8.20349634 + 8.20349634 44.90148253 1 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,