From bf3c7c244944b852db000376c38720e86a1803ee Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 10:43:42 +1000 Subject: [PATCH 001/135] initial commit --- .github/ISSUE_TEMPLATE/10_bug_report.yml | 71 ++++ .github/ISSUE_TEMPLATE/15_feature_request.yml | 24 ++ .github/ISSUE_TEMPLATE/config.yml | 6 + .github/dependabot.yml | 12 + .github/labeler.yml | 55 +++ .github/release.yml | 22 ++ .github/workflows/auto-labeler.yml | 14 + .github/workflows/documentation.yml | 119 ++++++ .github/workflows/linter.yml | 75 ++++ .github/workflows/package_and_release.yml | 168 +++++++++ .github/workflows/tester.yml | 99 +++++ .gitignore | 130 +++++++ .pre-commit-config.yaml | 82 +++++ .vscode/extensions.json | 13 + .vscode/settings.json | 45 +++ .vscode/tasks.json | 37 ++ CHANGELOG.md | 22 ++ CONTRIBUTING.md | 20 + LICENSE | 341 ++++++++++++++++++ README.md | 132 +++++++ docs/conf.py | 135 +++++++ docs/development/contribute.md | 2 + docs/development/documentation.md | 24 ++ docs/development/environment.md | 63 ++++ docs/development/history.md | 2 + docs/development/packaging.md | 47 +++ docs/development/testing.md | 33 ++ docs/development/translation.md | 44 +++ docs/index.md | 33 ++ docs/static/dev_qgis_enable_plugin.png | Bin 0 -> 47170 bytes .../static/dev_qgis_set_pluginpath_envvar.png | Bin 0 -> 37457 bytes docs/usage/installation.md | 19 + linters/pylintrc | 200 ++++++++++ map2loop/__about__.py | 115 ++++++ map2loop/__init__.py | 23 ++ map2loop/gui/__init__.py | 0 map2loop/gui/dlg_settings.py | 174 +++++++++ map2loop/gui/dlg_settings.ui | 245 +++++++++++++ map2loop/metadata.txt | 26 ++ map2loop/plugin_main.py | 172 +++++++++ map2loop/processing/__init__.py | 2 + map2loop/processing/provider.py | 88 +++++ map2loop/resources/i18n/plugin_map2loop_en.ts | 4 + .../resources/i18n/plugin_translation.pro | 13 + map2loop/resources/images/default_icon.png | Bin 0 -> 28083 bytes map2loop/toolbelt/__init__.py | 3 + map2loop/toolbelt/env_var_parser.py | 60 +++ map2loop/toolbelt/log_handler.py | 193 ++++++++++ map2loop/toolbelt/preferences.py | 177 +++++++++ requirements/development.txt | 8 + requirements/documentation.txt | 7 + requirements/packaging.txt | 4 + requirements/testing.txt | 5 + scripts/__init__.py | 0 scripts/generate_translation_profile.py | 86 +++++ setup.cfg | 52 +++ tests/__init__.py | 0 tests/qgis/__init__.py | 0 tests/qgis/test_env_var_parser.py | 76 ++++ tests/qgis/test_plg_preferences.py | 99 +++++ tests/qgis/test_processing.py | 40 ++ tests/unit/__init__.py | 0 tests/unit/test_plg_metadata.py | 86 +++++ 63 files changed, 3847 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/10_bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/15_feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/auto-labeler.yml create mode 100644 .github/workflows/documentation.yml create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/package_and_release.yml create mode 100644 .github/workflows/tester.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/conf.py create mode 100644 docs/development/contribute.md create mode 100644 docs/development/documentation.md create mode 100644 docs/development/environment.md create mode 100644 docs/development/history.md create mode 100644 docs/development/packaging.md create mode 100644 docs/development/testing.md create mode 100644 docs/development/translation.md create mode 100644 docs/index.md create mode 100644 docs/static/dev_qgis_enable_plugin.png create mode 100644 docs/static/dev_qgis_set_pluginpath_envvar.png create mode 100644 docs/usage/installation.md create mode 100644 linters/pylintrc create mode 100644 map2loop/__about__.py create mode 100644 map2loop/__init__.py create mode 100644 map2loop/gui/__init__.py create mode 100644 map2loop/gui/dlg_settings.py create mode 100644 map2loop/gui/dlg_settings.ui create mode 100644 map2loop/metadata.txt create mode 100644 map2loop/plugin_main.py create mode 100644 map2loop/processing/__init__.py create mode 100644 map2loop/processing/provider.py create mode 100644 map2loop/resources/i18n/plugin_map2loop_en.ts create mode 100644 map2loop/resources/i18n/plugin_translation.pro create mode 100644 map2loop/resources/images/default_icon.png create mode 100644 map2loop/toolbelt/__init__.py create mode 100644 map2loop/toolbelt/env_var_parser.py create mode 100644 map2loop/toolbelt/log_handler.py create mode 100644 map2loop/toolbelt/preferences.py create mode 100644 requirements/development.txt create mode 100644 requirements/documentation.txt create mode 100644 requirements/packaging.txt create mode 100644 requirements/testing.txt create mode 100644 scripts/__init__.py create mode 100644 scripts/generate_translation_profile.py create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/qgis/__init__.py create mode 100644 tests/qgis/test_env_var_parser.py create mode 100644 tests/qgis/test_plg_preferences.py create mode 100644 tests/qgis/test_processing.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_plg_metadata.py diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml new file mode 100644 index 0000000..0f09cd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -0,0 +1,71 @@ +name: Bug/Crash report. +description: Create a bug report to help us improve our plugin. +labels: + - "Bug" + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report correctly. + + - type: textarea + id: what + attributes: + label: What is the bug or the crash? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce the issue + description: | + Steps, sample datasets and qgis project file to reproduce the behavior. + Screencasts or screenshots are more than welcome, you can drag&drop them in the text box. + + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: textarea + id: about-info + attributes: + label: Versions + description: | + In the QGIS Help menu -> About, click in the table, Ctrl+A and then Ctrl+C. Finally paste here. + Do not make a screenshot. + validations: + required: true + + - type: checkboxes + id: qgis-version + attributes: + label: Supported QGIS version + description: | + Each month, there is a new release of QGIS. According to the release schedule, you should at least be running a supported QGIS version. + You can check the release schedule https://www.qgis.org/en/site/getinvolved/development/roadmap.html#release-schedule + options: + - label: I'm running a supported QGIS version according to the official roadmap. + + - type: checkboxes + id: new-profile + attributes: + label: New profile + description: | + Did you try with a new QGIS profile? Some issues or crashes might be related to other plugins or specific configuration. + You must try with a new profile to check if the issue remains. + Read this link how to create a new profile + https://docs.qgis.org/3.4/en/docs/user_manual/introduction/qgis_configuration.html#working-with-user-profiles + options: + - label: I tried with a new QGIS profile + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: | + Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/15_feature_request.yml b/.github/ISSUE_TEMPLATE/15_feature_request.yml new file mode 100644 index 0000000..b7acee5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/15_feature_request.yml @@ -0,0 +1,24 @@ +name: Feature request +description: Suggest a feature idea. +labels: + - 'Feature Request' +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request correctly. + + - type: textarea + id: what + attributes: + label: Feature description + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: Additional + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..14f744a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: true + +contact_links: + - name: Documentation + url: https://github.com/Loop3d/plugin_map2loop + about: Please read carefully the documentation before to submit an issue. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..86f197b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/requirements" + schedule: + interval: monthly + time: "04:00" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..ac349c3 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,55 @@ +ci-cd: + - changed-files: + - any-glob-to-any-file: .github/** + +dependencies: + - changed-files: + - any-glob-to-any-file: + - requirements/*.txt + - requirements.txt + +documentation: + - changed-files: + - any-glob-to-any-file: + - docs/** + - requirements/documentation.txt + +enhancement: + - head-branch: + - ^feature + - feature + - ^improve + - improve + +packaging: + - head-branch: + - ^packaging + - packaging + - changed-files: + - any-glob-to-any-file: + - requirements/packaging.txt + - setup.py + +quality: + - changed-files: + - any-glob-to-any-file: + - tests/**/* + + +tooling: + - head-branch: + - ^tooling + - tooling + - changed-files: + - any-glob-to-any-file: + - .pre-commit-config.yaml + - setup.cfg + +UI: + - head-branch: + - ^ui + - ui + - changed-files: + - any-glob-to-any-file: + - plugin_map2loop/**/*.ui + - plugin_map2loop/gui/** diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..64604cf --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,22 @@ +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci + categories: + - title: Bugs fixes ๐Ÿ› + labels: + - bug + - title: Features and enhancements ๐ŸŽ‰ + labels: + - enhancement + - UI + - title: Tooling ๐Ÿ”ง + labels: + - ci-cd + - title: Documentation ๐Ÿ“– + labels: + - documentation + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml new file mode 100644 index 0000000..c477465 --- /dev/null +++ b/.github/workflows/auto-labeler.yml @@ -0,0 +1,14 @@ +name: "๐Ÿท PR Labeler" +on: + - pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..791fd6b --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,119 @@ +name: "๐Ÿ“š Documentation" + +# Global environment variables +env: + CONDITION_IS_PUSH: ${{ github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main') }} + CONDITION_IS_WORKFLOW_RUN: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' }} + PROJECT_FOLDER: plugin_map2loop + PYTHON_VERSION: 3.12 + +# This workflow is triggered on: +on: + push: + branches: + - main + paths: + - '.github/workflows/documentation.yml' + - 'docs/**/*' + - "plugin_map2loop/**/*.py" + - "plugin_map2loop/metadata.txt" + - 'requirements/documentation.txt' + tags: + - "*" + + pull_request: + branches: + - main + paths: + - ".github/workflows/documentation.yml" + - docs/**/* + - requirements/documentation.txt + + workflow_dispatch: + + workflow_run: + workflows: + - "๐Ÿ“ฆ Package & ๐Ÿš€ Release" + types: + - completed + + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + id-token: write + pages: write + +# Allow one concurrent deployment per branch/pr +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + cache: "pip" + cache-dependency-path: "requirements/documentation.txt" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Cache Sphinx cache + uses: actions/cache@v4 + with: + path: docs/_build/cache + key: ${{ runner.os }}-sphinx-${{ hashFiles('docs/**/*') }} + restore-keys: | + ${{ runner.os }}-sphinx- + + - name: Install dependencies + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements/documentation.txt + + - name: Build doc using Sphinx + run: sphinx-build -b html -j auto -d docs/_build/cache -q docs docs/_build/html + + - name: Download artifact from build workflow + if: ${{ env.CONDITION_IS_PUSH || env.CONDITION_IS_WORKFLOW_RUN }} + uses: dawidd6/action-download-artifact@v9 + with: + allow_forks: false + branch: main + event: push + github_token: ${{ secrets.GITHUB_TOKEN }} + if_no_artifact_found: warn + name: ${{ env.PROJECT_FOLDER }}-latest + path: docs/_build/html/ + # run_id: ${{ github.event.workflow_run.id }} + workflow: package_and_release.yml + + - name: Save build doc as artifact + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: documentation + path: docs/_build/html/* + retention-days: 30 + + - name: Setup Pages + uses: actions/configure-pages@v5 + if: ${{ env.CONDITION_IS_PUSH || env.CONDITION_IS_WORKFLOW_RUN }} + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + if: ${{ env.CONDITION_IS_PUSH || env.CONDITION_IS_WORKFLOW_RUN }} + with: + # Upload entire repository + path: docs/_build/html/ + + - name: Deploy to GitHub Pages + id: deployment + if: ${{ env.CONDITION_IS_PUSH || env.CONDITION_IS_WORKFLOW_RUN }} + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..78d45c7 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,75 @@ +name: "โœ… Linter" + +on: + push: + branches: + - main + paths: + - '**.py' + + pull_request: + branches: + - main + paths: + - '**.py' + +env: + PROJECT_FOLDER: "plugin_map2loop" + PYTHON_VERSION: 3.9 + + +jobs: + lint-py: + name: Python ๐Ÿ + + runs-on: ubuntu-latest + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + cache: "pip" + cache-dependency-path: "requirements/development.txt" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install project requirements + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements/development.txt + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 ${{ env.PROJECT_FOLDER }} --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. + flake8 ${{ env.PROJECT_FOLDER }} --count --exit-zero --statistics + + qt6-check: + name: PyQt6 6๏ธโƒฃ + runs-on: ubuntu-latest + container: + image: registry.gitlab.com/oslandia/qgis/pyqgis-4-checker/pyqgis-qt-checker:latest + volumes: + - /tmp/.X11-unix:/tmp/.X11-unix + - ${{ github.workspace }}:/home/pyqgisdev/ + options: --user root + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Check PyQt5 to PyQt6 compatibility. + run: | + pyqt5_to_pyqt6.py --dry_run ${{ env.PROJECT_FOLDER }}/ + pyqt5_to_pyqt6.py --logfile pyqt6_checker.log ${{ env.PROJECT_FOLDER }}/ + + - name: Upload script report if script fails + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: pyqt6-checker-error-report + path: pyqt6_checker.log + retention-days: 7 + \ No newline at end of file diff --git a/.github/workflows/package_and_release.yml b/.github/workflows/package_and_release.yml new file mode 100644 index 0000000..63632c3 --- /dev/null +++ b/.github/workflows/package_and_release.yml @@ -0,0 +1,168 @@ +name: "๐Ÿ“ฆ Package & ๐Ÿš€ Release" + +env: + PROJECT_FOLDER: plugin_map2loop + PYTHON_VERSION: 3.9 + +on: + push: + branches: + - main + paths: + - .github/workflows/package_and_release.yml + - 'docs/**/*' + - "plugin_map2loop/**/*.py" + - "plugin_map2loop/metadata.txt" + tags: + - "*" + +# Allow one concurrent deployment per branch/pr +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + translation: + name: "๐Ÿ’ฌ i18n compilation" + runs-on: ubuntu-latest + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install translation requirements + run: | + sudo apt update + sudo apt install qt5-qmake qttools5-dev-tools + python3 -m pip install -U pyqt5-tools + + - name: Update translations + run: | + python3 scripts/generate_translation_profile.py + pylupdate5 -noobsolete -verbose ${{ env.PROJECT_FOLDER }}/resources/i18n/plugin_translation.pro + + - name: Compile translations + run: lrelease ${{ env.PROJECT_FOLDER }}/resources/i18n/*.ts + + - uses: actions/upload-artifact@v4 + with: + name: translations-build + path: ${{ env.PROJECT_FOLDER }}/**/*.qm + if-no-files-found: error + + # -- NO TAGS ---------------------------------------------------------------------- + packaging: + name: "๐Ÿ“ฆ Packaging plugin" + runs-on: ubuntu-latest + needs: + - translation + + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + cache: "pip" + cache-dependency-path: "requirements/packaging.txt" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements/packaging.txt + + - name: Download translations + uses: actions/download-artifact@v4 + with: + name: translations-build + path: ${{ env.PROJECT_FOLDER }} + + - name: Amend gitignore to include compiled translations and add it to tracked files + run: | + # include compiled translations + sed -i "s|^*.qm.*| |" .gitignore + + # git add full project + git add ${{ env.PROJECT_FOLDER }}/ + + - name: Package the latest version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + qgis-plugin-ci package latest \ + --allow-uncommitted-changes \ + --plugin-repo-url $(gh api "repos/$GITHUB_REPOSITORY/pages" --jq '.html_url') + + - uses: actions/upload-artifact@v4 + with: + name: ${{ env.PROJECT_FOLDER }}-latest + path: | + plugins.xml + ${{ env.PROJECT_FOLDER }}.*.zip + if-no-files-found: error + + # -- ONLY TAGS ---------------------------------------------------------------------- + release: + name: "๐Ÿš€ Release on tag" + runs-on: ubuntu-latest + permissions: + contents: write + needs: + - translation + + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + cache: "pip" + cache-dependency-path: "requirements/packaging.txt" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install project requirements + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements/packaging.txt + + - name: Download translations + uses: actions/download-artifact@v4 + with: + name: translations-build + path: ${{ env.PROJECT_FOLDER }} + + - name: Amend gitignore to include compiled translations and add it to tracked files + run: | + # include compiled translations + sed -i "s|^*.qm.*| |" .gitignore + + # git add full project + git add ${{ env.PROJECT_FOLDER }}/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: true + generate_release_notes: true + + - name: Deploy plugin + run: >- + qgis-plugin-ci + release ${GITHUB_REF/refs\/tags\//} + --allow-uncommitted-changes + --create-plugin-repo + --github-token ${{ secrets.GITHUB_TOKEN }} + --osgeo-username ${{ secrets.OSGEO_USER }} + --osgeo-password ${{ secrets.OSGEO_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml new file mode 100644 index 0000000..9053903 --- /dev/null +++ b/.github/workflows/tester.yml @@ -0,0 +1,99 @@ +name: "๐ŸŽณ Tester" + +on: + push: + branches: + - main + paths: + - '**.py' + - .github/workflows/tester.yml + - requirements/testing.txt + + pull_request: + branches: + - main + paths: + - '**.py' + - .github/workflows/tester.yml + - requirements/testing.txt + +env: + PROJECT_FOLDER: "plugin_map2loop" + PYTHON_VERSION: 3.9 + + +jobs: + tests-unit: + runs-on: ubuntu-latest + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + cache: "pip" + cache-dependency-path: "requirements/testing.txt" + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Python requirements + run: | + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements/testing.txt + + - name: Run Unit tests + run: pytest -p no:qgis tests/unit/ + + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:3.4 + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df33539 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +junit/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/_apidoc/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# -- CUSTOM ---- +*.zip +*.qm diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e2a3943 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,82 @@ +exclude: ".venv|__pycache__|tests/dev/|tests/fixtures/" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + args: + - --maxkb=500 + - id: check-case-conflict + - id: check-toml + - id: check-xml + - id: check-yaml + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: fix-encoding-pragma + args: + - --remove + - id: trailing-whitespace + args: + - --markdown-linebreak-ext=md + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.11.2" + hooks: + - id: ruff + args: + - --fix + - --target-version=py39 + types_or: + - python + - pyi + - id: ruff-format + args: + - --line-length=88 + - --target-version=py39 + types_or: + - python + - pyi + + - repo: https://github.com/python/black + rev: 25.1.0 + hooks: + - id: black + args: + - --target-version=py39 + + # Disabled until PyQt translation support f-strings + # - repo: https://github.com/asottile/pyupgrade + # rev: v3.15.0 + # hooks: + # - id: pyupgrade + # args: + # - "--py39-plus" + + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + args: + - --profile + - black + - --filter-files + + - repo: https://github.com/pycqa/flake8 + rev: 7.2.0 + hooks: + - id: flake8 + files: ^plugin_map2loop/.*\.py$ + additional_dependencies: + - flake8-qgis + args: + [ + "--config=setup.cfg", + "--select=E9,F63,F7,F82,QGS101,QGS102,QGS103,QGS104,QGS106", + ] + +ci: + autoupdate_schedule: quarterly + skip: [] + submodules: false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..84fbd3b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "ms-python.black-formatter", + + "ms-python.isort", + + "ms-python.python", + "njpwerner.autodocstring", + "redhat.vscode-yaml", + "zhoufeng.pyqt-integration" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ed81c92 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,45 @@ +{ + // Editor + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "files.associations": { + "./requirements/*.txt": "pip-requirements", + "metadata.txt": "ini", + "**/*.model3": "xml", + "**/*.ts": "xml", + "**/*.ui": "xml" + }, + // Markdown + "markdown.updateLinksOnFileMove.enabled": "prompt", + "markdown.updateLinksOnFileMove.enableForDirectories": true, + "markdown.validate.enabled": true, + "markdown.validate.fileLinks.markdownFragmentLinks": "warning", + "markdown.validate.fragmentLinks.enabled": "warning", + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "files.trimTrailingWhitespace": false, + }, + // Python + "python.analysis.autoFormatStrings": false, // breaking PyQt translation + "python.analysis.typeCheckingMode": "basic", + // "python.defaultInterpreterPath": ".venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports":"explicit" + }, + "editor.rulers": [ + 88 + ], + "editor.wordWrapColumn": 88, + }, + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": true, + // Linter + + // Extensions + "autoDocstring.docstringFormat": "sphinx" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f2bf80c --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Upgrade dependencies - Development", + "type": "shell", + "osx": { + "command": "${config:python.defaultInterpreterPath} -m pip install -U -r requirements/development.txt" + }, + "windows": { + "command": "${config:python.defaultInterpreterPath} -m pip install -U -r requirements/development.txt" + }, + "linux": { + "command": "${config:python.defaultInterpreterPath} -m pip install -U -r requirements/development.txt" + }, + "problemMatcher": [] + }, + { + "label": "Translation Update", + "type": "shell", + "linux": { + "command": "pylupdate5 -verbose plugin_map2loop/resources/i18n/plugin_translation.pro" + }, + "problemMatcher": [] + }, + { + "label": "Translation Compile", + "type": "shell", + "linux": { + "command": "lrelease plugin_map2loop/resources/i18n/*.ts " + }, + "problemMatcher": [] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..06590b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# CHANGELOG + +The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). + + + +## 0.1.0 - 2025-06-17 + +- First release +- Generated with the [QGIS Plugins templater](https://oslandia.gitlab.io/qgis/template-qgis-plugin/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f022098 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing Guidelines + +First off, thanks for considering to contribute to this project! + +These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Git hooks + +We use git hooks through [pre-commit](https://pre-commit.com/) to enforce and automatically check some "rules". Please install them (`pre-commit install`) before to push any commit. + +See the relevant configuration file: `.pre-commit-config.yaml`. + +## Code Style + +Make sure your code *roughly* follows [PEP-8](https://www.python.org/dev/peps/pep-0008/) and keeps things consistent with the rest of the code: + +- docstrings: [sphinx-style](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html#the-sphinx-docstring-format) is used to write technical documentation. +- formatting: [black](https://black.readthedocs.io/) is used to automatically format the code without debate. +- sorted imports: [isort](https://pycqa.github.io/isort/) is used to sort imports + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f5da4d --- /dev/null +++ b/LICENSE @@ -0,0 +1,341 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (c) 2025, Lachlan GROSE / QGIS + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bed0f2f --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# map2loop - QGIS Plugin + +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + + + + +## Generated options + +### Plugin + +> Here is a list of the options you picked when creating the plugin with the cookiecutter template. + +| Cookiecutter option | Picked value | +| :------------------ | :----------: | +| Plugin name | map2loop | +| Plugin name slugified | plugin_map2loop | +| Plugin name class (used in code) | Map2LoopPlugin | +| Plugin category | None | +| Plugin description short | Loop tools for augmenting geological map data into 3D model datasets | +| Plugin description long | Extends QGIS with revolutionary features that every single GIS end-users was expected (or not)! | +| Plugin tags | geology, modelling, structural geology, loop3d | +| Plugin icon | default_icon.png | +| Plugin with processing provider | True | +| Author name | Lachlan GROSE | +| Author organization | Monash University | +| Author email | lachlan.grose@monash.edu | +| Minimum QGIS version | 3.4 | +| Maximum QGIS version | 3.99 | +| Support Qt6 | True | +| Git repository URL | https://github.com/Loop3d/plugin_map2loop | +| Git default branch | main | +| License | GPLv2+ | +| Python linter | None | +| CI/CD platform | GitHub | +| Publish to using CI/CD | True | +| IDE | VSCode | + +### Tooling + +This project is configured with the following tools: + +- [Black](https://black.readthedocs.io/en/stable/) to format the code without any existential question +- [iSort](https://pycqa.github.io/isort/) to sort the Python imports + +Code rules are enforced with [pre-commit](https://pre-commit.com/) hooks. + +See also: [contribution guidelines](CONTRIBUTING.md). + +## CI/CD + +Plugin is linted, tested, packaged and published with GitHub. + +If you mean to deploy it to the [official QGIS plugins repository](https://plugins.qgis.org/), remember to set your OSGeo credentials (`OSGEO_USER_NAME` and `OSGEO_USER_PASSWORD`) as environment variables in your CI/CD tool. + + +### Documentation + +The documentation is located in `docs` subfolder, written in Markdown using [myst-parser](https://myst-parser.readthedocs.io/), structured in two main parts, Usage and Contribution, generated using Sphinx (have a look to [the configuration file](./docs/conf.py)) and is automatically generated through the CI and published on Pages: (see [post generation steps](#2-build-the-documentation-locally) below). + +---- + +## Next steps post generation + +### 1. Set up development environment + +> Typical commands on Linux (Ubuntu). + +1. If you didn't pick the `git init` option, initialize your local repository: + + ```sh + git init + ``` + +1. Follow the [embedded documentation to set up your development environment](./docs/development/environment.md) to create virtual environment and install development dependencies. +1. Add all files to git index to prepare initial commit: + + ```sh + git add -A + ``` + +1. Run the git hooks to ensure that everything runs OK and to start developing on quality standards: + + ```sh + # run all pre-commit hooks on all files + pre-commit run -a + # don't be shy, run it again until it's all grren + ``` + +### 2. Adjust URL and build the documentation locally + +> [!NOTE] +> Since it's very hard to determine which the final documentation URL will be, the templater does not set it up. You have to do it manually. +> The final URL should be something like this: . You can find it in Pages settings of your repository: . + +1. Have a look to the [plugin's metadata.txt file](plugin_map2loop/metadata.txt): review it, complete it or fix it if needed (URLs, etc.)., especially the `homepage` URL which should be to your GitLab or GitHub Pages. +1. Update the base URL of custom repository in [installation doc page](./docs/usage/installation.md). +1. Change the plugin's icon stored in `plugin_map2loop/resources/images` +1. Follow the [embedded documentation to build plugin documentation locally](./docs/development/documentation.md) + +### 3. Prepare your remote repository + +1. If you did not yet, create a remote repository on your Git hosting platform (GitHub, GitLab, etc.) +1. Create labels listed in [labeler.yml file](.github/labeler.yml) to make PR auto-labelling work. +1. Switch the source of GitHub Pages to `GitHub Actions` in your repository settings +1. Add the remote repository to your local repository: + + ```sh + git remote add origin https://github.com/Loop3d/plugin_map2loop + ``` + +1. Commit changes: + + ```sh + git commit -m "init(plugin): adding first files of map2loop" -m "generated with QGIS Plugin Templater (https://oslandia.gitlab.io/qgis/template-qgis-plugin)" + ``` + +1. Push the initial commit to the remote repository: + + ```sh + git push -u origin main + ``` + +1. Create a new release following the [packaging/release guide](./docs//development/packaging.md) with the tag `0.1.0-beta1` to trigger the CI/CD pipeline and publish the plugin on the [official QGIS plugins repository](https://plugins.qgis.org/) (if you picked up the option). + +---- + +## License + +Distributed under the terms of the [`GPLv2+` license](LICENSE). diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..6d7892e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,135 @@ +#!python3 + +""" + Configuration for project documentation using Sphinx. +""" + +# standard +import sys +from datetime import datetime +from os import environ, path + +sys.path.insert(0, path.abspath("..")) # move into project package + +# 3rd party +import sphinx_rtd_theme # noqa: F401 theme of Read the Docs + +# Package +from map2loop import __about__ + +# -- Build environment ----------------------------------------------------- +on_rtd = environ.get("READTHEDOCS", None) == "True" + +# -- Project information ----------------------------------------------------- +author = __about__.__author__ +copyright = __about__.__copyright__ +description = __about__.__summary__ +project = __about__.__title__ +version = release = __about__.__version__ + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + # Sphinx included + "sphinx.ext.autosectionlabel", + "sphinx.ext.extlinks", + "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + # 3rd party + "myst_parser", + "sphinx_copybutton", + "sphinx_rtd_theme", +] + + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = {".md": "markdown", ".rst": "restructuredtext"} +autosectionlabel_prefix_document = True +# The master toctree document. +master_doc = "index" + + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [ + "_build", + ".venv", + "Thumbs.db", + ".DS_Store", + "_output", + "ext_libs", + "tests", + "demo", +] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + + +# -- Options for HTML output ------------------------------------------------- + +# -- Theme + +html_favicon = str(__about__.__icon_path__) +html_logo = str(__about__.__icon_path__) +# uncomment next line if you store some statics which are not directly linked into the markdown/RST files +# html_static_path = ["static/include_additional"] +html_theme = "sphinx_rtd_theme" +html_theme_options = { + "display_version": True, + "logo_only": False, + "prev_next_buttons_location": "both", + "style_external_links": True, + "style_nav_header_background": "SteelBlue", + # Toc options + "collapse_navigation": True, + "includehidden": False, + "navigation_depth": 4, + "sticky_navigation": False, + "titles_only": False, +} + +# -- EXTENSIONS -------------------------------------------------------- + +# Configuration for intersphinx (refer to others docs). +intersphinx_mapping = { + "PyQt5": ("https://www.riverbankcomputing.com/static/Docs/PyQt5", None), + "python": ("https://docs.python.org/3/", None), + "qgis": ("https://qgis.org/pyqgis/master/", None), +} + +# MyST Parser +myst_enable_extensions = [ + "amsmath", + "colon_fence", + "deflist", + "dollarmath", + "html_image", + "linkify", + "replacements", + "smartquotes", + "substitution", +] + +myst_substitutions = { + "author": author, + "date_update": datetime.now().strftime("%d %B %Y"), + "description": description, + "qgis_version_max": __about__.__plugin_md__.get("general").get( + "qgismaximumversion" + ), + "qgis_version_min": __about__.__plugin_md__.get("general").get( + "qgisminimumversion" + ), + "repo_url": __about__.__uri__, + "title": project, + "version": version, +} + +myst_url_schemes = ("http", "https", "mailto") diff --git a/docs/development/contribute.md b/docs/development/contribute.md new file mode 100644 index 0000000..ef6daa8 --- /dev/null +++ b/docs/development/contribute.md @@ -0,0 +1,2 @@ +```{include} ../../CONTRIBUTING.md +``` diff --git a/docs/development/documentation.md b/docs/development/documentation.md new file mode 100644 index 0000000..5394777 --- /dev/null +++ b/docs/development/documentation.md @@ -0,0 +1,24 @@ +# Documentation + +Project uses Sphinx to generate documentation from docstrings (documentation in-code) and custom pages written in Markdown (through the [MyST parser](https://myst-parser.readthedocs.io/en/latest/)). + +## Build documentation website + +To build it: + +```bash +# install aditionnal dependencies +python -m pip install -U -r requirements/documentation.txt +# build it +sphinx-build -b html -d docs/_build/cache -j auto -q docs docs/_build/html +``` + +Open `docs/_build/index.html` in a web browser. + +## Write documentation using live render + +```bash +sphinx-autobuild -b html docs/ docs/_build +``` + +Open in a web browser to see the HTML render updated when a file is saved. diff --git a/docs/development/environment.md b/docs/development/environment.md new file mode 100644 index 0000000..82b47f0 --- /dev/null +++ b/docs/development/environment.md @@ -0,0 +1,63 @@ +# Development + +## Environment setup + +> Typically on Ubuntu (but should also work on Windows with potential small adjustments). + +### 1. Install virtual environment + +Using [qgis-venv-creator](https://github.com/GispoCoding/qgis-venv-creator) (see [this article](https://blog.geotribu.net/2024/11/25/creating-a-python-virtual-environment-for-pyqgis-development-with-vs-code-on-windows/#with-the-qgis-venv-creator-utility)) through [pipx](https://pipx.pypa.io) (`sudo apt install pipx`): + +```sh +pipx run qgis-venv-creator --venv-name ".venv" +``` + +Then enter into the virtual environment: + +```sh +. .venv/bin/activate +# or +source .venv/bin/activate +``` + +Old school way: + +```bash +# create virtual environment linking to system packages (for pyqgis) +python3 -m venv .venv --system-site-packages +source .venv/bin/activate +``` + +### 2. Install development dependencies + +```sh +# bump dependencies inside venv +python -m pip install -U pip +python -m pip install -U -r requirements/development.txt + +# install git hooks (pre-commit) +pre-commit install +``` + +### 3. Dedicated QGIS profile + +It's recommended to create a dedicated QGIS profile for the development of the plugin to avoid conflicts with other plugins. + +1. From the command-line (a terminal with qgis executable in `PATH` or OSGeo4W Shell): + + ```sh + # Linux + qgis --profile plg_plugin_map2loop + # Windows - OSGeo4W Shell + qgis-ltr --profile plg_plugin_map2loop + # Windows - PowerShell opened in the QGIS installation directory + PS C:\Program Files\QGIS 3.40.4\LTR\bin> .\qgis-ltr-bin.exe --profile plg_plugin_map2loop + ``` + +1. Then, set the `QGIS_PLUGINPATH` environment variable to the path of the plugin in profile preferences: + + ![QGIS - Add QGIS_PLUGINPATH environment variable in profile settings](../static/dev_qgis_set_pluginpath_envvar.png) + +1. Finally, enable the plugin in the plugin manager (ignore invalid folders like documentation, tests, etc.): + + ![QGIS - Enable the plugin in the plugin manager](../static/dev_qgis_enable_plugin.png) diff --git a/docs/development/history.md b/docs/development/history.md new file mode 100644 index 0000000..3139cd4 --- /dev/null +++ b/docs/development/history.md @@ -0,0 +1,2 @@ +```{include} ../../CHANGELOG.md +``` diff --git a/docs/development/packaging.md b/docs/development/packaging.md new file mode 100644 index 0000000..f98c8af --- /dev/null +++ b/docs/development/packaging.md @@ -0,0 +1,47 @@ +# Packaging and deployment + +## Packaging + +This plugin is using the [qgis-plugin-ci](https://github.com/opengisch/qgis-plugin-ci/) tool to perform packaging operations. +Under the hood, the package command is performing a `git archive` run based on `CHANGELOG.md`. + +Install additional dependencies: + +```bash +python -m pip install -U -r requirements/packaging.txt +``` + +Then use it: + +```bash +# package a specific version +qgis-plugin-ci package 1.3.1 +# package latest version +qgis-plugin-ci package latest +``` + +## Release a version + +Everything is done through the continuous deployment, sticking to a classic git workflow: 1 released version = 1 git tag. + +Here comes the process for a tag `X.y.z` (which has to be SemVer compliant): + +1. Add the new version to the `CHANGELOG.md`.You can write it manually or use the auto-generated release notes by Github: + 1. Go to [project's releases](https://github.com/WhereGroup/profile_manager/releases) and click on `Draft a new release` + 1. In `Choose a tag`, enter the new tag + 1. Click on `Generate release notes` + 1. Copy/paste the generated text from `## What's changed` until the line before `**Full changelog**:...` in the CHANGELOG.md replacing `What's changed` with the tag and the publication date. +1. Optionally change the version number in `metadata.txt`. It's recommended to use the next version number with `-DEV` suffix (e.g. `1.4.0-DEV` when `X.y.z` is `1.3.0` ) to avoid confusion during the development phase. +1. Apply a git tag with the relevant version: `git tag -a X.y.z {git commit hash} -m "This version rocks!"` +1. Push tag to main branch: `git push origin X.y.z` or `git push --tags` if you want to push all tags at once. +1. The CI/CD pipeline will be triggered and will create a new release on your Git repository and publish it to the [official QGIS plugins repository](https://plugins.qgis.org/) (if you picked up the option). + +If things go wrong (failed CI/CD pipeline, missed step...), here comes the fix process: + +```sh +git tag -d old +git push origin :refs/tags/old +git push --tags +``` + +And try again! diff --git a/docs/development/testing.md b/docs/development/testing.md new file mode 100644 index 0000000..6596fbd --- /dev/null +++ b/docs/development/testing.md @@ -0,0 +1,33 @@ +# Testing the plugin + +Tests are written in 2 separate folders: + +- `tests/unit`: testing code which is independent of QGIS API +- `tests/qgis`: testing code which depends on QGIS API + +## Requirements + +- 3.4 < QGIS < 3.99 + +```bash +python -m pip install -U -r requirements/testing.txt +``` + +## Run unit tests + +```bash +# run all tests with PyTest and Coverage report +python -m pytest + +# run only unit tests with pytest launcher (disabling pytest-qgis) +python -m pytest -p no:qgis tests/unit + +# run only QGIS tests with pytest launcher +python -m pytest tests/qgis + +# run a specific test module using standard unittest +python -m unittest tests.unit.test_plg_metadata + +# run a specific test function using standard unittest +python -m unittest tests.unit.test_plg_metadata.TestPluginMetadata.test_version_semver +``` diff --git a/docs/development/translation.md b/docs/development/translation.md new file mode 100644 index 0000000..0e0e290 --- /dev/null +++ b/docs/development/translation.md @@ -0,0 +1,44 @@ +# Manage translations + +## Requirements + +Qt Linguist tools are used to manage translations. Typically on Ubuntu: + +```bash +sudo apt install qttools5-dev-tools +``` + +## Workflow + +1. Generate the `plugin_translation.pro` file: + + ```bash + python scripts/generate_translation_profile.py + ``` + +1. Update `.ts` files: + + ```bash + pylupdate5 -noobsolete -verbose plugin_map2loop/resources/i18n/plugin_translation.pro + ``` + +1. Translate your text using QLinguist or directly into `.ts` files. Launching it through command-line is possible: + + ```bash + linguist plugin_map2loop/resources/i18n/*.ts + ``` + +1. Compile it: + + ```bash + lrelease plugin_map2loop/resources/i18n/*.ts + ``` + +## Notes + +- Remember that the resulting `*.qm` file should not be tracked in git history since it's autogenerated and a binary file. The CI/CD pipeline will take care of generating it and packaging it. However, you can add the `*.qm` file to `.gitignore` to avoid any accidental commits. +- The `pylupdate5` command is used to extract translatable strings from the source code and update the `.ts` files accordingly. +- The `-no-obsolete` flag is important to avoid removing obsolete translations, which can be useful for maintaining the history of translations and ensuring that no existing translations are lost during updates. +- The `-verbose` flag is used to provide detailed output during the translation update process, which can be helpful for debugging and ensuring that the process is working correctly. +- The `linguist` command is used to launch the Qt Linguist application, which provides a graphical interface for translating the `.ts` files. This can be more user-friendly than editing the files directly in a text editor. +- The `lrelease` command is used to compile the `.ts` files into `.qm` files, which are binary files used by Qt applications for translations. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9e1818a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,33 @@ +# {{ title }} - Documentation + +> **Description:** {{ description }} +> **Author and contributors:** {{ author }} +> **Plugin version:** {{ version }} +> **QGIS minimum version:** {{ qgis_version_min }} +> **QGIS maximum version:** {{ qgis_version_max }} +> **Source code:** {{ repo_url }} +> **Last documentation update:** {{ date_update }} + +---- + +```{toctree} +--- +caption: Usage +maxdepth: 1 +--- +Installation +``` + +```{toctree} +--- +caption: Contribution guide +maxdepth: 1 +--- +development/contribute +development/environment +development/documentation +development/translation +development/packaging +development/testing +development/history +``` diff --git a/docs/static/dev_qgis_enable_plugin.png b/docs/static/dev_qgis_enable_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..ec9cbc317978cbe1a641af71261741415458313a GIT binary patch literal 47170 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 literal 0 HcmV?d00001 diff --git a/docs/usage/installation.md b/docs/usage/installation.md new file mode 100644 index 0000000..ab930ed --- /dev/null +++ b/docs/usage/installation.md @@ -0,0 +1,19 @@ +# Installation + +## Stable version (recomended) + +This plugin is published on the official QGIS plugins repository: . + +## Beta versions released + +Enable experimental extensions in the QGIS plugins manager settings panel. + +## Earlier development version + +If you define yourself as early adopter or a tester and can't wait for the release, the plugin is automatically packaged for each commit to main, so you can use this address as repository URL in your QGIS extensions manager settings: + +```url +https://github.com/Loop3d/plugin_map2loop/plugins.xml +``` + +Be careful, this version can be unstable. diff --git a/linters/pylintrc b/linters/pylintrc new file mode 100644 index 0000000..cf496f9 --- /dev/null +++ b/linters/pylintrc @@ -0,0 +1,200 @@ +[MAIN] + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them +disable=locally-disabled,C0103,import-error,no-name-in-module + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +[BASIC] + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,x,y,z,w + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata,popo + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=88 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no else. +single-line-if-stmt=no + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 tab). +indent-string=' ' + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp,initGui + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/map2loop/__about__.py b/map2loop/__about__.py new file mode 100644 index 0000000..d7d3617 --- /dev/null +++ b/map2loop/__about__.py @@ -0,0 +1,115 @@ +#! python3 # noqa: E265 + +""" +Metadata about the package to easily retrieve informations about it. +See: https://packaging.python.org/guides/single-sourcing-package-version/ +""" + +# ############################################################################ +# ########## Libraries ############# +# ################################## + +# standard library +from configparser import ConfigParser +from datetime import date +from pathlib import Path + +# ############################################################################ +# ########## Globals ############### +# ################################## +__all__: list = [ + "__author__", + "__copyright__", + "__email__", + "__license__", + "__summary__", + "__title__", + "__uri__", + "__version__", +] + + +DIR_PLUGIN_ROOT: Path = Path(__file__).parent +PLG_METADATA_FILE: Path = DIR_PLUGIN_ROOT.resolve() / "metadata.txt" + + +# ############################################################################ +# ########## Functions ############# +# ################################## +def plugin_metadata_as_dict() -> dict: + """Read plugin metadata.txt and returns it as a Python dict. + + Raises: + IOError: if metadata.txt is not found + + Returns: + dict: dict of dicts. + """ + config = ConfigParser(interpolation=None) + if PLG_METADATA_FILE.is_file(): + config.read(PLG_METADATA_FILE.resolve(), encoding="UTF-8") + return {s: dict(config.items(s)) for s in config.sections()} + else: + raise IOError("Plugin metadata.txt not found at: %s" % PLG_METADATA_FILE) + + +# ############################################################################ +# ########## Variables ############# +# ################################## + +# store full metadata.txt as dict into a var +__plugin_md__: dict = plugin_metadata_as_dict() + +__author__: str = __plugin_md__.get("general").get("author") +__copyright__: str = "2025 - {0}, {1}".format( + date.today().year, __author__ +) +__email__: str = __plugin_md__.get("general").get("email") +__icon_path__: Path = DIR_PLUGIN_ROOT.resolve() / __plugin_md__.get("general").get( + "icon" +) +__keywords__: list = [ + t.strip() for t in __plugin_md__.get("general").get("repository").split("tags") +] +__license__: str = "GPLv2+" +__summary__: str = "{}\n{}".format( + __plugin_md__.get("general").get("description"), + __plugin_md__.get("general").get("about"), +) + +__title__: str = __plugin_md__.get("general").get("name") +__title_clean__: str = "".join(e for e in __title__ if e.isalnum()) + +__uri_homepage__: str = __plugin_md__.get("general").get("homepage") +__uri_repository__: str = __plugin_md__.get("general").get("repository") +__uri_tracker__: str = __plugin_md__.get("general").get("tracker") +__uri__: str = __uri_repository__ + +__version__: str = __plugin_md__.get("general").get("version") +__version_info__: tuple = tuple( + [ + int(num) if num.isdigit() else num + for num in __version__.replace("-", ".", 1).split(".") + ] +) + +# ############################################################################# +# ##### Main ####################### +# ################################## +if __name__ == "__main__": + plugin_md = plugin_metadata_as_dict() + assert isinstance(plugin_md, dict) + assert plugin_md.get("general").get("name") == __title__ + print(f"Plugin: {__title__}") + print(f"By: {__author__}") + print(f"Version: {__version__}") + print(f"Description: {__summary__}") + print(f"Icon: {__icon_path__}") + print( + "For: %s > QGIS > %s" + % ( + plugin_md.get("general").get("qgisminimumversion"), + plugin_md.get("general").get("qgismaximumversion"), + ) + ) + print(__title_clean__) diff --git a/map2loop/__init__.py b/map2loop/__init__.py new file mode 100644 index 0000000..b2bebd7 --- /dev/null +++ b/map2loop/__init__.py @@ -0,0 +1,23 @@ +#! python3 # noqa: E265 + +# ---------------------------------------------------------- +# Copyright (C) 2015 Martin Dobias +# ---------------------------------------------------------- +# Licensed under the terms of GNU GPL 2 +# +# 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. +# -------------------------------------------------------------------- + + +def classFactory(iface): + """Load the plugin class. + + :param iface: A QGIS interface instance. + :type iface: QgsInterface + """ + from .plugin_main import Map2LoopPluginPlugin + + return Map2LoopPluginPlugin(iface) diff --git a/map2loop/gui/__init__.py b/map2loop/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/map2loop/gui/dlg_settings.py b/map2loop/gui/dlg_settings.py new file mode 100644 index 0000000..e0b7b0a --- /dev/null +++ b/map2loop/gui/dlg_settings.py @@ -0,0 +1,174 @@ +#! python3 # noqa: E265 + +""" +Plugin settings form integrated into QGIS 'Options' menu. +""" + +# standard +import platform +from functools import partial +from pathlib import Path +from urllib.parse import quote + +# PyQGIS +from qgis.core import Qgis, QgsApplication +from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory +from qgis.PyQt import uic +from qgis.PyQt.QtCore import QUrl +from qgis.PyQt.QtGui import QDesktopServices, QIcon + +# project +from map2loop.__about__ import ( + __icon_path__, + __title__, + __uri_homepage__, + __uri_tracker__, + __version__, +) +from map2loop.toolbelt import PlgLogger, PlgOptionsManager +from map2loop.toolbelt.preferences import PlgSettingsStructure + +# ############################################################################ +# ########## Globals ############### +# ################################## + +FORM_CLASS, _ = uic.loadUiType( + Path(__file__).parent / "{}.ui".format(Path(__file__).stem) +) + + +# ############################################################################ +# ########## Classes ############### +# ################################## + + +class ConfigOptionsPage(FORM_CLASS, QgsOptionsPageWidget): + """Settings form embedded into QGIS 'options' menu.""" + + def __init__(self, parent): + super().__init__(parent) + self.log = PlgLogger().log + self.plg_settings = PlgOptionsManager() + + # load UI and set objectName + self.setupUi(self) + self.setObjectName("mOptionsPage{}".format(__title__)) + + report_context_message = quote( + "> Reported from plugin settings\n\n" + f"- operating system: {platform.system()} " + f"{platform.release()}_{platform.version()}\n" + f"- QGIS: {Qgis.QGIS_VERSION}" + f"- plugin version: {__version__}\n" + ) + + # header + self.lbl_title.setText(f"{__title__} - Version {__version__}") + + # customization + self.btn_help.setIcon(QIcon(QgsApplication.iconPath("mActionHelpContents.svg"))) + self.btn_help.pressed.connect( + partial(QDesktopServices.openUrl, QUrl(__uri_homepage__)) + ) + + + self.btn_report.setIcon( + QIcon(QgsApplication.iconPath("console/iconSyntaxErrorConsole.svg")) + ) + + self.btn_report.pressed.connect( + partial( + QDesktopServices.openUrl, + QUrl( + f"{__uri_tracker__}new/?" + "template=10_bug_report.yml" + f"&about-info={report_context_message}" + ), + ) + ) + + self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg"))) + self.btn_reset.pressed.connect(self.reset_settings) + + # load previously saved settings + self.load_settings() + + def apply(self): + """Called to permanently apply the settings shown in the options page (e.g. \ + save them to QgsSettings objects). This is usually called when the options \ + dialog is accepted.""" + settings = self.plg_settings.get_plg_settings() + + # misc + settings.debug_mode = self.opt_debug.isChecked() + settings.version = __version__ + + # dump new settings into QgsSettings + self.plg_settings.save_from_object(settings) + + if __debug__: + self.log( + message="DEBUG - Settings successfully saved.", + log_level=4, + ) + + def load_settings(self): + """Load options from QgsSettings into UI form.""" + settings = self.plg_settings.get_plg_settings() + + # global + self.opt_debug.setChecked(settings.debug_mode) + self.lbl_version_saved_value.setText(settings.version) + + def reset_settings(self): + """Reset settings to default values (set in preferences.py module).""" + default_settings = PlgSettingsStructure() + + # dump default settings into QgsSettings + self.plg_settings.save_from_object(default_settings) + + # update the form + self.load_settings() + + +class PlgOptionsFactory(QgsOptionsWidgetFactory): + """Factory for options widget.""" + + def __init__(self): + """Constructor.""" + super().__init__() + + def icon(self) -> QIcon: + """Returns plugin icon, used to as tab icon in QGIS options tab widget. + + :return: _description_ + :rtype: QIcon + """ + return QIcon(str(__icon_path__)) + + def createWidget(self, parent) -> ConfigOptionsPage: + """Create settings widget. + + :param parent: Qt parent where to include the options page. + :type parent: QObject + + :return: options page for tab widget + :rtype: ConfigOptionsPage + """ + return ConfigOptionsPage(parent) + + def title(self) -> str: + """Returns plugin title, used to name the tab in QGIS options tab widget. + + :return: plugin title from about module + :rtype: str + """ + return __title__ + + def helpId(self) -> str: + """Returns plugin help URL. + + :return: plugin homepage url from about module + :rtype: str + """ + return __uri_homepage__ diff --git a/map2loop/gui/dlg_settings.ui b/map2loop/gui/dlg_settings.ui new file mode 100644 index 0000000..463cdb8 --- /dev/null +++ b/map2loop/gui/dlg_settings.ui @@ -0,0 +1,245 @@ + + + wdg_plugin_map2loop_settings + + + + 0 + 0 + 538 + 273 + + + + map2loop - Settings + + + + + + + + + + 0 + 25 + + + + + 16777215 + 30 + + + + + 75 + true + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">PluginTitle - Version X.X.X</span></p></body></html> + + + true + + + Qt::AlignCenter + + + true + + + false + + + Qt::TextSelectableByMouse + + + + + + + + 0 + 100 + + + + + + + Miscellaneous + + + false + + + + + + + 0 + 25 + + + + + 16777215 + 30 + + + + + + + X.X.X + + + Qt::NoTextInteraction + + + + + + + + 200 + 25 + + + + + 500 + 30 + + + + + + + Report an issue + + + + + + + + 0 + 25 + + + + + 16777215 + 30 + + + + + + + Version used to save settings: + + + + + + + + 200 + 25 + + + + + 500 + 30 + + + + + + + Help + + + + + + + + 200 + 25 + + + + + 16777215 + 30 + + + + true + + + Reset setttings to factory defaults + + + + + + + + 0 + 25 + + + + + 16777215 + 30 + + + + Enable debug mode. + + + true + + + + + + Debug mode (degraded performances) + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 56 + + + + + + + + + diff --git a/map2loop/metadata.txt b/map2loop/metadata.txt new file mode 100644 index 0000000..8e8f813 --- /dev/null +++ b/map2loop/metadata.txt @@ -0,0 +1,26 @@ +[general] +name=map2loop +about=Loop tools for augmenting geological map data into 3D model datasets +category=None +hasProcessingProvider=True +description=Extends QGIS with revolutionary features that every single GIS end-users was expected (or not)! +icon=resources/images/default_icon.png +tags=geology, modelling, structural geology, loop3d + +# credits and contact +author=Lachlan GROSE +email=lachlan.grose@monash.edu +homepage=https://github.com/Loop3d/plugin_map2loop +repository=https://github.com/Loop3d/plugin_map2loop +tracker=https://github.com/Loop3d/plugin_map2loop/issues/ + +# experimental flag +deprecated=False +experimental=True +qgisMinimumVersion=3.4 +qgisMaximumVersion=3.99 +supportsQt6=True + +# versioning +version=0.1.0 +changelog= diff --git a/map2loop/plugin_main.py b/map2loop/plugin_main.py new file mode 100644 index 0000000..b352a74 --- /dev/null +++ b/map2loop/plugin_main.py @@ -0,0 +1,172 @@ +#! python3 # noqa: E265 + +"""Main plugin module.""" + +# standard +from functools import partial +from pathlib import Path +from typing import Optional + +# PyQGIS +from qgis.core import QgsApplication, QgsSettings +from qgis.gui import QgisInterface +from qgis.PyQt.QtCore import QCoreApplication, QLocale, QTranslator, QUrl +from qgis.PyQt.QtGui import QDesktopServices, QIcon +from qgis.PyQt.QtWidgets import QAction + +# project +from map2loop.__about__ import ( + DIR_PLUGIN_ROOT, + __icon_path__, + __title__, + __uri_homepage__, +) +from map2loop.gui.dlg_settings import PlgOptionsFactory + + +from map2loop.processing import ( + Map2LoopPluginProvider, +) +from map2loop.toolbelt import PlgLogger + +# ############################################################################ +# ########## Classes ############### +# ################################## + + +class Map2LoopPluginPlugin: + def __init__(self, iface: QgisInterface): + """Constructor. + + :param iface: An interface instance that will be passed to this class which \ + provides the hook by which you can manipulate the QGIS application at run time. + :type iface: QgsInterface + """ + self.iface = iface + self.log = PlgLogger().log + self.provider: Optional[Map2LoopPluginProvider] = None + + # translation + # initialize the locale + self.locale: str = QgsSettings().value("locale/userLocale", QLocale().name())[ + 0:2 + ] + locale_path: Path = ( + DIR_PLUGIN_ROOT + / "resources" + / "i18n" + / f"{__title__.lower()}_{self.locale}.qm" + ) + self.log(message=f"Translation: {self.locale}, {locale_path}", log_level=4) + if locale_path.exists(): + self.translator = QTranslator() + self.translator.load(str(locale_path.resolve())) + QCoreApplication.installTranslator(self.translator) + + def initGui(self): + """Set up plugin UI elements.""" + + # settings page within the QGIS preferences menu + self.options_factory = PlgOptionsFactory() + self.iface.registerOptionsWidgetFactory(self.options_factory) + + # -- Actions + self.action_help = QAction( + QgsApplication.getThemeIcon("mActionHelpContents.svg"), + self.tr("Help"), + self.iface.mainWindow(), + ) + self.action_help.triggered.connect( + partial(QDesktopServices.openUrl, QUrl(__uri_homepage__)) + ) + + self.action_settings = QAction( + QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"), + self.tr("Settings"), + self.iface.mainWindow(), + ) + self.action_settings.triggered.connect( + lambda: self.iface.showOptionsDialog( + currentPage="mOptionsPage{}".format(__title__) + ) + ) + + # -- Menu + self.iface.addPluginToMenu(__title__, self.action_settings) + self.iface.addPluginToMenu(__title__, self.action_help) + # -- Processing + self.initProcessing() + + + # -- Help menu + + # documentation + self.iface.pluginHelpMenu().addSeparator() + self.action_help_plugin_menu_documentation = QAction( + QIcon(str(__icon_path__)), + f"{__title__} - Documentation", + self.iface.mainWindow(), + ) + self.action_help_plugin_menu_documentation.triggered.connect( + partial(QDesktopServices.openUrl, QUrl(__uri_homepage__)) + ) + + self.iface.pluginHelpMenu().addAction( + self.action_help_plugin_menu_documentation + ) + def initProcessing(self): + """Initialize the processing provider.""" + self.provider = Map2LoopPluginProvider() + QgsApplication.processingRegistry().addProvider(self.provider) + + + def tr(self, message: str) -> str: + """Get the translation for a string using Qt translation API. + + :param message: string to be translated. + :type message: str + + :returns: Translated version of message. + :rtype: str + """ + return QCoreApplication.translate(self.__class__.__name__, message) + + def unload(self): + """Cleans up when plugin is disabled/uninstalled.""" + # -- Clean up menu + self.iface.removePluginMenu(__title__, self.action_help) + self.iface.removePluginMenu(__title__, self.action_settings) + + # -- Clean up preferences panel in QGIS settings + self.iface.unregisterOptionsWidgetFactory(self.options_factory) + # -- Unregister processing + QgsApplication.processingRegistry().removeProvider(self.provider) + + + # remove from QGIS help/extensions menu + if self.action_help_plugin_menu_documentation: + self.iface.pluginHelpMenu().removeAction( + self.action_help_plugin_menu_documentation + ) + + # remove actions + del self.action_settings + del self.action_help + + def run(self): + """Main process. + + :raises Exception: if there is no item in the feed + """ + try: + self.log( + message=self.tr("Everything ran OK."), + log_level=3, + push=False, + ) + except Exception as err: + self.log( + message=self.tr("Houston, we've got a problem: {}".format(err)), + log_level=2, + push=True, + ) diff --git a/map2loop/processing/__init__.py b/map2loop/processing/__init__.py new file mode 100644 index 0000000..4e05cc3 --- /dev/null +++ b/map2loop/processing/__init__.py @@ -0,0 +1,2 @@ +#! python3 # noqa: E265 +from .provider import Map2LoopPluginProvider # noqa: F401 diff --git a/map2loop/processing/provider.py b/map2loop/processing/provider.py new file mode 100644 index 0000000..908ac35 --- /dev/null +++ b/map2loop/processing/provider.py @@ -0,0 +1,88 @@ +#! python3 # noqa: E265 + +""" +Processing provider module. +""" + +# PyQGIS +from qgis.core import QgsProcessingProvider +from qgis.PyQt.QtCore import QCoreApplication +from qgis.PyQt.QtGui import QIcon + +# project +from map2loop.__about__ import ( + __icon_path__, + __title__, + __version__, +) + +# ############################################################################ +# ########## Classes ############### +# ################################## + + +class Map2LoopPluginProvider(QgsProcessingProvider): + """Processing provider class.""" + + def loadAlgorithms(self): + """Loads all algorithms belonging to this provider.""" + pass + + def id(self) -> str: + """Unique provider id, used for identifying it. This string should be unique, \ + short, character only string, eg "qgis" or "gdal". \ + This string should not be localised. + + :return: provider ID + :rtype: str + """ + return "plugin_map2loop" + + def name(self) -> str: + """Returns the provider name, which is used to describe the provider + within the GUI. This string should be short (e.g. "Lastools") and localised. + + :return: provider name + :rtype: str + """ + return __title__ + + def longName(self) -> str: + """Longer version of the provider name, which can include + extra details such as version numbers. E.g. "Lastools LIDAR tools". This string should be localised. The default + implementation returns the same string as name(). + + :return: provider long name + :rtype: str + """ + return self.tr("{} - Tools".format(__title__)) + + def icon(self) -> QIcon: + """QIcon used for your provider inside the Processing toolbox menu. + + :return: provider icon + :rtype: QIcon + """ + return QIcon(str(__icon_path__)) + + def tr(self, message: str) -> str: + """Get the translation for a string using Qt translation API. + + :param message: String for translation. + :type message: str, QString + + :returns: Translated version of message. + :rtype: str + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate(self.__class__.__name__, message) + + def versionInfo(self) -> str: + """Version information for the provider, or an empty string if this is not \ + applicable (e.g. for inbuilt Processing providers). For plugin based providers, \ + this should return the pluginโ€™s version identifier. + + :return: version + :rtype: str + """ + return __version__ diff --git a/map2loop/resources/i18n/plugin_map2loop_en.ts b/map2loop/resources/i18n/plugin_map2loop_en.ts new file mode 100644 index 0000000..4c23f0c --- /dev/null +++ b/map2loop/resources/i18n/plugin_map2loop_en.ts @@ -0,0 +1,4 @@ + + + + diff --git a/map2loop/resources/i18n/plugin_translation.pro b/map2loop/resources/i18n/plugin_translation.pro new file mode 100644 index 0000000..989ec2b --- /dev/null +++ b/map2loop/resources/i18n/plugin_translation.pro @@ -0,0 +1,13 @@ +# This file is auto-generated by the script generate_translation_profile.py +# Do not edit this file manually + +FORMS = ../../gui/dlg_settings.ui + +SOURCES = ../../gui/dlg_settings.py \ + ../../plugin_main.py \ + ../../processing/provider.py \ + ../../toolbelt/env_var_parser.py \ + ../../toolbelt/log_handler.py \ + ../../toolbelt/preferences.py + +TRANSLATIONS = plugin_map2loop_en.ts diff --git a/map2loop/resources/images/default_icon.png b/map2loop/resources/images/default_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..89a7044825d1fba4f8cef3199e75578bbf1092b2 GIT binary patch literal 28083 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 T: + """Retrieves an environment variable and converts it based on the default value type. + + Args: + name (str): The environment variable name. + default (T): The default value, used to infer the expected type. + + Returns: + T: The converted value, matching the type of `default`. + """ + value = os.getenv(name) + if value is None: + return ( + default # Return the default value if the environment variable is not + ) + + # Otherwise, treat it as a single value + return EnvVarParser._convert_single(value, type(default), default) + + @staticmethod + def _convert_single(value: str, expected_type: Type[T], default: T) -> T: + """Converts a string into a single value of the expected type.""" + try: + if expected_type is int: + return int(value) + elif expected_type is float: + return float(value) + elif expected_type is bool: + return EnvVarParser._convert_bool(value, default) + elif expected_type is str: + return value # String value + except ValueError: + return default # Return default value in case of conversion failure + + raise TypeError( + f"Unsupported type: {expected_type}. Value definition from environment variable is not possible." + ) + + @staticmethod + def _convert_bool(value: str, default: bool) -> bool: + """Converts a string into a boolean, handling explicit True/False values.""" + true_values = {"1", "true", "yes", "on"} + false_values = {"0", "false", "no", "off"} + value_lower = value.lower() + + if value_lower in true_values: + return True + elif value_lower in false_values: + return False + return default # Return default value if conversion fails diff --git a/map2loop/toolbelt/log_handler.py b/map2loop/toolbelt/log_handler.py new file mode 100644 index 0000000..161737d --- /dev/null +++ b/map2loop/toolbelt/log_handler.py @@ -0,0 +1,193 @@ +#! python3 # noqa: E265 + +# standard library +import logging +from functools import partial +from typing import Callable, Literal, Optional, Union + +# PyQGIS +from qgis.core import Qgis, QgsMessageLog, QgsMessageOutput +from qgis.gui import QgsMessageBar +from qgis.PyQt.QtWidgets import QPushButton, QWidget +from qgis.utils import iface + +# project package +import map2loop.toolbelt.preferences as plg_prefs_hdlr +from map2loop.__about__ import __title__ + +# ############################################################################ +# ########## Classes ############### +# ################################## + + +class PlgLogger(logging.Handler): + """Python logging handler supercharged with QGIS useful methods.""" + + @staticmethod + def log( + message: str, + application: str = __title__, + log_level: Union[ + Qgis.MessageLevel, Literal[0, 1, 2, 3, 4] + ] = Qgis.MessageLevel.Info, + push: bool = False, + duration: Optional[int] = None, + # widget + button: bool = False, + button_text: Optional[str] = None, + button_more_text: Optional[str] = None, + button_connect: Optional[Callable] = None, + # parent + parent_location: Optional[QWidget] = None, + ): + """Send messages to QGIS messages windows and to the user as a message bar. \ + Plugin name is used as title. If debug mode is disabled, only warnings (1) and \ + errors (2) or with push are sent. + + :param message: message to display + :type message: str + :param application: name of the application sending the message. \ + Defaults to __about__.__title__ + :type application: str, optional + :param log_level: message level. Possible values: any values of enum \ + `Qgis.MessageLevel`. For legacy purposes, it's also possible to pass \ + corresponding integers but it's not recommended anymore. Legacy values: \ + 0 (info), 1 (warning), 2 (critical), 3 (success), 4 (none - grey). Defaults \ + to Qgis.MessageLevel(0) (info) + :type log_level: Union[Qgis.MessageLevel, Literal[0, 1, 2, 3, 4]], optional + :param push: also display the message in the QGIS message bar in addition to \ + the log, defaults to False + :type push: bool, optional + :param duration: duration of the message in seconds. If not set, the \ + duration is calculated from the log level: `(log_level + 1) * 3`. seconds. \ + If set to 0, then the message must be manually dismissed by the user. \ + Defaults to None. + :type duration: int, optional + :param button: display a button in the message bar. Defaults to False. + :type button: bool, optional + :param button_text: text label of the button. Defaults to None. + :type button_text: str, optional + :param button_more_text: text to display within the QgsMessageOutput + :type button_more_text: str, optional + :param button_connect: function to be called when the button is pressed. \ + If not set, a simple dialog (QgsMessageOutput) is used to dislay the message. \ + Defaults to None. + :type button_connect: Callable, optional + :param parent_location: parent location widget. \ + If not set, QGIS canvas message bar is used to push message, \ + otherwise if a QgsMessageBar is available in parent_location it is used instead. \ + Defaults to None. + :type parent_location: Widget, optional + + :Example: + + .. code-block:: python + + # using enums from Qgis: + # Qgis.Info, Qgis.MessageLevel.Warning, Qgis.MessageLevel.Critical, Qgis.MessageLevel.Success, Qgis.MessageLevel.NoLevel + from qgis.core import Qgis + + log(message="Plugin loaded - INFO", log_level=Qgis.MessageLevel.Info, push=False) + log( + message="Something went wrong but it's not blocking", + log_level=Qgis.MessageLevel.Warning + ) + log( + message="Plugin failed to load - CRITICAL", + log_level=Qgis.MessageLevel(2), + push=True + ) + + # LEGACY - using integers: + log(message="Plugin loaded - INFO", log_level=0, push=False) + log(message="Plugin loaded - WARNING", log_level=1, push=1, duration=5) + log(message="Plugin loaded - ERROR", log_level=2, push=1, duration=0) + log( + message="Plugin loaded - SUCCESS", + log_level=3, + push=1, + duration=10, + button=True + ) + log( + message="Plugin loaded", + log_level=2, + push=1, + duration=0 + button=True, + button_label=self.tr("See details"), + button_more_text=detailed_error_message + ) + log(message="Plugin loaded - TEST", log_level=4, push=0) + """ + # if not debug mode and not push, let's ignore INFO, SUCCESS and TEST + debug_mode = plg_prefs_hdlr.PlgOptionsManager.get_plg_settings().debug_mode + if not debug_mode and not push and (log_level < 1 or log_level > 2): + return + + # if log_level is an int, convert it to Qgis.MessageLevel + if isinstance(log_level, int): + log_level = Qgis.MessageLevel(log_level) + + # ensure message is a string + if not isinstance(message, str): + try: + message = str(message) + except Exception as err: + err_msg = "Log message must be a string, not: {}. Trace: {}".format( + type(message), err + ) + logging.error(err_msg) + message = err_msg + + # send it to QGIS messages panel + QgsMessageLog.logMessage( + message=message, tag=application, notifyUser=push, level=log_level + ) + + # optionally, display message on QGIS Message bar (above the map canvas) + if push and iface is not None: + msg_bar = None + + # QGIS or custom dialog + if parent_location and isinstance(parent_location, QWidget): + msg_bar = parent_location.findChild(QgsMessageBar) + + if not msg_bar: + msg_bar = iface.messageBar() + + # calc duration + if duration is None: + duration = (log_level + 1) * 3 + + # create message with/out a widget + if button: + # create output message + notification = iface.messageBar().createMessage( + title=application, text=message + ) + widget_button = QPushButton(button_text or "More...") + if button_connect: + widget_button.clicked.connect(button_connect) + else: + mini_dlg: QgsMessageOutput = QgsMessageOutput.createMessageOutput() + mini_dlg.setTitle(application) + mini_dlg.setMessage( + f"{message}\n{button_more_text}", + QgsMessageOutput.MessageType.MessageText, + ) + widget_button.clicked.connect(partial(mini_dlg.showMessage, False)) + + notification.layout().addWidget(widget_button) + msg_bar.pushWidget( + widget=notification, level=log_level, duration=duration + ) + + else: + # send simple message + msg_bar.pushMessage( + title=application, + text=message, + level=log_level, + duration=duration, + ) diff --git a/map2loop/toolbelt/preferences.py b/map2loop/toolbelt/preferences.py new file mode 100644 index 0000000..0809ee8 --- /dev/null +++ b/map2loop/toolbelt/preferences.py @@ -0,0 +1,177 @@ +#! python3 # noqa: E265 + +""" +Plugin settings. +""" + +# standard +from dataclasses import asdict, dataclass, fields + +# PyQGIS +from qgis.core import QgsSettings + +# package +import map2loop.toolbelt.log_handler as log_hdlr +from map2loop.__about__ import __title__, __version__ +from map2loop.toolbelt.env_var_parser import EnvVarParser + +# ############################################################################ +# ########## Classes ############### +# ################################## + +PREFIX_ENV_VARIABLE = "QGIS_PLUGIN_MAP2LOOP_" + + +@dataclass +class PlgEnvVariableSettings: + """Plugin settings from environnement variable""" + + def env_variable_used(self, attribute: str, default_from_name: bool = True) -> str: + """Get environnement variable used for environnement variable settings + + :param attribute: attribute to check + :type attribute: str + :param default_from_name: define default environnement value from attribute name PREFIX_ENV_VARIABLE_ + :type default_from_name: bool + :return: environnement variable used + :rtype: str + """ + settings_env_variable = asdict(self) + env_variable = settings_env_variable.get(attribute, "") + if not env_variable and default_from_name: + env_variable = f"{PREFIX_ENV_VARIABLE}{attribute}".upper() + return env_variable + + +@dataclass +class PlgSettingsStructure: + """Plugin settings structure and defaults values.""" + + # global + debug_mode: bool = False + version: str = __version__ + +class PlgOptionsManager: + @staticmethod + def get_plg_settings() -> PlgSettingsStructure: + """Load and return plugin settings as a dictionary. \ + Useful to get user preferences across plugin logic. + + :return: plugin settings + :rtype: PlgSettingsStructure + """ + # get dataclass fields definition + settings_fields = fields(PlgSettingsStructure) + env_variable_settings = PlgEnvVariableSettings() + + # retrieve settings from QGIS/Qt + settings = QgsSettings() + settings.beginGroup(__title__) + + # map settings values to preferences object + li_settings_values = [] + for i in settings_fields: + try: + value = settings.value(key=i.name, defaultValue=i.default, type=i.type) + # If environnement variable used, get value from environnement variable + env_variable = env_variable_settings.env_variable_used(i.name) + if env_variable: + value = EnvVarParser.get_env_var(env_variable, value) + li_settings_values.append(value) + except TypeError: + li_settings_values.append( + settings.value(key=i.name, defaultValue=i.default) + ) + + # instanciate new settings object + options = PlgSettingsStructure(*li_settings_values) + + settings.endGroup() + + return options + + @staticmethod + def get_value_from_key(key: str, default=None, exp_type=None): + """Load and return plugin settings as a dictionary. \ + Useful to get user preferences across plugin logic. + + :return: plugin settings value matching key + """ + if not hasattr(PlgSettingsStructure, key): + log_hdlr.PlgLogger.log( + message="Bad settings key. Must be one of: {}".format( + ",".join(PlgSettingsStructure._fields) + ), + log_level=1, + ) + return None + + settings = QgsSettings() + settings.beginGroup(__title__) + + try: + out_value = settings.value(key=key, defaultValue=default, type=exp_type) + except Exception as err: + log_hdlr.PlgLogger.log( + message="Error occurred trying to get settings: {}.Trace: {}".format( + key, err + ) + ) + out_value = None + + settings.endGroup() + + return out_value + + @classmethod + def set_value_from_key(cls, key: str, value) -> bool: + """Set plugin QSettings value using the key. + + :param key: QSettings key + :type key: str + :param value: value to set + :type value: depending on the settings + :return: operation status + :rtype: bool + """ + if not hasattr(PlgSettingsStructure, key): + log_hdlr.PlgLogger.log( + message="Bad settings key. Must be one of: {}".format( + ",".join(PlgSettingsStructure._fields) + ), + log_level=2, + ) + return False + + settings = QgsSettings() + settings.beginGroup(__title__) + + try: + settings.setValue(key, value) + out_value = True + except Exception as err: + log_hdlr.PlgLogger.log( + message="Error occurred trying to set settings: {}.Trace: {}".format( + key, err + ) + ) + out_value = False + + settings.endGroup() + + return out_value + + @classmethod + def save_from_object(cls, plugin_settings_obj: PlgSettingsStructure): + """Load and return plugin settings as a dictionary. \ + Useful to get user preferences across plugin logic. + + :return: plugin settings value matching key + """ + settings = QgsSettings() + settings.beginGroup(__title__) + + for k, v in asdict(plugin_settings_obj).items(): + cls.set_value_from_key(k, v) + + settings.endGroup() diff --git a/requirements/development.txt b/requirements/development.txt new file mode 100644 index 0000000..703f00a --- /dev/null +++ b/requirements/development.txt @@ -0,0 +1,8 @@ +# Develoment dependencies +# ----------------------- + +black + + +isort>=5.13 +pre-commit>=4,<5 diff --git a/requirements/documentation.txt b/requirements/documentation.txt new file mode 100644 index 0000000..40be618 --- /dev/null +++ b/requirements/documentation.txt @@ -0,0 +1,7 @@ +# Documentation (for devs) +# ----------------------- + +myst-parser[linkify]>=2 +sphinx-autobuild>=2024 +sphinx-copybutton>=0.5,<1 +sphinx-rtd-theme>=2 diff --git a/requirements/packaging.txt b/requirements/packaging.txt new file mode 100644 index 0000000..b4996a0 --- /dev/null +++ b/requirements/packaging.txt @@ -0,0 +1,4 @@ +# Packaging +# --------- + +qgis-plugin-ci>=2.8,<3 diff --git a/requirements/testing.txt b/requirements/testing.txt new file mode 100644 index 0000000..1940035 --- /dev/null +++ b/requirements/testing.txt @@ -0,0 +1,5 @@ +# Testing dependencies +# -------------------- + +pytest-cov>=4 +packaging>=23 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/generate_translation_profile.py b/scripts/generate_translation_profile.py new file mode 100644 index 0000000..3ff8390 --- /dev/null +++ b/scripts/generate_translation_profile.py @@ -0,0 +1,86 @@ +#! python3 + +"""Script to generate the translation profile for a PyQt project, listing Python, +forms (ui) and targetted translation files. This script is intended to be run from the +root of the project, and will create a file named `plugin_translation.pro` in the +`oslandia/resources/i18n` directory. + +TODO: remove the get_relative_paths function if your stack is Python 3.12+, which added +the walk_up option in pathlib.Path.relative_to(). It would be a cleaner solution. +Listing files would become: + + python_files = [ + f.relative_to(i18n_path, walk_up=True) + for f in sorted(list(Path(src_path).rglob("*.py"))) + if not f.name.startswith("__") + ] + ui_files = [ + f.relative_to(i18n_path, walk_up=True) + for f in sorted(list(Path(src_path).rglob("*.ui"))) + ] + ts_files = [ + f.relative_to(i18n_path, walk_up=True) + for f in sorted(list(Path(src_path).rglob("*.ts"))) + ] + +So update the script accordingly by removing the function and references. +""" + +# -- Imports +from os import path +from pathlib import Path + +# -- Variables +src_path = Path("plugin_map2loop") +i18n_path = src_path.joinpath("resources/i18n") +output_file = i18n_path.joinpath("plugin_translation.pro") + +# make sure the output directory exists +i18n_path.mkdir(parents=True, exist_ok=True) + + +# -- Functions +def get_relative_paths(filepaths_list: list[Path]) -> list[str]: + """Parse a list of file paths and return a list of relative paths to the i18n + directory, escaping the backslashes to make them compatible with Qt Linguist. + + :param filepaths_list: List of file paths to parse + :type filepaths_list: list[Path] + + :return: List of relative paths to the i18n directory + :rtype: list[str] + """ + return [ + path.relpath(filepath, i18n_path).replace("\\", "/") + for filepath in filepaths_list + ] + + +# -- Run + +# Get the list of all files in directory tree at given path +python_files = [ + f for f in sorted(list(Path(src_path).rglob("*.py"))) if not f.name.startswith("__") +] +ui_files = [f for f in sorted(list(Path(src_path).rglob("*.ui")))] +ts_files = [f for f in sorted(list(Path(src_path).rglob("*.ts")))] + + +# Generate the translation profile +forms = "FORMS =" + " \\\n".join([f"\t{f}" for f in get_relative_paths(ui_files)]) + +sources = "SOURCES =" + " \\\n".join( + [f"\t{f}" for f in get_relative_paths(python_files)] +) + +translations = "TRANSLATIONS =" + " \\\n".join( + [f"\t{f}" for f in get_relative_paths(ts_files)] +) + +# write to output file +with output_file.open("w", encoding="UTF-8") as f: + f.write( + "# This file is auto-generated by the script generate_translation_profile.py\n" + "# Do not edit this file manually\n\n" + ) + f.write(f"{forms}\n\n{sources}\n\n{translations}") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f7ce32e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,52 @@ +# -- Packaging -------------------------------------- +[metadata] +description-file = README.md + +[qgis-plugin-ci] +plugin_path = plugin_map2loop +project_slug = TO BE SET WITH THE GITLAB/GITHUB SLUGIFIED PROJECT NAME (IN PROJECT'S URL) + +github_organization_slug = TO BE SET WITH THE GITHUB USER/ORGANIZATION NAME + + +# -- Code quality ------------------------------------ + + +[isort] +ensure_newline_before_comments = True +force_grid_wrap = 0 +include_trailing_comma = True +line_length = 88 +multi_line_output = 3 +profile = black +use_parentheses = True + +# -- Tests ---------------------------------------------- +[tool:pytest] +addopts = + --junitxml=junit/test-results.xml + --cov-config=setup.cfg + --cov=plugin_map2loop + --cov-report=html + --cov-report=term + --cov-report=xml + --ignore=tests/_wip/ +norecursedirs = .* build dev development dist docs CVS fixtures _darcs {arch} *.egg venv _wip +python_files = test_*.py +testpaths = tests + +[coverage:run] +branch = True +omit = + .venv/* + *tests* + +[coverage:report] +exclude_lines = + if self.debug: + pragma: no cover + raise NotImplementedError + if __name__ == .__main__.: + +ignore_errors = True +show_missing = True diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/qgis/__init__.py b/tests/qgis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/qgis/test_env_var_parser.py b/tests/qgis/test_env_var_parser.py new file mode 100644 index 0000000..ce4b73d --- /dev/null +++ b/tests/qgis/test_env_var_parser.py @@ -0,0 +1,76 @@ +import os +import unittest + +from map2loop.toolbelt.env_var_parser import EnvVarParser + + +class TestEnvVarParser(unittest.TestCase): + """Unit tests for EnvVarParser.""" + + def setUp(self) -> None: + """Prepare the test environment before each test.""" + self.env_backup = dict(os.environ) # Backup environment variables + + def tearDown(self) -> None: + """Restore the original environment variables after each test.""" + os.environ.clear() + os.environ.update(self.env_backup) + + def test_int_conversion(self) -> None: + """Test integer conversion from environment variable""" + os.environ["MY_INT"] = "42" + self.assertEqual(EnvVarParser.get_env_var("MY_INT", 0), 42) + + def test_float_conversion(self) -> None: + """Test float conversion from environment variable""" + os.environ["MY_FLOAT"] = "3.14" + self.assertEqual(EnvVarParser.get_env_var("MY_FLOAT", 0.0), 3.14) + + def test_bool_conversion_true(self) -> None: + """Test bool conversion from environment variable""" + for true_value in ["1", "true", "yes", "on"]: + os.environ["MY_BOOL"] = true_value + self.assertTrue(EnvVarParser.get_env_var("MY_BOOL", False)) + + def test_bool_conversion_false(self) -> None: + """Test bool conversion from environment variable""" + for false_value in ["0", "false", "no", "off"]: + os.environ["MY_BOOL"] = false_value + self.assertFalse(EnvVarParser.get_env_var("MY_BOOL", True)) + + def test_bool_invalid_defaults_to_original(self) -> None: + """Test invalid bool conversion from environment variable""" + os.environ["MY_BOOL"] = "maybe" + self.assertFalse( + EnvVarParser.get_env_var("MY_BOOL", False) + ) # Default should remain + + def test_string_conversion(self) -> None: + """Test string conversion from environment variable""" + os.environ["MY_STRING"] = "Hello, World!" + self.assertEqual( + EnvVarParser.get_env_var("MY_STRING", "default"), "Hello, World!" + ) + + def test_default_value_when_env_missing(self) -> None: + """Test default value is used when environment variable is missing""" + self.assertEqual(EnvVarParser.get_env_var("MISSING_INT", 99), 99) + + def test_invalid_int_fallback_to_default(self) -> None: + """Test default value used when the environment variable is not a valid int""" + os.environ["MY_INT"] = "not_an_int" + self.assertEqual(EnvVarParser.get_env_var("MY_INT", 10), 10) + + def test_invalid_float_fallback_to_default(self) -> None: + """Test default value used when the environment variable is not a valid float""" + os.environ["MY_FLOAT"] = "not_a_float" + self.assertEqual(EnvVarParser.get_env_var("MY_FLOAT", 1.23), 1.23) + + def test_unsupported_type(self) -> None: + """Test exception is raised when the type expected is not supported""" + os.environ["INT_LIST"] = "1,2,3,4" + self.assertRaises(TypeError, EnvVarParser.get_env_var, "INT_LIST", [1, 2, 3, 4]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py new file mode 100644 index 0000000..aca745c --- /dev/null +++ b/tests/qgis/test_plg_preferences.py @@ -0,0 +1,99 @@ +#! python3 # noqa E265 + +""" + Usage from the repo root folder: + + .. code-block:: bash + + # for whole tests + python -m unittest tests.qgis.test_plg_preferences + # for specific test + python -m unittest tests.qgis.test_plg_preferences.TestPlgPreferences.test_plg_preferences_structure +""" + +# standard library +import os +from unittest.mock import patch + +from qgis.testing import unittest + +# project +from map2loop.__about__ import __version__ +from map2loop.toolbelt.preferences import ( + PREFIX_ENV_VARIABLE, + PlgOptionsManager, + PlgSettingsStructure, +) + +# ############################################################################ +# ########## Classes ############# +# ################################ + + +class TestPlgPreferences(unittest.TestCase): + def test_plg_preferences_structure(self): + """Test settings types and default values.""" + settings = PlgSettingsStructure() + + # global + self.assertTrue(hasattr(settings, "debug_mode")) + self.assertIsInstance(settings.debug_mode, bool) + self.assertEqual(settings.debug_mode, False) + + self.assertTrue(hasattr(settings, "version")) + self.assertIsInstance(settings.version, str) + self.assertEqual(settings.version, __version__) + + + def test_bool_env_variable(self): + """Test settings with environment value.""" + manager = PlgOptionsManager() + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "true"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, True) + + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "false"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, False) + + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "on"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, True) + + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "off"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, False) + + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "1"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, True) + + with patch.dict( + os.environ, {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "0"}, clear=True + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, False) + + with patch.dict( + os.environ, + {f"{PREFIX_ENV_VARIABLE}DEBUG_MODE": "invalid_value"}, + clear=True, + ): + settings = manager.get_plg_settings() + self.assertEqual(settings.debug_mode, False) + +# ############################################################################ +# ####### Stand-alone run ######## +# ################################ +if __name__ == "__main__": + unittest.main() diff --git a/tests/qgis/test_processing.py b/tests/qgis/test_processing.py new file mode 100644 index 0000000..5624341 --- /dev/null +++ b/tests/qgis/test_processing.py @@ -0,0 +1,40 @@ +#! python3 # noqa E265 + +""" +Usage from the repo root folder: + +.. code-block:: bash + + # for whole tests + python -m unittest tests.qgis.test_plg_processing + # for specific test + python -m unittest tests.qgis.test_plg_processing.TestPlgprocessing.test_plg_processing_structure +""" + +# PyQGIS +from qgis.core import QgsApplication +from qgis.testing import unittest, start_app + +from map2loop.processing.provider import ( + Map2LoopPluginProvider, +) + +provider = None + + +class TestProcessing(unittest.TestCase): + """Tests for processing algorithms.""" + + def setUp(self) -> None: + """Set up the processing tests.""" + if not QgsApplication.processingRegistry().providers(): + self.provider = Map2LoopPluginProvider() + QgsApplication.processingRegistry().addProvider(self.provider) + self.maxDiff = None + + # Start App needed to run processing on unittest + start_app() + + def test_processing_provider(self): + """Sample test.""" + print(f"Processing provider name : {self.provider.name()}") \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py new file mode 100644 index 0000000..51a2e0e --- /dev/null +++ b/tests/unit/test_plg_metadata.py @@ -0,0 +1,86 @@ +#! python3 # noqa E265 + +""" + Usage from the repo root folder: + + .. code-block:: bash + # for whole tests + python -m unittest tests.unit.test_plg_metadata + # for specific test + python -m unittest tests.unit.test_plg_metadata.TestPluginMetadata.test_version_semver +""" + +# standard library +import unittest +from pathlib import Path + +# 3rd party +from packaging.version import parse + +# project +from map2loop import __about__ + +# ############################################################################ +# ########## Classes ############# +# ################################ + + +class TestPluginMetadata(unittest.TestCase): + + """Test about module""" + + def test_metadata_types(self): + """Test types.""" + # plugin metadata.txt file + self.assertIsInstance(__about__.PLG_METADATA_FILE, Path) + self.assertTrue(__about__.PLG_METADATA_FILE.is_file()) + + # plugin dir + self.assertIsInstance(__about__.DIR_PLUGIN_ROOT, Path) + self.assertTrue(__about__.DIR_PLUGIN_ROOT.is_dir()) + + # metadata as dict + self.assertIsInstance(__about__.__plugin_md__, dict) + + # general + self.assertIsInstance(__about__.__author__, str) + self.assertIsInstance(__about__.__copyright__, str) + self.assertIsInstance(__about__.__email__, str) + self.assertIsInstance(__about__.__keywords__, list) + self.assertIsInstance(__about__.__license__, str) + self.assertIsInstance(__about__.__summary__, str) + self.assertIsInstance(__about__.__title__, str) + self.assertIsInstance(__about__.__title_clean__, str) + self.assertIsInstance(__about__.__version__, str) + self.assertIsInstance(__about__.__version_info__, tuple) + + # misc + self.assertLessEqual(len(__about__.__title_clean__), len(__about__.__title__)) + + # QGIS versions + self.assertIsInstance( + __about__.__plugin_md__.get("general").get("qgisminimumversion"), str + ) + + self.assertIsInstance( + __about__.__plugin_md__.get("general").get("qgismaximumversion"), str + ) + + min_version_parsed = parse( + __about__.__plugin_md__.get("general").get("qgisminimumversion") + ) + max_version_parsed = parse( + __about__.__plugin_md__.get("general").get("qgismaximumversion") + ) + self.assertLessEqual(min_version_parsed, max_version_parsed) + + def test_version_semver(self): + """Test if version comply with semantic versioning.""" + self.assertTrue(parse(__about__.__version__)) + + +# ############################################################################ +# ####### Stand-alone run ######## +# ################################ +if __name__ == "__main__": + unittest.main() From ef6debaf14ce82e159ccf56de98e4f9a4d86b049 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 10:54:00 +1000 Subject: [PATCH 002/135] adding pyproject.toml for ruff --- pyproject.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6e5991 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ + + +[tool.ruff.lint] +external = ["E131", "D102", "D105"] + +ignore = [ + # whitespace before ':' + "E203", + # line break before binary operator + # "W503", + # line length too long + "E501", + # do not assign a lambda expression, use a def + "E731", + # too many leading '#' for block comment + "E266", + # ambiguous variable name + "E741", + # module level import not at top of file + "E402", + # Quotes (temporary) + "Q0", + # bare excepts (temporary) + # "B001", "E722", + "E722", + # we already check black + # "BLK100", + # 'from module import *' used; unable to detect undefined names + "F403", +] +fixable = ["ALL"] +unfixable = [] +extend-select = ["B007", "B010", "C4", "F", "NPY", "PGH004", "RSE", "RUF100"] + +[tool.ruff.lint.flake8-comprehensions] +allow-dict-calls-with-keyword-arguments = true +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] From bfd0a9c499e4d07c0b292086f1411c0f21d89f1d Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 10:55:08 +1000 Subject: [PATCH 003/135] change linter to ruff --- .github/workflows/linter.yml | 62 +++++++++++++----------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 78d45c7..14e2275 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,21 +2,21 @@ name: "โœ… Linter" on: push: - branches: - - main paths: - '**.py' pull_request: branches: - - main + - master paths: - '**.py' + workflow_dispatch: env: PROJECT_FOLDER: "plugin_map2loop" PYTHON_VERSION: 3.9 - +permissions: + contents: write jobs: lint-py: @@ -32,44 +32,28 @@ jobs: uses: actions/setup-python@v5 with: cache: "pip" - cache-dependency-path: "requirements/development.txt" python-version: ${{ env.PYTHON_VERSION }} - - name: Install project requirements + - name: Install dependencies run: | - python -m pip install -U pip setuptools wheel - python -m pip install -U -r requirements/development.txt - - - name: Lint with flake8 + python -m pip install --upgrade pip + pip install black ruff + - name: Autoformat with black run: | - # stop the build if there are Python syntax errors or undefined names - flake8 ${{ env.PROJECT_FOLDER }} --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. - flake8 ${{ env.PROJECT_FOLDER }} --count --exit-zero --statistics - - qt6-check: - name: PyQt6 6๏ธโƒฃ - runs-on: ubuntu-latest - container: - image: registry.gitlab.com/oslandia/qgis/pyqgis-4-checker/pyqgis-qt-checker:latest - volumes: - - /tmp/.X11-unix:/tmp/.X11-unix - - ${{ github.workspace }}:/home/pyqgisdev/ - options: --user root - steps: - - name: Get source code - uses: actions/checkout@v4 - - - name: Check PyQt5 to PyQt6 compatibility. + black . + - name: Lint with ruff run: | - pyqt5_to_pyqt6.py --dry_run ${{ env.PROJECT_FOLDER }}/ - pyqt5_to_pyqt6.py --logfile pyqt6_checker.log ${{ env.PROJECT_FOLDER }}/ - - - name: Upload script report if script fails - uses: actions/upload-artifact@v4 - if: ${{ failure() }} + ruff check ${{env.PROJECT_FOLDER}} --fix + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "style: style fixes by ruff and autoformatting by black" + branch: lint/style-fixes-${{ github.run_id }} + create_branch: true + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 with: - name: pyqt6-checker-error-report - path: pyqt6_checker.log - retention-days: 7 - \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} + title: "style: auto format fixes" + body: "This PR applies style fixes by black and ruff." + base: master + branch: lint/style-fixes-${{ github.run_id }} From 70aafd533075b3990102e0f180471364b61b347c Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 10:57:07 +1000 Subject: [PATCH 004/135] adding release please action --- .github/workflows/release-please.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/release-please.yml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..44f0f6b --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,44 @@ + +on: + push: + branches: + - main + env: + PACKAGE_NAME: plugin_map2loop + permissions: + contents: write + pull-requests: write + + name: release-please + jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v4 + id: release + - name: debug + run: echo "release_created=${{ steps.release.outputs.map2loop--tag_name }}" + + outputs: + release_created: ${{ steps.release.outputs.releases_created }} + package: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + steps: + - name: Extract tag name + id: tag + run: | + full_tag="${{ needs.release-please.outputs.tag_name }}" + tag="${full_tag#*--}" # removes everything before the -- + echo "tag=$tag" >> $GITHUB_OUTPUT + + - name: Trigger release.yml + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GH_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ + -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" + + \ No newline at end of file From a3309331571e1f9a4201ee07b276605fef50c244 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 10:57:12 +1000 Subject: [PATCH 005/135] linting --- map2loop/__about__.py | 2 +- map2loop/__init__.py | 6 +++--- map2loop/gui/dlg_settings.py | 2 +- map2loop/plugin_main.py | 24 ++++++++++-------------- map2loop/processing/__init__.py | 4 ++-- map2loop/processing/provider.py | 4 ++-- map2loop/toolbelt/__init__.py | 6 +++--- map2loop/toolbelt/log_handler.py | 2 +- map2loop/toolbelt/preferences.py | 2 +- scripts/generate_translation_profile.py | 6 +++--- tests/qgis/test_plg_preferences.py | 2 +- tests/qgis/test_processing.py | 12 ++++++------ tests/unit/test_plg_metadata.py | 2 +- 13 files changed, 35 insertions(+), 39 deletions(-) diff --git a/map2loop/__about__.py b/map2loop/__about__.py index d7d3617..62e8a1c 100644 --- a/map2loop/__about__.py +++ b/map2loop/__about__.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 """ Metadata about the package to easily retrieve informations about it. diff --git a/map2loop/__init__.py b/map2loop/__init__.py index b2bebd7..1d91ae8 100644 --- a/map2loop/__init__.py +++ b/map2loop/__init__.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 # ---------------------------------------------------------- # Copyright (C) 2015 Martin Dobias @@ -18,6 +18,6 @@ def classFactory(iface): :param iface: A QGIS interface instance. :type iface: QgsInterface """ - from .plugin_main import Map2LoopPluginPlugin + from .plugin_main import Map2LoopPlugin - return Map2LoopPluginPlugin(iface) + return Map2LoopPlugin(iface) diff --git a/map2loop/gui/dlg_settings.py b/map2loop/gui/dlg_settings.py index e0b7b0a..bf6c8b1 100644 --- a/map2loop/gui/dlg_settings.py +++ b/map2loop/gui/dlg_settings.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 """ Plugin settings form integrated into QGIS 'Options' menu. diff --git a/map2loop/plugin_main.py b/map2loop/plugin_main.py index b352a74..f803c9a 100644 --- a/map2loop/plugin_main.py +++ b/map2loop/plugin_main.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 """Main plugin module.""" @@ -22,10 +22,8 @@ __uri_homepage__, ) from map2loop.gui.dlg_settings import PlgOptionsFactory - - from map2loop.processing import ( - Map2LoopPluginProvider, + Map2LoopProvider, ) from map2loop.toolbelt import PlgLogger @@ -34,7 +32,7 @@ # ################################## -class Map2LoopPluginPlugin: +class Map2LoopPlugin: def __init__(self, iface: QgisInterface): """Constructor. @@ -44,7 +42,7 @@ def __init__(self, iface: QgisInterface): """ self.iface = iface self.log = PlgLogger().log - self.provider: Optional[Map2LoopPluginProvider] = None + self.provider: Optional[Map2LoopProvider] = None # translation # initialize the locale @@ -52,9 +50,9 @@ def __init__(self, iface: QgisInterface): 0:2 ] locale_path: Path = ( - DIR_PLUGIN_ROOT - / "resources" - / "i18n" + DIR_PLUGIN_ROOT + / "resources" + / "i18n" / f"{__title__.lower()}_{self.locale}.qm" ) self.log(message=f"Translation: {self.locale}, {locale_path}", log_level=4) @@ -96,7 +94,6 @@ def initGui(self): self.iface.addPluginToMenu(__title__, self.action_help) # -- Processing self.initProcessing() - # -- Help menu @@ -114,11 +111,11 @@ def initGui(self): self.iface.pluginHelpMenu().addAction( self.action_help_plugin_menu_documentation ) + def initProcessing(self): - """Initialize the processing provider.""" - self.provider = Map2LoopPluginProvider() + """Initialize the processing provider.""" + self.provider = Map2LoopProvider() QgsApplication.processingRegistry().addProvider(self.provider) - def tr(self, message: str) -> str: """Get the translation for a string using Qt translation API. @@ -141,7 +138,6 @@ def unload(self): self.iface.unregisterOptionsWidgetFactory(self.options_factory) # -- Unregister processing QgsApplication.processingRegistry().removeProvider(self.provider) - # remove from QGIS help/extensions menu if self.action_help_plugin_menu_documentation: diff --git a/map2loop/processing/__init__.py b/map2loop/processing/__init__.py index 4e05cc3..fb022ab 100644 --- a/map2loop/processing/__init__.py +++ b/map2loop/processing/__init__.py @@ -1,2 +1,2 @@ -#! python3 # noqa: E265 -from .provider import Map2LoopPluginProvider # noqa: F401 +#! python3 +from .provider import Map2LoopPluginProvider diff --git a/map2loop/processing/provider.py b/map2loop/processing/provider.py index 908ac35..6e0add8 100644 --- a/map2loop/processing/provider.py +++ b/map2loop/processing/provider.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 """ Processing provider module. @@ -21,7 +21,7 @@ # ################################## -class Map2LoopPluginProvider(QgsProcessingProvider): +class Map2LoopProvider(QgsProcessingProvider): """Processing provider class.""" def loadAlgorithms(self): diff --git a/map2loop/toolbelt/__init__.py b/map2loop/toolbelt/__init__.py index 435f023..461211d 100644 --- a/map2loop/toolbelt/__init__.py +++ b/map2loop/toolbelt/__init__.py @@ -1,3 +1,3 @@ -#! python3 # noqa: E265 -from .log_handler import PlgLogger # noqa: F401 -from .preferences import PlgOptionsManager # noqa: F401 +#! python3 +from .log_handler import PlgLogger +from .preferences import PlgOptionsManager diff --git a/map2loop/toolbelt/log_handler.py b/map2loop/toolbelt/log_handler.py index 161737d..222fa3f 100644 --- a/map2loop/toolbelt/log_handler.py +++ b/map2loop/toolbelt/log_handler.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 # standard library import logging diff --git a/map2loop/toolbelt/preferences.py b/map2loop/toolbelt/preferences.py index 0809ee8..85986b1 100644 --- a/map2loop/toolbelt/preferences.py +++ b/map2loop/toolbelt/preferences.py @@ -1,4 +1,4 @@ -#! python3 # noqa: E265 +#! python3 """ Plugin settings. diff --git a/scripts/generate_translation_profile.py b/scripts/generate_translation_profile.py index 3ff8390..5536d3e 100644 --- a/scripts/generate_translation_profile.py +++ b/scripts/generate_translation_profile.py @@ -60,10 +60,10 @@ def get_relative_paths(filepaths_list: list[Path]) -> list[str]: # Get the list of all files in directory tree at given path python_files = [ - f for f in sorted(list(Path(src_path).rglob("*.py"))) if not f.name.startswith("__") + f for f in sorted(Path(src_path).rglob("*.py")) if not f.name.startswith("__") ] -ui_files = [f for f in sorted(list(Path(src_path).rglob("*.ui")))] -ts_files = [f for f in sorted(list(Path(src_path).rglob("*.ts")))] +ui_files = sorted(Path(src_path).rglob("*.ui")) +ts_files = sorted(Path(src_path).rglob("*.ts")) # Generate the translation profile diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py index aca745c..23daa19 100644 --- a/tests/qgis/test_plg_preferences.py +++ b/tests/qgis/test_plg_preferences.py @@ -1,4 +1,4 @@ -#! python3 # noqa E265 +#! python3 """ Usage from the repo root folder: diff --git a/tests/qgis/test_processing.py b/tests/qgis/test_processing.py index 5624341..b52b3de 100644 --- a/tests/qgis/test_processing.py +++ b/tests/qgis/test_processing.py @@ -1,4 +1,4 @@ -#! python3 # noqa E265 +#! python3 """ Usage from the repo root folder: @@ -13,10 +13,10 @@ # PyQGIS from qgis.core import QgsApplication -from qgis.testing import unittest, start_app +from qgis.testing import start_app, unittest from map2loop.processing.provider import ( - Map2LoopPluginProvider, + Map2LoopProvider, ) provider = None @@ -28,13 +28,13 @@ class TestProcessing(unittest.TestCase): def setUp(self) -> None: """Set up the processing tests.""" if not QgsApplication.processingRegistry().providers(): - self.provider = Map2LoopPluginProvider() + self.provider = Map2LoopProvider() QgsApplication.processingRegistry().addProvider(self.provider) self.maxDiff = None - + # Start App needed to run processing on unittest start_app() def test_processing_provider(self): """Sample test.""" - print(f"Processing provider name : {self.provider.name()}") \ No newline at end of file + print(f"Processing provider name : {self.provider.name()}") diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py index 51a2e0e..2baf6ed 100644 --- a/tests/unit/test_plg_metadata.py +++ b/tests/unit/test_plg_metadata.py @@ -1,4 +1,4 @@ -#! python3 # noqa E265 +#! python3 """ Usage from the repo root folder: From 720bbf572af3dd14fbd16072491249380676245c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 02:00:05 +0000 Subject: [PATCH 006/135] Bump dawidd6/action-download-artifact from 9 to 11 Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 9 to 11. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v9...v11) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-version: '11' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 791fd6b..5876b31 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -82,7 +82,7 @@ jobs: - name: Download artifact from build workflow if: ${{ env.CONDITION_IS_PUSH || env.CONDITION_IS_WORKFLOW_RUN }} - uses: dawidd6/action-download-artifact@v9 + uses: dawidd6/action-download-artifact@v11 with: allow_forks: false branch: main From 0122b598ce79059548355bb647e1f8fb3fe5e44a Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:27:51 +1000 Subject: [PATCH 007/135] ci: fix formatting of release-please --- .github/workflows/release-please.yml | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 44f0f6b..840c297 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -3,42 +3,42 @@ on: push: branches: - main - env: - PACKAGE_NAME: plugin_map2loop - permissions: - contents: write - pull-requests: write - - name: release-please - jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: GoogleCloudPlatform/release-please-action@v4 - id: release - - name: debug - run: echo "release_created=${{ steps.release.outputs.map2loop--tag_name }}" - - outputs: - release_created: ${{ steps.release.outputs.releases_created }} - package: - needs: release-please - if: ${{ needs.release-please.outputs.release_created }} - runs-on: ubuntu-latest - steps: - - name: Extract tag name - id: tag - run: | - full_tag="${{ needs.release-please.outputs.tag_name }}" - tag="${full_tag#*--}" # removes everything before the -- - echo "tag=$tag" >> $GITHUB_OUTPUT - - - name: Trigger release.yml - run: | - curl -X POST \ - -H "Authorization: token ${{ secrets.GH_PAT }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ - -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" + env: + PACKAGE_NAME: plugin_loopstructural + permissions: + contents: write + pull-requests: write + + name: release-please + jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v4 + id: release + - name: debug + run: echo "release_created=${{ steps.release.outputs.loopstructural--tag_name }}" + + outputs: + release_created: ${{ steps.release.outputs.releases_created }} + package: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + steps: + - name: Extract tag name + id: tag + run: | + full_tag="${{ needs.release-please.outputs.tag_name }}" + tag="${full_tag#*--}" # removes everything before the -- + echo "tag=$tag" >> $GITHUB_OUTPUT + + - name: Trigger release.yml + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GH_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ + -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" \ No newline at end of file From 90ebef3253fa7c2f6f6f0b84210c4436de028680 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:32:34 +1000 Subject: [PATCH 008/135] Update linter.yml --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 14e2275..57c2289 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -13,7 +13,7 @@ on: workflow_dispatch: env: - PROJECT_FOLDER: "plugin_map2loop" + PROJECT_FOLDER: "map2loop" PYTHON_VERSION: 3.9 permissions: contents: write From ec42a7961c0e649589a5c988efb368b1add12c59 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:33:31 +1000 Subject: [PATCH 009/135] Update release-please.yml --- .github/workflows/release-please.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 840c297..7310788 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -27,18 +27,18 @@ on: runs-on: ubuntu-latest steps: - name: Extract tag name - id: tag - run: | + id: tag + run: | full_tag="${{ needs.release-please.outputs.tag_name }}" tag="${full_tag#*--}" # removes everything before the -- echo "tag=$tag" >> $GITHUB_OUTPUT - name: Trigger release.yml - run: | + run: | curl -X POST \ -H "Authorization: token ${{ secrets.GH_PAT }}" \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" - \ No newline at end of file + From d85b5818305b1376b7fa34ca50d534870925ff12 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:35:13 +1000 Subject: [PATCH 010/135] fixing syntax --- .github/workflows/release-please.yml | 86 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 7310788..95792d3 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,44 +1,46 @@ - on: - push: - branches: - - main - env: - PACKAGE_NAME: plugin_loopstructural - permissions: - contents: write - pull-requests: write - + push: + branches: + - main + +env: + PACKAGE_NAME: plugin_loopstructural + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: name: release-please - jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: GoogleCloudPlatform/release-please-action@v4 - id: release - - name: debug - run: echo "release_created=${{ steps.release.outputs.loopstructural--tag_name }}" - - outputs: - release_created: ${{ steps.release.outputs.releases_created }} - package: - needs: release-please - if: ${{ needs.release-please.outputs.release_created }} - runs-on: ubuntu-latest - steps: - - name: Extract tag name - id: tag - run: | - full_tag="${{ needs.release-please.outputs.tag_name }}" - tag="${full_tag#*--}" # removes everything before the -- - echo "tag=$tag" >> $GITHUB_OUTPUT - - - name: Trigger release.yml - run: | - curl -X POST \ - -H "Authorization: token ${{ secrets.GH_PAT }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ - -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" - - + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v4 + id: release + - name: debug + run: echo "release_created=${{ steps.release.outputs.loopstructural--tag_name }}" + + outputs: + release_created: ${{ steps.release.outputs.releases_created }} + + package: + needs: release-please + if: ${{ needs.release-please.outputs.release_created }} + runs-on: ubuntu-latest + steps: + - name: Extract tag name + id: tag + run: | + full_tag="${{ needs.release-please.outputs.tag_name }}" + tag="${full_tag#*--}" # removes everything before the -- + echo "tag=$tag" >> $GITHUB_OUTPUT + + - name: Trigger release.yml + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GH_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/Loop3d/${{ env.PACKAGE_NAME }}/actions/workflows/release.yml/dispatches \ + -d "{\"ref\":\"${{ steps.tag.outputs.tag }}\"}" + + From 8ad1c4e65a9b473cd41abc3a999bb29092b334d4 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:38:15 +1000 Subject: [PATCH 011/135] adding release please files --- .release-please-manifest.json | 4 ++++ release-please-config.json | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..b18442c --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + ".": "0.0.1", + "map2loop": "0.1.0" +} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..9a6e491 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,7 @@ +{ + "packages": { + "map2loop": { + "release-type": "python" + } + } +} \ No newline at end of file From 6ae8d3f6b2a7daedd79ed039eac9de129af7713b Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 12:54:43 +1000 Subject: [PATCH 012/135] fix: import typo --- map2loop/processing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/processing/__init__.py b/map2loop/processing/__init__.py index fb022ab..ca75285 100644 --- a/map2loop/processing/__init__.py +++ b/map2loop/processing/__init__.py @@ -1,2 +1,2 @@ #! python3 -from .provider import Map2LoopPluginProvider +from .provider import Map2LoopProvider From c0bb5f27c48ab639017ab30ee65c00c2972f56a2 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:09:35 +1000 Subject: [PATCH 013/135] template for basal contacts processing tool --- map2loop/processing/algorithms/__init__.py | 1 + .../algorithms/extract_basal_contacts.py | 76 +++++++++++++++++++ map2loop/processing/provider.py | 3 + 3 files changed, 80 insertions(+) create mode 100644 map2loop/processing/algorithms/__init__.py create mode 100644 map2loop/processing/algorithms/extract_basal_contacts.py diff --git a/map2loop/processing/algorithms/__init__.py b/map2loop/processing/algorithms/__init__.py new file mode 100644 index 0000000..618a57d --- /dev/null +++ b/map2loop/processing/algorithms/__init__.py @@ -0,0 +1 @@ +from .extract_basal_contacts import BasalContactsAlgorithm diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py new file mode 100644 index 0000000..11d406c --- /dev/null +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -0,0 +1,76 @@ +""" +*************************************************************************** +* * +* 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 typing import Any, Optional + +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, +) + + +class BasalContactsAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm to create basal contacts.""" + + INPUT = "INPUT" + OUTPUT = "OUTPUT" + + def name(self) -> str: + """Return the algorithm name.""" + return "loop: basal_contacts" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Basal Contacts" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT, + "Geology Polygons", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + pass + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + pass + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() diff --git a/map2loop/processing/provider.py b/map2loop/processing/provider.py index 6e0add8..ba879bb 100644 --- a/map2loop/processing/provider.py +++ b/map2loop/processing/provider.py @@ -16,6 +16,8 @@ __version__, ) +from .algorithms import BasalContactsAlgorithm + # ############################################################################ # ########## Classes ############### # ################################## @@ -26,6 +28,7 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" + self.addAlgorithm(BasalContactsAlgorithm()) pass def id(self) -> str: From f9878b0396c19f57160fc4c27b4086221e83c3b6 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:11:57 +1000 Subject: [PATCH 014/135] Update linter.yml --- .github/workflows/linter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 57c2289..f87c4e5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,7 +7,7 @@ on: pull_request: branches: - - master + - main paths: - '**.py' workflow_dispatch: @@ -55,5 +55,5 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} title: "style: auto format fixes" body: "This PR applies style fixes by black and ruff." - base: master + base: main branch: lint/style-fixes-${{ github.run_id }} From b51f758b6405827fa2bceb47976b987b3b756442 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:13:21 +1000 Subject: [PATCH 015/135] disable qgis testing for now --- .github/workflows/tester.yml | 104 +++++++++++++++++------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 9053903..4543564 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -45,55 +45,55 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - test-qgis: - runs-on: ubuntu-latest - - container: - image: qgis/qgis:3.4 - env: - CI: true - DISPLAY: ":1" - MUTE_LOGS: true - NO_MODALS: 1 - PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - QT_QPA_PLATFORM: "offscreen" - WITH_PYTHON_PEP: false - # be careful, things have changed since QGIS 3.40. So if you are using this setup - # with a QGIS version older than 3.40, you may need to change the way you set up the container - volumes: - # Mount the X11 socket to allow GUI applications to run - - /tmp/.X11-unix:/tmp/.X11-unix - # Mount the workspace directory to the container - - ${{ github.workspace }}:/home/root/ - - steps: - - name: Get source code - uses: actions/checkout@v4 - - - name: Print QGIS version - run: qgis --version - - # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # - name: Setup plugin - # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - - name: Install Python requirements - run: | - apt update && apt install -y python3-pip python3-venv pipx - # Create a virtual environment - cd /home/root/ - pipx run qgis-venv-creator --venv-name ".venv" - # Activate the virtual environment - . .venv/bin/activate - # Install the requirements - python3 -m pip install -U -r requirements/testing.txt - - - name: Run Unit tests - run: | - cd /home/root/ - # Activate the virtual environment - . .venv/bin/activate - # Run the tests - # xvfb-run is used to run the tests in a virtual framebuffer - # This is necessary because QGIS requires a display to run - xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + # test-qgis: + # runs-on: ubuntu-latest + + # container: + # image: qgis/qgis:3.4 + # env: + # CI: true + # DISPLAY: ":1" + # MUTE_LOGS: true + # NO_MODALS: 1 + # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + # QT_QPA_PLATFORM: "offscreen" + # WITH_PYTHON_PEP: false + # # be careful, things have changed since QGIS 3.40. So if you are using this setup + # # with a QGIS version older than 3.40, you may need to change the way you set up the container + # volumes: + # # Mount the X11 socket to allow GUI applications to run + # - /tmp/.X11-unix:/tmp/.X11-unix + # # Mount the workspace directory to the container + # - ${{ github.workspace }}:/home/root/ + + # steps: + # - name: Get source code + # uses: actions/checkout@v4 + + # - name: Print QGIS version + # run: qgis --version + + # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # # - name: Setup plugin + # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + # - name: Install Python requirements + # run: | + # apt update && apt install -y python3-pip python3-venv pipx + # # Create a virtual environment + # cd /home/root/ + # pipx run qgis-venv-creator --venv-name ".venv" + # # Activate the virtual environment + # . .venv/bin/activate + # # Install the requirements + # python3 -m pip install -U -r requirements/testing.txt + + # - name: Run Unit tests + # run: | + # cd /home/root/ + # # Activate the virtual environment + # . .venv/bin/activate + # # Run the tests + # # xvfb-run is used to run the tests in a virtual framebuffer + # # This is necessary because QGIS requires a display to run + # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml From b3fc47d10223ee0d3bc4596b8179e09366564e67 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:17:36 +1000 Subject: [PATCH 016/135] Update release-please.yml --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 95792d3..8d4c529 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,7 +9,7 @@ env: permissions: contents: write pull-requests: write - + metadata: write jobs: release-please: name: release-please From 56a20aecd365b443aede564c4365263c06c48f0f Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:26:28 +1000 Subject: [PATCH 017/135] Update release-please.yml --- .github/workflows/release-please.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8d4c529..8ff38dc 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,7 +9,7 @@ env: permissions: contents: write pull-requests: write - metadata: write + issues: write jobs: release-please: name: release-please From 773ef4844f12f3183da8caed672eb7af34dfbd35 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 23 Jun 2025 13:55:03 +0930 Subject: [PATCH 018/135] feature: Implement StratigraphySorterAlgorithm --- map2loop/processing/algorithms/sorter.py | 201 +++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 map2loop/processing/algorithms/sorter.py diff --git a/map2loop/processing/algorithms/sorter.py b/map2loop/processing/algorithms/sorter.py new file mode 100644 index 0000000..999fcad --- /dev/null +++ b/map2loop/processing/algorithms/sorter.py @@ -0,0 +1,201 @@ +from typing import Any, Optional + +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsVectorLayer, +) + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# map2loop sorters +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +from map2loop.map2loop.sorter import ( + SorterAlpha, + SorterAgeBased, + SorterMaximiseContacts, + SorterObservationProjections, + SorterUseNetworkX, + SorterUseHint, # kept for backwards compatibility +) + +# a lookup so we donโ€™t need a giant if/else block +SORTER_LIST = { + "Ageโ€based": SorterAgeBased, + "NetworkX topological": SorterUseNetworkX, + "Hint (deprecated)": SorterUseHint, + "Adjacency ฮฑ": SorterAlpha, + "Maximise contacts": SorterMaximiseContacts, + "Observation projections": SorterObservationProjections, +} + +class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): + """ + Creates a one-column โ€˜stratigraphic columnโ€™ table ordered + by the selected map2loop sorter. + """ + + INPUT = "INPUT" + ALGO = "SORT_ALGO" + OUTPUT = "OUTPUT" + + # ---------------------------------------------------------- + # Metadata + # ---------------------------------------------------------- + def name(self) -> str: + return "loop_sorter" + + def displayName(self) -> str: + return "loop: Stratigraphic sorter" + + def group(self) -> str: + return "Loop3d" + + def groupId(self) -> str: + return "loop3d" + + # ---------------------------------------------------------- + # Parameters + # ---------------------------------------------------------- + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT, + self.tr("Geology polygons"), + [QgsProcessing.TypeVectorPolygon], + ) + ) + + # enum so the user can pick the strategy from a dropdown + self.addParameter( + QgsProcessingParameterEnum( + self.ALGO, + self.tr("Sorting strategy"), + options=list(SORTER_LIST.keys()), + defaultValue=0, # Age-based is safest default + ) + ) #:contentReference[oaicite:0]{index=0} + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr("Stratigraphic column"), + ) + ) + + # ---------------------------------------------------------- + # Core + # ---------------------------------------------------------- + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + # 1 โ–บ fetch user selections + in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context) + algo_index: int = self.parameterAsEnum(parameters, self.ALGO, context) + sorter_cls = list(SORTER_LIST.values())[algo_index] + + feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") + + # 2 โ–บ convert QGIS layers / tables to pandas + # -------------------------------------------------- + # You must supply these three DataFrames: + # units_df โ€” required (layerId, name, minAge, maxAge, group) + # relationships_df โ€” required (Index1 / Unitname1, Index2 / Unitname2 โ€ฆ) + # contacts_df โ€” required for all but Ageโ€based + # + # Typical workflow: + # โ€ข iterate over in_layer.getFeatures() + # โ€ข build dicts/lists + # โ€ข pd.DataFrame(โ€ฆ) + # + # NB: map2loop does *not* need geometries โ€“ only attribute values. + # -------------------------------------------------- + units_df, relationships_df, contacts_df, map_data = build_input_frames(in_layer, feedback) + + # 3 โ–บ run the sorter + sorter = sorter_cls() # instantiation is always zero-argument + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + map_data, + ) + + # 4 โ–บ write an in-memory table with the result + sink_fields = QgsFields() + sink_fields.append(QgsField("strat_pos", int)) + sink_fields.append(QgsField("unit_name", str)) + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + sink_fields, + QgsWkbTypes.NoGeometry, + in_layer.sourceCrs(), + ) + + for pos, name in enumerate(order, start=1): + f = QgsFeature(sink_fields) + f.setAttributes([pos, name]) + sink.addFeature(f, QgsFeatureSink.FastInsert) + + return {self.OUTPUT: dest_id} + + # ---------------------------------------------------------- + def createInstance(self) -> QgsProcessingAlgorithm: + return StratigraphySorterAlgorithm() + + +# ------------------------------------------------------------------------- +# Helper stub โ€“ you must replace with *your* conversion logic +# ------------------------------------------------------------------------- +def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: + """ + Placeholder that turns the geology layer (and any other project + layers) into the four objects required by the sorter. + + Returns + ------- + (units_df, relationships_df, contacts_df, map_data) + """ + import pandas as pd + from map2loop.map2loop.mapdata import MapData # adjust import path if needed + + # Example: convert the geology layer to a very small units_df + units_records = [] + for f in layer.getFeatures(): + units_records.append( + dict( + layerId=f.id(), + name=f["UNITNAME"], # attribute names โ†’ your schema + minAge=f.attribute("MIN_AGE"), + maxAge=f.attribute("MAX_AGE"), + group=f["GROUP"], + ) + ) + units_df = pd.DataFrame.from_records(units_records) + + # relationships_df and contacts_df are domain-specific โ”€ fill them here + relationships_df = pd.DataFrame(columns=["Index1", "UNITNAME_1", "Index2", "UNITNAME_2"]) + contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) + + # map_data can be mocked if you only use Age-based sorter + map_data = MapData() # or MapData.from_project(โ€ฆ) / MapData.from_files(โ€ฆ) + + feedback.pushInfo(f"Units โ†’ {len(units_df)} records") + + return units_df, relationships_df, contacts_df, map_data From e7bb9f660416ef3a6ea9fa08f5a2df9a1fb91267 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 12:35:07 +0930 Subject: [PATCH 019/135] feature: functions to convert layers to GeoDataFrame and DataFrame --- map2loop/main/vectorLayerWrapper.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 map2loop/main/vectorLayerWrapper.py diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py new file mode 100644 index 0000000..7e146db --- /dev/null +++ b/map2loop/main/vectorLayerWrapper.py @@ -0,0 +1,78 @@ +import pandas as pd +import geopandas as gpd +from qgis.core import QgsRaster, QgsWkbTypes + + +def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: + if layer is None: + return None + features = layer.getFeatures() + fields = layer.fields() + data = {'geometry': []} + for f in fields: + data[f.name()] = [] + for feature in features: + geom = feature.geometry() + if geom.isEmpty(): + continue + data['geometry'].append(geom) + for f in fields: + data[f.name()].append(feature[f.name()]) + return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + + +def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: + """Convert a vector layer to a pandas DataFrame + samples the geometry using either points or the vertices of the lines + + :param layer: _description_ + :type layer: _type_ + :param dtm: Digital Terrain Model to evaluate Z values + :type dtm: _type_ or None + :return: the dataframe object + :rtype: pd.DataFrame + """ + if layer is None: + return None + fields = layer.fields() + data = {} + data['X'] = [] + data['Y'] = [] + data['Z'] = [] + + for field in fields: + data[field.name()] = [] + for feature in layer.getFeatures(): + geom = feature.geometry() + points = [] + if geom.isMultipart(): + if geom.type() == QgsWkbTypes.PointGeometry: + points = geom.asMultiPoint() + elif geom.type() == QgsWkbTypes.LineGeometry: + for line in geom.asMultiPolyline(): + points.extend(line) + # points = geom.asMultiPolyline()[0] + else: + if geom.type() == QgsWkbTypes.PointGeometry: + points = [geom.asPoint()] + elif geom.type() == QgsWkbTypes.LineGeometry: + points = geom.asPolyline() + + for p in points: + data['X'].append(p.x()) + data['Y'].append(p.y()) + if dtm is not None: + # Replace with your coordinates + + # Extract the value at the point + z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) + if z_value.isValid(): + z_value = z_value.results()[1] + else: + z_value = -9999 + data['Z'].append(z_value) + if dtm is None: + data['Z'].append(0) + for field in fields: + data[field.name()].append(feature[field.name()]) + return pd.DataFrame(data) From 95ba89b4544eea809501c1844060b1d78a47b32d Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:21:09 +0930 Subject: [PATCH 020/135] feature: add geodataframe to qgsLayer conversion --- map2loop/main/vectorLayerWrapper.py | 122 +++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7e146db..81af364 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,6 +1,20 @@ +from qgis.core import ( + QgsVectorLayer, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProject, + QgsRaster + ) +from qgis.PyQt.QtCore import QVariant, QDateTime + +from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon import pandas as pd import geopandas as gpd -from qgis.core import QgsRaster, QgsWkbTypes + def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: @@ -76,3 +90,109 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: for field in fields: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) + +def gdf_to_qgis_layer(gdf, layer_name="from_gdf"): + """ + Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). + Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. + """ + + if gdf is None or gdf.empty: + raise ValueError("GeoDataFrame is empty") + + # --- infer geometry type from first non-empty geometry + def infer_wkb(geoms): + for g in geoms: + if g is None: + continue + if hasattr(g, "is_empty") and g.is_empty: + continue + if isinstance(g, MultiPoint): return QgsWkbTypes.MultiPoint + if isinstance(g, Point): return QgsWkbTypes.Point + if isinstance(g, MultiLineString): return QgsWkbTypes.MultiLineString + if isinstance(g, LineString): return QgsWkbTypes.LineString + if isinstance(g, MultiPolygon): return QgsWkbTypes.MultiPolygon + if isinstance(g, Polygon): return QgsWkbTypes.Polygon + raise ValueError("Could not infer geometry type (all geometries empty?)") + + wkb_type = infer_wkb(gdf.geometry) + + # --- build CRS + crs_qgis = QgsCoordinateReferenceSystem() + if gdf.crs is not None: + try: + crs_qgis = QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()) + except Exception: + epsg = gdf.crs.to_epsg() + if epsg: + crs_qgis = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + + geom_str = QgsWkbTypes.displayString(wkb_type) # e.g. "LineString" + uri = f"{geom_str}?crs={crs_qgis.authid()}" if crs_qgis.isValid() else geom_str + layer = QgsVectorLayer(uri, layer_name, "memory") + prov = layer.dataProvider() + + # --- fields: map pandas dtypes โ†’ QGIS + import numpy as np + fields = QgsFields() + for col in gdf.columns: + if col == gdf.geometry.name: + continue + dtype = gdf[col].dtype + if pd.api.types.is_integer_dtype(dtype): + qtype = QVariant.Int + elif pd.api.types.is_float_dtype(dtype): + qtype = QVariant.Double + elif pd.api.types.is_bool_dtype(dtype): + qtype = QVariant.Bool + elif pd.api.types.is_datetime64_any_dtype(dtype): + qtype = QVariant.DateTime + else: + qtype = QVariant.String + fields.append(QgsField(str(col), qtype)) + prov.addAttributes(list(fields)) + layer.updateFields() + + # --- features + feats = [] + non_geom_cols = [c for c in gdf.columns if c != gdf.geometry.name] + + for _, row in gdf.iterrows(): + geom = row[gdf.geometry.name] + if geom is None or (hasattr(geom, "is_empty") and geom.is_empty): + continue + + f = QgsFeature(fields) + + # attributes in declared order with type cleanup + attrs = [] + for col in non_geom_cols: + val = row[col] + # numpy scalar โ†’ python scalar + if isinstance(val, (np.generic,)): + try: + val = val.item() + except Exception: + pass + # pandas Timestamp โ†’ QDateTime (if column is datetime) + if pd.api.types.is_datetime64_any_dtype(gdf[col].dtype): + if pd.isna(val): + val = None + else: + val = QDateTime(val.to_pydatetime()) + attrs.append(val) + f.setAttributes(attrs) + + # geometry (shapely โ†’ QGIS) + try: + f.setGeometry(QgsGeometry.fromWkb(geom.wkb)) + except Exception: + f.setGeometry(QgsGeometry.fromWkt(geom.wkt)) + + feats.append(f) + + if feats: + prov.addFeatures(feats) + layer.updateExtents() + + return layer # optionally: QgsProject.instance().addMapLayer(layer) From f744d83bb484218d11c3583a8154ef5c19e3f1ac Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:23:52 +0930 Subject: [PATCH 021/135] refactor: update GeoDataFrameToQgsLayer --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 81af364..b106fb3 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -91,7 +91,7 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) -def gdf_to_qgis_layer(gdf, layer_name="from_gdf"): +def GeoDataFrameToQgsLayer(gdf, layer_name="from_gdf"): """ Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. From ea67c2e2819416cb81a642f9a6312ae8a0afeee7 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:37:22 +0930 Subject: [PATCH 022/135] fix: GeoDataFrameToQgsLayer to FeatureSink --- map2loop/main/vectorLayerWrapper.py | 221 +++++++++++++++++++--------- 1 file changed, 150 insertions(+), 71 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index b106fb3..90d0625 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -91,91 +91,170 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) -def GeoDataFrameToQgsLayer(gdf, layer_name="from_gdf"): +def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ - Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). - Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. + Write a GeoPandas GeoDataFrame directly to a QGIS Processing FeatureSink. + + Parameters + ---------- + alg : QgsProcessingAlgorithm (self) + gdf : geopandas.GeoDataFrame + parameters : dict (from processAlgorithm) + context : QgsProcessingContext + output_key : str (e.g. self.OUTPUT) + feedback : QgsProcessingFeedback | None + + Returns + ------- + str : dest_id to return from processAlgorithm, e.g. { output_key: dest_id } """ + import pandas as pd + import numpy as np + from shapely.geometry import ( + Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon + ) + + from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink + ) + from qgis.PyQt.QtCore import QVariant, QDateTime + + if feedback is None: + class _Dummy: + def pushInfo(self, *a, **k): pass + def reportError(self, *a, **k): pass + def setProgress(self, *a, **k): pass + def isCanceled(self): return False + feedback = _Dummy() + + if geodataframe is None: + raise ValueError("GeoDataFrame is None") + if geodataframe.empty: + feedback.pushInfo("Input GeoDataFrame is empty; creating empty output layer.") + + # --- infer WKB type (family, Multi, Z) + def _infer_wkb(series): + base = None + any_multi = False + has_z = False + for geom in series: + if geom is None: continue + if getattr(geom, "is_empty", False): continue + # multi? + if isinstance(geom, (MultiPoint, MultiLineString, MultiPolygon)): + any_multi = True + g0 = next(iter(getattr(geom, "geoms", [])), None) + gt = getattr(g0, "geom_type", None) or None + else: + gt = getattr(geom, "geom_type", None) + + # base family + if gt in ("Point", "LineString", "Polygon"): + base = gt + # z? + try: + has_z = has_z or bool(getattr(geom, "has_z", False)) + except Exception: + pass + if base: + break + + if base is None: + # default safely to LineString if everything is empty; adjust if you prefer Point/Polygon + base = "LineString" + + fam = { + "Point": QgsWkbTypes.Point, + "LineString": QgsWkbTypes.LineString, + "Polygon": QgsWkbTypes.Polygon, + }[base] - if gdf is None or gdf.empty: - raise ValueError("GeoDataFrame is empty") - - # --- infer geometry type from first non-empty geometry - def infer_wkb(geoms): - for g in geoms: - if g is None: - continue - if hasattr(g, "is_empty") and g.is_empty: - continue - if isinstance(g, MultiPoint): return QgsWkbTypes.MultiPoint - if isinstance(g, Point): return QgsWkbTypes.Point - if isinstance(g, MultiLineString): return QgsWkbTypes.MultiLineString - if isinstance(g, LineString): return QgsWkbTypes.LineString - if isinstance(g, MultiPolygon): return QgsWkbTypes.MultiPolygon - if isinstance(g, Polygon): return QgsWkbTypes.Polygon - raise ValueError("Could not infer geometry type (all geometries empty?)") - - wkb_type = infer_wkb(gdf.geometry) - - # --- build CRS - crs_qgis = QgsCoordinateReferenceSystem() - if gdf.crs is not None: + if any_multi: + fam = QgsWkbTypes.multiType(fam) + if has_z: + fam = QgsWkbTypes.addZ(fam) + return fam + + wkb_type = _infer_wkb(geodataframe.geometry) + + # --- build CRS from gdf.crs + crs = QgsCoordinateReferenceSystem() + if geodataframe.crs is not None: try: - crs_qgis = QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()) + crs = QgsCoordinateReferenceSystem.fromWkt(geodataframe.crs.to_wkt()) except Exception: - epsg = gdf.crs.to_epsg() - if epsg: - crs_qgis = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + try: + epsg = geodataframe.crs.to_epsg() + if epsg: + crs = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + except Exception: + pass - geom_str = QgsWkbTypes.displayString(wkb_type) # e.g. "LineString" - uri = f"{geom_str}?crs={crs_qgis.authid()}" if crs_qgis.isValid() else geom_str - layer = QgsVectorLayer(uri, layer_name, "memory") - prov = layer.dataProvider() - - # --- fields: map pandas dtypes โ†’ QGIS - import numpy as np + # --- build QGIS fields from pandas dtypes fields = QgsFields() - for col in gdf.columns: - if col == gdf.geometry.name: - continue - dtype = gdf[col].dtype + non_geom_cols = [c for c in geodataframe.columns if c != geodataframe.geometry.name] + + def _qvariant_type(dtype) -> QVariant.Type: if pd.api.types.is_integer_dtype(dtype): - qtype = QVariant.Int - elif pd.api.types.is_float_dtype(dtype): - qtype = QVariant.Double - elif pd.api.types.is_bool_dtype(dtype): - qtype = QVariant.Bool - elif pd.api.types.is_datetime64_any_dtype(dtype): - qtype = QVariant.DateTime - else: - qtype = QVariant.String - fields.append(QgsField(str(col), qtype)) - prov.addAttributes(list(fields)) - layer.updateFields() - - # --- features - feats = [] - non_geom_cols = [c for c in gdf.columns if c != gdf.geometry.name] - - for _, row in gdf.iterrows(): - geom = row[gdf.geometry.name] - if geom is None or (hasattr(geom, "is_empty") and geom.is_empty): + return QVariant.Int + if pd.api.types.is_float_dtype(dtype): + return QVariant.Double + if pd.api.types.is_bool_dtype(dtype): + return QVariant.Bool + if pd.api.types.is_datetime64_any_dtype(dtype): + return QVariant.DateTime + return QVariant.String + + for col in non_geom_cols: + fields.append(QgsField(str(col), _qvariant_type(geodataframe[col].dtype))) + + # --- create sink + sink, dest_id = qgs_algorithm.parameterAsSink( + parameters, + output_key, + context, + fields, + wkb_type, + crs, + ) + if sink is None: + from qgis.core import QgsProcessingException + raise QgsProcessingException("Could not create output sink") + + # --- write features + total = len(geodataframe.index) + is_multi_sink = QgsWkbTypes.isMultiType(wkb_type) + + for i, (_, row) in enumerate(geodataframe.iterrows()): + if feedback.isCanceled(): + break + + geom = row[geodataframe.geometry.name] + if geom is None or getattr(geom, "is_empty", False): continue + # promote single โ†’ multi if needed + if is_multi_sink: + if isinstance(geom, Point): + geom = MultiPoint([geom]) + elif isinstance(geom, LineString): + geom = MultiLineString([geom]) + elif isinstance(geom, Polygon): + geom = MultiPolygon([geom]) + f = QgsFeature(fields) - # attributes in declared order with type cleanup + # attributes in declared order attrs = [] for col in non_geom_cols: val = row[col] - # numpy scalar โ†’ python scalar - if isinstance(val, (np.generic,)): + if isinstance(val, np.generic): try: val = val.item() except Exception: pass - # pandas Timestamp โ†’ QDateTime (if column is datetime) - if pd.api.types.is_datetime64_any_dtype(gdf[col].dtype): + if pd.api.types.is_datetime64_any_dtype(geodataframe[col].dtype): if pd.isna(val): val = None else: @@ -189,10 +268,10 @@ def infer_wkb(geoms): except Exception: f.setGeometry(QgsGeometry.fromWkt(geom.wkt)) - feats.append(f) + sink.addFeature(f, QgsFeatureSink.FastInsert) + + if total: + feedback.setProgress(int(100.0 * (i + 1) / total)) - if feats: - prov.addFeatures(feats) - layer.updateExtents() + return dest_id - return layer # optionally: QgsProject.instance().addMapLayer(layer) From f11d7131477b1ee28b6ae7effdec951541b3fef0 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:37:36 +0930 Subject: [PATCH 023/135] refactor: clean up imports in vectorLayerWrapper.py --- map2loop/main/vectorLayerWrapper.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 90d0625..e58b1b2 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,19 +1,19 @@ from qgis.core import ( - QgsVectorLayer, + QgsRaster, QgsFields, - QgsField, - QgsFeature, + QgsField, + QgsFeature, QgsGeometry, QgsWkbTypes, QgsCoordinateReferenceSystem, - QgsProject, - QgsRaster + QgsFeatureSink ) from qgis.PyQt.QtCore import QVariant, QDateTime from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon import pandas as pd import geopandas as gpd +import numpy as np @@ -108,17 +108,6 @@ def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, out ------- str : dest_id to return from processAlgorithm, e.g. { output_key: dest_id } """ - import pandas as pd - import numpy as np - from shapely.geometry import ( - Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon - ) - - from qgis.core import ( - QgsFields, QgsField, QgsFeature, QgsGeometry, - QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink - ) - from qgis.PyQt.QtCore import QVariant, QDateTime if feedback is None: class _Dummy: From d718b962120d91b310eef219acc9b25c8a789ce5 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:38:02 +0930 Subject: [PATCH 024/135] feature: add basal contacts extraction algorithm --- .../algorithms/extract_basal_contacts.py | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py index 11d406c..534958e 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -8,9 +8,10 @@ * * *************************************************************************** """ - +# Python imports from typing import Any, Optional +# QGIS imports from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -22,17 +23,23 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, ) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): """Processing algorithm to create basal contacts.""" - - INPUT = "INPUT" - OUTPUT = "OUTPUT" + + + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_FAULTS = 'FAULTS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + OUTPUT = "BASAL_CONTACTS" def name(self) -> str: """Return the algorithm name.""" - return "loop: basal_contacts" + return "basal_contacts" def displayName(self) -> str: """Return the algorithm display name.""" @@ -48,20 +55,37 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" + self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT, - "Geology Polygons", + self.INPUT_GEOLOGY, + "GEOLOGY", [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, "Basal Contacts", ) ) - pass def processAlgorithm( self, @@ -69,7 +93,26 @@ def processAlgorithm( context: QgsProcessingContext, feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - pass + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 0c7e7688c50f10ba74704c539070ac3984a96be6 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 14:55:57 +0930 Subject: [PATCH 025/135] refactor: add imports in sorter.py --- map2loop/processing/algorithms/sorter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/map2loop/processing/algorithms/sorter.py b/map2loop/processing/algorithms/sorter.py index 999fcad..a159855 100644 --- a/map2loop/processing/algorithms/sorter.py +++ b/map2loop/processing/algorithms/sorter.py @@ -3,7 +3,10 @@ from qgis import processing from qgis.core import ( QgsFeatureSink, - QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, QgsProcessing, QgsProcessingAlgorithm, QgsProcessingContext, @@ -13,6 +16,7 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsVectorLayer, + QgsWkbTypes ) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ From aeed68b750cbb46634a646f0cf67ae56d16d43d3 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:18:44 +0930 Subject: [PATCH 026/135] feature: implement sampler algorithm --- map2loop/processing/algorithms/sampler.py | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 map2loop/processing/algorithms/sampler.py diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py new file mode 100644 index 0000000..5962a2a --- /dev/null +++ b/map2loop/processing/algorithms/sampler.py @@ -0,0 +1,149 @@ +""" +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" +# Python imports +from typing import Any, Optional + +# QGIS imports +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterNumber +) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing + + +class SamplerAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for sampling.""" + + INPUT_SAMPLER_TYPE = 'SAMPLER_TYPE' + INPUT_DTM = 'DTM' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SPATIAL_DATA = 'SPATIAL_DATA' + INPUT_DECIMATION = 'DECIMATION' + INPUT_SPACING = 'SPACING' + + OUTPUT = "SAMPLED_CONTACTS" + + def name(self) -> str: + """Return the algorithm name.""" + return "sampler" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Sampler" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + + self.addParameter( + QgsProcessingParameterString( + self.INPUT_SAMPLER_TYPE, + "SAMPLER_TYPE", + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_DTM, + "DTM", + [QgsProcessing.TypeVectorRaster], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_SPATIAL_DATA, + "SPATIAL_DATA", + [QgsProcessing.TypeVectorAnyGeometry], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_DECIMATION, + "DECIMATION", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_SPACING, + "SPACING", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Sampled Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) + sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + + # Convert geology layers to GeoDataFrames + geology = qgsLayerToGeoDataFrame(geology) + spatial_data = qgsLayerToGeoDataFrame(spatial_data) + + if sampler_type == "SamplerDecimator": + feedback.pushInfo("Sampling...") + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + + samples = qgs + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file From 78f151123c69a48df9216dd21eb28c77d675638e Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:19:05 +0930 Subject: [PATCH 027/135] feature: add thickness calculator algorithms --- .../algorithms/thickness_calculator.py | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 map2loop/processing/algorithms/thickness_calculator.py diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py new file mode 100644 index 0000000..9b790ce --- /dev/null +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -0,0 +1,318 @@ +""" +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +""" +# Python imports +from typing import Any, Optional + +# QGIS imports +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, +) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint + + +class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_DTM = 'DTM' + INPUT_BOUNDING_BOX = 'BOUNDING_BOX' + INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() + + + +class InterpolatedStructureAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() + + + +class StructuralPointAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file From 40694065ebf7a0d40a6312f2e2f45aa1742c67a1 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:25:55 +0930 Subject: [PATCH 028/135] fix: correct return value --- map2loop/processing/algorithms/sampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index 5962a2a..be6b134 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -142,8 +142,8 @@ def processAlgorithm( samples = sampler.sample(spatial_data) samples = qgs - return {self.OUTPUT: basal_contacts} + return {self.OUTPUT: samples} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # SamplerAlgorithm() \ No newline at end of file From f67be330d9a63cb7543ab7e9b4e58e2e252e3cfa Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:27:31 +0930 Subject: [PATCH 029/135] feature: add support SamplerSpacing --- map2loop/processing/algorithms/sampler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index be6b134..b9a182b 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -137,11 +137,16 @@ def processAlgorithm( spatial_data = qgsLayerToGeoDataFrame(spatial_data) if sampler_type == "SamplerDecimator": - feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - samples = qgs + feedback.pushInfo("Sampling...") + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + if sampler_type == "SamplerSpacing": + feedback.pushInfo("Sampling...") + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + + #TODO: convert sample to qgis layer + # samples = qgs return {self.OUTPUT: samples} def createInstance(self) -> QgsProcessingAlgorithm: From 3c465534b83e1d61789e479d49850484e66b4ca6 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:28:40 +0930 Subject: [PATCH 030/135] fix: update sampler type strings for consistency --- map2loop/processing/algorithms/sampler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index b9a182b..1011cf6 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -136,11 +136,12 @@ def processAlgorithm( geology = qgsLayerToGeoDataFrame(geology) spatial_data = qgsLayerToGeoDataFrame(spatial_data) - if sampler_type == "SamplerDecimator": + if sampler_type == "decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) - if sampler_type == "SamplerSpacing": + + if sampler_type == "spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) From 7e76267655033124d0434498d385306f4145ccb4 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Tue, 26 Aug 2025 11:47:41 +0930 Subject: [PATCH 031/135] fix: convert dataframe to qgis layer --- map2loop/processing/algorithms/sampler.py | 48 ++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index 1011cf6..3feb3e6 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -10,9 +10,9 @@ """ # Python imports from typing import Any, Optional +from qgis.PyQt.QtCore import QMetaType # QGIS imports -from qgis import processing from qgis.core import ( QgsFeatureSink, QgsProcessing, @@ -23,10 +23,15 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterString, - QgsProcessingParameterNumber + QgsProcessingParameterNumber, + QgsField, + QgsFeature, + QgsGeometry, + QgsPointXY, + QgsVectorLayer ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing @@ -145,10 +150,41 @@ def processAlgorithm( feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) + - #TODO: convert sample to qgis layer - # samples = qgs - return {self.OUTPUT: samples} + # create layer + vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") + provider = vector_layer.dataProvider() + + # add fields + provider.addAttributes([QgsField("ID", QMetaType.Type.QString), + QgsField("X", QMetaType.Type.Float), + QgsField("Y", QMetaType.Type.Float), + QgsField("Z", QMetaType.Type.Float), + QgsField("featureId", QMetaType.Type.QString) + ]) + vector_layer.updateFields() # tell the vector layer to fetch changes from the provider + + # add a feature + for i in range(len(samples)): + feature = QgsFeature() + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) + feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) + provider.addFeatures([feature]) + + # update layer's extent when new features have been added + # because change of extent in provider is not propagated to the layer + vector_layer.updateExtents() + # --- create sink + sink, dest_id = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + vector_layer.fields(), + QgsGeometry.Type.Point, + spatial_data.crs, + ) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From c42a4ddfc86a9adb1191f9160de25d884193c502 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 10:36:57 +0930 Subject: [PATCH 032/135] feature: add dataframe to point sink conversion --- map2loop/main/vectorLayerWrapper.py | 189 +++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index e58b1b2..7069c88 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,3 +1,5 @@ +# PyQGIS / PyQt imports + from qgis.core import ( QgsRaster, QgsFields, @@ -6,8 +8,10 @@ QgsGeometry, QgsWkbTypes, QgsCoordinateReferenceSystem, - QgsFeatureSink + QgsFeatureSink, + QgsProcessingException ) + from qgis.PyQt.QtCore import QVariant, QDateTime from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon @@ -208,7 +212,6 @@ def _qvariant_type(dtype) -> QVariant.Type: crs, ) if sink is None: - from qgis.core import QgsProcessingException raise QgsProcessingException("Could not create output sink") # --- write features @@ -264,3 +267,185 @@ def _qvariant_type(dtype) -> QVariant.Type: return dest_id + +# ---------- helpers ---------- + +def _qvariant_type_from_dtype(dtype) -> QVariant.Type: + """Map a pandas dtype to a QVariant type.""" + import numpy as np + if np.issubdtype(dtype, np.integer): + # prefer 64-bit when detected + try: + return QVariant.LongLong + except AttributeError: + return QVariant.Int + if np.issubdtype(dtype, np.floating): + return QVariant.Double + if np.issubdtype(dtype, np.bool_): + return QVariant.Bool + # datetimes + try: + import pandas as pd + if pd.api.types.is_datetime64_any_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_datetime64_ns_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_datetime64_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_timedelta64_dtype(dtype): + # store as string "HH:MM:SS" fallback + return QVariant.String + except Exception: + pass + # default to string + return QVariant.String + + +def _fields_from_dataframe(df, drop_cols=None) -> QgsFields: + """Build QgsFields from DataFrame dtypes.""" + drop_cols = set(drop_cols or []) + fields = QgsFields() + for name, dtype in df.dtypes.items(): + if name in drop_cols: + continue + vtype = _qvariant_type_from_dtype(dtype) + fields.append(QgsField(name, vtype)) + return fields + + +# ---------- main function you'll call inside processAlgorithm ---------- + +def dataframe_to_point_sink( + df, + x_col: str, + y_col: str, + *, + crs: QgsCoordinateReferenceSystem, + algorithm, # `self` inside a QgsProcessingAlgorithm + parameters: dict, + context, + feedback, + sink_param_name: str = "OUTPUT", + z_col: str = None, + m_col: str = None, + include_coords_in_attrs: bool = False, +): + """ + Write a pandas DataFrame to a point feature sink (QgsProcessingParameterFeatureSink). + + Params + ------ + df : pandas.DataFrame Data with coordinate columns. + x_col, y_col : str Column names for X/Easting/Longitude and Y/Northing/Latitude. + crs : QgsCoordinateReferenceSystem CRS of the coordinates (e.g., QgsCoordinateReferenceSystem('EPSG:4326')). + algorithm : QgsProcessingAlgorithm Use `self` from inside processAlgorithm. + parameters, context, feedback Standard Processing plumbing. + sink_param_name : str Name of your sink output parameter (default "OUTPUT"). + z_col, m_col : str | None Optional Z and M columns for 3D/M points. + include_coords_in_attrs : bool If False, x/y/z/m are not written as attributes. + + Returns + ------- + (sink, sink_id) The created sink and its ID. Also returns feature count via feedback. + """ + import pandas as pd + if not isinstance(df, pd.DataFrame): + raise TypeError("df must be a pandas.DataFrame") + + # Make a working copy; optionally drop coordinate columns from attributes + attr_df = df.copy() + drop_cols = [] + for col in [x_col, y_col, z_col, m_col]: + if col and not include_coords_in_attrs: + drop_cols.append(col) + + fields = _fields_from_dataframe(attr_df, drop_cols=drop_cols) + + # Geometry type (2D/3D/M) + has_z = z_col is not None and z_col in df.columns + has_m = m_col is not None and m_col in df.columns + if has_z and has_m: + wkb = QgsWkbTypes.PointZM + elif has_z: + wkb = QgsWkbTypes.PointZ + elif has_m: + wkb = QgsWkbTypes.PointM + else: + wkb = QgsWkbTypes.Point + + # Create the sink + sink, sink_id = algorithm.parameterAsSink( + parameters, + sink_param_name, + context, + fields, + wkb, + crs + ) + if sink is None: + raise QgsProcessingException("Could not create feature sink. Check output parameter and inputs.") + + total = len(df) + feedback.pushInfo(f"Writing {total} featuresโ€ฆ") + + # Precompute attribute column order + attr_columns = [f.name() for f in fields] + + # Iterate rows and write features + for i, (idx, row) in enumerate(df.iterrows(), start=1): + if feedback.isCanceled(): + break + + # Build point geometry + x = row[x_col] + y = row[y_col] + + # skip rows with missing coords + if pd.isna(x) or pd.isna(y): + continue + + if has_z and not pd.isna(row[z_col]) and has_m and not pd.isna(row[m_col]): + pt = QgsPoint(float(x), float(y), float(row[z_col]), float(row[m_col])) + elif has_z and not pd.isna(row[z_col]): + pt = QgsPoint(float(x), float(y), float(row[z_col])) + elif has_m and not pd.isna(row[m_col]): + # PointM constructor: setZValue not needed; M is the 4th ordinate + pt = QgsPoint(float(x), float(y)) + pt.setM(float(row[m_col])) + else: + pt = QgsPointXY(float(x), float(y)) + + feat = QgsFeature(fields) + feat.setGeometry(QgsGeometry.fromPoint(pt) if isinstance(pt, QgsPoint) else QgsGeometry.fromPointXY(pt)) + + # Attributes in the same order as fields + attrs = [] + for col in attr_columns: + val = row[col] if col in row else None + # Pandas NaN -> None + if pd.isna(val): + val = None + # Convert numpy types to Python scalars to avoid QVariant issues + try: + import numpy as np + if isinstance(val, (np.generic,)): + val = val.item() + except Exception: + pass + # Convert pandas Timestamp to Python datetime + if hasattr(val, "to_pydatetime"): + try: + val = val.to_pydatetime() + except Exception: + val = str(val) + attrs.append(val) + feat.setAttributes(attrs) + + sink.addFeature(feat, QgsFeature.FastInsert) + + if i % 1000 == 0: + feedback.setProgress(int(100.0 * i / max(total, 1))) + + feedback.pushInfo("Done.") + feedback.setProgress(100) + return sink, sink_id From 54a0bc98eea26f2b22632e4e8f8c03c66798c360 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 10:37:27 +0930 Subject: [PATCH 033/135] fix: add input parameters --- .../algorithms/thickness_calculator.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py index 9b790ce..24b684a 100644 --- a/map2loop/processing/algorithms/thickness_calculator.py +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -31,7 +31,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): """Processing algorithm for thickness calculations.""" - + INPUT_THICKNESS_CALCULATOR_TYPE = 'THICKNESS_CALCULATOR_TYPE' INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' @@ -62,7 +62,33 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" + + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_THICKNESS_CALCULATOR_TYPE, + "Thickness Calculator Type", + [QgsProcessing.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_BASAL_CONTACTS, + "Basal Contacts", + [QgsProcessing.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_DTM, + "DTM", + [QgsProcessing.TypeVectorRaster], + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, @@ -90,7 +116,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Basal Contacts", + "Thickness", ) ) From df54c1b16304bb386dc2ddb988fa4951a8e58f7f Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 11:58:30 +0930 Subject: [PATCH 034/135] refactor: rename to dataframeToQgsLayer --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7069c88..d68ed3d 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -315,7 +315,7 @@ def _fields_from_dataframe(df, drop_cols=None) -> QgsFields: # ---------- main function you'll call inside processAlgorithm ---------- -def dataframe_to_point_sink( +def dataframeToQgsLayer( df, x_col: str, y_col: str, From 703a89c6baf2561d0f4eee6406c43905b8ba4bed Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 11:58:42 +0930 Subject: [PATCH 035/135] refactor: update thickness calculator parameters and processing logic --- .../algorithms/thickness_calculator.py | 308 ++++++------------ 1 file changed, 93 insertions(+), 215 deletions(-) diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py index 24b684a..f56b09c 100644 --- a/map2loop/processing/algorithms/thickness_calculator.py +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -22,9 +22,13 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterEnum, + QgsProcessingParameterNumber, + QgsProcessingParameterField, + QgsProcessingParameterMatrix ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -62,25 +66,15 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" - - self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterEnum( self.INPUT_THICKNESS_CALCULATOR_TYPE, "Thickness Calculator Type", - [QgsProcessing.TypeVectorPoint], + options=['InterpolatedStructure','StructuralPoint'], + allowMultiple=False, ) ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_BASAL_CONTACTS, - "Basal Contacts", - [QgsProcessing.TypeVectorPoint], - ) - ) - self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_DTM, @@ -88,103 +82,36 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorRaster], ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_GEOLOGY, - "GEOLOGY", - [QgsProcessing.TypeVectorPolygon], + QgsProcessingParameterEnum( + self.INPUT_BOUNDING_BOX, + "Bounding Box", + options=['minx','miny','maxx','maxy'], + allowMultiple=True, ) ) + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_MAX_LINE_LENGTH, + "Max Line Length", + minValue=0, + defaultValue=1000 + ) + ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", + self.INPUT_UNITS, + "Units", [QgsProcessing.TypeVectorLine], - optional=True, ) ) - self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", + self.INPUT_BASAL_CONTACTS, + "Basal Contacts", [QgsProcessing.TypeVectorLine], ) ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Thickness", - ) - ) - - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( - self, - contact_extractor.basal_contacts, - parameters=parameters, - context=context, - feedback=feedback, - ) - return {self.OUTPUT: basal_contacts} - - def createInstance(self) -> QgsProcessingAlgorithm: - """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() - - - -class InterpolatedStructureAlgorithm(QgsProcessingAlgorithm): - """Processing algorithm for thickness calculations.""" - - - INPUT_UNITS = 'UNITS' - INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' - INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' - INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' - INPUT_GEOLOGY = 'GEOLOGY' - INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' - - OUTPUT = "THICKNESS" - - def name(self) -> str: - """Return the algorithm name.""" - return "thickness_calculator" - - def displayName(self) -> str: - """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" - - def group(self) -> str: - """Return the algorithm group name.""" - return "Loop3d" - - def groupId(self) -> str: - """Return the algorithm group ID.""" - return "loop3d" - - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - """Initialize the algorithm parameters.""" - self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, @@ -193,122 +120,32 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", - [QgsProcessing.TypeVectorLine], - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Basal Contacts", - ) - ) - - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( - self, - contact_extractor.basal_contacts, - parameters=parameters, - context=context, - feedback=feedback, - ) - return {self.OUTPUT: basal_contacts} - - def createInstance(self) -> QgsProcessingAlgorithm: - """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() - - - -class StructuralPointAlgorithm(QgsProcessingAlgorithm): - """Processing algorithm for thickness calculations.""" - - - INPUT_UNITS = 'UNITS' - INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' - INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' - INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' - INPUT_GEOLOGY = 'GEOLOGY' - INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' - - OUTPUT = "THICKNESS" - - def name(self) -> str: - """Return the algorithm name.""" - return "thickness_calculator" - - def displayName(self) -> str: - """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" - - def group(self) -> str: - """Return the algorithm group name.""" - return "Loop3d" - - def groupId(self) -> str: - """Return the algorithm group ID.""" - return "loop3d" - - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - """Initialize the algorithm parameters.""" - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_GEOLOGY, - "GEOLOGY", - [QgsProcessing.TypeVectorPolygon], + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=[] ) ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", - [QgsProcessing.TypeVectorLine], - optional=True, + self.INPUT_SAMPLED_CONTACTS, + "SAMPLED_CONTACTS", + [QgsProcessing.TypeVectorPoint], ) ) - self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + self.INPUT_STRUCTURE_DATA, + "STRUCTURE_DATA", + [QgsProcessing.TypeVectorPoint], ) ) - self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Basal Contacts", + "Thickness", ) ) @@ -319,25 +156,66 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( + feedback.pushInfo("Initialising Thickness Calculation Algorithm...") + thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) + units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) + geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + + # convert layers to dataframe or geodataframe + geology_data = qgsLayerToGeoDataFrame(geology_data) + units = qgsLayerToDataFrame(units) + basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) + structure_data = qgsLayerToDataFrame(structure_data) + sampled_contacts = qgsLayerToDataFrame(sampled_contacts) + + feedback.pushInfo("Calculating unit thicknesses...") + + if thickness_type == "InterpolatedStructure": + thickness_calculator = InterpolatedStructure( + dtm_data=dtm_data, + bounding_box=bounding_box, + ) + thickness_calculator.compute( + units, + stratigraphic_order, + basal_contacts, + structure_data, + geology_data, + sampled_contacts + ) + + if thickness_type == "StructuralPoint": + thickness_calculator = StructuralPoint( + dtm_data=dtm_data, + bounding_box=bounding_box, + max_line_length=max_line_length, + ) + thickness_calculator.compute( + units, + stratigraphic_order, + basal_contacts, + structure_data, + geology_data, + sampled_contacts + ) + + #TODO: convert thicknesses dataframe to qgs layer + thicknesses = dataframeToQgsLayer( self, - contact_extractor.basal_contacts, + # contact_extractor.basal_contacts, parameters=parameters, context=context, feedback=feedback, ) - return {self.OUTPUT: basal_contacts} + + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 8bb0c99dadd8298bb715da0a9a263ea65aa03cf5 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 27 Aug 2025 15:10:03 +0800 Subject: [PATCH 036/135] update BasalContactsAlgorithm --- .../algorithms/extract_basal_contacts.py | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py index 534958e..ba90572 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -22,10 +22,12 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterField ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop import ContactExtractor +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): @@ -63,6 +65,16 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='unitname' + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -71,12 +83,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterString( self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + "Stratigraphic Column Names", + defaultValue="", + optional=True ) ) @@ -94,22 +107,31 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) + + if strati_column and strati_column.strip(): + strati_column = [unit.strip() for unit in strati_column.split(',')] + + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) faults = qgsLayerToGeoDataFrame(faults) if faults else None + if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: + geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) + feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - + contact_extractor = ContactExtractor(geology, faults) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, parameters=parameters, context=context, + output_key=self.OUTPUT, feedback=feedback, ) return {self.OUTPUT: basal_contacts} From fcdf4a2934176cd5f7ad3d856361a432659f629c Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 28 Aug 2025 09:29:44 +0800 Subject: [PATCH 037/135] fix import in vectorLayerWrapper.py --- map2loop/main/vectorLayerWrapper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index d68ed3d..7502ab4 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -9,7 +9,9 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint, + QgsPointXY, ) from qgis.PyQt.QtCore import QVariant, QDateTime From 7aaa076306b684fadb65125d93809f1d4482a416 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 28 Aug 2025 09:36:39 +0800 Subject: [PATCH 038/135] rename unused loop idx in vectorLayerWrapper --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7502ab4..d89f6ff 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -394,7 +394,7 @@ def dataframeToQgsLayer( attr_columns = [f.name() for f in fields] # Iterate rows and write features - for i, (idx, row) in enumerate(df.iterrows(), start=1): + for i, (_idx, row) in enumerate(df.iterrows(), start=1): if feedback.isCanceled(): break From d0d9e7790f57ce59441470f2f351f62489d28b00 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 29 Aug 2025 12:42:40 +0800 Subject: [PATCH 039/135] rename directory to m2l to avoid import conflicts --- docs/conf.py | 2 +- {map2loop => m2l}/__about__.py | 0 {map2loop => m2l}/__init__.py | 0 {map2loop => m2l}/gui/__init__.py | 0 {map2loop => m2l}/gui/dlg_settings.py | 6 +++--- {map2loop => m2l}/gui/dlg_settings.ui | 0 {map2loop => m2l}/main/vectorLayerWrapper.py | 0 {map2loop => m2l}/metadata.txt | 0 {map2loop => m2l}/plugin_main.py | 8 ++++---- {map2loop => m2l}/processing/__init__.py | 0 {map2loop => m2l}/processing/algorithms/__init__.py | 0 .../processing/algorithms/extract_basal_contacts.py | 2 +- {map2loop => m2l}/processing/algorithms/sampler.py | 0 {map2loop => m2l}/processing/algorithms/sorter.py | 2 +- .../processing/algorithms/thickness_calculator.py | 0 {map2loop => m2l}/processing/provider.py | 2 +- .../resources/i18n/plugin_map2loop_en.ts | 0 .../resources/i18n/plugin_translation.pro | 0 {map2loop => m2l}/resources/images/default_icon.png | Bin {map2loop => m2l}/toolbelt/__init__.py | 0 {map2loop => m2l}/toolbelt/env_var_parser.py | 0 {map2loop => m2l}/toolbelt/log_handler.py | 4 ++-- {map2loop => m2l}/toolbelt/preferences.py | 6 +++--- tests/qgis/test_env_var_parser.py | 2 +- tests/qgis/test_plg_preferences.py | 4 ++-- tests/qgis/test_processing.py | 2 +- tests/unit/test_plg_metadata.py | 2 +- 27 files changed, 21 insertions(+), 21 deletions(-) rename {map2loop => m2l}/__about__.py (100%) rename {map2loop => m2l}/__init__.py (100%) rename {map2loop => m2l}/gui/__init__.py (100%) rename {map2loop => m2l}/gui/dlg_settings.py (96%) rename {map2loop => m2l}/gui/dlg_settings.ui (100%) rename {map2loop => m2l}/main/vectorLayerWrapper.py (100%) rename {map2loop => m2l}/metadata.txt (100%) rename {map2loop => m2l}/plugin_main.py (96%) rename {map2loop => m2l}/processing/__init__.py (100%) rename {map2loop => m2l}/processing/algorithms/__init__.py (100%) rename {map2loop => m2l}/processing/algorithms/extract_basal_contacts.py (98%) rename {map2loop => m2l}/processing/algorithms/sampler.py (100%) rename {map2loop => m2l}/processing/algorithms/sorter.py (98%) rename {map2loop => m2l}/processing/algorithms/thickness_calculator.py (100%) rename {map2loop => m2l}/processing/provider.py (98%) rename {map2loop => m2l}/resources/i18n/plugin_map2loop_en.ts (100%) rename {map2loop => m2l}/resources/i18n/plugin_translation.pro (100%) rename {map2loop => m2l}/resources/images/default_icon.png (100%) rename {map2loop => m2l}/toolbelt/__init__.py (100%) rename {map2loop => m2l}/toolbelt/env_var_parser.py (100%) rename {map2loop => m2l}/toolbelt/log_handler.py (98%) rename {map2loop => m2l}/toolbelt/preferences.py (97%) diff --git a/docs/conf.py b/docs/conf.py index 6d7892e..d7cc5df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ import sphinx_rtd_theme # noqa: F401 theme of Read the Docs # Package -from map2loop import __about__ +from m2l import __about__ # -- Build environment ----------------------------------------------------- on_rtd = environ.get("READTHEDOCS", None) == "True" diff --git a/map2loop/__about__.py b/m2l/__about__.py similarity index 100% rename from map2loop/__about__.py rename to m2l/__about__.py diff --git a/map2loop/__init__.py b/m2l/__init__.py similarity index 100% rename from map2loop/__init__.py rename to m2l/__init__.py diff --git a/map2loop/gui/__init__.py b/m2l/gui/__init__.py similarity index 100% rename from map2loop/gui/__init__.py rename to m2l/gui/__init__.py diff --git a/map2loop/gui/dlg_settings.py b/m2l/gui/dlg_settings.py similarity index 96% rename from map2loop/gui/dlg_settings.py rename to m2l/gui/dlg_settings.py index bf6c8b1..351a008 100644 --- a/map2loop/gui/dlg_settings.py +++ b/m2l/gui/dlg_settings.py @@ -18,15 +18,15 @@ from qgis.PyQt.QtGui import QDesktopServices, QIcon # project -from map2loop.__about__ import ( +from m2l.__about__ import ( __icon_path__, __title__, __uri_homepage__, __uri_tracker__, __version__, ) -from map2loop.toolbelt import PlgLogger, PlgOptionsManager -from map2loop.toolbelt.preferences import PlgSettingsStructure +from m2l.toolbelt import PlgLogger, PlgOptionsManager +from m2l.toolbelt.preferences import PlgSettingsStructure # ############################################################################ # ########## Globals ############### diff --git a/map2loop/gui/dlg_settings.ui b/m2l/gui/dlg_settings.ui similarity index 100% rename from map2loop/gui/dlg_settings.ui rename to m2l/gui/dlg_settings.ui diff --git a/map2loop/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py similarity index 100% rename from map2loop/main/vectorLayerWrapper.py rename to m2l/main/vectorLayerWrapper.py diff --git a/map2loop/metadata.txt b/m2l/metadata.txt similarity index 100% rename from map2loop/metadata.txt rename to m2l/metadata.txt diff --git a/map2loop/plugin_main.py b/m2l/plugin_main.py similarity index 96% rename from map2loop/plugin_main.py rename to m2l/plugin_main.py index f803c9a..a286f1e 100644 --- a/map2loop/plugin_main.py +++ b/m2l/plugin_main.py @@ -15,17 +15,17 @@ from qgis.PyQt.QtWidgets import QAction # project -from map2loop.__about__ import ( +from m2l.__about__ import ( DIR_PLUGIN_ROOT, __icon_path__, __title__, __uri_homepage__, ) -from map2loop.gui.dlg_settings import PlgOptionsFactory -from map2loop.processing import ( +from m2l.gui.dlg_settings import PlgOptionsFactory +from m2l.processing import ( Map2LoopProvider, ) -from map2loop.toolbelt import PlgLogger +from m2l.toolbelt import PlgLogger # ############################################################################ # ########## Classes ############### diff --git a/map2loop/processing/__init__.py b/m2l/processing/__init__.py similarity index 100% rename from map2loop/processing/__init__.py rename to m2l/processing/__init__.py diff --git a/map2loop/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py similarity index 100% rename from map2loop/processing/algorithms/__init__.py rename to m2l/processing/algorithms/__init__.py diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py similarity index 98% rename from map2loop/processing/algorithms/extract_basal_contacts.py rename to m2l/processing/algorithms/extract_basal_contacts.py index 534958e..f329229 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -25,7 +25,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop import ContactExtractor +from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): diff --git a/map2loop/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py similarity index 100% rename from map2loop/processing/algorithms/sampler.py rename to m2l/processing/algorithms/sampler.py diff --git a/map2loop/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py similarity index 98% rename from map2loop/processing/algorithms/sorter.py rename to m2l/processing/algorithms/sorter.py index a159855..5c4340a 100644 --- a/map2loop/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from map2loop.map2loop.mapdata import MapData # adjust import path if needed + from m2l.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py similarity index 100% rename from map2loop/processing/algorithms/thickness_calculator.py rename to m2l/processing/algorithms/thickness_calculator.py diff --git a/map2loop/processing/provider.py b/m2l/processing/provider.py similarity index 98% rename from map2loop/processing/provider.py rename to m2l/processing/provider.py index ba879bb..4095979 100644 --- a/map2loop/processing/provider.py +++ b/m2l/processing/provider.py @@ -10,7 +10,7 @@ from qgis.PyQt.QtGui import QIcon # project -from map2loop.__about__ import ( +from m2l.__about__ import ( __icon_path__, __title__, __version__, diff --git a/map2loop/resources/i18n/plugin_map2loop_en.ts b/m2l/resources/i18n/plugin_map2loop_en.ts similarity index 100% rename from map2loop/resources/i18n/plugin_map2loop_en.ts rename to m2l/resources/i18n/plugin_map2loop_en.ts diff --git a/map2loop/resources/i18n/plugin_translation.pro b/m2l/resources/i18n/plugin_translation.pro similarity index 100% rename from map2loop/resources/i18n/plugin_translation.pro rename to m2l/resources/i18n/plugin_translation.pro diff --git a/map2loop/resources/images/default_icon.png b/m2l/resources/images/default_icon.png similarity index 100% rename from map2loop/resources/images/default_icon.png rename to m2l/resources/images/default_icon.png diff --git a/map2loop/toolbelt/__init__.py b/m2l/toolbelt/__init__.py similarity index 100% rename from map2loop/toolbelt/__init__.py rename to m2l/toolbelt/__init__.py diff --git a/map2loop/toolbelt/env_var_parser.py b/m2l/toolbelt/env_var_parser.py similarity index 100% rename from map2loop/toolbelt/env_var_parser.py rename to m2l/toolbelt/env_var_parser.py diff --git a/map2loop/toolbelt/log_handler.py b/m2l/toolbelt/log_handler.py similarity index 98% rename from map2loop/toolbelt/log_handler.py rename to m2l/toolbelt/log_handler.py index 222fa3f..284365e 100644 --- a/map2loop/toolbelt/log_handler.py +++ b/m2l/toolbelt/log_handler.py @@ -12,8 +12,8 @@ from qgis.utils import iface # project package -import map2loop.toolbelt.preferences as plg_prefs_hdlr -from map2loop.__about__ import __title__ +import m2l.toolbelt.preferences as plg_prefs_hdlr +from m2l.__about__ import __title__ # ############################################################################ # ########## Classes ############### diff --git a/map2loop/toolbelt/preferences.py b/m2l/toolbelt/preferences.py similarity index 97% rename from map2loop/toolbelt/preferences.py rename to m2l/toolbelt/preferences.py index 85986b1..8742313 100644 --- a/map2loop/toolbelt/preferences.py +++ b/m2l/toolbelt/preferences.py @@ -11,9 +11,9 @@ from qgis.core import QgsSettings # package -import map2loop.toolbelt.log_handler as log_hdlr -from map2loop.__about__ import __title__, __version__ -from map2loop.toolbelt.env_var_parser import EnvVarParser +import m2l.toolbelt.log_handler as log_hdlr +from m2l.__about__ import __title__, __version__ +from m2l.toolbelt.env_var_parser import EnvVarParser # ############################################################################ # ########## Classes ############### diff --git a/tests/qgis/test_env_var_parser.py b/tests/qgis/test_env_var_parser.py index ce4b73d..b968008 100644 --- a/tests/qgis/test_env_var_parser.py +++ b/tests/qgis/test_env_var_parser.py @@ -1,7 +1,7 @@ import os import unittest -from map2loop.toolbelt.env_var_parser import EnvVarParser +from m2l.toolbelt.env_var_parser import EnvVarParser class TestEnvVarParser(unittest.TestCase): diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py index 23daa19..8ac07a3 100644 --- a/tests/qgis/test_plg_preferences.py +++ b/tests/qgis/test_plg_preferences.py @@ -18,8 +18,8 @@ from qgis.testing import unittest # project -from map2loop.__about__ import __version__ -from map2loop.toolbelt.preferences import ( +from m2l.__about__ import __version__ +from m2l.toolbelt.preferences import ( PREFIX_ENV_VARIABLE, PlgOptionsManager, PlgSettingsStructure, diff --git a/tests/qgis/test_processing.py b/tests/qgis/test_processing.py index b52b3de..6f5c719 100644 --- a/tests/qgis/test_processing.py +++ b/tests/qgis/test_processing.py @@ -15,7 +15,7 @@ from qgis.core import QgsApplication from qgis.testing import start_app, unittest -from map2loop.processing.provider import ( +from m2l.processing.provider import ( Map2LoopProvider, ) diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py index 2baf6ed..e6373a6 100644 --- a/tests/unit/test_plg_metadata.py +++ b/tests/unit/test_plg_metadata.py @@ -18,7 +18,7 @@ from packaging.version import parse # project -from map2loop import __about__ +from m2l import __about__ # ############################################################################ # ########## Classes ############# From 34f3d25289fc12d44b5752832ef27e90355b7309 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 29 Aug 2025 13:40:31 +0800 Subject: [PATCH 040/135] fix linter.yml --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index f87c4e5..a725d82 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -13,7 +13,7 @@ on: workflow_dispatch: env: - PROJECT_FOLDER: "map2loop" + PROJECT_FOLDER: "m2l" PYTHON_VERSION: 3.9 permissions: contents: write From b72fbf4ae519cab812818beb7326791f625aed18 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:01:43 +0930 Subject: [PATCH 041/135] fix: add QgsPoint import --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d68ed3d..f981737 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -9,7 +9,8 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint ) from qgis.PyQt.QtCore import QVariant, QDateTime From 04d3937dadab0945f11ccc45700b25ee7c0395c7 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:04:46 +0930 Subject: [PATCH 042/135] fix: add QgsPointXY import --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index f981737..98597bb 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -10,7 +10,8 @@ QgsCoordinateReferenceSystem, QgsFeatureSink, QgsProcessingException, - QgsPoint + QgsPoint, + QgsPointXY ) from qgis.PyQt.QtCore import QVariant, QDateTime From d4a1d7df88c7f0bc4510a7065eebbd556308d6d1 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:57:36 +0930 Subject: [PATCH 043/135] fix: enforce str type --- m2l/processing/algorithms/extract_basal_contacts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 83f42d0..59793f0 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -28,8 +28,6 @@ # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer from map2loop.contact_extractor import ContactExtractor -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): @@ -112,15 +110,16 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) - + if strati_column and strati_column.strip(): strati_column = [unit.strip() for unit in strati_column.split(',')] unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) + geology['UNITNAME'] = geology['UNITNAME'].astype(str) + faults = qgsLayerToGeoDataFrame(faults) if faults else None - if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) From cf8321e5fcc2adac476d09e606c387e308b8784a Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:09:00 +0930 Subject: [PATCH 044/135] refactor: handling of string fields --- m2l/main/vectorLayerWrapper.py | 64 +++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d89f6ff..9be3761 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -23,22 +23,68 @@ +# def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: +# if layer is None: +# return None +# features = layer.getFeatures() +# fields = layer.fields() +# data = {'geometry': []} +# for f in fields: +# data[f.name()] = [] +# for feature in features: +# geom = feature.geometry() +# if geom.isEmpty(): +# continue +# data['geometry'].append(geom) +# for f in fields: +# data[f.name()].append(feature[f.name()]) +# return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: + """ + Convert a QgsVectorLayer to a GeoDataFrame with: + - Shapely geometries + - Pandas nullable string dtype for QGIS string fields + """ if layer is None: return None - features = layer.getFeatures() + fields = layer.fields() - data = {'geometry': []} - for f in fields: - data[f.name()] = [] - for feature in features: - geom = feature.geometry() + string_field_names = { + f.name() for f in fields + if f.type() in (QVariant.String,) # extend here if you use other text types + } + + rows = [] + for feat in layer.getFeatures(): + geom = feat.geometry() if geom.isEmpty(): continue - data['geometry'].append(geom) + + row = {"geometry": wkb_loads(geom.asWkb())} for f in fields: - data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + val = feat[f.name()] + # Normalize None in string cols to pandas.NA so StringDtype works cleanly + if f.name() in string_field_names: + row[f.name()] = pd.NA if val is None else str(val) + else: + row[f.name()] = val + rows.append(row) + + if not rows: + # Empty GeoDataFrame with correct schema & crs + gdf = gpd.GeoDataFrame(columns=["geometry"] + [f.name() for f in fields], + geometry="geometry", + crs=layer.crs().authid()) + else: + gdf = gpd.GeoDataFrame(rows, geometry="geometry", crs=layer.crs().authid()) + + # Enforce pandas' nullable string dtype on QGIS string fields + for name in string_field_names: + if name in gdf.columns: + gdf[name] = gdf[name].astype("string") + + return gdf def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From 60bd84eb3796b7d4a856fc101210045f2045f19d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:09:38 +0930 Subject: [PATCH 045/135] fix: add missing import for wkb_loads --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 9be3761..ca610fc 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -14,9 +14,10 @@ QgsPointXY, ) -from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon +from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np From 1c7266793a89df434f97ee1769ea8bd06352f65c Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:16:49 +0930 Subject: [PATCH 046/135] refactor: implementation and enforce string dtype --- m2l/main/vectorLayerWrapper.py | 67 +++++++--------------------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index ca610fc..339ae0b 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -24,70 +24,29 @@ -# def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: -# if layer is None: -# return None -# features = layer.getFeatures() -# fields = layer.fields() -# data = {'geometry': []} -# for f in fields: -# data[f.name()] = [] -# for feature in features: -# geom = feature.geometry() -# if geom.isEmpty(): -# continue -# data['geometry'].append(geom) -# for f in fields: -# data[f.name()].append(feature[f.name()]) -# return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: - """ - Convert a QgsVectorLayer to a GeoDataFrame with: - - Shapely geometries - - Pandas nullable string dtype for QGIS string fields - """ if layer is None: return None - + features = layer.getFeatures() fields = layer.fields() - string_field_names = { - f.name() for f in fields - if f.type() in (QVariant.String,) # extend here if you use other text types - } - - rows = [] - for feat in layer.getFeatures(): - geom = feat.geometry() + data = {'geometry': []} + for f in fields: + data[f.name()] = [] + for feature in features: + geom = feature.geometry() if geom.isEmpty(): continue - - row = {"geometry": wkb_loads(geom.asWkb())} + data['geometry'].append(geom) for f in fields: - val = feat[f.name()] - # Normalize None in string cols to pandas.NA so StringDtype works cleanly - if f.name() in string_field_names: - row[f.name()] = pd.NA if val is None else str(val) - else: - row[f.name()] = val - rows.append(row) - - if not rows: - # Empty GeoDataFrame with correct schema & crs - gdf = gpd.GeoDataFrame(columns=["geometry"] + [f.name() for f in fields], - geometry="geometry", - crs=layer.crs().authid()) - else: - gdf = gpd.GeoDataFrame(rows, geometry="geometry", crs=layer.crs().authid()) - - # Enforce pandas' nullable string dtype on QGIS string fields - for name in string_field_names: - if name in gdf.columns: - gdf[name] = gdf[name].astype("string") + data[f.name()].append(feature[f.name()]) + gdf = gpd.GeoDataFrame(data, crs=layer.crs().authid()) + # โœ… Convert only QGIS string fields to pandas string dtype + for f in fields: + if f.type() == QVariant.String and f.name() in gdf.columns: + gdf[f.name()] = gdf[f.name()].astype(str) return gdf - def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame samples the geometry using either points or the vertices of the lines From 3dd9aea7304ba872f77c87eadffd68151887081b Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:25:29 +0930 Subject: [PATCH 047/135] fix: QGIS string fields are converted to str --- m2l/main/vectorLayerWrapper.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 339ae0b..b02f373 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,14 +38,12 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) - gdf = gpd.GeoDataFrame(data, crs=layer.crs().authid()) - - # โœ… Convert only QGIS string fields to pandas string dtype - for f in fields: - if f.type() == QVariant.String and f.name() in gdf.columns: - gdf[f.name()] = gdf[f.name()].astype(str) - return gdf + if f.type() == QVariant.String: + # Ensure string fields are converted to str, not QVariant + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) + return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame From 86e3f7b2154f707835e5ba454adbbbc1f9afa62d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:28:23 +0930 Subject: [PATCH 048/135] fix: remove type conversion issues --- m2l/processing/algorithms/extract_basal_contacts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 59793f0..4b95ab0 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -117,7 +117,6 @@ def processAlgorithm( unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - geology['UNITNAME'] = geology['UNITNAME'].astype(str) faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: From 2c376671f14081731fc8cf1d4d8f5d99450a24ee Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:36:03 +0930 Subject: [PATCH 049/135] fix: simplify string field handling --- m2l/main/vectorLayerWrapper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index b02f373..666df27 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,11 +38,7 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - if f.type() == QVariant.String: - # Ensure string fields are converted to str, not QVariant - data[f.name()].append(str(feature[f.name()])) - else: - data[f.name()].append(feature[f.name()]) + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From 510d82f1a3003157cf00bc8b45e932643431f6ad Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:47:22 +0930 Subject: [PATCH 050/135] fix: ensure string fields are explicitly converted --- m2l/main/vectorLayerWrapper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 666df27..72ff281 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,7 +38,10 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) + if f.type() == QVariant.String: + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From c0282376fe49aad108c133a92f560ff13ea78b69 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 1 Sep 2025 13:08:11 +0800 Subject: [PATCH 051/135] sampler --- m2l/processing/algorithms/__init__.py | 1 + m2l/processing/algorithms/sampler.py | 118 +++++++++++++++----------- m2l/processing/provider.py | 3 +- 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index 618a57d..f3dd275 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1 +1,2 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3feb3e6..3a6a1c8 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -11,6 +11,8 @@ # Python imports from typing import Any, Optional from qgis.PyQt.QtCore import QMetaType +from osgeo import gdal +import pandas as pd # QGIS imports from qgis.core import ( @@ -22,17 +24,21 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterString, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterEnum, QgsProcessingParameterNumber, + QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, - QgsVectorLayer + QgsVectorLayer, + QgsWkbTypes, + QgsCoordinateReferenceSystem ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame -from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing +from map2loop.sampler import SamplerDecimator, SamplerSpacing class SamplerAlgorithm(QgsProcessingAlgorithm): @@ -68,17 +74,18 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterString( + QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", + ["Decimator", "Spacing"], + defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], ) ) @@ -104,6 +111,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + defaultValue=1, optional=True, ) ) @@ -112,6 +120,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + defaultValue=200.0, optional=True, ) ) @@ -130,60 +139,71 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) - spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) - sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsDouble(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) + sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) + sampler_type = ["Decimator", "Spacing"][sampler_type_index] # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) - spatial_data = qgsLayerToGeoDataFrame(spatial_data) + spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) + dtm_gdal = gdal.Open(dtm.source()) - if sampler_type == "decimator": + if sampler_type == "Decimator": feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) - if sampler_type == "spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") - sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - - # create layer - vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") - provider = vector_layer.dataProvider() - - # add fields - provider.addAttributes([QgsField("ID", QMetaType.Type.QString), - QgsField("X", QMetaType.Type.Float), - QgsField("Y", QMetaType.Type.Float), - QgsField("Z", QMetaType.Type.Float), - QgsField("featureId", QMetaType.Type.QString) - ]) - vector_layer.updateFields() # tell the vector layer to fetch changes from the provider - - # add a feature - for i in range(len(samples)): - feature = QgsFeature() - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) - feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) - provider.addFeatures([feature]) - - # update layer's extent when new features have been added - # because change of extent in provider is not propagated to the layer - vector_layer.updateExtents() - # --- create sink + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) + + fields = QgsFields() + fields.append(QgsField("ID", QMetaType.Type.QString)) + fields.append(QgsField("X", QMetaType.Type.Float)) + fields.append(QgsField("Y", QMetaType.Type.Float)) + fields.append(QgsField("Z", QMetaType.Type.Float)) + fields.append(QgsField("featureId", QMetaType.Type.QString)) + + crs = None + if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: + crs = QgsCoordinateReferenceSystem.fromWkt(spatial_data_gdf.crs.to_wkt()) + sink, dest_id = self.parameterAsSink( parameters, self.OUTPUT, context, - vector_layer.fields(), - QgsGeometry.Type.Point, - spatial_data.crs, - ) + fields, + QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, + crs + ) + + if samples is not None and not samples.empty: + for index, row in samples.iterrows(): + feature = QgsFeature(fields) + + # decimator has z values + if 'Z' in samples.columns and pd.notna(row.get('Z')): + wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" + feature.setGeometry(QgsGeometry.fromWkt(wkt)) + else: + #spacing has no z values + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) + + feature.setAttributes([ + str(row.get('ID', '')), + float(row.get('X', 0)), + float(row.get('Y', 0)), + float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, + str(row.get('featureId', '')) + ]) + + sink.addFeature(feature) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 4095979..b6a5a97 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,7 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm +from .algorithms import BasalContactsAlgorithm, SamplerAlgorithm # ############################################################################ # ########## Classes ############### @@ -29,6 +29,7 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) + self.addAlgorithm(SamplerAlgorithm()) pass def id(self) -> str: From bc99ec9ba2452addfcc29c3279d09253bb894387 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:01:37 +0930 Subject: [PATCH 052/135] fix: enhance contact rextraction tool --- .../algorithms/extract_basal_contacts.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 4b95ab0..947e823 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -23,7 +23,9 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterString, - QgsProcessingParameterField + QgsProcessingParameterField, + QgsProcessingParameterMatrix, + QgsSettings ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer @@ -37,6 +39,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_FAULTS = 'FAULTS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" def name(self) -> str: @@ -83,12 +86,25 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + ignore_settings = QgsSettings() + last_ignore_units = ignore_settings.value("m2l/ignore_units", "") self.addParameter( - QgsProcessingParameterString( - self.INPUT_STRATI_COLUMN, - "Stratigraphic Column Names", - defaultValue="", + QgsProcessingParameterMatrix( + self.INPUT_IGNORE_UNITS, + "Unit(s) to ignore", + headers=["Unit"], + defaultValue=last_ignore_units, optional=True ) ) @@ -107,25 +123,35 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: + feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) - - if strati_column and strati_column.strip(): - strati_column = [unit.strip() for unit in strati_column.split(',')] - + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) + + ignore_settings = QgsSettings() + ignore_settings.setValue("m2l/ignore_units", ignore_units) + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - + mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) - + feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, From af2056d283711d0fddd0f2a6364540189e8a2e9d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:09:41 +0930 Subject: [PATCH 053/135] fix: update algorithm imports and registration in provider --- m2l/processing/algorithms/__init__.py | 3 +++ m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 2 +- m2l/processing/algorithms/thickness_calculator.py | 2 +- m2l/processing/provider.py | 11 +++++++++-- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index 618a57d..e3201dc 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1 +1,4 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sorter import StratigraphySorterAlgorithm +from .thickness_calculator import ThicknessCalculatorAlgorithm +from .sampler import SamplerAlgorithm \ No newline at end of file diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3feb3e6..99086d8 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -32,7 +32,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame -from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing +from map2loop.sampler import SamplerDecimator, SamplerSpacing class SamplerAlgorithm(QgsProcessingAlgorithm): diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5c4340a..7d9873c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -22,7 +22,7 @@ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # map2loop sorters # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -from map2loop.map2loop.sorter import ( +from map2loop.sorter import ( SorterAlpha, SorterAgeBased, SorterMaximiseContacts, diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index f56b09c..ae2baa4 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -29,7 +29,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer -from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint +from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 4095979..318e944 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,12 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm +from .algorithms import ( + BasalContactsAlgorithm, + StratigraphySorterAlgorithm, + ThicknessCalculatorAlgorithm, + SamplerAlgorithm +) # ############################################################################ # ########## Classes ############### @@ -29,7 +34,9 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) - pass + self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(ThicknessCalculatorAlgorithm()) + self.addAlgorithm(SamplerAlgorithm()) def id(self) -> str: """Unique provider id, used for identifying it. This string should be unique, \ From 546724b6849a9a3bfcef93473ee261e43c7f97ca Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:18:44 +0930 Subject: [PATCH 054/135] fix: correct parameter types --- m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 4 ++-- m2l/processing/algorithms/thickness_calculator.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 99086d8..69eb644 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -78,7 +78,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 7d9873c..3b9919f 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -74,7 +74,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT, - self.tr("Geology polygons"), + "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) @@ -83,7 +83,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterEnum( self.ALGO, - self.tr("Sorting strategy"), + "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default ) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index ae2baa4..313ab90 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -79,7 +79,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) self.addParameter( From 671fed23f60bfc06991fdcc64fb9692d66e47520 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:21:36 +0930 Subject: [PATCH 055/135] fix: standardize algorithm group ID --- m2l/processing/algorithms/extract_basal_contacts.py | 2 +- m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 4 ++-- m2l/processing/algorithms/thickness_calculator.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 947e823..d7beb34 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -56,7 +56,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 69eb644..a4b61f6 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -61,7 +61,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 3b9919f..215e79a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -58,13 +58,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "loop: Stratigraphic sorter" + return "Loop3d: Stratigraphic sorter" def group(self) -> str: return "Loop3d" def groupId(self) -> str: - return "loop3d" + return "Loop3d" # ---------------------------------------------------------- # Parameters diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 313ab90..71f72eb 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -62,7 +62,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" From 8cc49af8f4a9fd2ea6a9d5dc72b86ecfd7710f98 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:45:04 +0930 Subject: [PATCH 056/135] fix: add user-defined sorting option --- m2l/processing/algorithms/sorter.py | 31 +++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 215e79a..ee081ab 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -46,9 +46,10 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): Creates a one-column โ€˜stratigraphic columnโ€™ table ordered by the selected map2loop sorter. """ - - INPUT = "INPUT" - ALGO = "SORT_ALGO" + METHOD = "METHOD" + INPUT_GEOLOGY = "INPUT_GEOLOGY" + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" # ---------------------------------------------------------- @@ -65,19 +66,41 @@ def group(self) -> str: def groupId(self) -> str: return "Loop3d" + + def updateParameters(self, parameters): + selected_method = parameters.get(self.METHOD, 0) + if selected_method == 0: # User-Defined selected + self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': False}}) + self.parameterDefinition(self.INPUT_GEOLOGY).setMetadata({'widget_wrapper': {'visible': False}}) + else: # Automatic selected + self.parameterDefinition(self.INPUT_GEOLOGY).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': False}}) + + return super().updateParameters(parameters) # ---------------------------------------------------------- # Parameters # ---------------------------------------------------------- def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + self.addParameter( + QgsProcessingParameterEnum( + name=self.METHOD, + description='Select Method', + options=['User-Defined', 'Automatic'], + defaultValue=0 + ) + ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT, + self.INPUT_GEOLOGY, "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) + # enum so the user can pick the strategy from a dropdown self.addParameter( From ffeb1bc3bf3fa8daae0112a2650bf6e7ccea1b2f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 3 Sep 2025 12:38:44 +0800 Subject: [PATCH 057/135] input files --- tests/input/faults_clip.cpg | 1 + tests/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/input/faults_clip.prj | 1 + tests/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/input/faults_clip.shx | Bin 0 -> 380 bytes tests/input/folds_clip.cpg | 1 + tests/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/input/folds_clip.prj | 1 + tests/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/input/folds_clip.shx | Bin 0 -> 156 bytes tests/input/geol_clip_no_gaps.cpg | 1 + tests/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/input/geol_clip_no_gaps.prj | 1 + tests/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/input/structure_clip.cpg | 1 + tests/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/input/structure_clip.prj | 1 + tests/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/input/structure_clip.shx | Bin 0 -> 1044 bytes 20 files changed, 8 insertions(+) create mode 100644 tests/input/faults_clip.cpg create mode 100644 tests/input/faults_clip.dbf create mode 100644 tests/input/faults_clip.prj create mode 100644 tests/input/faults_clip.shp create mode 100644 tests/input/faults_clip.shx create mode 100644 tests/input/folds_clip.cpg create mode 100644 tests/input/folds_clip.dbf create mode 100644 tests/input/folds_clip.prj create mode 100644 tests/input/folds_clip.shp create mode 100644 tests/input/folds_clip.shx create mode 100644 tests/input/geol_clip_no_gaps.cpg create mode 100644 tests/input/geol_clip_no_gaps.dbf create mode 100644 tests/input/geol_clip_no_gaps.prj create mode 100644 tests/input/geol_clip_no_gaps.shp create mode 100644 tests/input/geol_clip_no_gaps.shx create mode 100644 tests/input/structure_clip.cpg create mode 100644 tests/input/structure_clip.dbf create mode 100644 tests/input/structure_clip.prj create mode 100644 tests/input/structure_clip.shp create mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 z5g=fjra@{1+jZ;>uFciuV*>)Wr^{zpXl4iadSjX^xVFg?pLoqBr1qMpjWzosWeq0m zz~i-j^qzwhg0hwDkRa1m5d?59KLW27L11T}dubL-=BXDZP;~j6@~0qyXl;x?25AIG z+4Px3!OG`8#C!(gZthK|DHk!ZAh27I7J=Q$^a@kKV6rSomynPl6h^LD9OPci)8&Fg ze61Jp7F^pU>Dpcd;|-p~j@MRkpRGCFI9%JlTm~6DF49H_ycGn9YeV`z51nhnp0K2AlS8yOU7HTVHPh8y z6`V*nb#1C<*@_8lo1t&`klK!okU0aapmJk|s-OxxKYxR8`QIn^5&`WuM*L6Clew3s z9{aL*=8+(jq&9eZiGf@~+X{Oa2~yWBYI>PMe`lUw^jPde-+?uQhycD12_cFU5qpy* zoWhmK!cdI%KPOLqh?94=i3Pl7R#(fVOm2jV+ri3vxgH0}IVg)d)h7gS5V$n1ZD)Ov zgmK57m6dcz=#oO))ySEZH!QC2iF>7AE?deL*Qa_BYcL;fgEyg zLuv^AZw~K@k{h-?$l-wMqlxL>C!i{FOh{tZu8zv9fZ>bPwSjKU&>TgzA(7`M*9KxY z%Q00=a~!=LuT3>IM?U4Jup+OHYo-KoZ4DM(_K0h{A+D{<#GyKbz^fxbT$|hwH1EPV zzYP%ACO00WYm>u|Y7Yy6W_=;9Em^0I9M%X?YlemZac#u4b()jJwRM86uGEvRO}_$P zPFIJdy9Nl{Hn=v;Fg05@O-m7h+}b{*ri+k5R0S)Tgf4Jx@&sTFH)Dm;iBJ5nbxdaW zJ?GIp2xcDpJ%v2Z($v<_b#1?bTWIYa%I@J)1n!_-943&;dd5NkVRA3Dd8L*vUd^(c zCm^%}|I&QDD?CsAMUdvbOWcO8YXcwHFb3yXFMVb_2S@}iap;4x5&UVVA+J9kH%A5h ztOfsZwWt&!FiZr9Ym*z7=3N+vYs0?V$@Q-P+N5jiA7i@0coAsU7vkE8Ya8!8K4;5` zYa1@thAQ_jLGGNcvJem(KwGod4h{Fa18$@5@aT#MROhh!c5Lh`sFZ@m;V42EFf_vL! ziLO}@CTj*?G>Gi>-iyFZ*JFQM$8rsn)UZ^;GELjq(Y48^rVzR|Od*=4 zVdqBp?Is3U8&)P#$eaW$(5)9wag=S?Tw7reF<#pTp+{qEZzMLx8X245&uS?f=+_FW z&G*tN7XrB61#RoE{|yA^`#5=e$dQDM&F{Et4m0B4W#YB9xwawd+SbF2y(*JELf|L_ zs{7VagzDV|I$DZVRg?+b^>|XEHiEHZ}V!FVH5op$zOJ&*^+vZM2Je<_@ zGazuWx;E@w2H5t7o_jzuU4(%8%?!uZ-C=WM*gCnMxHjcVH5~`&G+^5bvTcoUk%v?> z4Fis=ny#u_KBV&ApMq<{97N#SP$!sRUuKgBG-2YOVV(yG=drx|XfZ}Z=i0t5!UViV zr@>5!&MMtnkqKe_0fJs;@S6;%+P;VRV$HHmBhP;*9Y?{Pv^?G7>so$~1xeTFD|v%V15RwwobAc2{sn zf-EYJ-aSW;kdpHxX&Fo~)J&u&UHph)+v`JG1YbKSb`yY9)X%vDZR?(liEG2~A+42! zzcdQNBL;P}MQ+0DmT6IS1M>;@SqwwHfL_=S43gky<>Dp|^Mi8Wd70fiGjC@GtzW&%;8~Ko?Srj~1vH2u@vWCv> zkPoTcw~uPeAyD17cXA^rwyWud7ipVoGc1fiYdBUKP@x<0j;)Qk zG7l+)%Et=W_CDqQLkuA-ang{MBDkp4V98xmTSY#Wq@rD;@A(G%OW3A zxvyz&T$^IquA?g8Dk@w^t9KURL#mk|YI8K$ahxq(8-n*I=-LphG@yd64fP99D4qVA zJo({-VZCV_Ri;V%tg|Kzr1W2MqwWMkd(TSxz!RkR-{o# zR&kK>RVL!N1!&BpcgMJ>yj<1GouQ#qu)m+U1p2~)}Y@1x0?Lx)`*HK*1y|uYE+g9XLBQUUn z>hw&ywwKoJAD_eE|Jqj}T^s4z+VkOc_p!OQp~ugAU9``C^+7*d034&^+9cyklCDke zJ0Q3_cC*Uz)Qdt0Jn0->Hc+G?)4ukQ|^(jZ5t8#ObCnzf$F|B)E1A9Ym?NqVc+eBI<8f_NY_>&L4OH>T?B}0Bd%?C zR?v}`5V%;b4O@qXZBH`{1OlWPdQ@Xz7Q1~-OL6vo&v?$4;17hfjfQJ;4Db{M|54o$ z?xNK@OTo3NK(^_crDzIh66XPrzClR0hbyR26_Wr9tf2aQYMP3wle?Mx`U%H-v;+yb zu{_B9RZwN7l)z3@g$c&f*hBLo#n!d`t#bODJwF7Dr-k#UWS{%1UX1qBbdn)m0(-Q{ zXAx;&$jqBW0O5wzD=yev8}|CrYv(V(IB{(kFv!%!&ImN?)#b)(>kJ-MyhsH8K8sh$ zOsJYarFHH1>DtsQ-82-%RZYu<3FYnxxEIF0uF@cYr7Q>@P}nwD^AMRfhSH`6J@A@Z z%`$L%fe==$5NsJ_9bF&wc`}bUL}LS)8x!H!cnsm#K)@CzK+M4{CJy~v1ZZPX7=uTR zFeP%9io~YuUUWVWd2(IG$`Tn*r_b!Y7tO^DqvrXF;BM_!9ooNv;l6!80k#c?ZO4AM zZu=}kr$RsUBQJwscZWJe#|eQ8Lg3Q4Htf5}1x=M+x=LJIFR+r)H-bQWy}DGkZJQW* zBSTBw=m~-C`69TsE<{>S&FjxBwhj&3-q3Ypz}KKsA=o30&5dE(>xpYqt_;Y50OXlr z>xR9-wJC5I*l%0cEePe-(zU4uq)d>;Us%BuqG_5b->6ovZYICo#Na|YZ5@-@z4cOH zfo{EcisMC!&9xQw1brpb$yVJDiy+Mzn*u-fqRXh@&uTWVqS|~fopRx9+Pm}Uy2WC& zKTe(=av1SeCpzyoJ`*^3{n}g`&jDHnzO}$cg02ym$!W;zkH-Kwt_4 zmf?sq#I-eZuhSlJZIfD!)=oT1T-!0ziMY07P@*!Iia>SW8fyQsb*|03>zd1#?D&p7 z;@VC^l(@E&;G)78hCs8v5Z6{Y{9V`-pzfZC0C8AkM6I|PD@&GwB zakf9hJP#7iV}|3cIdmV=uZu8&%%js_CVWV%cx*+;we<%GHkLuqPKe0%JvR-(&d2#OeZztkfmVU4O10l!k2sJ7A!S{71<)vOvKNNgkgkY)uZ7JFG1J_ZoIE%PD~ z(f=OKW!KGT$a~}ap;r{H3qA74_Yy(Lfum``4At@61Q&PBy!d%O^CBL?KWkg1WyXDf zl|tSGwpuPBxY^Q6^NjKPhN{~N1SdxO&Vbwq8P1Rc6S|poHVgi>3KBnH-B!WPJft-+ zHQJ}{lo+2{>u~vHHQI-Z&>2FYHv}$?Yuj00B$3^*C%Il>-|Zy3*L!truI;0j&I4Ah z`pc9*hH&%9%Acx4V(;v`m&R}h6&GeVt1M5wNMxG(0J(yW=bPdA3TPc(Y_WYlvF&NLiW(Xo z8&qf?w%aFa6S285Y+SK z2;TnkG9YL|Al`m%hi|LXB(UPWB3VJEQt?s%@~a>fX-SLJqFYRv1dQvd#c2Pt80~v` zYm7C&Olt)mw1v-2)dKCV9Fl$mFh9sHb;V0~cQJ z(zv#r^+j_0ckD^7SJ-zu$?o-D9h+-Al0xgfUvvRN00V($eZk?{Fxqr(8{2s$&wYKm z-nbCBSX~=--cW3Nnqi_+4cOBX+BfLuVQg*;+g^WMo2r{OsMkzLh9GhvlpEwmP;?iP zBDj{KskYVT+Ehz*t8(3^iLfHCj%%)O7*;pQP-9|a?`D!sK7aYg#xa=>U%3Ae1(1U@ zi9%3X&C?)A3awVw%_>YV!Pud(Z8$Nuo4(uyvoH>l+)JNX3Vmm6mCVA&5Ijn+ft>K8 z)eL^8=TSK4kWW)^ZUwov3}Rx&N$U31O|Wracu(_gdUr)y@qYwqffr_t(VNb zl!Hq7Q_AN-^32|;mZPx=6YtLK*8BOIZO*_#hHbAWu1&czK+t9y zx})d@xQbSDNN{bM3tvb@QBBv};@T`#-d$6*EL5c@GIruH=2XssB(&2^GAZ41OJ#f$i0}S%LNCj>Es5SRAJ~` z+oq1Hg6pI%Qei^i%_30Ux87_Aw>q}<954OL&6aukcp?JCwVepqR`Kok)UDI>kq{U? z0^7XE=sQFm*g#-=4cod$ZQKxwfJiu5G9-A5zs;RCz_3u!1Q>Bd+c66VwXSKM!}na9gF{}B|30BGpEM78sD{FsOXd4jsNL~DgW&Uc002Z zQx}Wl7-Q$Qs7j2-&V$vfXMwfHe>h%r|L3#pYt>H($n?YsPMx(#Mk}8Ddu%wD9ZR{Fqz+(=H*RUF>6V?@tMA_h8W6X;)0H;M~dfmcx?% z47$_%g2^SYkH@08jvxko!a`0X0?fYlc7yj)i@|zFcv@70F`gQd(W2n^O{)wX(Vwpt zF#nFDEz!o;1a&d~?6&O^UaN8p<7@8i>&A=o+4p5QaOn=;znI-VG@A$i{QiBPC2AUZ zIq{}+<@^p77P}qnYqNQ$y|Tp|T>Hf$LxoVZdk0fUYA*PsmjN>h_jS!c;6L2K z`+P?~)`aT<&+*MTesg7{Nf^f9L3v-}Nw7$hmVv>wf7-f^G3resG(tE;0Xizp&Q?d(N}x5^H@5{I_4&_r;!97QG?qf5umoxQ{@kZ^y6WT-!9o<9^r&yt74KS z^Y?e8pUGj+-TvO5Y4~h{er4&$w+ycdhtvG-zPq#a9rsLgI;h` zVnP>e(v#hm`IJFh2CRDK3f8Jo{q-du&yzn|GWWpGH|qplf6kzjMeS}Df@?$W86GNN z&>0%?9?jsy1>5%P65Kfs1H6Yuz#a+mGz=u=Fh*6250j$7HYcQNsK@7_c>30j#B|k|2Wp>D9UW z4VqxS-*SS{n2(#6v^BbdLlyhnbTD74-kur1238fgBy}8n-*Wf;`yYb~p3F(Eh+)v% zB;)N~g1MA^U9006wC(BC=FebV-lVUelNogOSy2xGwC%`x;k(qS z2>mps80oPdT$ZU9`3d7T{g=Rto#1cDOu;&gv(ap>7xv()5Ke`v@_+7?eebom{5W>t z_)Lqk)0(xoAM+dQHi35@vyq>HIo2IC{RIQuJUPg$L_aTIJC|xi`|c<@>+oY-YM1c0 zz6HyExK~$@$DmU^c50P?vz4lSjWNz`J|?XN;F48i9G@RCXd4xS=c!=-wcb1V((oJ# zY`f(KRy?p(Y!dyEcydB>1^D39Jcj)Z20h#KsQV&tu}=J>VXW`69j_W^flq9nawGK$ zo)7z8H%?$I6@FDYYQS#T@qznnCD{1+orLMw7d5K2Odo*R)8v zXxA6}(+z20{jZPH20R&bUP@ZSP4I$N4jUIdrz#yHZkz+N+r|EFHnaB$avvf08FC*Y z_o@H2j}iMExet>2}1yqYxC6vFxoqdP5sVV-tZoN%84X7?3)Kd_lC_EqvB zgU)*WOS%&CD45C6vrlEvS}Gh9N#Jnz)6GBe%qZvh?Bfkyu|U1&D)!xx_@q2(Fi*P6 znQrXkQ!Fwxx>@~~R?%07anF)_qQRPXWdCDWU~6;!$Nn2Sz!#UoFZ;i2>R2DcurmMu zS?>$~JC4-rq;FF5|2cQg(t!Xqu%uL~xH0VUt+~>St_8BReAw&kTCi|($d(6S>04sq zTIXR`onO244$i+}@x&+z?6x?k;36eU`>nNJDe7ZNd+G_^I8wEuH5`iODS2p->YOrC3w!Q zcFj0^{`Y9}ij5ac>2$q&o~-NLI8`_D87#S@x^dSWS^9L)g{-X?P3gSIV`I}KW$FD> zjm2()Kef+^NuMc87pDiDIOJza|6M;f>g6<9`s~8SVR7`$ixZO;T0*k)s2T6oFfh9> z*?p(adq?9l>-^cD9vb2JdGQ{fC{bD3d|Tsi6|VP?rzPh!zE3|qcvl2ieD!5sWwifk zsQpz%uw3Js`jhC7>nlcnRHHrK?m;8c=&$^UIU{ahd8bSh6Ioe$eXRLU9&qXL#gkfc zvb1Pvs>=@CpZE*6#Afiv<5?@dp+8ogGXKx`D4$+q6bYWHe~Q;|p)B1zr_o3o%w^!_ zDkm>X?{V(P?nHlIms_M+2VI9DFVF^W^GMw5N1+H_NWmD&?uzQbo}VZC_dM2kUNM zd4W+&(I1!IEN0mg%ha2TI6hMJU$dvh*TCGrW8(B1DSGo>`R;tMA=mVhnih&~xx9pb z1iZY<-go`B)Ix|bnfAA6uoLazik0{<{Y!u%ixM-!%X=UQ@W$& zZ=xO8VELe*Gx$oNQlB_je9(ef1}++Gjs1x0MeW;@I4{+do_|hH_X^l2mB;@mxbVyA zrSoup>+LbYWog(qM86j|fq$Ai+`kImqobi11NM=3O`VZ$N+0GlQcS?-|E|d0q6bbp zk))7`_E+uCv3CGJ%g_m*+D*|5c#`^3!H(Y_jX1PZwDjT*`#$h3g}0t+Z4{lEd9Z&G z;t&Su%406z*T$Wx7T|5A6`pB0|I|#y%U8fIhxZ0VcTjZE&NRi_;N(q@B3aiz%c1q{ z9+={E==e^9~Pe`8m?f7$(=qL)oGT2>D35Zd>j;~|?7lr)CrEcuiSSA{Kxjpw|iifw-+|=0mtqUDqM@} zr;jR^4q!fxYjdTW<9-{S+)26u{(R{tk1N`@`lcDb9@uF1A;XzqFFz(z930K{&ioPX z*P*)4uN3p$P=9?=<`6}Hv+GNo4^B?AKj{Z9sXUNai}iEl&;v~qoUd|6Twe!#^znv= z3&7E{|M+o(s>v3S7Ic1^U7E+dF%(N77jM1U|qfGvj^+3Ke->(699icz2{`sdx}o(yT$h% z`%zg^c)WQvMdymeSpNdgemW%9SwYd)4#r*_0$WcW+hbcs(Z6Pu={A8+J@a^Zzl5T< z>Ri8f53FW0v*{T2zX-L1C9}Y_+f(e$Vt;I%wNtMN`=_3JT;bst6umvXKRFYu{-m-a z_8CQg%3Eb)1rC2^m+zKG(f4P)?H<8?uW6uR>GzbP6YWhC%fY_a-Sy^y*E(%r7J}C_ zj_8_$g>`k95#T_HF~xrD--)w06nw!mZl_uLV!vlUi^%5<9z()#p-NcKtu;A0J_lGf z`A<0w^$1fsMR1|@S*&IDT4g=ABv}80Kk_m+-{`ji-#ceVyU*Ywica_` zm$V!#>>|M{nN88N&vo!~fxnF|yB+wDqBYB+lR6Tx531JITRouY5IqzAB5;9Tqe)i= zMZ2CFvk3#ANdGB0nNHC^cHhv~0zd8=sv8Ao_cbS$$77$HFq>Ties*hSk3X328kg}E z@P?&X`a8e_o44x*rcw092N|}{;!Np5&JfexV1>q;J;%W7V_PS#rea@T?W(^7d||db zV|NP1mG=!(0=)0d9J|BG6y2Te{iZV(?GdMLNhD#OD!sD34&Hd`m&?FCiq;WIwOs=) zKWVRe^e#nceAMeLw6dfR7)m@G_)*Agg zo}5Q1x})cC;zJgvY}Yh9LeW8YcKcli=RHb~cxp}28V2k6{lG`InT=Liz%E!R+jRu| zEHHmi&kWD;p9y?Bz!&6}iMi~f=$A#)x;BD+xATuF?uH$6qgq}QywY`>TaO{e`v+@ig$h1fDS8h7>AQB|!+O@1e%r8*B<|{Do$s}#k;iQlMR)N#c5Vjql-bAV>QZ#* zqL;7Dz;g!Dz3kS*Zn{vpF%Ue@W4c$g21Oq)N_)*}|Kb4gJ=N;?yjbsoIB<(3m)G%? zm@i!7|6xnjt?!hVW1p=(_2n*(S5My`oTW_BwNvCf9)r(pavO49M$uodQkL(*uC8Yz zHz-mx|DTJmTEMauYA02fQgr>`+5hm)c{YoR7gO}m{EoOP9Czw}q4H=E?5t;t|8uu7DEi#VlzCUcy9^myZ_mQ`<@Xf$ zfM0$%WZFIx>!Bu&>lj!)`037{Z zzk*gTgEqMuDdP$D`*W0Lwm>#Z6X?Ve)=U3?ctwf$<=lhU-5swky zl*=^D1pj>WE=T}zugnI$yj5Utsb9x7;CuzYvcF;2TkHCsTK>lIJ*yY|Jqk8p=EjC0 zp7J%d)pQ$}>(i$)JzS5y7w0eX-nbx(LFf1{ioS;9!7c%g+3>SfYJ5#v!R8A)&i#g8 zEpBpV&<5Yf-pl)!_qck3Gi$P9LlK|KuJax%0RM3k;KD{l|dI4JI4%w2d+I$^}&2HU!XDh9lTa0(_rfloFBY_ zA&mWLs6u+j+24puEEImB2-eo9QZeJi{`7XdW&%kgWH$_|T z5Ad=F2fq4SpfrV|r`^(*JqgzA`pDzTkMkexJLd{+Qxx-66{cw2WCIIFFm-cJM5qY% z>!sg0kAa28gtpqw!G7v0t9=5T86-}1U_It+7X9oDR$D5)W&1q%T}KudI)G>TuUB}6 z^?NFKULmVJ?}IjmJ1(GTS>yWotmDH`j#r+{r*OY>C6mF)1;-3yWhpv5c2N5}m}Bba z_1-vtb#j6BF!-3=*7@(SU&I($y&41`JpDs%0rsEj$dC=6!9VD`sovPHRIe|7-3k^G zDRkvqP0__m@)Lf6+hqeC9%28JD(dSP13UA#yFJGK_%`Wz$0+!~3T>+y8*sn->Ujn5 zJYoJYYS<5Ue`tSqDmZY4?!iquxPQB2Q!;V>Ow(GsG3>VnPdM(q0M~2FwN_(4HWcK! z*aKEvclyZrE%0l-$8~t{`8yH^i+^B05C6$iECQ}=>e-!PK+$VnMC!7*VsvuJ>75kq zxMHG{58RYfS7*PMq7_RXUu?qlT0(^9EZU%@|l9M$#BDZ1xlvix7LmTmQ| zm_x9m1N3cH;`4U;UwjT&V*k6(rEnH(a8Q=xJy_lEy?5 zDjnd{cSMrpz(J`F6_!>M%@A&5>VpGLm(DB0^KR8>V2>S`uiQ8BtSv>`TVK}q10U82 znKQ+nqAkZeZ6d(}Ll0AL<9W+Dk)UuNJdaO0Sn3q^=fMX(@4%ZyI2oVtybj+I@P-HW zfnVlKuRZP*{oW(6XE%65_6nwv7e!muDkNVA_pDsr_S>7HMIL`=z66IdRBxGHfL)&A z+cOpRN7C=-lg0tKK6keNUU0?V<6HHD;fDsE&@Tj+kEp2fg;4Y(ZcqPj;2p1@cZ7xE z`I~XS#}xL?HuWzWmUj3EdPP(8w!j+#N5G-$4pwx+J{p!UOiBSOe7xNy5BsTh({+VO z@UZQjEpNeMSr-*n!2Wxhb>Um~9gOEzPTN56*3?v$SFse`wkWNq87#o*B9;;dyZ>fN z(h}I04u9ep@8c;tWnCA)57?gTinYaET<`9f-aGKov*yfQi4-lb^^1Qt?Ai58!d@D{ zUKCrIEMNz2jJ5T8luFTpRShNK;5R4FCY^*m+WyR2fw2-L%;2L7Dc;k=J3x1Z=3qv^#$y2yNiGH`oOD>b!o1Hy)Kn&o;V+V zkW$>k$vyBBteaA z{u;2FNTb2LVu}vlzb}ywepyOZ^!nRxD4NSvPfrtU`0TdANICo>ll0>4;IE@)IT>#$ z+W%I6rvdoos=3v??wT~!g_vrs@o78q)}I()r`1=%QL>a;5SXFd&=Pl?r(Co=>R`% zPR?1|hWYSp7ynfFrRwh7owlFhAL!fiuK^E;2Y4;`g1D0PF26HiK7WhJL*T|^PcHrh z2k0!dTa5G1_kQ}h=eyZkE(N5G|Sopoo>-Y3#K zdhdcA&b~L@@d5Vv3B%rnEWbWQh;sq%_gByFWOH!3cIMSZ?=fHcn+0RQI!h+$@EYvz z+@F$5z){=om{e53K67sE9Rs%{C&`RfV0_-*y2Og(+`jDVV}bF=`E4t78m!vkZo!Jf z^`Dv{914Ek(x`U&B}Gr2qh5X={EX{jr#}2rU+x9=tT;=;3y%hs=ZN#v7xcA)b51Ym zrZN6(U(i(Ral0n^gUw4e?;JhTPSKxdSZ)7-^Dk#yd@zpwc%}4k+W^@5;9u89-2a1t z`IRj8_*88fhVhJ(kT}*0K6B{TY(eyg+s@v=MsVkG{q_&FugtgobW^%ssWa;@9Mio0BuT@!N{*E8jEyp#q073w4Ls)3@p zryLOq0duOhY>vSA?Z!9%_-iuPo@>{|hT#;>g&2JYcvrYr*YPUkCs4c_OazC{k4pLIRwD|qK- z`7Ki515VINOQE zT>h;2rv8{Cej|>nOdjbj!F){hU$|(vm!hc$Jxtboo#Lro>2lb@uf3+nH=C#u?6|f_l^ly>tF7u3SqyP)tlQ@2A-?!6CaE? zONVRBy%cb~^Qd$H=I@G&rh2yE-Q3Z$=VHFzTr$$F1D=yHo^w8rqVL_^+Re)UtBG$@ z-GTkilp1Fq!v6QugtNTx5&SEI0seX5)s%g)JdP*xIoXV0KlK>gCKrX{Q}Yh$6@ya} zK6|~!{;4H-_9-h){^>z@zzOh$pJv_3V71uE-P^F=O4+vfd4ON0MSU&DejL_vxI`PQ z8)WZ$4V-eL*{=)x|LcSqTdso5i@6ojz+^lHAr?ruc)b3duQW*upQ40w|4!FGQ6los z+GB-eg7A;^N9O0^h}1 zYe7EnjfQMGm`mx8jA}cB_IP_pwiX;|Q);OMzwE<)t8=XQT1NYaoHt$2w;cM$DT{GC zdd^Q=5`J81+JJ=(_?z^psBZXq`o8HeSkIlE=C?Mo{J!ru?yOscdGt>GzFq-%=THR4 z5pWle<%mu{{EBR^b3FOq}x+^A3LWo8dCXu0=+x6gsAx8#4_F$}-Z z_JVmm__k+%eIL=6|9ph`bmeS;72@JulGC)eDITTWIfJ5HP_bD4(pPqq4e|a zUkrNvw?3^TaEEDm;rvlt|I}@cE-+v4lVjh<81y}(S#5Gy*X?4Zl<7Ewej#CFVGs6p z+CKRUd|&NtK{z<^X=LRXSn_qlof2@%`@JbEC*g;kS{Jtg`-oS*SFZ+mo2cTU>tL4y zU1yRe7_>$1tGEx~WBr2d?f87nvdF`d*e8vJc&#JB;e{>Sn&58biz?5+VgrZg90qGP zpQ+l4>xJ+KA9ez3d|qMd4!`Hu$)FAP;FT*}CZBL2&eor?fOY=dM4!Pw+!Sr$e$2`p z9Jgv^$5z%Pr+$poW>fw6fMM+S^N!b_<6%(DKPuFLC)pr+S3l6$J}7@CsA`=O{3_8KSP8vz_$grsxO&}ePPC@BN5=pN4Mh+31D9o zJ7{AIp4<0bMvD)2-nEe8wct&ci>>xdp=du^zwQ#gJIyK zt|c2;@%hrK1?DpNe8HjQ1RpRp^}^3>@au+dpU`mvYnDl!Y(pHR?%?6OTftv)!+$ml zQ*@+Iez7{(LUHN*^Jw3(N=e;WU_*av^B}O)){9f>FrK=C8pgvoUvi~WQ5M+$TilN2 zxL%Wx@J3(oncfhqbGV;-`4Mt7nA3~jd=J{Q*n4AP5ayFZ|2@w&Xm7A#Yk@5I(wp3T zU-a*UQ7z{y%ukJ@o7eYXJo=_yIA{U>D&{nQ1mmU0Ur-5JExfj4 z@k`7Ew1R$#{lKPjl=%?rH|W<*E;n$K_RDk`>W=it*qy`3kz^uih6gfY;!&GhLo+t7g#D`4DqIuu^aI^^1u6eEZH=$Fj>sk^&-!5x1CX-Em+ym~-ms zg6+`7?6vw4BM*MK!LM)#aS)%Gub;_+d!KI3anE4TGrvx{Ph#&5nVS*x6*?fXBY&HI zfIZ&76aN{*pacGT=eK~ZC{=6{)i#by%cMi_~#(6FM(a z2ezEhiIF-oQfEf$&~6htHTHP2bxv$%>x<+F{gED_Pa^e8T!g-f)Zge4`W$XTzeDPK zt`hnmQXfR>he-PcCJiLSTolU=Vr!Au+M`af5M9nP#{d5;Vc9`6{;~eZ>##q|)-jPf zC$pATk zMDSLw^58A$47xyIpL7*CLdn>0F4jHA>=vmpFmGQNuN(OI+)GyCtbMY_z-tnpZ{p#~ zR{~$tU3N4H@!T@GcY-W`_}r3@=NIDqZSg6+j^NXmRo~4;yp>ZuSmP2{ufJNM1YG{G z=0+a)l-sZFbI@Tae=@oB13WcNEWZbA{iDTk5BBLn&OISK_`ZS?t8u?u_k4-j0&dOTDRmQkw_+l}6P&#C=&!lBpHP`D|8HQ0$B|;FRo_!9PHz4Hk&;Zu{xhY7k~bv)`s&1K5FskK7$@nJowjDaQs=xatp+> z)BGFj_kq{S&N+7ttZKn6D-B-1KmM;7cw5YMzxQa*%lq<|4!lBtul+MO47|v5Q+-@1 z`ZwBOpE8&|mwkCZJSoEcxi;>KKZfIUT^J(_obtQm+8=!W2uJC1X>fEzA@eSDvI5s% z-FuJzRQz>)_{(z!y-H*8#B%VD)=Mh2Xz#9u868a+ub73qgy-Xa(zf2@a{?zkFg)Od ze1mxQo1xj@$Zx(2b#ec}5g|%pm>=8w*4EeK_~kttHSEAG0?f2W;QWN!XZgVgo^;lh zg72#>|NDY9|D5JNZU?`8XWf(vCUv`{uD6cR{gS$1Qa4QMib>rusY@o;KemMFY~3Kc zZEOv-;H3`BJFwx|W6#=0*t$W|zhQrtt*L7m0dpG+qtfVqq6o z4nDs;E?Nuo!04!21@#g32UVvL3+{~F^wJsjjF6_t>N_&X-t{xBTD$@;cX(DV9I7A?7=O+#vL{t;sL$=EZOJKXH93NKVe4Pl+b!uRB<=Zd4zA@-S zoCjh);(l+B>VJHI_W0&>N}mNE_*u%#?1ug4B4D-FB?j%Up(+ zU_TZaewbjbi?vfl(xY*|b_Wv7{L%mIUD{W~nz0X5pJ}PZ_*v&AOrAu4ivH2pG6MI_ z*(QCtjzQlTtMq8YzMs8-ArM#to%F`P2abcu`Glz^f`2xw=i6#p`H{tP_)hv~(=VfM z`scfw*sS}gyb>he;s3=yM47$a1%b8@5tY_5XV>NNmj9RQ7-Ufr0hdRg{(D>)OVsm~pl&(U2%eQYk>wI%h!FYLf=+=hE-{ykl@BgtXT#0zm z@!xN=!R)rO$DM2sn#w9!SjW1r>T4;})-9!I{olsRhVfaB+J$SKRR7=mVx9M&KjPT6 zmE3zwkQZlvwpjVT>r&*w7l)P%{KU1`*Jihayf&^m6DGty*7e!85nIE_)_Af%%PK4@ zVRbV8F5$m96+djc5hG)tA8<0nqzkNTrDdda9$K9lVJ1&#tlfVNlwl%N8TZ5aO`@u6z*BQizJ|hQ;8X zH`W!J9}!~@|C=cn0AJ)i=d)16Xdm_LRsIN;u)gBF{vBebbE?`#z$!=2|Jhj$tzu-D z2z3e1n8iFv5{PL>Ec~%d3H<$MHcwC`a%R%w4(q@bMvqNymmx-Xvt_yiScIo1qV^SH zgi@gyZ^1&LFB`fGkfYQ2v1|-%c4LN~{R_mzpXh8C4MhLMXM0K^rq@!pAw3zaUuWqg zm4W_$*_{3dtlQ))E|fyiu}%*Z=LMP4d)-@IXC@-2L)}+g1+L)pN>YtS>>!LUZ7+D! zMNRR~G4Q3QS8NFaU#?aO-w=)d=zneh68tWjv6Yq6azDIOnDa7XL2jig16Pp)OkmVW-KT?Bq6Y}b^q5wVf5?6?^r z$mxF;9okJJwv(lJ-WjaA@?m0%xMZvUKJ2rgZA{QW^y=zua2G8HIhY)cu4ckDX@rQh=sT`#xLmd zy0hTgnOu42=0MA;5Kgg<8-A{f3z9%i=k&ANYH06en<15PXma%We=ru}ergUYhdUv5 zc6X=$?GHFUJK=3L$26>;qYG}QgEOL?U!E6)#_ZfzPfIZWZz=5<#FDZ{9+}C2-+vu% z&_`^q{rLXLd-y&-x9arLNyJY2(mhS`Usf>8_jYg&Nt!m{W@23++S7U)>NJ8(Mr_1k8SZwrB)0xcr{A({&BFmf3Fl z8ttETP;K9JaQyLLgBIMcn62UJVXzkG@Qdb4u$8-wKP`pEDaouy$Nw^e)~l9y@*T&= z9`5zgxyqo=o^E~;3vQa0EcOune^R_IY8lw1KGrnuHiPcH*zVeb_EeoeqiP=qTRb%X zQ#Y%<)(fK;7|)Dsr-o@b-?5-eQ{yg!Zv5-gumybgmbu_Cc`HP@sjo78OoFHJY0`6e~tq-LDdl#`lsQj<<<)=5n}sd*z&XKisWbGYUn@85}k+pqf?H?JBVYBmU&R@mQY!`(x#SECT^a8&P z>CeH{oqG+X8L~8w^P|flU{&6PVmH)O7nkNMo&vA>IN`W&7HS38n{Q{;aCpvAxurZE zHOn_&R82s${^Q-n@Rg{6zI5b!dOg^xDJuLuY7K82e@=f2-uY#R$q^1&`na2@h&8zH z*OcjcW6;ED-4+>zJvYH2c}d_8{N3Hxl~|hkq&2#Rn!liRI6m?19Qe@Guziyedplvkk25ZoG&H@$HH{_)w5=Y7Cd1+PT45O2D;T%Uz@Z+)0=L>IdiV26Mb`D5QT7!JO(QKtrEj3Alw2X~2e#^eX3z5)H9DnJ$}Pb& zBO;g+&nbEb_sPwg;7?_b47u{*kMHtctN^}jJiVm(3C1I*T~HEy+Ox@WEEigh{Dwv1 z-~-1FT5U$WCr_|Q@DJ?uC0da(-49U9DBRxr7;N}jK>8IlxJY(wasr=~w(#OZ{AXHk zkpEI}ap%fqQTL!p&butY4_;reFHSKIpHB%B5Ci{^j$F@;_)oK%w}K+LW#?5di5nFC zLFK!R5?CT!%Xc5*Kea;FljealH$Jxfjrh;Vy)Px4;6%Q0?)RaHzkHbC-voaoXxZ@k zWmgd2S;%dB4b0s!+f@HD;thk%dON_E&7}`Y1YrKG`(Bb5Zc4lR^Z2?VUi3(Sw)p|( zS!`x~%!i_v|2Fpf2v(92X>9jK{Hsi~=P~%pn3-S);z>VNOcjU*t2~cAxy=Ldytzm8 zJiw=?Y#&+d22EXRW1>0uPyAML!*xZ#a~e`7XV_tW^TzT0LVTjt<6ac+5yY=X0*h#@U_pLPQB z3&FXEkB#Yqr5z6yF~FhQH7rICLDQ%j)Abkei)QgRt<~Vj^ZB~@;NL47eMeZw(>tfI z;xjecmg|))p{bv@kWT^3*D9L-3g6eRDWLlW@rWkZC6OP{KBK?Vd`aMw-)HV;-QU)f z`MTC%o;z`|Gtpn|g<-`q;Lx&yd0Ob-s^{u{KM}83C%JNtJH}(W`!Abh@ZNmg04`7T zXJ@Ld7g)Ogd*XJCe}VB$0Uhwk_i|efV16)vIhW1_cZH0n4h2#4)3dZ|FDf-1dZjlpUt)i!%K9y)s z-S4-fh|hlNFv;JB{UnDu^GhDMTc#&32m6tIh|$nVFhg-)=lL&)uR4u~cq9J%z+po5 zB=*Oji2@HC5pOP=F<21y8}ZolMIjT2XD6OGHoAV4HUC#PH!O~#cD>}3!+Q4QpX)W3l9mZ#WygG*#|Tt+Znhhje^Z%x7TCiZ4o z4#unFv(=_VaG2_;R8frAscU6NhQNp7FD>%}H!V;-BA<%**s6m`OIhQ0K5k1gSXXB#jZCt# zmrKyT0Qb?LUL1cn{5@bJ?#H%CFpc%@hoL)dUN6CN`LBgp?}q4&DhObm@9S@{iS_+8 zTmv%Pn4hZGFZlC=U;Q#nvPS>;7EYGrqy2?N@`B$nzs0K>n08=KPmzE$@S)!?Y%cATF#;{<&mQGFH9NpIr?2{-04Mp}cbozH=hCBafjQt$ z=ZcQp$M}5_yry4(@i}(2KY;b_fT>fn1VX_dYqfZ<525BXecXmsQ&KDb+_wkziiBuO z_ge6d9mWTFF<(rs>?m0P&h}~HX+{2Ocm|jL46vV$RB>uI?15Sxe%8Cq;tTJN`oo^- z+wi^Q19+ibRNi;kGd<;pna9D>>cX1(&Dc*DIVY-u69UFAM>aq!dhyOhVesYY_oq46 z<9TOhV)G31Id^i4oet)Ie&=#O8?cPv@-Mt#ciF=J1K|0lsza>xbK;V)|5vP!jqMWU zS>@Odw2YbC!G8i0CB3o!q$KJSd$8VA-^`yqqXhT+JwaeUI9jjvKio3+)|;+)tex&uQ%oQ|CPIB=Nj3$3|88*k(Ic_ADKrX^DJZ@hRoBDc^op& zL*{|VJQ0~kBJ)gS9*WFUk$EgK&qe0J$UGUDMeZmdw+Vd0aBjOXh*eJTaL^CiBc>9-7QklX+}1 z&rRmR$viokM`OKF9fM+=oIOpbNNpZhN&1SXJIQ zvm2b>vC**O6W(1AwbseR7j~5WmH1XXr_DUWs|a?*uF8s2a0)({xJN2yC!=yWaC^yo*C_Q^*^zW$}XY zM(lgFH93v~ez4oC{mcDOt0S46lW`CneQjEaD)zBk@k8me{E^3&7|&~mAGctmRu@CM(z z$eRvHRi=SY>(`lP$KqWH7N=5wgZnhQ+f;7Bj#k;ebVDGtgX{UnVk6<#7RK101HW-> z`+D>$_W9S6!Y{$|-UC7yTI?2P8eFdQ}p2eKm0CW?A9S7Qb^w9y?&8 z%^UErdHjQw4)D`Uv-w8BPA~1$y^dghm_~FlFT?K8Sf}P=!K(dqzH1KlQR)6VYK)qN zPz~Q(;2GNM%I_Jm{7AV4Przwya~x+G;yK7|?b8Lml8~Yzxe2wa(k-85VCSd&*qD1> z7dn>mzWHk4WoI%pXKg^ih5wgTVsFj!@{4!V*yr9um^S_0!C z|60t4;Q(((u=lB83s-fDKCB%&kEJ7+Y!$d*wF2X7R$q_@-d1<9aDxio1;f#2@fqx) zK1GIa8SekbFefkkmZ1CrBd^8qTSBxrSAwf9ten1bA!?^yX`G7zpPHC*_1ApVYStN_ z%LH2%Mx{NP2fH-JobwmByMOi9^$hrhvyEgg!>{_V%XVnjT)a!<&Xd+-;9qHzgWlq( zH4J)Wz6PA<)(}k1#P~lL(h>vf+t@I^OoNW2`0#wbxqCw;NDjMT(Kz>tzF<0;|{K}GE{iLjrHSrzu6U>{<-^WHU~bhlf2d(YUN9@enj~tM=Wj1?z-WjCevb7k%zz{&TR~ajOds(5M+Fy5#$T zT~zf1vyn%ialbWM6xW+S(%MTQkDh+G(DVzAOD+5$UklCP0$r7}Uf{nJZ+le*YA76} zG-5FR8@`Ih-hj4LOE2~Y4bGTCFIrxLcbSH7i{=3DUDjh@jXZn6ufVYnn17Ef+pcFp zdpc9U-Zc*#OC1os`GP?|i*;@A0P8P4#KC~JbkVx1)my+jG*$-h!n-R1Hn0CA1>VW& z)<2MgcjKCvtmX#~R7%M>fcMV&^JD_+>FH)EpBZ?!#czG7sKq$GPHIdz724#+%-BC2 zIKE0k*fQikw5Y*n_PT&C%$pUr4|&*zqhYhTS;vKZK2ad0lX!k|~y{gjXZD?>7LItaQ0ivto5u%DS8 z9~WJZymNuW#cO-OMNWSn&BHsmYuCPTX~llH?Vez6NDT|AA|Thb!2$w&P_K?r+3tikAnTCuz;YAaw<#?ts)Kkh%p@*FfqXNL>V}n;>-+r0#;$ zWstfJQrAK1K1f{%sT(16C8X|z)TNNR6;jti>Rw1)45^zTbv2~!hScSdx*by2L+XA= zT@a}oB6UTi?ugVSk-8;P*F@@`NL>`En<8~pr0$B;Ws$nA|E239bYG+{jMR;hx-wFC zM(WZ?-5RNDBmFhFaANTLBw2nRdoN+HWw!0eezvjvz8Cn%`XisKQmfg0wqGyM(k`NV|r#dq}&8w3|q~inO~(yNtBkNV|@-`$)Txv>QpglC(QX zyOgwBNxPP`dr7;Pw3|u0nzXw~yPUM!NxPo3`$@lm^czUOg7iB`zl8K#NWX^kdq}^C z^qbgggY8$5ei!MNO80Y6B&>eshuJjdr9)rn`tTZ2tSlzj2rH$Hwk<_h9NZxh7^ z--6>y>Xf4{&%u$6 zcM1k69jk<$6297{fgADm(k~(dXJOYwk4{83#vy+g;2ks*cF%`e-S{t2$WKkY4GaaB zSPXb*fo-xxg(JZ?rGDgC!_OI7r{9+VE>cJ+xDG#uH%YbcJ^1kGwuamO@N@L03ax=% z6(28@Tjm8nNNZ`SFPO*0I%$C`Y6@N-e^UUS`6S586MoA*fpEV6=%<4@)}KXw=XmP$ zi!!j|vQ1*^Par=sn@d*y4ES_WQ9*$j-qA3UurU=J)m1u}whMN0qjUTOICepXUBnjX zt@97?=)ew@3_QELNDuMl#lBw-v-t7+riB|RddklahupzlrH5u)z%MZ=G(N=AN7tOR z%yrSE=&Tj`+_~VtTtBz4Y8q0Pxg6{UTk_e*g{;Kqiw4d~!!F+7E2NnOyiluK|xVC1bT@3Fa;axMi3_Lg6&rTbD4Oh4M^Ec>^gAK7q zI(d*U36Szw2Hwb@Taf_0@1Kfq<3BMT#nTo^cl|(ZP+nLQ%P&1Bo8TsbTAk$lwzG;j z|AWtfWeaL+S~+X}4&%5>FOTYI7yN>cHv7}Sp2}xGxT2P5v%8W8H~6RA@*PW2J9DXF zb6zv%Pr$Qf)RHE=D}Qvp#uM<1Bf0zbH{u=t>rXaZ0l!WERjviT^5)E%yz5{;B z{1rvB9$`I8eG1o;fZlYU^^JOMaL~-Y*pNF6`rPEm?x)xfRA?I;P3ToQiYIq#fwksu zj(ik@-ws$BdFmtfkJUCC>VJcKq68w1!Dr{alVITU%PjBCF2erfX;nAe6pMFF#DqOv z39cSJl=BeZS7CGYMilldt_No))$}yoP~_TkZ~F^jzh+I$T$!gCnDoWWSohNLy>VRGLA*YxyU#e z87Cv{eYgPPhG@;Bqi-;^VNvrUrtO*is4 z-y9G19~XnJ-*V(sgDG@Y8S0eGMbt$!3;x|V2;C8>n1*JZGoJ-!%v3A|e>g5_Td z1`=Goz^WIk7MFDKu&B|6ny}W%w8e4 z0KZ`}&3UzI99Ztk8o5H~^*H^aj%R>1Pp)ek6_cfd$^&Xx%)Q;tYiI`a5&iKhm%ygs zZS~$$W$D&~LOV8qBMV=zr|>%)&WAgyCBTc^>wN^F=le1y^L+&Rsp|PW7E$=^fMwQA zGg`s6SN$DNAs-)7sgs$_;{1e%Q;?6ptM_pz7`#_!`J_Jb=Z5@BA?v{-b_+~YknfHS zHy>hv`2EGN#^RKg znbRFV!vFXkmsSJzdG?_E_dCS#>sr#v!0(59*93vXo?R_uar=w+TMofLTA0S6SPu46 z&B^&xi5lUc*~i|1t=?9tHJ3r3Xy)2~7o2{p!bPtH`W+9Ssm|a{GtXvO!M|y0=?&Zn z7RroK%zc44|M84+IdGWA6}4-~1JS&OTV{gm65qT$@dSB@!^@V;0RMcmH@Fpfpu_=> z`%A#jCpXI7%R>LkUN18Q=Z1&}gk{2i87SX$1pNC0SH5r>^dS26rN_aOD*W27lcDcw zm`FYb4!T~=ymk+I3M;W*1F+VOUR~h?_(yv`1t^0z4nDUCjlnxFy_Ax>pg%J>c7Vqr z8vf5yrP7CBrbOERN7jFU_4tPW|9GLDvT0CRm6R40tygGiOIAf88d@q!R1z(P(4f-N zpfpgC(9n=nC?k?eLsUk{==Z$e|4+x~^FO~G2gmU^?$@|q_kG>hb)D;Z1{hpK-0g+w zKpgn;->NBbu&1Qj6vF+%Zr8WyMaRKji@WD|9=v<4#{%E;$RE_u9XJDyi%@i#bq;YV zuPEsNu)?7@Hp>{izvThXR`9E<%^JUu52$@scVHQ~>fh_u0dVmr4X1y|PkXPC&S3+d z8Zk5H5jgixUi{iK$mh{j;!Ou#Ma)SqXdZ`tD$97jWjOFLq8R5l2ahk+uRm1wBX#34#40n%l1cRIZX z_eG&e{~|bCM=wDM=i@H&v-TeN;ZONwew?qa?v?Bo@XnL-D$-9d`A~^GL*QHH=eMhe zqTX}fVaLh1zEO#NX_ol?oH9eT{NN+c17DBg`ULd4h6#eLHP4>g8wq^uLc9gw1=Tn2uEhQO>z!QJ1b)YREz1P=ceiCn?PsuwzV!Q-7#~`*&N^~p ze2xSb>TSgMTEU}}GY_mB^Lx7}#-qr?RpD~rO-s8HZ(zKd@LJ@Uf*bs~0*sSzy_Mw* zkAbh=PTiw;8ToQXN9(e|e4FKS&!=KMd+Irrfz_-mc(bp-o=&u^e+ZUwNn86j8{_e$ z8ZR>+Xuk^1o(}(mMgFJyYVgvMQ~~~c%opFQa*DvCVa-DpMR;C4U)86B&qz9bKXV7Y zF9sYR#e-#k-cNW8|Az7vwdDn1|G{swx-lPQuDCsT2Yj?i->4e%h4QLLkxc$7@19Pc z_8R2V-0WCB1M_QnxU7>r0rg+$y!?oWa8K zpR~_lJ}bMvU)~Np{rryNh0k$3bBWwm@Mpe-KOA2m-&yh3Co8aI#6jb4ov6=s3cS7@ zyl}Z!TgV64~AbpH1rMgUu3-d0dRQV&yr7AKYqoh44(ymY={&5^%MSw zqZPxI;O{oul}`V`?~_a$o{#k`*k#mh-#@HR^DYeQf?1nS1@tia>CM&(yTDcTgFAVU zFF0wF$H)=z>F(u4V_5H|mW_`jf!A?GpPPyG@PNpAg%4nd>)Eom!HT(jUpTN{Z}+eD zt_9zyQZQcu4*X)(X^UPw38#Ne7XyD=ll58*>uYM^5``_`r2(v+df?9mo16W?PYdRK zUjhEor7}_o&Z`wpeu4Lo@3I+r1Ln3a++2?JyJ?=ac_Y~4?u*iKtoI3H0%!BVLRwcA zH=&1^iO{2hNU%!Z+68OT^X%U3x{(BM+9b~GesF5?!|A2q{ROMiCW%R~eog8xcmY1= zHoVvb`-@L8h6SI&1&{ru?=F&H1-tzyn1Q_v-^-$%cIfGrHuCSxe6alN*nuQT~U)G9FhCJp94`~?~g_Y9!-ShIe- zJX2@z+BQI71n1EmpWMXsEq=+t<#HasBk|jxC(pr44W%FVqK?5Hx(AJ5XvYdJnj^vb zwo|kz2YilYA3uMZ1WQ&wJl_eNCGw#&k6(g?#wtw)U>*0}$qu;gzr}*hRKU^aPgmq{ zq8FQor@S2a_stb|pJH5`3r?K21Z$1D& zWpgX`Mae6|&)9c-Aa1f47Z-0C^!1eRz_MlFq>tjU| z(_4XC-Baf*de3c+Q}=iVUQ+quV*$p^%OMBz=ioDCk#;|Ey>!kL=J$fPS3Tg^hU+;Le?IH2Q%jw3qG z=s2X~l#XLM&gpYNpA-5V(dUdlhx9q6&oO<@={!K^2|AC^d4|qIbe^K~7@go{HK z={|t&6X-sI?lb5cj*j0~TD##gwyVO=&PsYA3UgEwSql0l(y>jO*?9&3HUER#U`+1KDZNk3mhkSZa z7X1Fp=bieGSI+(s%rNYGKSk`rO83c}W^{8DoJ~ zcY|L)GU!ElF!oI=b`2@)f?s}StdL1BzjeY7A9=Py^&$A8OO5wIu-ubvtt;{RqVW3Ja^ROLb| zp0~p<|7XUp#AftXjq=;ODiqvlKl$)uAqm#L(>>31!5@9iDvPn+x#_y!%7x!Oe`M{VzT39U1sbttq>h1RjqIu}|8 zL+fN{9SyCsp>;U4PKVa<&^jMl2Sn?HXdMx)Gop1!v`&fEG0{3FS_ehzq-Y%#t+S$a zShP-y)^X7~FIopi>%?ds8Lcy;b!fCsjn=WzIyYJeN9*Kh9o@uUdZNyb*5T1QJzB>{ z>-=aPAgvRmb%eCekk%p6Iz?K?Nb4MF9VD%jq;-_E&XU$)(mG9A$4TovX&or76Qy;e zw9b^)q0%~4TE|N3TxlIFt&^p7w6xBa*5T4RU0TOW>wIY)Fs&1&b;PvJnARcFI%QhN zOzWI!9WiTh$4%?JX&rb*NKOgU8{|RC5kV2yLm#5j1TOYt{pR&v z@e}r$k93mMDy+ALn|+>ifxTnt-1$wfl&LFB?T~e~0w)#TDM`Zq z;E4M}A%@qTkuo-b-)8&N-TOCzZ&b-DB{ahCXzXWf1nyB*Z!w3TdCzI@sVl)EoyUf` z;0LNmzkN*{th0M+cuzHYd;FGhVF&*doDou4fjpM02c3uE=UBaxv%uh!&fow=Z!H<@3N;(U?>d6LOBkafK#Qx;=2Xn@H>+XYJ zP%7kWDC76iEq_6KCNyYMx69E zuz*z6csBBoss(Nt90mUo8SUAch&nr~!2US!g|z8nci@+-eB-jTb#XtbrpVp=LPk?V3AiH&8_fDme)!0GJd~Xf5sYq;QlPx zt6aYa+;FHo_hk{rmv@TuKJdkA`6;!CE2zHg$zl9%hx^-$VoKqc7dZB@9BlhNFxMO7 z<<9$+gUtPIu-?6==mC0@?vz;W3ick6DXfK`(oXeOB;#kxag%c~twnE5@ybXauwl}Y zkur?eM=KvRc!EWWO9h>tpjV&pA~}Y4q{`REVf>qTWiIvw7iXl!eZ%uo6P7J-416F| zSw#naNcGQWoQKJaSpwhY5T=uR|Dvtq{o zkG!`RqKBYkDEC!#499)`+8IX=!;kzdf^RvF?;I9ev~~n_o0qMmcY}w<%m%-L?|Q3G za|J)RG87hw*Qcxv8!7?c9n<_6_XTmNN?!M`V5(z5buOq52Gz-+IvP}GgX(Zloerww zL3KW;4hYo=p*kWH{+|io-2|IVjTGsCKZyG8;L@9i_~{nL4>)0qz22r8o(J1%;=L0- zL&gOdihs=C#IcPDzO#66UyMpWPWc1BV8Sl%|G2@nty32MNRNSg8-HRPhOax+-UL6E z)Srh+?cg8FuAFm#op8)*fzo~Ob(39fnuw1!Dlhtv=e-&d8h~GrOFr@JZ5%gN<~+O) z@zLGYv4z=SnMSS!Rq*q51~!??fkm~GV*KIf+orBxzz_CO3rq`wAFo-p-JE&O&y6p( zxiW_5W|{N!a_}A(p32kTQFkP8%AL`VSomQ@J3GdWv`We82(aH~(H7N@s9#<+=oSce ztX44!?}h(c8IvB7UR(b~zJ&oYQM-l8WbktTX2$G6tuBf5=Oj zJ7S)&IX-RpFXA_;+KY^jm*f0Z+Xwr=4OQj*lffI!O3wC!d*AaLFGRf4P+~?T6Yr@J zY-20Hyd>t4tnveF*`?F5_Xg^jpQOB-gwOx9{bs5r)*-LukrBM$YYo%h_ha3;k^eB9 z;kTR?Y2Mcm|2y+J=NI1Z7m{Plj`-&#{We2Jet5fV~83bTv{C|15f*!|0=`2{(IWCnNsdt7&KtZg^hDyAt#Ef$R#WmEd=OjU1L^ z9lL#NXZ>Pu(xC$Z{g~$qbIo&1!JAK-C2vK1bX&Y~&P=fQ9=qRdSl0&nLL4eb;c_wB-;nPzI<6*>KTm3xw($c;A<9T3*KYh%J;D}GzHr( z7VFK#x|ZfEQf~m>TRyJ3F#_}Urj$BSut#vc&|nyJro$F3`wc%xWUADx5cok(#bii# zfFHK8b8a|^eMDWfBctyzy5XPt$O**bM>f}A1c&y7C{}@UJ3^)Xz&D@1jcLZ~4;z|< z3xWfhKk012=cQ!8tbw;=e(?XFeL2dAFM{=km&JIS*ucm?={ zUaj9rKg8`tGmfc)HE&;Wc!A&h<7vWW1u&1!(#{*+$O}28VK)t|yX3`U=L688zqLcF z1O8a$gnK2+Jy93pTxMGaUK3k-_pm$m4Y6gSF<{q>TeJ4Kq8?fI^`)cWB@f$#bX>3w zhPC}+>OJ4~dRa9)L03y^)ujk<%h#7P2OVL5vng920m~bOl&FEvcZYAc2ZveR7#-Y) zynyzBF>Ub84V~?u_9AZ}@SGYa*yB=m(;WxcfBQ6E)ZyM9^7LulwFmchZ(ea6_*Qp+ zbI&gPUe~l(rk*r0KGiG8{(r~91^om86+85{RT`ah2FKU_;{3RNC-OkrM_1K=Bh|_q zW`YA{ZnZJrci;Tw(0;IVrPx*nFn3nrWqUAx@yylC^^uf{wn*Or-I_eZu{pSY(zpJ} zzO=>ln& ztSH6xztfm|SqH2UB^W5^hCCG!>rPd$d7~ZgSKPnE8BIYe!JpTtd*rslA%2NxXQHk^U$KQsQVrc;eaKcYD~z56ls^B3BEquhM>A&cr+P zKigCeTf;syoGQ-5N3vEZD&N8NHMu$OvLK%QS4jp&Z*l)7=7Qz_be_(}czSy9BWnz= z-?(f0?*$ltw-w$7F?}M2?e#;pIzzYV>Z{7j;8*nQ*{WdvXL0?9!LBz>I|YOHU%bh)8(h6*tw$jIM0qAx>$Jg% zzT)G|{(XJ!YyE}biSHhcVs0Zth#)-esIZ zGT=!!|At-zA86d%u8Ys_eeCBL16JN#_HoWF*!j0lWQT%9=}43;g$|i%{it_@#8N@XP|=&^$KF7JiXYG+}&t)tYs?HQme!_Q4UEP@V*^ z^33cK0r30A}k_~-9-lYTl;6Ky%TRqLhyssh~ zE&>kL?^o~2z__*zGY|$J+#fpaWg2?gstPUR0qg9o&z_QmxM2%=%;%s7&EjRXRTp8u zh5jDU1ncQ+H1SF&~fttEvyZdIMhQ zbTaH-7UuE!f7LU=Kc5!x*nrR2ZW$Sez46mJAWRDUrFZ^s>sXx6+K9Rt;K%8)nvY;_ z*u+K}tOdL637h3`8uf4utLlTmdkRh-t__FZE=$X>7yR(;F4?8}pu}KIbHGQPHv~yR@(m5IwMTDwuEZ+{bkquotyW9X5gQa`4_AUMp6NUfASz}QxIPOGeyxV-Nb21H;7s37P zwiVWR{rTR$$)?~10cR^_;P^VR>P1FigV$b_|E6Qz`<$br2^Lw+xAzhc{HRwl9?F7u z+G;NPz=rFkbn>z&*dgMQ`o*8fz`v)D!NyF=OXyu+wr$Tf0 z(xH(z(X(x6XvF$O_M8qU*qqysu1hg04upu$nb99(GFzRaE1+$)z=a zT)?MvJldY2-$m(@%0H*Um-HgU*F(4E-kyfCVsONKo{JlCKX|M*9%%y4G+$I~U4i;U z(@f27uuN*soRiRwum5l-2k#{^BAw+c|-+sJ6TLb78%2w30XrR`M<^ zdN~96&dWpI9|y~twkGAyfo@f1-lPcdSN7Dvm9w#4AJdG#Bca~ZSf2j)pNqg{!JY zO$+vkKZ-1!?t$gD>l(kr{QJIht>IHJ&(X~0J1$sXB;PIL#(KcF;EBT)%;%;z^q1L! zHCgtXuOGzzqE3tF%xJIgzR+7Xy(Y5^>**=iAuA!QKPS3RevAPpus=1^@WK4*zGko=+);P` zA#?uY>my|ty;Hs~XTLK#TsOv=2ZO*{&n(|RhV|&+BQd*Dur}+Q@m28oIn^$A!C#vS z1Q@%+;Z(ovT44FB-pig)_3Oq+ODr{{D{vVuTbZ!V8QM@E+kL| zZXHZ0&;i#5SGkvghhO?TuxlWW_-l^IVQ}*sb{%Vsui~fQA8UcT`NTD(S3v)^%e)Zw##aK_4e;>%&1K#$S&3^>r|C{U`N#=SE8oFyp;rWq& zw2!k7*ZcfcePI&;tlw(WGvdIWAD+I;;>7(weQfbj@GqyOcINDOean-s!(dUNExGM~ z(a$HyIM@wrQN7#$Xb*IgbRPtXgX4v7>zj0;mtyTFmN59+!Ypg^PBGTCEt^?$!5RZI zuwzF5C5^5icJS;;2bFm-f8@Jd(`LqtUyAOnr>IB%=_e4_f$`M)gX4@K<|8q#>zen$ zai5gj1^Qa8r)#C&ZU5@^}-f4Ejfy{s6ok8rE@ zID`3rFHhTleEdiAH?tGyE!k38)`H`k=AJq}4Z2V+oF(ml!B<~L^ESnxKK%Z+H%j<@ zjf+-}HztX()|qlIH~>zK-Yq&A>zB3*cakMo%B!wI7X7X?pKRZu3;wz9yWiK_Vywz# z*2!DJuIBI2B^a+?Y`S{~_()h6rxfbpAJyguhk!$cMz;5%p5FaHOke_7ZN$8l?-kZh zb&m{2Pk8y=+3ary#8~_%PapdTF6>gO9D+Uo-=!R#OSsr#a)>nymYC&!KNAJ)_T8!C^1drXoyYJv|JbWc73uH8QDG#&H#i}9gF=fE2c z6g$^q{N(OgRapdXS7~g|o&;UPe{GfBVEqi_K~9Fwo#mm$pTQ!J|AYr{K!3tWZv8i~ zT+RN$MOY6)5=1h%G2Z1wxks$H@w|QSY4C@*gw8!Q(~2hnMn+v$lF< zmD~ec7KuML1Q$-{UeEN37w2ovbejrYyqoTvU%{@gBE8k7VZT@BT<6Sx{wkiwg898U4&Z_nB9&fPZx)=9;Xew7Jyytz^(Om_%<`MyYbP9e z>#$!GFU%eM3r;`q(tMXJULU^WlR50E-#JMw>EQJ*Tx5g6#!^OY4&VzxzOs4XLjw2P ze3s+-)JinAfls$6wsGO}lttD&=7Rn9a_*64!)4;EKgLrdRlwrkpDb1apTBd>#T?wP zeON~W%y#hU@~z-t|6lH#!CFFlWOsr&w!hAc17~lFmfZ(lYJ0(31)qQUP3JYH=k>QI zJJf5L^SL}vAOO7a(wl%ySU->45otUQ_8K-?{2B9CM#=Q3i{N`-SdXLSaXnKnMWurI z3nz`_DTuR-l#*Su!M~jn?lxjS-MY2@`c?4#e=BunVg23Gqw|F6S)AOZzf&K)pvZox z0Bo7dUbzc=;n$k$CE(@U;r4sM+I7mN55VF#cE)W5Z~JW#T@U^=)9<(_xFuRA`W1MW z)50WQe4p$d^A#iDw!9?KczTg@2&%K({tGUjK1JqU&~W_m+s{E%-d+=u3^4z|Yt$ zwtIp5MN*^Mz^8j_$L{0xOMY9J^1|P7=D_DH1!d?YhH^}k2QT0j-8_30_BVVL`6^)N z@(dLpRjfybi-xCyCHF5?ex(M#lX<*5^M2W_uQwlny~D@OnLiI466r3N1#UP#8odFW z^JkZl4S3R#$@1>t==j)=nOM&ngvy62!Nc-rrNUuf8TD|wKLy7rx{unx{>qyxYSs>B z^NQn0H9#JQ=?AyhV81ktnlD&y*DYZ;YXdLbCBEkh*5lA53sdIxa{F?V%HaoNC2owa z0+(O0Pj|M5A9ZN+ihJNgWAm){?tz~#H%qPwtWkDcN*L>X)QS5h{CTh+(zI$>;7M$o zhUCGlkqn)Fu;q!HuKU2p?tiT~-~{`6XqIa{SYzqU);as(=d&w}x(Rm8`tdIY`wfB1 z9iN|pwL0#UsCZ(3IAc=dU+`$WM}jH*WTz{p<;h%!za>O7&f6RHvJUKW8^QT9rRnWp zSFYKgJ;8iS-vlo65ogU<-|{&T+)}gMYCX7F^@Yhju#~s*6kTwx^9{KcaJu-J?RT+X zGt&7Xy9mBBOX;i+p4dOFe#0SW2$qyw93+bU)~V5%p98_%9g`z?1OM08w!-XO`p;u{ z{SLiTry1XycE!R}#82BNAC+a^pQkUQdO8g8>4PD;eBg_Tzs)_PkazNGjQ=^lf2C~p z_HAbok8XWoR1FS!|Lf?z3)ru#FNwSh#{D|a_G21EWtW5{^Cu%7d}3pyHh7M6 ziJV$0_U}pE{L)~nzicmBGof!jcEDK(d?4Waw>8)Be)Uk9F2K_f!X; z>f}=$eX6rhb@-`HKh^Q4I{&l}0PPb%`v}lJ1GEpp#8{i?Q$YI|&^`yW4+8CzK>H}r zJ`1!D1MSm5`#8`(53~;i?Gr)!NYFkLv=0UCQ$hP!&^{Nm4+ia%LHlUXJ{$D;#Um}s z#x_}>iC0u-OgB1L1Rt(qFSix;q_Q)=)Vvw`|L=wFz)S-8ll$nWlJOb!%fq%Ke=%Q* zbGPJv|A_r$=IYTar znUBq`EByBEguislhg>^wlJt`R6>vd;{ikJM_b<1;yW#coWqMo&F<+MF>z-#mf60{W z!CY{jSGIJ)AokZb#V&5(-Rzs>nDhIyvcyOqyjfRWE*Zb&xTIh1bnwJ)8{O%4mFZtm z*?mZMA&$Sh{F|5QU$H1iU+*;De>`_e*nZ5P4~O5Sz62+1+nmM3Q%*<956;HxXAj#+ zJ;MH@r{hnSGuTXdIP?H`hOq6sDDdMIU!^3$&9RqCc7xS)xE&v0zmwwA_uU_#m(npU z+Xp;&MZv=g=YQqIpp+2!_)_<=@P4d!Z+_NRVt>{zcun~J2dua8D*J-K%DpZjrd?RS z`L=T_gUu%i*0ywD{f}DC{T=(^$v>0R4BDZ8u~o9~E_j9N+lOo~aXoDB)La1TnXWn^ z_8j>{vWYc*V6FNokByp;H|ton*9Lr{plf_}J?!!N)qT?7PdDGspH~BalZIj+vtM^! zb79r}D)?(IXiG8ihBFGXV@oQrUe@lC@&c=GwNFrbfV^Ol2YXk6eFR?&v)_Z>M#@(G zN#OA3b8k$(i|b=$-}egkiOh@o!j{{JPxR|&G4Y3UvMOuS!E7siwF<$aE|=3bmg0Q! z&GpZMZ5O}!P+9^V_Q$PKUf|{%|7QAu4_U9&cL6_f+8NEPf96kyYE8jBuO7{pV%~pz zZXM%K5O!_JS+&5))%uEpo8q>fveARZ>_*b#aZ{HpMLxEI#z6Qj0$IpAv(|hs$H%ckt+T_J&Fv|7iA^8?FYH%4`!Q z3mmr=|0}f%ES&$hIJgY)fh)#+GjPAGCSMW~!SzmD{=H8ZoPJsH`v;9dJ(@{xu$jw#{1?5=guaK_qTtoq#VHqQy<)YI{^D@M~i+Tcy~Qt=T6Ko zvNHyx*zi1E<6XMfAFO7gBee?58L+m-9lZP|OWz!v8@6;AQ&%8f*e~S^RxM>qUysjM z9Czxg1%Kd;oK=VU!%JpT*a1Axc~VPe>EgJYp5`((Jpb&Qp2Zvo&)mTIXb;%)t6~@z zIBc>1hDTu3y%&GP`ET4HX(WpIWI&Lsp&Gm`P}OKLxSV^fY7Dr4lGL?j;E{m`MO@$j zv5LW!;BD8N6#wFS_;s7|>w;}84XoL_VSil?cisvv`&u?Q1@qmbf+d5y!R$&M8{*z# zzKzt23A)LC0}I1@bV2C$9!@AUDF**zk#i_ zO5bbX_f0RjyZaw_=9vPC>zMy0n64*SV&+3<_z$x#8S|~z>P4((qDJsul+*BubkuD0 z=PzJ-GEBy6Ci=@T>(h1o`;uQ2@D2W$3zt5(Cx>Fa(Q@_qGzsyx-11AUVDu$p`@{kJ zBga|e4|wOAd3wuvVZWc;V51g>@fvb)P+}&2kErwA3*bFGyB-F~{O@;mEM3u}2R?Xl zTgPP_7Yegf(N;y?jHz+Q4RGE@)$rTvV6VDPyWa<%!+%RP$PD&*W`OoEIC=VN_ph+$ zY?9fJrGwcn&v~=M5&jIDW8FKz@4~fD)VgAR5WUhP558=n%gf~PJP|&m>jO@o;=9NT z_7ST&Me-(i>70%jU-*~5&UW2b1AcvI@@P#Ep4aNNIR)U|dbeZpVed3OEO%o1UbHCQ zY}*@-JQ+{j0V(j{7YnN!r!n8kb49$x^_YD&yxrm)p8qNFSFeG0dmWp#7xqrXKetB* z!EP!2iqGPZmvP?ekq+2sastoEOV~Tep1nG767izWIlN6N@V5u`3>bmeg~}8)r{aG9 ztK<~`hgwRFd`-jiH=}ES(WN)+&$hgG1mJs+~qF)vk**Mxq0w=I&Y5)v2ZafP<>k+ppq!w>VvO+zUSIxQK(%!_D|s zP!|riSr{QF3VXDFTd;H*xbo`7fSI^|OK->4m4eSbty$!G3jW21DgBM$&8O?!Cr7}3 zmGbI;0d61FklPr>jK}f5H(;+CgNDy1QRgz6U)KgM&Ah4<3jSBY7WM%Aog>gX%Tslk59<}cH5RsP@4=Hx zR&hKI#(c%cvavXizA?Le)}05xon>p+58kO>UFpmmPvkkxjQ68WAJTgfw9MU zK8za{Wy}KC-qf7MjISl0i!&H~(#FshodXzuuZvzTQ3v0Ae#7iBSX_5W^%n581EFH< z$#~vDhnMZn%ACb}WuEPj(#hBw!3I1vM$rf@poCAc?do}@QeFk5A$7UTax zgSUeQSi(K)+;Ti0b#GrS;Q~LI*3`Qe95!B^QGX2m$zngXvcU3NuPnI$K2~t6WE}55 za*%(?CUD;Rmc>uO6LYe4X|0^-l>g;M)xj(-%(o%_BU>u*dXHVL;R~D-JxnsP{m^tUnTCj;q`|W4oi8Ui>@Z@0&j7PI;d95ipzS=Qr&3*7O zH}pTn_g(ya^|dVKBfmVuQ#)`z4+8eQX6oz<7vzf8kBhM!1C+`pVSc0Q3C-)Id7m^d zl;(}nyi%HXO7l|xpS)F)*GltVX*9xyh)l@N%JmgUM9`kq#nQZ4npaEnZfRaF&D*7Uy)^Ha<^|KdVVYM=^Nwj=GR<42dCfHM zndU{)ylI+OP4lj4UN+6!rg_~o@0;d@)4Xvy|JWZ$J3F=&>xpvA4@=CC7q~424{t-f zCG<|Q7g(UUPqqvEHE_M^4X}_7w{$r;GS}iz3pk0#I4=|LPafV|@*TX~t$o44E!Z!P zURIle`E-l)O81b>@c(yZZDsh|xrog%R`6eUzB=>^ubED*!xDob^f}!7Su+~udcQ+SDH z=4>(>uwP$)RJjYhbipT~mH7Vkm)vGO0>5ERwNt|H6_2r0eh5z7`0)E~rt~TGf_Xju#c1O9&)DR7V*$RuaQ#X5y&(^b!l zz-x>GVlr_5&dv&6^$|RFIneJ2#*6BD<5fKPy=Gap-mVy5+=<+6i@zt-I{XY;?-P3s4yoA{AC@AwgwL4$QPlr0k9sR!$NUw4c=WIvc>xozl}y_5Eu&eC zrMtX++ZLR6p1tMm70AO_c~<&%6xbuGd|*?(7_0i`3gI{4fUM?=tE$CV1IS=89uJY1pmD8xHM1{$wmLyQ_cih3_MOF@1yT$Ur7(RGJ#)mb}8}} zv_=oD#XjWtEFbp<S-z*nn?YI`FHV^xf6j9&zYwjbD?%9Q{$HAWa zW=|4?uKly8Cw=?C_KVX^AA)m)75wC2=lyiGdY*?oMfN#sUWI}WKQRmOL!R63_obzE z;Ikj{H`d~Mw9lM;kqfpp``XKc^0;0_*Siv&!4+YBY5$s#N1^&t_!fAP_$o2OH^|F- zCh4CQfjFe?26Y4E!E|a#SdV}ucdmUx*^5{Y?SRtf&sXSbgJs~D?Fe3Q**@C<`PPf|yb=f`{7U+@(C2K|eqo5zsXCv;7H z8CZz@Q2qz(+ZLAlB(4Hmd~vV}XNO%Y9IjypHhkvb{d_X^_1-Ma^WdJ^S#7^y_n-EZ zT5ty}n`=~)&5b$9K&O@!c+0i>0)g}KTveabwgZQKSrBC<4nOIgf8$KwaskJY`P>U(Hx$pjp8|%S zhKB8u{~h~W{xEZ;}g$Dm$JUZ74Uehw08(zf5cun#S_fA&TZ5bpQk^g zSB&Ya;q-24r3$`JE$!aUU-*483w_l- z{DwP!?zjvt{hq3nKLdGL=~iNAz{P93huH;TS3KQt&jk#T;Dm>oSJLF7Aa3# zmvSOb?kQ%w5PYwqtYrXp!tyOXN7%tb+`?aQEBUr&oSP-j(69KU2Wme=^xr`Z4~ahSjfO{CP$c`%Xb#X5)jE z{`%l+1`9))-iony&I$Sb1mk!6hwX8}$QzPb?|0Y}T(mGkdjaMTos8r=kMTS>US9aF z_%ZT=zq3}mgU7-SzGuht(RF0QHpXwH*!^PZY|IyDY>S0h;8hl@c_Q$&kN71 z?4@yCH9ViUkBI)73l=OAY5j-i*J9O0-$Xp`r$f0vz5#!X((_~btd9y@i08uVZ)%?w zdV%@E&1Rn8Uwq!o(d?H;!D}wA(_4h^GuBzLZ3cMhiNVr$cz(4$t-qas`AAACt8@X* zH#dIQD@E{r_2PHSF#kEoDGR^G{N*NcTk`TVG1h_*fo&JSpV!nDAAE`agex|VG5TCW zYtJ=?B5y4(U4BduyzfmBw{nLVYhRY?SO?}uE}x2bQ<3+!SM*!S8SphezNWW$z9;5e zI{#uyhLr!LKxQ7F(Egav`k2sa;P!tRd>q;@Oq|&m|CqnGP16pi4ny~XgQv+H$A%PA zqY_6D&tR>*&H_*S`litI3!cNnv!|^EYy8;qhW9J{PmZ_b8NJkr&%W+EJAVb3>O@c- z392(ebtrs@P6gGmpgI>+2P2c{WKbOqsV!}o5vnsnbx5dA z3Dq&7Iww>Ih3ceG9TlpxLUmZEP7Bp>p*k;A2ZrjzP#qbnGedP~s7}qqSf0?ap*lBI z2Z!q9P#qnrvqN=ws7?>n@u50DR0oLa1W_F!sxw4&h^S5x)iI(vM^p!i>LgJeC91PT zb(pA56V-8|I!{ywit0pB9Vx0aMRlmCP8HR$dKX?M*!~&&+5ef#==_B#O`MI5`@j9H z*8lWQnwT>udc95b?qcRaA?82)(Y{->FBk3GMf-ZuzF)L280{NI`-;)NW3(?B?OR6s zn$f;zv@aU%n@0Pp(Y|Z6FWVu~w~h97(<6P~XkR$mH;(p|qkZRSUpm^ij`p>qeeY;r zJlZ#p_SK_(_h?@}+P9DP^`m|NXkS3uH<0!fqpw|g1p9-sBPP~Y8^l@m$6ie>$9`fZ>w63fJi?ZqXN~HN3uJX-AX>_A{^b zlRsM5A@8C5zQ8beuL8euc`f`FW_E5`*dJ}P4R~;~26=^Nt^ebzqc%Kq!KlVPWNA!mxu3blnCSvtVEulK;UC$f7Bpxx>XY#cBbBi zu}k}=tSjOH7vyZ2%ESrA{+hQ8R*17MUR`g*_}%}7^9xPJaaT{pi4H{0hytveg_d3 zcsGsbljTnFwq)=(S9`~P};UFruOb($@90GzTfp|=3s*s&{V6mgBm zbJy=11GX|hi1BaR3pxGBIXR= z2G|=ZYvOvW5w}wLbWWxSJb0_wxa$n!C|{d%M!-{^d*nStoG(@I+(#Ddmoqbi^e!Q; zXCIv;a}exj*y^<*1a+$18x2#y0g2ki;z97MNoYD%f=id1+L<1~{o{BqH3{~SCRcGt znlJX<(_Tm|248ZVvSRE2{CFvSS5|>L6i?(9IKuB!c&}O$e7;joW9Ay^!7=wA- zt_iN)i9S0wswaDc=M+x)9SpzXspk)ZZ-aMn)U?cjAIQwGv8x6gl;5`VAN)@7!m&Cp z!4BsRdBwmFrgdCm@^|nW->`I@^@!t_oUmUA`>uZ1bB%c{#Iej{>@C4fHQW_DG!Pfe zi_|#`_SAZjuB3+Nt-6uL_!nB71UMw+VW0lVWitos+RcsFxeRsm(xvy=zz6o2)ohYN zUE3GI`yKeccW+PgdBC2Q)85!o3iiFK7#O)2*SB@%{W!2z*{zCi3*qO2y?-3s!@WAZ zL;~~I92XEN(pWkfi&;b`4pXe!qKfua3_pK;6@yeBtuCSLku$pXZas4^F zl-|sRz5MB3t!5DTt=hS@!u*IkALX&t0FNxbz9<^@v)o*7o2R(n(MhK&1YtirPTD=+ z7d(F0C4D90ZnMYq?k)h+JO>)LqH!%6_o8tz8aJbHH5zxLaXA{dqj5bN_oHz^8aJeI zMH+Xc_9(SisXa^WU1|?gdzsqP)ZV7{IJMWQJx}d@>JOm)0_sno{s!ugp#BQ#&!GMe z>JOp*66#N({ub(wq5c}`&!PSv>JOs+BI-|~{wC^=qW&uC&!YY=>JOv-GU`vG{x<55 zqy9ST&!hf6>JOy;Lh4VX{zmGLr2b0k&!qlN>JO#JO&= zV(L$({$}crrv7T`&!+xv>JO*>a_Uc~{&wn*r~Z2C&!_%=8V{iH0vb=C@dg@?pz#VC z&!F)R8V{lI5*kmT{yy4Qm-gMIeR*l$UfS39|LOZn`U2Cw!L+Y1?K@2S64SoL6Snz8 zUt`+$nD#}ceUoWlW!iU{_GPAhn`vKX+V`3Eg{FO@X*9A2w! zX}R+~_U)&60*!Zpbpl?0NQ#CISDy1HF|hGTS&qUB(2>6EF!ej~d0hfO2Jpj<;1T3- zX#)FqwZ4l?h5tKo(clSij{J1LTbYQ{Y?VVD8RGLk_QR}P?91oui_ieinJ!htQ;2#) z`O^{nV8t7Xc~@^D|8I%$z^y#kZByKDTrWj@T|~{0sTWLK6)(H*4)W2tlj`2(!cI`! zJ94TF`&##C=`&!lzNb#Q<WqTq>Zb133l=+kS^@t&?E z?SkVUgJXHZ!F_L@6_?}l#FeKR>;R8nTl3*|3HHG^_#KtOGOq``G;d*_``~212>6qi zg2v7w#LZ^d40KEsNHt zW|%VdI&&{)$aeH(>`Z{E}%<0iGi9TyhQA@PqBPo8WvE z3q`d!)Ncj|@dkj^jZa3s$9PaqNX=dYw*ET1<w(^7Q+N6NT|DS;=;k2q_`V0`G^4EvLgdIj;%9~qtXl336F zE8sQvpS!DLJa(5W<|u+wBVq&jz0@z;1V`oAH@;h&b zX2+!>Kj2AZO(({;Rce;wVsLt&uatBs@<%PVhSx%`Bela&>P8^yO+9|}@q)!THizFi zjC@dm^U~L$50ddTQ0Ju=zE3}nhXpP@$YH?gfw-$rqVycF=bIlD2F|Dth%c{efd0tg zvo)oedtvv=o|Mi5j~gkxVeP{AKNjZ60{5E9G$-#s{_C}~j<>;g7WHlqu|a<7rn&ze zPnBwVd~rMKLHOCTcjLHwUx7d-@@3wgZQy3~Q$*OblJ9Q8^(?cJ%7wm)ZFp5%%tq*$ zSv}&?0tY?*q2Yx5iA#l9Tms;KJ;6ModdSyI6_k7f{g~GeJ{phcAYXNSeoZ}i$O)ceD5@_-^{1#l71ghz`c_o`it1xg{Vb}lMfJC+J{Q&RqWWG`|BLE_QT;HgFJ?mY z$EZFT)i0y^X8(`=8PP|h`e{^Ojq0yaeKxA!M)lpO{u|YYqxx}FUyka}QGGh9Uq|)r zsQw+*$D{grR9}zk?@@g|s^3TT{iyyQ)d!^dfmC0R>JL(VLaJX#^$n^1A=O8u`iWFu zk?Jo}eMYL^NcA15{v*|gr23ImUy|xiQhiFQUrF^Xss1I^$E5n1R9}35h{j~m{>H|>y0IDxQ^#`av0o5;{`UX`0fa)Vq{RFD7K=l`>J_FTnp!yC} z|AFd5Q2hw1FG2Mus6GYNub}!CRR4nNV^IAJs;@!yH>f@b)$gGC9#sE>>Vr`I5UMXi z^+%{a3Dqy5`X*HWgzBSE{S>ONLiJauJ`2@vq53XV|Ap$qQ2iLHFGKZbs6GwVuc7)j zRR4zR<52w^s;@)!cc?xO)$gJDK2-mQ>H|^zAgV7!^@pfF5!ElE`bJd$i0UIz{UoZd zMD>@bJ`>e%qWVr$|B32DQT-^YFGcmIs6G|ducG=^RR4Vr}JFsd&`^~b0_8PzYN`es!BjOwFN{WPktM)lXIJ{#3a9|z;pZ7I_Q}$nH<;vKE{9M!TdHcmKkdmrgGSS66|H;}FT9e-8#S80U{?+JhS3b;?d<3e6VZ~!*vo?UdwIWbpuY0!rYVfxfOfjS znFr!(HbKTe89g=L%mrhiywI0?KioYX_PWc-k>R_@1FpDowOR-K#Na?f$V~VXbfvih z!JbcaIhP~PS5)_~K_&Q0tV+fSapXDJit_e>XFTty2}c}J=1}5QPWT6&rUnW!_SlWY z;}I%gcHYz^32EdZ$CMc{`qFlrA3OAb3+!$zXX@|gKfj;N99Kw_iByHZAz$@v+A!jl zFW2+&GyVwM`%5jdVeftNww_uFKC{6p{iH1PlyiilCc)p)RPB{)1s;!75eNhGnOvB; zUk3gPXM@MH;ZGSI2z8%>_rGsUA3OpcbJa}_#`iraZMpUmEVOU0&MO?}Fmv9&68@YE zD>V}O@p*dXYfNInJ+X_O0~N(t`<_m9Zv>|g4+!w9!d@-NHX8@Ou5O-U2K!aAY>FA< zPqH|X`dEJ*>f5#X|Klw2_7BT8K>s3%y-6R(J!8%Ggjpgl`Lj~AI=Cn1w)0MF_`l41 zqxXXq`Shiv?4Xy>p}V3I?7oMi!Q26Q0rJmH6yVQ#$C+`r!V&kc_p*Q$IPS*EL8blB zD_c_R>IzofscF>d20iArg;5#cxnC{z4tt{B!ES{s7yNDe%8n-|dc(gkzoe1ThgvkJ z@qrfN;>|y2<{buSj4o{G^oKvpoGt1KnELnN`di4x#{OR)u8czS9UiC=crNugbK}064r;cwfulCBS9b=-4}~n%!*diJcSb%2JaI0wZ`O?n;l3s< zwd7xfdWJmXL@p2Tw|5bW&ykO8`FeWK8?fw;*stC9#8_(TGXIouA1}pKN?k!c1W#;L z<}>iMpS2f$p`O5rtzLOP?t@>uWx6Z!qgkynVlm(kv(D(TL$@)6;BPbDthum5kVd_q%KU5{Uup*rfllMgFr%a%|``u%?7?&;_uP-DHXRxX$VR^BU~H zd138s+rT_8wWjFde80|){=xWBZG=YRR^#{7bp2B41P4rdD_M-+KXI+;wU0e`qcb1% z3Vp2t--M4&nQcMNdwvNB;B8z{57|;CaQ-Qg-0kJUiz%F~2X> zu;Kvn#}#znS_^m+J!CN)I1Nqzy z53MxVzGAvbW>@`;e>v8$+6$#A>ioVscL_5J`>l@ylb6| z=>NHv6W5k+QbElSUZ1A6D*hSrI`;Zc(Ypf{$vQs#YqB`Y?ue_y5%8*&-s86?VLtqn zzhpO9y=$fQ5v*@Nd`_)%0c*VVZ89E5T>^LXZVzyu!b!z))XB{Hyjz%g{T4osz474l zl^v5cz;2&--@ga9tcc&uoWJ*uUf=A0Vl2CoslcW*D1BQUdKF4$A5sYsyumc9{he*y|^_P@drB>Fn9EA*Dt@t zSecvVb}k0@ST|HILS4_|gO}NQaep{o*LPX{Kpn==pjI~6{?(TFHr$VcB8~yh;L`@# zyG3#RZ|msayMgK<#pjzo)8K^&bpE-+UyF%?Z45XVUs1@Ry4z z+RMO0{~ud#9!_P{_WhG7Lx!R<6EY@4N`=~_5QRde6hcIyNGOENL@87hrOYZtC9_CW zrY4jjGF8SV%I|yiKKs4z-+Erh;g8R8?S1XN*R{@Zo$FkpCsGE%hM@%&EttBIiK)W|oPKF9*$$3x zv-b=7hiWx8g89#|)NqEO*P^=+^{7^lvJ zb&_({!)K{|teIWQyjEOBJ=c6Tr(*EiiSQ@OsdbZ!(rmJ!R3E4_zMUA058GVi_JQeR zlhw)=(Fo>9<2CQW_01x`=YIf`eL2X!9b{h*vhN4k7lf==BFw zdhIN|_LX{${P?H-$k&o}r(|6!S+`2owUTwOWL+#-H%r#ll6ALaT`pO-OV;(0b-!d? zFj+TD9~*jIF@a>~=tQ~GmX;+Iu@7vz}CGComG`(!j z`+PVXk{b6Q{8N8kH{Ha~EtkZ4Bdf*_oa4BhwNvh{f+X!Zdu4kknEu|jpS}M-4%AjP zbHnvJx;h-pczyWD6M=(Z#&xV)QspFROj+*}_k&v!7@Y!e&g$l(u7AwPwc2(+KF{5< z(aRp!%N515eoITzG`4nlxq>$;osrwVOp(5Fvccq_ zgd{ClSJ5j9yvmG0P9NvCX6A$zMS{cUxr%j(O46b>skMiK#TS-!_=rf-425l0odR3E z*Qgd1mZW*Eww`hacQ4=`@mwrPJGwt&)jIHo3BR;jKAh8?QkzZ>Vej<~#Q zWdJWwx_j&Cd`VjN@@Kd`l$1N?nB%o#yPeD zTsJ#iS=fy}c}@bn%HZU#sFMCJ)Svgtm~wzGUp*n%`W}5%H%xq+#P?my5_iI<6@7+7 z#Et90Ii6YnIHY81JHd(!JW4D1j)1tnc)?%G8aPbjQaDUI8!@f1x zPsuLwq8;aWx-R-#d>q$zec^p^8NBeOUveAn$D&sLVmmndj;{T$-vGkoRF_y%S?S->mRx}P5Z0R25s{i-t#e#O;UM}-lbv*f42rvR=r zTU2*t41JV#yjE?Gh2Jp3+O*-f1ns1$UyC1j&q@cO0L+J`^U*D?;H7>!BeH+dhn)GQ znhiMZxZZf}JW1M=sg)P7usjam1MIVEYrd_(GAA>01yH_pGn z{8TFTUey5>U>??VUkG`6et2ggIQGFo@0W`p&+9G}WrAO}ZM&x-AW6%05Y@O2wwpDP zAHw{9qNr$*27bYoET$$XNlW7W(MDZ2+W%CI3-V($e&>D`SaxlSc`fAe;~Q>^a`30w zXC2})lC<|H6Ym|9O>{|Ha~OUM|}S{>IM1`y29q>fYeR&ER7eg<3&klK)p% zB@Qd{lq{8`*&hDQzY*8vmIyvhf;@_yytrU5SbZ+^*BF;1jnVhZjw9g4@muUZ7$27w z%N;e~Y##l%12foP?2?sIdTIVrR`5o)VZ*p3w{lbI6^&Iyn zBxuai7Z+{^qi^QObL@LMRoBXwK=H#(^4suyjhWVu=YrW+w6FvZqEDXtzJJg6$fwm( zERerkp2ey>c>ewe6taVQk!Q%47+(+Oj+|S@19>~o(qE7cc6fJLijucgH(19Sz`818 zcJnabCU&zFeg(6MJ*7pqVVzraY`Zk(+o{}TqmG!Lr*=Nq(gvrS7b@`OX%C%tMS&282ke4jZim&6~rMtFzk5r?N_LBu) zS3y2f)>xVqRH84&!$(K_!H2VV$GKIYUcKcW_hT^KmKbMyF8B-PoLeZg9M^F!*$V~8 ztITKCE%D$+X{CSiyzKVdS5CNJ(VE$R^6nS@`l>Dpx7JTs# zET;V7^}c7Q1KC`{-3$K8`+AKO=o{d@pv3t z!6*PuG7VTX1NQ1aQLqFo)1@bL=`H%S-Lj372Y>#}Y`quq+raZ_r6t&=rRMLwJ)cmQ^I_^}7`SF-qo&?x^f8=QQ5Xw8XwawD3jOQ*C!042%$S)qXEhH0 zOvmuf5%9a?;ab!=-M8L88{Y#C>YO;=4f)Qg3w~w>Uj5xi$`a>(mkJ$HGX~3lT|Ldi zB1zl6E68&zm_P38)Jk?qnvnC*qy<<~IXdJ4`*f98##q@Bs4V`TjURCv5+Z zh~cLWkYBnlK-zz}1hViW{>97mGc_~?u5XJ+jfI@)U%@}e(1Y}`)C=m27RryfL^%=o zqrcXEzSaV7OfPHuiql5jwq3CTPsJbBeh?sz^BR>)4}&i*UzcnXhPd84WM2?i zH@j)-aSYy-QTmcH9%Af@dWj15z1VU60n?4G!5F{9GtX`pA)lGe(q%M<=O^utTH+R4w_67mED1UKAMV=>%p_-6V^%?FR{BP z4Es^P)x2w_y>AP~Z)$Y@1+Y-9S(7&MyPS?PK1~trPYOqW8jJFiQhHf?nDk|6~v}Jbre6YvD3*8zk(cdomP^KKXWnF}r zGxF0H*iH!nZ+skyAS3VfwuSLJd+=+$3M)qY^k z4PAmg{HP~%5$tmSC#~vJlUa!I_?*y7afSzHrycVDiHXX=)Omczt91{5VnbgM!Ru!z z<{1_IuFinCQ_hek4W1vhawHb{9=mlJ43nt;^08Jlf88NYJF?9_jXIC3e=N62v=RO9 zYHDomfPZOMs#u{<%+>PXTu}zW{Qd7woWWCWHs`pmArHIngs49Fn0@L*sUANKM>}%9@ zHhlh0E*-Ta&|e46g!)nEpIzB?kz>ml^l>rqUq6HQXBe$Dbq@#2$j$x%8@MokRF8xH z&{}$B5nk_^y;PhZzh}E?n}{MfMMe^|ScCTno4RQ|F(l3dcCu z;r(CCnie;K|McB&GEWkxu_&r{j)ChYUhZ|vKp!6_lT(6t9+!_?>e0$9;0m#Ow8Z$O_7N2j7P7x2d2d5iYo`DnUT*qjG%`rB%H7 z!QEfY=06=k-;r2932J_^t&tZmh5j?hR%Dz5zm)yWvXT}1!M)#I*T8&y9+%&8ZvpzE zaHpnI%sx8z-Ua&Y;O%>T)?ltC_0DMMv-WLs2k(LJw=b!-kVU`8Q2(bdz)UgBz8|6A z=INc<+y>Usk+dDfeBW3p%{C3bZI>uVt>@T(Sr{e;`T7{fX2J}4c{O=6%mu96!2Z^A z73xoUZfwZ~@0h5JlUaxQ!XJi%@4#DbspUCC{`8{`$_;^c&)KVSLqAT%S^KT}?JLuqzXy!-!VcuDeO zS<`lH$NCr}jl&80=E^QJ@c)*PdJE8xg4Hfdi zi%i~Yhg(2@?R9(J0bV;#iqqc``}i)!Ki?1icgCoWxf1=!t|qhPUI9N@ACp1ptGk2F zQEgx=-kE?nYxs?^{)*IjRT-a_gfKwA$=kW)*g>DJKGWSUg7u^eW$Mvs;HNqleSLA= zwsOPhG`Qk>&6@Kzc%Jh(r>CYE zzm+PZYw-FKB>{gA6ZnxWy8o^p$#d@bY=pRO>hr(*v6`8`@!pL3@VNTMF-pG={nGBD zpe2nKi*`Q&>2DVO>;1H9Cec4o)^#Q+cI2z(-scxoTs+#WL=GN{v_Rh?sIMC;Tvt2N_Gj<8ksP&hxM@^GQm2GsZU| za5#M(`Y!dxc<7dZH?U}xXkvX;``tiLBzR54llAZ559A%usHFTA+a9Lpm$0soJ3UrT z*^`EOLDP?laUR%%Rr{&=kiM)X!#q=*cH{7~zg^(&st$E-=(C%dKSgW7ScJ+AO~AU# zszaxO!HU7>zb}s#r=4pH|8oEw?KE`qSt$C@lvZeM1@}gtmt#ML^T2uqN;SZAeYEX! zk&fbjG2Op{%Y`L2rUO*IhyGf+E}=hr`m>_^G-v!%fArVV?@N9z`Mnf~?@j)$kHqgL z$Aug>a$L!AC!Y)X+{oujK6i3nkn@I|SLD1S=OsCB$$3rAdr~e)xgq81f5{yom!#a% zpDkUkNx3KW0;xAhy+Z08QZJEui_~kR-XrxQsW(ZzO6pxwFOzzk)a#_)C+z}hH%Ple z+8xp^k#>u;Yoy&H?ILM6NxMqgUD7U-cAK>8q}{(l_ywfjK>8J=-?54COZsafM0F6S z9f&Xyq5QzZA^Y}jIFEJ6iAQ@WKcjF7l1LUmZ~Vtt=;rt2}dOZF3o z64tAgEGu=|!Q!D@{65v6>>&L6`RFLt<;EWzu!J8_ zP_6H+0l#$7g@K+taN%0RC;sqbgR(}etHI}U#n$eWz&YXvRca+*=dT*oZ21d6+2`*5 z%y6*Qs%O)4E0Aw3$o#w*di2wo;>Hu|=qnl@bjTjO-0G-%!&<~`dhhFBfdzLgdc&*- zzk0-?Apm+mN`0u=4u1aImxC^q;H=PT_5pC_yEiV2D7&Qp)P(Ab&nNei!vg$n$K^HQ zc>nsbKMjH4(Qv(@5Gegyn{;S1|zE`wEB zf5ZfFQRgN!*&hV!3G|E3XFz`U>mFS>aJ1rfQz5M9%S*&|Q-01R>tn->cd)M4mQ{8P z%$8x$(|j9#eTd^6b*}87b^EH>RB_rRV+I>laOFcUhr<`)hbty@EdcMn**a_rKlbKi zpB5D-RbLDm*>)O!sY=(CQZU`#gvAmCg(jV7o^;faz)A=IX^LF73lR-g_;s#VTfhFtQztS zKrcVd8uye+M)a7`+f;cAXNgnuoL)c0AidpeB1fFYF{CMSHU;`%dGVI4TZm&HJh@^G z{^h~Rkb}5zV~|j%DELlGQF;R8_5SNKGnKfvR&jC2e&o&QpGE)d4;-_5#&F%h*~eNE zadu?n_4O=x|E26AZSFLjgUcq>=>s-A93DFVCi=#I3lS~`znuvcFhkyLwchf*!{Bwi zIq80d;$J9jAMGXC zGMf!R}bbzM*Em6{EnMgPk`AML^8O`_)hKVjdNb5$sVb=2;dnoJ=N7`I8B z3;gZk=(Es;Pjr9R@B{`xcy-g9DLkqD!d7G9PZAdAHd-W+FIW+ zKElFkHWgsDXKw6j7{8N8kDZDJKRNVHE94RCe$)fIEWv5d+d`Hh57XT6WbbnD^K);S zOrh`7Z6{~H;Q8@37T!#Sz3$GI6nO+D=NqOXR4yX}rJvpHzj*Q^=R%)5^szVKeIPc1 zv7qKH4!QaFw|ec>q-xZ4-SMmz>%_H4EyH1^*T^UQ&udSG?y5HI5~o$|aF~n5b^5*N z&yjr2-%6YlY3QfAg3AgHkbYdul*9;Ck(l^kxhxf&(p%yMFUvs-qFpDn?>X%hb61YS5&KKc#w zhAnxSSPsPlCM8roa^OaevS(oO`Qf1|VICw=^FYIO{;Kj2^o0JO-y#-}P_+;~NlW}L z@g(HJsCl6BZx%2xSPYs~`y$Rg^ChX{6Mh%{y#qT&AB}+>Ox@f!MvBwCt)_(d@V$>T zPn@hzLS1x>?!Vu8b9LmLRXXNB&-Tt2xE`lN3%v{d_hwP)Y#X>h>;pd|es@EGZ`TO; zTa2Vu7xa_iY1z`{_#GQwde5DJ{)uJu7u^BQuW6Q5y@$Fh_qy2(uz9RSjW+a=w_R}8 zC$QMBfs73JtINNQcg=ylo^zWyVjbc7iPay3@Y^nz?yM4RM}4HjUORm-o879jOHlU| zCu_Ce5$vKQYh&~Ub!Y!G7M?kNaedGi_C*U1tiyH3s4H5Rp-&1O`3|5r7wv$Jj?@7B zGa2=;Ya(Fbb(~IDu#TXadGqNCuwlpX>c4#0=leBVR|iZV>rLXdQpVs#&#p^FFGb$G zO}g(dzV8b4#JV}W{;t>GpkALB=TX{!y0qnJ$bWk0nUsi%f5zH*`Z8X54y<)j zaNJej@7zkQzKTL+Fk*>NF%2p>&asCw$gkE0`o{?;@_X=taVg?Ptr#Mcqnzh}rq)5y<}?pR?V7I)dLe zEEmp$XLW^Bs<wWj1;Fonh0+QT+SL_A9=8ui+>v}_h8ydQ`xfltzZ!;hWb@|E zokEf{pO~{nCg1{tTLH|dySaSg^>p7^tdFGjYG;6d{{En$58fgzHQ<0cf-iyf8ZScO zXN0NC)nT2oQNFyL>I1#v=o;x+aK|G3`@-PoH`4s5`^{rAeNY>M^&Z2R9vap$eFGl} zID`EZiz~xW2NbU3w0aTPAXvxbx)eU&_K`(aF!FEN@y2&CwxX&0iBunG`dG`Juf0$8 z{})>EHF6DJzf>eofa;?!QJeKm1WfO1sIJF#(F>nX=L6=F@@K)%miRn)hu0U5FPM3Q z-(&mP?g4fGk&~v?#rXb5H@iQ`1NYc5`8A=A;?TMN_RaWyvkQM+ZN~V;`Z}!+2ftf0 zx7limB+a9{w7nCo{BqB04Xo?li);{3#_x|#sM36jI*rS&bzUdHJVQcqf54&h-c9v@ z^OCHeMB(*!I`4VOV|Z=N&E+d;9@-K(N;<5_K2Kdp@={f=i$AOIPto(wsUY zcTR#M-S)KH#C+A|kh?^03EX{epL8b~m^+$TM|i zIMxek8zUW1_du6jx~xWN)zVCGUEIpMuLjqToE}Y!jzFKsJ(`jE;KWV4S>MDWPIokD zpz4%9Xt<_UB%wdYHn**LxWCNoTJG1^&FBPE-by`UGl58SDIGls$)S$}oyX?~gx|=4qH)^co&psSlIR7ypl5y8J(S ziGGd#+GTQzx`U|yE4lhy<160lZG1-PELbpj&ui*Dh_Am)IiAZ)(j@&^tn$EEFAA6k zHh7vTK+QwF!zO_{P%rqqrBq`p=9Nuk#uh=;3o_JIdgX#22&)UUK#qs1N)tJtheqFo z2zp3M(lQ)7rmewq75cwrA-4}EuT2MoS1j2(!-D#T#K5=ruY$YdtJLgKA9$>C)%1O^ zxH|WY6Xe*a@onNq@X1)?)U%LVgF3$ZJebD^XDm~bAh#Uc>i4&Rc{f+9=|ir9KM5v= zfZOf`2Wmo22IUgB&&LuJ>XXL%&Fe;pQQ*et&$w->3vF|LIxt6_Asy zeiCLKuoJlq6=vpOFYcXHtewi$f`(`BJ&&Gtx5*yPDmk)6@FzRiCvM<6(XpQX$R z_fz}6!l(v(e{?RDAr9;QMFq~&;O!g2ip$QS{^*XO0x#-sHgF2w@K3x-DjDGY9t>|{9)z4m3%UzcZiJ#$;*NJJXWyZF%S@&#=3cQ1<%^o+lN zngUh-Gn%eYzcd}^po(rbQUPa2PsXX-K;6b6)kq8K{_#q-wOJUiw_?Nj;7;wv*oS$j zdr*Fy-3U%PBNDgz4*C`?2;po42bW)W(JDfpou?wX)O8m(p^;O?Scm0X+4uxpZ?c@l zrwnxsvpbt8)-Uj2cZDANTh^&m3vQAx9{F8^{=>KLMR$PLUsG#d2)!Qp*xhXy%$6`C z*YylOQ`zAKOA?_6_dKyOc!B(=(*8U>aL0wyV<({Z9QB1_&A~MS{a+)X_wM&q@tc89 zcIufozrnec>_=8^1#e_r(HZvveScnUE#LvGdOj`@Lw(=<$g(jRaF}h}>xgf7-V)8{ z#K5(m8|J1aC1^Ce6!#Ti*6MTG2Y$lNv}W<{0eia6lq{Xac-_`)J^}ubRCAyicJM*f z*E?syOu8e@5lpDBv^h4B1HR(w==G3Il4i$rQmq+mdaRIPhC`AT;L+dw2OM&Ccd`QP ztWRg1rvh|uRz_gzP1xazwFc@2;IV26>HY8n&hFPgrUm9ar!1%szv!=C>G}O&%dLWC zTTwqv*WDLxMU_^g-cr%0XUA?_r|a*YeasTu|BC~DT;+!SigsP_~VFecz@nrvvf4Wn%mGOkloGJ~fQ%eu@1S zN7}ht!R1>zJKmvwxYftfEgyX5f>&~MIo6pUS;Pc_8-M1-ij-oX-6t&`q4kMIF`ejnVHQ|AODT zncZ{H&)Ysk@dB7P==a=FFb9tqmkxO7^QG8&@Y5U1T+5+v9Qda1M&kXSnnrRrf={w8 zCC=&m(@7Nj?`?$+qBhC-~{K*E- zf5)l30B_}UbuH+^KH$JJW4FPJZ~Qz+)obpqlaD_I-tjt1ekJCI<I+Tft#-JB2l zA&(`iRH^fejGmQd{DFNFI;EjH2zw^;BC?kW^1VgOd7P?O{*u%6I^rw7uk0uvRo^_g z$8byR1nl9N_U2UZZ(7v3MLR&(}0J}LdZJ5o;avtRl@ex4WM=h5{9 zsYggXL+T+?Pmy|z)N`aBB=sbzM@c

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 7b1140802d0d08fe00193554d0c0faa89b2b0c0d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 3 Sep 2025 13:37:06 +0800 Subject: [PATCH 058/135] fix: change stratigraphic column QgsProcessingParameterFeatureSink description parameter to string --- m2l/processing/algorithms/sorter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 17fcfc2..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) From 27d2ee5b67434fd5a68972b6bc1b63f6c6dda3d7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 16:17:23 +0800 Subject: [PATCH 059/135] feat add formation column mapping and validation for strat column --- .../algorithms/extract_basal_contacts.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..c78653e 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -78,6 +78,17 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterField( + 'FORMATION_FIELD', + 'Formation Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='formation', + optional=True + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -127,7 +138,14 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") + + if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): + feedback.pushInfo("no units to ignore specified") + # if strati_column and strati_column.strip(): # strati_column = [unit.strip() for unit in strati_column.split(',')] # Save stratigraphic column settings @@ -138,10 +156,15 @@ def processAlgorithm( ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) - geology = geology[mask].reset_index(drop=True) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + feedback.pushInfo(f"filtered by formation field: {formation_field}") + else: + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -154,7 +177,7 @@ def processAlgorithm( feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, - contact_extractor.basal_contacts, + basal_contacts, parameters=parameters, context=context, output_key=self.OUTPUT, From 03a6ffcd2d1204b58553210421000f3c84388ba5 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 16:39:04 +0800 Subject: [PATCH 060/135] delete input files --- tests/input/faults_clip.cpg | 1 - tests/input/faults_clip.dbf | Bin 49957 -> 0 bytes tests/input/faults_clip.prj | 1 - tests/input/faults_clip.shp | Bin 9308 -> 0 bytes tests/input/faults_clip.shx | Bin 380 -> 0 bytes tests/input/folds_clip.cpg | 1 - tests/input/folds_clip.dbf | Bin 9713 -> 0 bytes tests/input/folds_clip.prj | 1 - tests/input/folds_clip.shp | Bin 1804 -> 0 bytes tests/input/folds_clip.shx | Bin 156 -> 0 bytes tests/input/geol_clip_no_gaps.cpg | 1 - tests/input/geol_clip_no_gaps.dbf | Bin 305246 -> 0 bytes tests/input/geol_clip_no_gaps.prj | 1 - tests/input/geol_clip_no_gaps.shp | Bin 103704 -> 0 bytes tests/input/geol_clip_no_gaps.shx | Bin 588 -> 0 bytes tests/input/structure_clip.cpg | 1 - tests/input/structure_clip.dbf | Bin 45216 -> 0 bytes tests/input/structure_clip.prj | 1 - tests/input/structure_clip.shp | Bin 3404 -> 0 bytes tests/input/structure_clip.shx | Bin 1044 -> 0 bytes 20 files changed, 8 deletions(-) delete mode 100644 tests/input/faults_clip.cpg delete mode 100644 tests/input/faults_clip.dbf delete mode 100644 tests/input/faults_clip.prj delete mode 100644 tests/input/faults_clip.shp delete mode 100644 tests/input/faults_clip.shx delete mode 100644 tests/input/folds_clip.cpg delete mode 100644 tests/input/folds_clip.dbf delete mode 100644 tests/input/folds_clip.prj delete mode 100644 tests/input/folds_clip.shp delete mode 100644 tests/input/folds_clip.shx delete mode 100644 tests/input/geol_clip_no_gaps.cpg delete mode 100644 tests/input/geol_clip_no_gaps.dbf delete mode 100644 tests/input/geol_clip_no_gaps.prj delete mode 100644 tests/input/geol_clip_no_gaps.shp delete mode 100644 tests/input/geol_clip_no_gaps.shx delete mode 100644 tests/input/structure_clip.cpg delete mode 100644 tests/input/structure_clip.dbf delete mode 100644 tests/input/structure_clip.prj delete mode 100644 tests/input/structure_clip.shp delete mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg deleted file mode 100644 index cd89cb9..0000000 --- a/tests/input/faults_clip.cpg +++ /dev/null @@ -1 +0,0 @@ -ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf deleted file mode 100644 index 35f7f0eb882f291f8a96c7470670505b2ea251e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49957 zcmeHQOK%*x5jGMmf*gWegPa=3HE!_zK#l=&T;v}Jv)0(wMlVS6!p?2~dA`V~ zzc@ZVoqfJVetGq9R(^l|>Gs1&{Kx_+AO9;U~Sc+Tm+celsCPdA6} z@y@GW$Ith7kH^bb=}Jo9;(?_1_wrsf+dj$LTI-e;L)_BZG;RFf-S**rgVxT^-+xT6 znTyWY^wy-eZqx6=$;_!HlS8(~;g2yR<~5$(&&5S!ohi!}zWo}fzhr%cqh1=XSEdBqiH zDdb>I33&C-6%g=h8}u9CyaVZ{zP_yl{PkE#pVSqwgo?g*4uSLYcV`5Q-~OYkDFnCh z?}!z!k1pgnms(wkCB^E!iHv~pBfy>m@@wDU!Ui2na3B?Tx0QgqA!7@?mz{{$4_6B=>!RMH2jIpp4FhbC) z_oTP5L47JXkczw8O2FNaF$E=H28VVU2nh3cp9E~H#nxEO(NsGo28`{uZ0EWpSu9)+ zvUMe&5O63BJey+Y*4vFoQ!o z4FrVwyH5hPNI=UbMB_sm6oOcedCVplhovx%OH_xvG)BN485j*bo8m2O&_M(TQgL@% z3Ah_Frl17O;LuJ30b%~`lYpId2+tx5Ef(y-jmQIb&g3#19QCll7Ml{r;(5Rz&($&D zEo@Mq3J#>=?zR$eH)Kpf37Em5odyEJ{M{!3+mLMV7Q15+bs0twFfxFPoe|?exH(|Y z&qgE!vlVc}nz(^yQ@n)@I*8yvD(-G80e3^j6qJA&9NK9hAk5!=60q?eXVgHywT3!` zfF~bHn7zx%6$Bun%Q%p6Lcr@+z&OsgdKFE5DmajeyW2{@-H^LM8N zoO5vn0RmghAOfC>Lmn_jK_&!_V)$Y+Pea2o;?;S;TiBpJ6&y&#-EAe{ZpfH|5-@{9 zI}HSc`MXmB#_=?dQ^*bn)dn5JK$V|7I}A~g34tRYCcBdm1eVARJe%UUfeku{;6N(w zZYu$IL&g-8fEgUxX&@lX-<=XLD&%b5CTT{7quWzaLs_` zov*pZbV|Uh4`LvOz1jx#so+2=?rtjqcSFV$lz3Sw%q}+K7u55Mnkns!d=J@H}?QlHn`hY}Q{lvxN=nQ^A2$+}&0J?uLvh zC;>A#w9`O9n7=zEVC3i4<(dlo9h3!;a!7MP`Y(bH1y?I!EzD2C;YN#jOE{1`W(AC67H$sF;vjM=IF8|PGJ}O6 zP%L6~2x1Ex)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2ED@wRl{%cGLjaX+}EQN9OnGvuxI3?Erv?boc1|36iAQgAFm4Le;V+u;Z3=Zuy5D@0? zP6@bF7f{J&sT4VA6E1^vXOC)Y1!W;DA|B516|hA~wzWs$wy;5cDmajeyW2{@-H^LM8NjBA2WDjf@8>j$lXL#P3_A_vq#LkA#w9`O9n7{iZU>k6mk4H^*vt*$S2|-M`q#1DsoVfG200~vs z5>G>0$a8H$;4N%Wp9&77;_kK*a5rR3K?#__p`8W-!u;JQ0h@q}f@;PN9LklAA>a^G z!kutAR)irkHvF*G<}qLp@Y*Xe76M*tgZfl(AQgAFm4Le;V+u;Z3=Zuy5D@0?J_*=j zd5ThwzC>KuHiUpJE}e^W#GOY-b}Og}0S5ln3K;h!uX}-jSKFXZAUKeUyW2{@-H^LM8NjH@zHpw}Wh4E#GH1YskNIO9s9go~G#i-WKSSNNT9AkVZm5qJw5 z)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2CLbpbU>Q&W(xzgW}Wtd>;`mp0gFZai~5? zYe67BoL8@aH?To{DmajeyW2{@-H^LL*F?EF#-*jW?Gpj(lxL89?; zBL>bpN4r!+3+9A?V_d%pw}B1nQ^A2$+}&0J?uLvhC;>A#w9`O9n7{iZU<4m>j1l({ zSu=x81RGvBFa&EgACv$+Kju2PB!6EMtPofFC1@PZG%36;6N(w zZYu$IL&g-8fEgUxX&@lX-+dA=P649~k4J0(p@=~Qj5{~x6j27)mr_GO^-KfVcq zM|GlH8Db?iG{eM`N4#FH&wVysj)-RzZ2pnXBmepO|NbXGaOQuFrCw3oY#Pjvx2D7O zL%n!}6XfK$hxXBVLq=^kcz2W`PWDBy%8;dc(RcBR0NM-geKTFh{qF==V(!uQW{EG4 z=r@lOskp$BxYy|uZ36zDM+$P>LuI#IEsU)4q)>AG=XrwbBUe60xQ+7X9P>SIpI$$C6Lx zOB=6)Qep{$ ziLi}l>w0igfS%7iN0#WyB(}{2$1XDzQQpmxYUu-p`@t87xK7@?pC#n43kq3a`EiXQ zu5K(D80%<%7hHQl@bg^{jDK4{B?G+KC@&=4hb1+43b*e7SE^+_-x|P@qq_~?D1tkC zmaV)V#1g)l!;Uv#J;t66s9Sc1C5l7NU%mp~yw_7=>sgkZ{jsau1-$FRYqh6gEHMjR zS-udwL@->~Hsh`Q#qsIp?y@=YwT<9@G)}4)~xPh1>Q9odO^Eoq#W@bUz2Ht@Az(WxW!6T zjzl(>NuCFPSU)WO%5XVy)!Q{s6+u4E%bu4+jG&krPnDxDpWBr6B zY)(Yzj6fdIj^P)Ts$_}A=O5;J;FCfA`qKAUGTAOKF&liQORjQJ3G7h4KJ7cWWPF}; zcrHsWj!1NLJq7(28XTLH%96a=PfP2-Q~NbXR>iVpMv%@GqaYq>(AW`Ogmu4awsY}S zaHzp*q33}tS!rkdr+F}sI3DSYIpNQewl(ttQo+ZyZ%)5jce3Pk*w3xG;H%?uG*`hdx7xNEy#jw|7Ao9s z2YtWD4jUiBBfJ&I4?1pU$$1B11$nU5oA~B_9!nYy|Q+8_ig<%QUR)5ID{? zLBZ6BB~cp7FBX6wlpCiQEMUn8-+n>kGdv>D(=v)^Vtq;T!i`|dGzTr`*(^y}I7Qz9 z?31^8Q-U%}JX{kdT>RWKBA!vhf zs@77Izx*;oQmrZ?&tYxcb>Aw=RWRg~!uSY2eCw}qiuO_2fz-8*NAED?kUJ|=fOqLw z^j*v`6wR3PckP@U@AVN3NenK&S$d8~X2*KnT~@%5FY2!kx}X1h9wEqa5ACC}{53V@ z#y1!;{__;=d0{+Kpyat|>UD+``zuAB1)tg%;FNWhA!?P!);57{<=Z^IL@{LSTGwuS zj7{gJb72Z@s(GAVG`7;%>wC0jeCBC}+`Yc7dDpSO_2L$bduSh(Z4@aL z@e|i6%uo2fOXI^V!wLkWI<^3jDC&_RTgH$!cQAgv(8lW3a#4 z+haD!zF>&_wjt|E!Mg*`!1IT{FMhU^4asMScdZm}AN*Bm;LfS9Sq#~e=k$Fp{PhN(y8MwV3>jG9e6tU{ z_IVsHK9(V$d|w`HJj)~XBU26-dfftX^R=a$VAq|=u(krm`)Hc)> zu-S051DwCi@yJ=01mEf{`?$s@iAT1cd|%`EnjwqoThD*I^0&W?fE@SGJ}Mg&lX^O} zogoRXquv(b-5ZaaWF1==;^VtYPAHj2+I+otThuY6G+^-sUvPrSqd%)3Go(`8-re95 zkIc^~u6Di6kS4_eoe?QKvfp{ajh8nW^7zTt`m136j$yW|au_mGMlXNpWgeN8F<-L@ z`=tHT?8rsnKT;(MUS~37cW@Ur`ji-%HzvP zVoC1mh_^Ga4|fap?kv8@663N8(E@Od!hvmJs1@7Blq>0BU+$gWIC5VMO9XN(O_RV+ z=8kgI7N>7T%BgW}Ux)=mhB9G_b@@8C;fa`{xvJ^npF@ufth#?W$#p zIXHHpY)II7mdt)WE8q}#SDLlaDdcU-uv^ zmT1qLm3#>6e*C2Mgeug7uguQ*DR6dasXL^Jda*HL_ij7bt!V!I`k`R!l7?5#VAZf4 zO4Ih>`?R7vdcha^Hnl$31^K=5Mh@_U!=`cu0}fb^1UN@yI}C`u3Km0*098(C(|$&tXt9i&3+Z$*D-tb z1WavBZ3~l%!Vd=Xbi%fgVj7LuGnHQY`w`k+6_0lfd%Bm`nV^nNmDt<*vx5Lh;Q(hQbJE!n-0AAVQSxA@z5)Gy5+F7fMwRi@k3#!q2M(E08S z>hQDa0<#KsPGm`gdb`H~_{rq^UQz2Nu%u$UTk(7F@Ab@W^*KkI#^{fk$z_6mp2RTn4ZQ4@?@>4OzkJrt z=Lb8crS7y?CSuEwEH26}E%0__ z1sl_M$P>ZVOc;1$`MP&oTNxs8zOQj8e1rEbhQInHLykV!$BYM4U!uMPx8YJ&6Bp<2 z3)sK^slt+l>8ZwoSnJv5-Rpa>N0e*Ag)C$L_OtQ-_!;e^vJy)*o-e@u(Ji;|oqr zWkj&MH8!-p%mVxi;B?^dugX{(FI)JCu zBzwycmPF|9ymJ{m^@;4z1K0=77o~?)fHO0dhHTPhNsG)L`9H9(qMc{n8tAa3UC-Rk z3LN-u*`zT)@ZAVrG&aB61a-b} zr~92)u*psvflpB!u<)RObKk>_0bZiVY3eBGT9@k|l zUq>R=(lZ-9!~O9#znwcXar4p(9x6-wHV&Ee`77e~y=UH=g&}+EL|>_v6H8{56)2V> z-V{AsZZmTqOS;!(`T2rNohG%eKZLj}k(w+D7CoV`pb&ZG&)c(&?;&2@mt4*N9QO)7 zL+M zvmpU?x?GdHwvZ*C1ut%1ff|wfE_yDava~OHl|flh8uYL#y!#;$^CpNdIg`l}+q;6( zQo&s-4EUULa37)eYWPa9cx>vHV>ehbM8V2W0Ziwjb7LxTG;40XX)Qs|< zt|;an+DByrUVL&<$>z?>cU3ZxA8wye`)Y6vdGo@2mC48tx({DIkVq6ePwa{jAItkJSnLJwoXR5z*%R2YpG%jx}Q zj79ZZoc-zD!M}G2|I`ee-rPrW&%b^D+%r_)TK_-;^R_&Es-h0Pl2;gA=|%lyHQBe5 zyJzSe)iFmfLmRwJ;4j&4?dph;f z&T`a4GLMQ(W`h?WiQwCHk|q2215LcZyalJvY(0Vdx_Cd6X7J%>M{7SHXNmhONB>dq zb9e3Xj_0VCuIZQg#e(VDye=>`nS*%zdp(6E8H47;t)qjHs>RhYENO~U>l}?(s<}ws zVu>QYttD9h6Ji;+jwntBAKFJ{pPpP4>NFKO!}|H;U3m9nxKmKxWccOO2~sKGlwwEA zI60Pt#&zrWfLkuv4>4j{A`;OaKMis9HNQ;>k3s*d4ncEpm{0ThU9v1mp8lXT0^I0( zyJUt8^09-?xz-a*=b~d{$Z=?c zdT8wMeLf(?k~5~cj&|uhA~e%rhPN{N?;ZKCdQe$W1rjE>1?j=t6aa&fHbb;J%_c zQ99wF+hX{encA@9Ebdagwv~)^IrYE$iuO_2g5#PJwy=5dqqcAUc(+IHP4@M2hHM=d zaWfnI{*lbYT{!=5lKh(Y1O9Y#v5dDk&i`LbWWuMTj&BvWo%%Ti_aJjb)=pJ+CHjGB}F z#^W9QrllXu=|$@zDogvQEPWTE6VL|Z$nP=g_u7#gm&TFbWqm|dhYnLfo~~;1;_A## z(wUN{lvol_6nl!}i-zJ3ZK(UM`Q^uOoD^pSl^GaoSefeVaWc(N6pr-saOuLK}ZcXw0Y8kR=msUN$YzmOZx(}P2LDrF~nFb zd*WNf?Zd~cOHJw-QnGx@&p_~UjqPjK;NEb=yPAon;C)L^_wK;GqgAtgV-wCrnN^od zBYGH86q8w)2Ch)vY_e_uchBx$V&C8#HLS2sIg+0xnsavQy+Zx}W0E=1mWKi*0%5HWR2 zX?SY(B#a+p%ikBoBQ8IL&h#OVEvmBi$N)R8bCU`|p3AG>POQN`ZrUGYkt3^rCb2KD zKST^KO^pJFU)Vb87Fcgw+^*1REE)cDvs@tfn9p_XsjzcVRezH+c*mP{T3>PYYG>+5 z_b;}gwt{K6tDZqSu1Z>&6rIYFz=YM4hvMwB^PcVa>kTaV>#@YObGSSF)s9q__NjCY zJ=v1Ml8JwcbjU(h$*kbJMizb_8JB$j8fwm(YoP&q!9JgDI#R%ox7(h2f;!Ujao0U_ zuy9SetRm{jVsFX&L%_$*9X6hH6aMRLpje8!a(>+>BU`Yfk)dMPf7F?N#l52wQRBWB ztPHt>npb~g<~`FQoS*p?Y#sqNRCnTQ1AjW=_d@$3;#AgJ@k?NF_51Sb@jS9Y?VHW2 zTP%5^BC~cm&OynYDUZ@I-f{Cqo3(Ln8a0Bo*$@7lQvYrT^8dBF+I@zQzr6CH^JDn< ze7B>Pq9w>XW?xGWf=7-}yjc#m2{$U;1U~iTjn6o22*HBs7*LFiItsM8(rCuI4Z$NgIk4c{~ z&Qp+KjGy#lPb_kM}WHHMfxUc259JhRHR`v~rzN1pzxXgrweN_B^-4B8j2uA;gM zpYiI?!`|(PS~8^$zZLm5h)M+gbC19UCGMfJw9olPVqu;uL&gO9M;wQ&yA)r51HSXc zf*HI6;G4~Ft5b01Tzp6=e+}&2R^~6ez?UJm{!3-1fDKfQ=lA2jUvjkhA+G+*Xp!~_ zJ;{)Qn|zm|!QMxd;@iM`H%!qE13RZkF%tOwz}(`g%yjU9$cc{!z}_#V^TolTi^^WV zJ;#u~7!jEw_{V0s@g=vBH_amF_wN9g?@JWej{D;K(waQ3Uj3fidFNvSL&^<}WwPKO zwVMKc4k7Qx9x2h*0(YI1)2;`vIHHg*4^ErdAi-tf53MYv5TY{5+Y&>#4jUi!zRoZG`EwRgSZm2_>O7mq# zfy;xQKeWhTNZf{x{qGQW#U7+5M8mFcQ{-gwz{{TouL;BbbnTQ3UIyZDU-pxe!UYV; zs5Q>l1bfQ#IT{o(WSU=je+c3-wI#JJOvs`Q-Ve|mu;Gwhj0wK&_q#35pXvR;?|t+7 zR?U_~eppkuqmjE8yV?0=YZ!i8p<~gpX@0>_a%jn%ztQ-YVO;&;6mrA1f`m+=pZ^~p zxp}!mBKOcfD(k*>QF+BTmJB;>^Pv^*TB>FBEcn6_wT`1=dhp%iB^P4lds(vSb7v4& zBXoVU==}JBB?bGm#T>C7#fe@1N4r^aLEpbC7i?EtHb%D-zaJ~VFjL0*g%ABc#`g`* zC%J3Px!)tHt~4I39ldh;msZ?62dnZQfIV~-_B^t9&64=043VMWrhxoWb?=ck4pn=3 zfML|Fnt+4C$HnZ9Vt9*~M>%mO}R)UI65>m1c_r)Xp;y! z_&*5YBu>)M+$6ofI0+v2@Ve*weSVyjiEIBeT?Zsy9R{Ax-)p7|)PA!5-2dU0_{`6_L5I7ren=>`QTzH>8BwcZtlCi>BYWLh7vxq|W+6=2#nK mPcn=2C0n?Z4(_+6%s%#}O#Po^e@5lj%(3ppSt#U^^URp}WSf_pv)5;f#p2_Ocdy#N!7s@~n8y9>&)xM}d7`-e!K0>sVidpQ ztXWmO^BTpQP1%Fn@VlI4PY;axM@78TKTgZv5s&(}c*yH!YkVCZr`1-x6stUWj9`@i z5$}iCg?B@Y;u%|qEa4&lxdl)2op`;;ZVS$0b8d`fG$-oV>&j=n>6Z@{s8#8ENUL> zusr*GE20O%8Te9U#<4xK+OwcNs{ev%Zuw4qQq=yv1ZEwGA@VMucY&$Z%;C}_PCzIO zeB@&Y;orjOxQ?+fTHugVRtqa*I(EIuZR#TUR1_lNJZ9kLiCHJA{rSvIDCU;${@xrr z%xeE$0<#X>n=leW3B;6O>e4Y`bPVf(T46NhkV8-lD`Psg*|ZEtJPG5lCda&U62@az ze>k?pK}+rs#LRpq>zs4BJvnwwC} zEnl4;IHZr-zmpY4(w;6lZhOp#sIP9Nsb{_mw!tA!oLX2J)3MEtesxAXuFu?rVs7~! z_wD-k=xYB?^y3R!V(266Qsi{9sIC|hg{B{SkOdC8+-hNEOvkppV$_M@kUn!0in--` b$ahI+EVX|pyJExvbb@v#OW1Y-9rn5;d?Q#kUk6XE%jx?I$jj~bgD5slEl89)uso1RP zHr1+jrczB?thHlnT}ow*OJP(P_dS~^jA-|n><`=NJ@fwYnfH00dA{#+8HTYP#tgdj z6Cdd_41Sn#`tdH=+qkg$GsB9dyD22xU4P=5Jr71!oDS$Kr!eUC|9`267&MqK7tfBU zy}1M=WlwV?i4=5(Y(VVqvS)5*Ok6ffxG+6q&f4W@>$uY7{*X>Cm&3a`LR- z8ZG?@#M@W57$Ki9=r|UWL}1FXxBlZ3D6p}z?|`8WVHmysH`>Wz#G)83w9cJMqoov5 zICuG-dOYyXYD)ru{>hm#0*<$Aqzv-whuelV93^o7>+8=;ODQ<_ z)prdS69_pSI%*;Ejg6Nw?BWRQ?=kvASwi9QicyoTw-Q*VO|o!6cH%sl7#xc6%=elW zAg7>Ob!uaph(MKQR8E(S!bD-MIwFX`mPOv?k;s;nCr5_)6FAbIa@qMO3a{>ZOn>A{ z;GBG4?}TCsI}V-SI|*5As|fq02=ABdD6qqKPkZg~U=nhKrboi~5KuoYOI=b(p}URK zJw`yF)xu%X++!5HzNqCZR}knb^z;`0Na0E>^C)uxfya5z%HI@Fs4e);a;PH#x9A9a z5i)Q5#l?EI1a{dUYW*jlg4GIxr5U3MXu1XIQsg|Ts#&T>Apf|Ha3pd8&mb%N0T&`> z=Uh9TMPNqUej3+@>F@oTp`C8Aw(ME`7$3csT?7&hE=27? z+jCteaU~f98fV^E=76k?81m|3F@ZOxe&4=Jqac#`91J-_V4g~0ehRro5ca3(S?sUr zj@DJkcYYnA4!B4lBTwlrLH;H@Id{ty?9Z*e#_`B64R5WzbB{pa3|Z`A#Uxi4ET z?5rT5a+R5SW@0~XkM}UGB~b0It&7RV{_to#xZx53qp@ptF3hDMcvxa~6?J#44wyF< zYiMo0Ji#1w3k9Og9;{d2&732X-{QL^%)7S``$1)H+9l-kK+*O2VHGl)e^v`h8Hzhq zm+zV?N$=tNT(C;{-L46HZ+-5ULAH71!z|b79)Vl8*fZ8P-}>F0A_;-lS1L?=v9CL) z$+rg|!hC4P-~J1^M^d+pgSvzBqvI4f2R2`*qPy@BKrD1(~0Q%RE`z|36#(TY71b3>-8gbzIRr|C9=d1P|I$ziqjX2Q0 z`uu_RiP1|vG0OYfpRJZbnytb-ICNf|Dz?4&7xm{UUoH37FHU`Xss6OAeE^?(p2l7t z@@oEUe?Q_No(Ab*`_rKOazTmx?T5MjQvbfgX+M7fzXH2&Q0Rbt0c~-5|9L<4a?!bi z?GxjZ2WjfPR39#o?ms`{u@@%A0POAmJPm>*gC-mtf4KhC%fe*8{qodH?VnafitE>- zQB?Wqr)*W8|90T|JmvG?1$>q%e+(1<;QA%bf6M=UuzzB7zd<8r;r{-K>wkkucxZg8 z^Vw>DeZZct@(31~{pW*6SpKpGzyG_&rTXK|gZq@n;^W@&gY!8r3-3K2dr#k>%(wW! z{DJc;fG^FT_uG%5125JOlb7qSuRl9<{k8o==YuC$zF>$BozE8DGWZq+$^5531PuS> zKbK%s|CZT({@}YyMYx~l{M0;uW6>!`Y647Gx_Z%p7`NZ z+EQuY)`P`~-ForVW51_7VZT;OcDD-@)uV28_tlbY&W_0s3;4D$HuaKOFk>O~cmnG~ zd6Qx+Ud@WtlrcEZvM|bvkJk*oNIYe}mri*C_Y^n~{+n_Q%Q7Lk=U3 zagE>LSt`w<;F`pwZ7EkltE$^%^-AxrWckD^7 zSJ-zu$?o-D{m&qScZ-DGti*d{6nc_Z6!scp3HwmI|4168>V&{6A<(QZ@5M{$88K#j9i%dae4ZqFGSHHI20aFPVEOZ(l;S91V)7mg<&ewr4FgLv>VJl(B8C zEc0o($QND|Z(K~%RMA>ZHO#Wq9WXo#_Bo8E9=r+Pt)^Kx3xHH%?=;I5P1lUF=pO<1 zQh1?(rlV}w_B2bA=RZ(-w%>?my7vAv-Hw{wsvWHLdvXsec0=OWqGJsA_%26%>k=tuDLVCVIv9+O`h*7Jx# z9c__Y@mnuq>xa&LB6f8YrZDd$eRTNZxVC~@#Jk(|F(#NciEG0|lWe^;1c+;6Y@4-s zYhz6P!9{?$wxM!urh%yv!ksl>O*552w-23bL+0*!Q5_4!w+uEK`Mk5W$ zuVE5Q<|(L!1<7#_f1+m=d7xW^pKD$cy45sLsd+2tx!#$#VzmD`dGf=_Jq7^Hv*lXX zwpxOirMqR~cDiFnHQzo0^7o^DF&Z7$Cb?c=-|hMx9eL}-waH^h7Tzub&GQiH+A5*Z z+Z}D{83qCu%e8$5B?<>mOOW;PfAh|If_ewr&i&eeylrw1BNc!{+Y# z)&rEIy>9Ydd|W zldkP_%*IrMxHh@bYTku$eBQBD+;Y9^zcz7g{bNj57%u|N`a)bAac$$B$LDPM#d2-f zKA+h3G}5(6eGjm?F>HIiaczS0GHk_C9SdB&t9O>dhg8!w;X>+KE_hyV#A|cq6=@<~ z8>SEq;yfRYUYfr@x-m1}6> zIJ^D;KUhGRKyapI-yvG;I#Sy-^8AO&*K~cl74xkXW2^}D2LG(%z8wVWs1sxIA+3Yi z9o4h_g~SjP!2ME>%>IIOln@vJ0@ZzMs7)Q6u1zvt8}{99sN-6-i@3H53HnP2>>@y1 z8~Kp#&I&s65(31vA^BM0b8W+phXy{Rn9RycbF|aBi`BJhmTG9O;;Im~ZHsGz22U6rg${;<0cMojaBaeJ9ft~WGAkfP62=_sh?VP`c90MXBKtngJjjaR0ts}kx z(s>{vZ0@c zyD(t~og3kAJSr)#+4Dmnl4T0&t4l}EQl}X0r|INaZth2o!XE8SZYNCgbd`mVAPMZa zftsxxosRXX8X+(w1c+-J(r8>>TZn7ByfLR9ACExuJcPrw9e-siNeGM+fgy5j*RA*1 zFX4S2CUf@p#{hDy*Q}gv7THN9FvsS`uXKwR5~uC2?EwCWJo z)&hfe34tvHh-=#dgg%}X0pi-u3O8MBWC#$~HnIVu=Ds=t#I+68hg4|&2D}m1CO0_! z>Dp}7avj@tm7REPplNg9KU-5kg4V=q1C|WI9T>+-11fQC4HjMYh-;gWuC2?&p*n=X zt0O>M+p7=3MK^%Bwu>Hq>iiE7Ag=8P0MTheU<(1_+K6l0ni9u-B(Ci^oDP-hk83j> zM**Tu)gVK{rmjubTvbtBkh|G!U7KSl2(&I(L5TX+z(u(c8Z5f(VRLPi8zIf2;JJ#| zCK+qI9j~nkR@Y^m%Q_+>0_8^Njvv(=76O;XwKcKE*Lg!FhhxW{ zQhVdtOv7>vTQ^+AP&c_Y4JbI%byO45k+yRqsD`e~heiV{2IdEJ z!;fkY3xQ^R!Qt8{H^Q*Sms*35zz}t9_`Td?5j(ybwmr&?uxHRtIE2lOVRLu=aBZq_ zWviNPK(s#~*2K1kOu14V+cZs8cPvAYa4jEF;@VD_S9MYBw~s=HhL~)iu!k70jkq@A z+Um*w>N~`>z4{RJ+JNf5)$2thj1$);0p19zk`L(!28o*ZY6uY5_G-}6g-412acx7@ zwNV~Ysjn1qZN#;0Odye&@~_ELHkoc5?)-tcwiwdYca;iBFHsD=J3&&cqw;=Gk(s zJCW3hkgl!s7q6&DT$>zzL$6I-o7@OtuUFW2JGtKVUz@nL{xPO2j2D4seIZ@jct??1 zeoh34Ya_0$(|p5@*M<-=Y@;85+5+K6lGG&hNB>jYa}iF`Mv9N7$$y@mC1je9|D$r_tF^dphpVe zn)^{{`SyW^{T!QdHl>ZKmLTTLhML<5g|v=IVt1p(sP zkiO4<1l&vEmC-}8r`f7Aq>XoZ0re#~Mt+--a zH&?mROxspnO*2fzQ51#E10H=d73JZ|HtnjIMCWYNL4832uC3c^!GS&q_GLMl9-GMW zfR*O(=jYU$g#i}TR@xad*Y=$9ryzm1f@PS5_rcR751#Us_)64dTEVy#?++zc!y@m| zW8Xt~jM~gn8hM#u=tR=d4Z5wUTc##`;3C{ymVjm}G8(K$k3 zUxHK)_cyAImoZzsRJbkL1AaeTwA4d0!)N`BFDr$^CI$IIhkkIX1pNnvPB=}vwiCdjqUZ=z_bqh8 zNCwBtwRKysugVv2{ z_OQ9Y3+39>E8Er-OSfIebT_#+#c?#(uq;JUEs&r!x;E8tH2HTMVFifEGF1SS-l}8I=E=+jJ?t>@;DO>GFTAaUTrHIY<(kV~8 zJSe1YV61Q|Wu=P^kAf8RZGy6k;M7rD!L^x_CvYJ>s{D@%HP4pYTwAwQx{V+7tA*^3 zT0-Xq0u-<9ZO;Sw`%%C6jF)Tcwq8+g1iATiq-&_$MW{9*a3%zr=OG-f?GK*j;1rhS zyMCnCtT_2S*ng5(x-$(qT}BcC;@SqwwW-~Hfs~vO$5y9!ZO0 zo*3bz8HJu>lZwOo(*S531NFuvqPjbD&zS~K5ulMTMx;En4 zdLJ^nz!?!}))ySE?Tk%FyVM77nT9@36ZR?P^8krRKx-Zu8W;kfK}(Qk@SI*wmuL3Q z(oHsDrlG)lde19Ai*Je#wo0%t)RQ=UD7s=M{T)hY#ttHucEYBaC{tbkjZ*`Bq~ z4AoI>Q3hl>WtmUQMZWN&IE093b#pWgqJ~YFrh*8pXzmWnorSXiqL8w8n&pb7YercV z+vgM89yXVSJ@0_Au`z9S`?>XS3($3K8PE9=ggHoCe_WesLQFQxH4S5%Ytt0n1UFLG z0v2vV*QQ(Yv=*c?un@w&)$o$8t%jo0PQl&fA*{PN8|m7D%wGi+*OmqUS`{~zZVOkN zYa?CT=`)mcZKq>4rW%*VwKY1CHJ2|rSUdJ4*DKPs$qfi`ZF2Zg?O`F%tS>lR8|m7H zHNMmud<2dl%kYS>D$U4;)Pp2+4C48C53426V76T_W@{P@zn6O~Vsm5I_8=Y_g4k$J z$B_XGSgjA{}BuZ+N@aczxG1cz(eu_vi(!@k?S^7Q)Q371SXwhiZ`scE8v&tFT|C@`Yc!!edk}>x})BV zBN)c!#<1=6#?#fe|CntS_W%t72M>csQx)XF%X$b!`PVwoEyMtKn5ewuJRx~GX)fukUU35Kd`E4Vwvcx`{H(t*yN z9|94;AHs~GiSSzEgtJ?W_S1CIQDnH&2<*|`)U}04p02X+5!`P(Zs0juCTs0jpQ;f8 zLqg!vxHjzdWk_Q=#c^QfNOL(7$ z$(;TDF-Y+Z0M-DuY>?`Z73e3 zj1kxNN;CMtWwE(2YH@s&S00tI*w{P?!YmU~ zU{JTk;apI9B6QmZ1j54>@8Tz05~Gbc*bIBL1y+-aaJvs80jG?*t)6=2;GB#XRUib0 zguu|bHf*f0*B9*eU79`O+T=!jJJyk$HL6Vr3=e_r_*=tkA+<;bf$cSH>mGfw>02+E zdnpI?!>5$bgG9t@vmA|0n0R+)x8Bd!YHVaG$$PM#VsSAh1R`j8?h zt2P>po*Mx<*FtYxn`^nY0eKM&a2eguwW+oVY?x}=hVE|okgBHZ$fx`iRxq7DAt^CJ zcGPB0lwO5=;x&h)_L^VzF2jfPqxT%75OOW{?tvE)2@+NjqzEWK3V%{ae&9m{%q*Ay z@e?Lcbord}rvP|I@Hu@9(g=>S=`)LhmCt>VUBG8^Z#qr6AnhtI$ZHmHA5uh2MJ@)G z1?dtL&7m-IN^*ru^C6X*$|J0N8$O|i|Ep-v)|{51K1Yb?u$nw{t_?e08}{8~Sfe?< z))3b=zA>W~pAUg%y~5G8-SJhvU=_P@x0+7i`41i!X(I&Q3IapawVef9Y#kc5J@O%y zn+DiAG;Djlac#O|8=7eWW3GyTZEF;ZJfw=Dsg|pOb1CHAXnaT^Z7D)%K(KahJ;8hT)?pv>N@gpuuT-y;KjkpMLZ6h8$YI^SoU~_G~H<&I! z2)r@^L*&{hUK>mb;@afC8Q9zyw!PlCHq9}03;ai2TPboNlrR=~NNp2>xEU@a9niEE z*9Hn!M_xiEbg-CjNRZtDQzlpBMp#UqGMv1WjQ!@Nv*5W*ABqtct>DHGlMP^VZFj*e zj6q`NrO$<`ZSDB@$Ri&-XMHA|MnT$EiqaZQP;pr9tK6lw(6!xZc*EU1Tdq}%S@5q_ zkoW=XCKzt#Aq7!j=!bsfWkTsk+c60ArEA-s)yK4j%HSh_c_+bd9b-f1+OXFv?7Lkb zV}fZDn``^%rSpJQ>RHIi_87v=BP;HtMY5snyO+jr{}dNyH>(UJ;}EkUVIM&My(i{s zGrpze-ywDYUuEp`+A<~#brZt1X)Yv309jl0&H`MUrU=EGs<^fVDG^#+n`tTt?!Y)!nn?ut9T3!Z zjlG-6Z#OF-U!sX0Hb=Yfm2X?O{ueJ`cMA_56og-8dGzi%dJMhOcsL+)QRB72(-5kz z?RGWG;He)%zyUXuuXp`LkV0g-DRdoN`ofealcE}J9=hyx|sj?Il>+tW+~fdJ`mGHNIU zO@Hmf)}dkB>q*z9UO^x|TXSvO1#)eJYlF-Px^7vzp&2%OyBi-;$8r!}ZNLiZge4zR zxi1Gc*G73rDG#aKjHKFxz_AD*+>nmFRHt&-TpO&`Ip+G(sZ&&`Mu51s8VcGW1a=T; z)~n0q+IB#p1BAc}2pmC{ZPT}1QzKm0TF=D;Cd74tZwk}hL>JS32i~w}$j z*R|O;wp-=S;L9A)EDhC%RHiNa>SA+a*!Ftk+BDU$Ekh(nfQW5vA5ujD=@~?8Q*_nV z)fU<DC1ONfTBOD7$(rA*!5wqiSoch}V`*rmZ7#yQQ+>3l~8qAY&$YQp#-wH#XpS zZDlVZpx8!)wkkkdb#NVYOn8rOh9Nt1+_oZKn+j|j65ZB3TW-f|>o!TN$US8m*9twk z8AP=SfwzJHacy!ViM?K7-|ggj*MDtnu8rcg^*?5Gh4UfMtS>mawv*$vov*8OB|-oN zfgy5jC>{He5o~S@+a9*AO+|ish4yue3VvW

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx deleted file mode 100644 index 78d33f6fad506c30cb2a02c956aeb3c09e2ed983..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg deleted file mode 100644 index cd89cb9..0000000 --- a/tests/input/structure_clip.cpg +++ /dev/null @@ -1 +0,0 @@ -ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf deleted file mode 100644 index 7f15994662bc5ff545f2de71f06d3f62ed447adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx deleted file mode 100644 index cb743f810babb48dc0aae0199ad824ac44b006f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu From e23f02355c2d81bf14da37f6b9955f3e4ba3e2ca Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 17:34:51 +0800 Subject: [PATCH 061/135] fix sorter --- m2l/main/vectorLayerWrapper.py | 15 ++-- m2l/processing/algorithms/__init__.py | 2 + .../algorithms/extract_basal_contacts.py | 76 +++++++++++++++---- m2l/processing/algorithms/sampler.py | 3 +- m2l/processing/algorithms/sorter.py | 14 ++-- .../algorithms/thickness_calculator.py | 6 +- m2l/processing/provider.py | 10 ++- 7 files changed, 94 insertions(+), 32 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d68ed3d..72ff281 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -9,12 +9,15 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint, + QgsPointXY, ) -from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon +from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np @@ -35,10 +38,12 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) + if f.type() == QVariant.String: + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame samples the geometry using either points or the vertices of the lines @@ -392,7 +397,7 @@ def dataframeToQgsLayer( attr_columns = [f.name() for f in fields] # Iterate rows and write features - for i, (idx, row) in enumerate(df.iterrows(), start=1): + for i, (_idx, row) in enumerate(df.iterrows(), start=1): if feedback.isCanceled(): break diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f3dd275..f0aaedb 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,2 +1,4 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sorter import StratigraphySorterAlgorithm +from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index f329229..d7beb34 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,13 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterField, + QgsProcessingParameterMatrix, + QgsSettings ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer from map2loop.contact_extractor import ContactExtractor @@ -35,6 +39,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_FAULTS = 'FAULTS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" def name(self) -> str: @@ -51,7 +56,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -63,6 +68,16 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='unitname' + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -71,12 +86,26 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + ignore_settings = QgsSettings() + last_ignore_units = ignore_settings.value("m2l/ignore_units", "") + self.addParameter( + QgsProcessingParameterMatrix( + self.INPUT_IGNORE_UNITS, + "Unit(s) to ignore", + headers=["Unit"], + defaultValue=last_ignore_units, + optional=True ) ) @@ -94,22 +123,41 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + feedback.pushInfo("Loading data...") + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) + + ignore_settings = QgsSettings() + ignore_settings.setValue("m2l/ignore_units", ignore_units) + + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None + mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: + geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) + feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - + contact_extractor = ContactExtractor(geology, faults) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + + feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, parameters=parameters, context=context, + output_key=self.OUTPUT, feedback=feedback, ) return {self.OUTPUT: basal_contacts} diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3a6a1c8..f8b38ee 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -67,7 +67,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -86,6 +86,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", + [QgsProcessing.TypeRaster], ) ) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5c4340a..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -22,7 +22,7 @@ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # map2loop sorters # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -from map2loop.map2loop.sorter import ( +from map2loop.sorter import ( SorterAlpha, SorterAgeBased, SorterMaximiseContacts, @@ -58,13 +58,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "loop: Stratigraphic sorter" + return "Loop3d: Stratigraphic sorter" def group(self) -> str: return "Loop3d" def groupId(self) -> str: - return "loop3d" + return "Loop3d" # ---------------------------------------------------------- # Parameters @@ -74,7 +74,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT, - self.tr("Geology polygons"), + "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) @@ -83,7 +83,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterEnum( self.ALGO, - self.tr("Sorting strategy"), + "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default ) @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + from map2loop.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index f56b09c..71f72eb 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -29,7 +29,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer -from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint +from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): @@ -62,7 +62,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -79,7 +79,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) self.addParameter( diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index b6a5a97..318e944 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,12 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm, SamplerAlgorithm +from .algorithms import ( + BasalContactsAlgorithm, + StratigraphySorterAlgorithm, + ThicknessCalculatorAlgorithm, + SamplerAlgorithm +) # ############################################################################ # ########## Classes ############### @@ -29,8 +34,9 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) + self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) - pass def id(self) -> str: """Unique provider id, used for identifying it. This string should be unique, \ From ccd1c93b5286aef86f70b95f104517cb10ca93ae Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 17:59:42 +0800 Subject: [PATCH 062/135] fix data type of spacing and decimator in sampler --- m2l/processing/algorithms/sampler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index f8b38ee..c018e72 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -112,6 +112,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + QgsProcessingParameterNumber.Integer, defaultValue=1, optional=True, ) @@ -121,6 +122,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + QgsProcessingParameterNumber.Double, defaultValue=200.0, optional=True, ) @@ -129,7 +131,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Sampled Contacts", + "Sampled Points", ) ) @@ -143,7 +145,7 @@ def processAlgorithm( dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsDouble(parameters, self.INPUT_DECIMATION, context) + decimation = self.parameterAsInt(parameters, self.INPUT_DECIMATION, context) spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] From 26233f0afd0179a088a6c50c3a5b26b9e9af2ff0 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 18:09:28 +0800 Subject: [PATCH 063/135] rename unused loop idx in sampler --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index c018e72..9e4ec6e 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -186,7 +186,7 @@ def processAlgorithm( ) if samples is not None and not samples.empty: - for index, row in samples.iterrows(): + for _index, row in samples.iterrows(): feature = QgsFeature(fields) # decimator has z values From 1709581bd85f624d7b4abad012b79bfec4e5e68a Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Sat, 6 Sep 2025 13:14:23 +0800 Subject: [PATCH 064/135] add dtm to input --- tests/input/dtm_rp.tif | Bin 0 -> 161316 bytes tests/input/dtm_rp.tif.aux.xml | 11 +++++++++++ tests/input/faults_clip.cpg | 1 + tests/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/input/faults_clip.prj | 1 + tests/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/input/faults_clip.shx | Bin 0 -> 380 bytes tests/input/folds_clip.cpg | 1 + tests/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/input/folds_clip.prj | 1 + tests/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/input/folds_clip.shx | Bin 0 -> 156 bytes tests/input/geol_clip_no_gaps.cpg | 1 + tests/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/input/geol_clip_no_gaps.prj | 1 + tests/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/input/structure_clip.cpg | 1 + tests/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/input/structure_clip.prj | 1 + tests/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/input/structure_clip.shx | Bin 0 -> 1044 bytes 22 files changed, 19 insertions(+) create mode 100644 tests/input/dtm_rp.tif create mode 100644 tests/input/dtm_rp.tif.aux.xml create mode 100644 tests/input/faults_clip.cpg create mode 100644 tests/input/faults_clip.dbf create mode 100644 tests/input/faults_clip.prj create mode 100644 tests/input/faults_clip.shp create mode 100644 tests/input/faults_clip.shx create mode 100644 tests/input/folds_clip.cpg create mode 100644 tests/input/folds_clip.dbf create mode 100644 tests/input/folds_clip.prj create mode 100644 tests/input/folds_clip.shp create mode 100644 tests/input/folds_clip.shx create mode 100644 tests/input/geol_clip_no_gaps.cpg create mode 100644 tests/input/geol_clip_no_gaps.dbf create mode 100644 tests/input/geol_clip_no_gaps.prj create mode 100644 tests/input/geol_clip_no_gaps.shp create mode 100644 tests/input/geol_clip_no_gaps.shx create mode 100644 tests/input/structure_clip.cpg create mode 100644 tests/input/structure_clip.dbf create mode 100644 tests/input/structure_clip.prj create mode 100644 tests/input/structure_clip.shp create mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/dtm_rp.tif b/tests/input/dtm_rp.tif new file mode 100644 index 0000000000000000000000000000000000000000..a9ba843ebb8ff37ef232f9f26cbed3d5e2c1cdd4 GIT binary patch literal 161316 zcma&uOVIzxbsqG7|1)|m%d%|8c5D*k%=MkojHHq0rnyPeqZw(eyKPCfY+WqLmM?Lz zU1gg%!Gtz3b`_OkL9xst3zk$7SFwRqQN4gF7O7$Z3t-J6Koaf{AOW7wc~8ImJeFig z9nR^~r*G#xPxtrt{>S>*$DSB(A7gyq7+3d=t62YWW_r?FNzxmEVewF`~UuvAN~LL|9;|~&t}9w zePE3L=+!a))>p>(+`pVz{`MIE^?xwNzw=*?@!S7oj8}f>YTW<))%g3LyBh!2Z(WTq z{hL?gkw3l~|L{M(8vouuz8b&x-S@^D@3}YL`H6euKmXNxAm2|7(5k{y+YsA4t&E+yD5F zuE!6I>x5j5f8*hIz5VYdSNZ?msr?`R`u(Nf{qULZt@_P3UVHJ`2cCN1Q*XZZz+eB3 zU;oAf&%OWm%J+L?y!V;+J^TKrUwHbNXPC9uUGvDqxSFbU} z#kNjqb7XJpLd-wEH~t@tWH0ldi~NPy(fdatfALn&2Qu!#_#cY=^S5z3aVvYBiLv(` z|IRr3UXg9yKNoF{<7<0JQ{}ouh+-=UnBo#WTYK_%*eu_d$F%#2j{ED zt?T&sn@2O;+V~Z0$zgFRxWm8V66~d~>WoA2Z|!LI;c>L0r>(;VD#89c9Wamgh<1cSAQ;C;e- z1RwP8h}@fhslV|gcCBZA^En5G6qKExX5@_UN(veEW=zHqTVY=)n)S?rL`^JO<+nCi(xj@ZtY>J&#HJ;lTKR zULTWZuneqnOxF^`|I0S^Nrqzy@=OOW@C99 zBj@Hx9_4ukLvX3)q|TmtRNcajv!=1^5`Jgz`xYkUN{wq=@>IFX8Z$n3eBo5?_&UGF zP3wxM8;_eEG3Q;Zy}|{ou+3{dcz7^=b3PIc!wrBpFGg|iS}&GK5b_bN6fb_+B>#(tz)f-{SI*# zZGPvUk#(W(;^SjG+ld?TjcyxrgkS7syL=tv>?>ziPjBjoUhX0O>x_lL;I=RZ!;SCt zPr?bj$lX6;5B$jcG5+^kZm_)}_^|$2?DX&OVEx~TiL7vB!1Zp$(w<@sl{eToO8gHHvzWci!Zf@nLGT*5B!tFt_#`hj9unYXX9Zn zGBd|i2X)mKoXMm7&Bmg7SFP+Ta}4o^3(4_R2kZAdcXPpsc{9eD&Y0z$2p80>zJVT1 zf6E9@#>4Tay<+bNBi<8dO>MWmt7{AY&U|=<)9#HOU1zgLXNWHT7#o?{XXV(QoasW& zUPt@t?8e}kJ=k;F+gba}VurJdi>yfPk+#-}yBM?eMZ}Axm$q|7Z+1i<(dIugVw;y= zKYzuZcFtY7#RvO7xe@DbhdZ2T{Nl+7hU@$3e-6HVLnW@hqi`c_Z$|ZpALlJ8zJw#| zj14cmC58L;*SA&X!V$9LKm3@%{SFUmeeZ)caRE==@3E&lp6163{_Gvj2Rm9V)Bt{A zV9J}b%TxT`EBqKoa49ljvoXEoQ=N^Et**xM!zx^D>&UTr;m6qM8Y|~rizz0tGgA1< zJjN{jvH7R@@%LeO8<3)o$sxo4eQay*ZUHF@!`WHI1ua??z5+v@za|?;5Pl* zG`ZslZiE+AFZhIgcR;>FMt?U9xk#?6zKQWZ;0R8@GknT()x=oe9P->XqUCZ#%W=(> zdG6MH8sWsokN9V_IoK;4sP7wm!JEV{e4r!6r?l&?ruD%)jwCmAUie5|3ODes z#>qj|Cov12+a zrgYjnZR?X2yutNsZu`Bl(=NV09Q(mr8zv{th_`?Db>=#o?>l?kG+$)&ew?)=`x!0vx2zw1WF&5f+dcE^hM9<00rzK47Z6-M3l{kvh|g72sQAriZ=A3hWhvl@8f49nf=EfECo^|S5EL0YGyEu=Uq$!(;q8F?5$ySGJlTG?M0Vn5 zbE!Cj5BaWw(E+D<1LSMSLFzcz>KClY1MItZ2MqOnD0d5OKEc?b^yznl=|F__Tvld6Ye`O5L^DVhM z__R+Ljg0oK_(=Jq`7irseA^f?Cr*t0ojhgTlNW1_UUL~I7S_EHco2LQwqYC(U>uhJ z`?UXYbYfTRdNZ0MzN@N@J8WZpYZ>3R_ow*MoT)aS-z3(>i@XOV;l+r(Xm80?bZpvN z!NwWyg8kyd2gP>Pe^0YQN&2(XgWC;>=^-9id0J zCm)XT8;4VwtLB9nZ8$QYd9toG3Uf4WwbcGnw@Fev^&_i=U(8Os1m1%|Jb)=!gvtF_ z+7CtWB6ssle9hVCx3@{MFJeD+PW$Zk(fakR=(4rNwEy%~4q@&soEQ%#KJVJj-teRL zm^~H8od?&QBYo5U#hOE3uvzsI!@iK6{g1SdW;@er`4h3?stLV@ckKg)hFH;0=bG1Mpoo48wxl%E!rXe8$XZ zc!gu}=Gf*jj@CzTuz83R<6tLt&2KE*`jJf#yu*%2C!E z*37(eau?fnapE5ZYyU9#ixd~&|G$g$dr-V^zk35<7^dCr!Dsz$I5_p5@cUMR=YxB9 zJzl|ne025sz3sd0_)om~`Oi03-jG_$oVak6^IW_w-jB_X8`qIw)NhEAw!IrL?VHMP z^U=S<@j(uzyvd^+t9x*-@MhvkF=+?U_NWqI@W^yVEyRgPv+PWcJbt! za6*k&_Hk;hUNEDMtM^%$Q&%`?9H})d6`s^ST$oQpo{Z$(fQf^Japrec>-Bl>^jkPl zdlA!O%%$MajE`)R`C)G3K)+XhPt5Fh!@9ja98I#X!n*S`FT~leH^cenIjgpqp53FGF#3j+JS#_AYx z?1>Zl+4vMWetpB*ElZ!dn}?(EWqtM8Bzt3*bZvL=u_AD@;El!&*a-&LB02Pf*cFRb==Phs4*5zLGCR(KD>lX@F6 zrg-JsC;h&GiVNNvVqEJ9+c2KHe~7aVZEXDB4DE%h%?WP=pZ5Yr2mS<;JNX{T_ex&n z7H$i>88`JO4%enOKHwfc(Atree!Rn*;toDo1LkS%^i`X$*D(KZ*Y^E94))X*1|o&C zwAK9JrS43;2ℜ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 u`9Q?|`E=UPCFX@l`u8i5mu_PcJXSd3UBZ!R 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

J@~-neKlvhm@zs1gyjwY}`PBS(6$ABM$z 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≪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 + + + 1175 + 561.72162965205 + 297 + 107.52060579962 + 99.73 + + + diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 z5g=fjra@{1+jZ;>uFciuV*>)Wr^{zpXl4iadSjX^xVFg?pLoqBr1qMpjWzosWeq0m zz~i-j^qzwhg0hwDkRa1m5d?59KLW27L11T}dubL-=BXDZP;~j6@~0qyXl;x?25AIG z+4Px3!OG`8#C!(gZthK|DHk!ZAh27I7J=Q$^a@kKV6rSomynPl6h^LD9OPci)8&Fg ze61Jp7F^pU>Dpcd;|-p~j@MRkpRGCFI9%JlTm~6DF49H_ycGn9YeV`z51nhnp0K2AlS8yOU7HTVHPh8y z6`V*nb#1C<*@_8lo1t&`klK!okU0aapmJk|s-OxxKYxR8`QIn^5&`WuM*L6Clew3s z9{aL*=8+(jq&9eZiGf@~+X{Oa2~yWBYI>PMe`lUw^jPde-+?uQhycD12_cFU5qpy* zoWhmK!cdI%KPOLqh?94=i3Pl7R#(fVOm2jV+ri3vxgH0}IVg)d)h7gS5V$n1ZD)Ov zgmK57m6dcz=#oO))ySEZH!QC2iF>7AE?deL*Qa_BYcL;fgEyg zLuv^AZw~K@k{h-?$l-wMqlxL>C!i{FOh{tZu8zv9fZ>bPwSjKU&>TgzA(7`M*9KxY z%Q00=a~!=LuT3>IM?U4Jup+OHYo-KoZ4DM(_K0h{A+D{<#GyKbz^fxbT$|hwH1EPV zzYP%ACO00WYm>u|Y7Yy6W_=;9Em^0I9M%X?YlemZac#u4b()jJwRM86uGEvRO}_$P zPFIJdy9Nl{Hn=v;Fg05@O-m7h+}b{*ri+k5R0S)Tgf4Jx@&sTFH)Dm;iBJ5nbxdaW zJ?GIp2xcDpJ%v2Z($v<_b#1?bTWIYa%I@J)1n!_-943&;dd5NkVRA3Dd8L*vUd^(c zCm^%}|I&QDD?CsAMUdvbOWcO8YXcwHFb3yXFMVb_2S@}iap;4x5&UVVA+J9kH%A5h ztOfsZwWt&!FiZr9Ym*z7=3N+vYs0?V$@Q-P+N5jiA7i@0coAsU7vkE8Ya8!8K4;5` zYa1@thAQ_jLGGNcvJem(KwGod4h{Fa18$@5@aT#MROhh!c5Lh`sFZ@m;V42EFf_vL! ziLO}@CTj*?G>Gi>-iyFZ*JFQM$8rsn)UZ^;GELjq(Y48^rVzR|Od*=4 zVdqBp?Is3U8&)P#$eaW$(5)9wag=S?Tw7reF<#pTp+{qEZzMLx8X245&uS?f=+_FW z&G*tN7XrB61#RoE{|yA^`#5=e$dQDM&F{Et4m0B4W#YB9xwawd+SbF2y(*JELf|L_ zs{7VagzDV|I$DZVRg?+b^>|XEHiEHZ}V!FVH5op$zOJ&*^+vZM2Je<_@ zGazuWx;E@w2H5t7o_jzuU4(%8%?!uZ-C=WM*gCnMxHjcVH5~`&G+^5bvTcoUk%v?> z4Fis=ny#u_KBV&ApMq<{97N#SP$!sRUuKgBG-2YOVV(yG=drx|XfZ}Z=i0t5!UViV zr@>5!&MMtnkqKe_0fJs;@S6;%+P;VRV$HHmBhP;*9Y?{Pv^?G7>so$~1xeTFD|v%V15RwwobAc2{sn zf-EYJ-aSW;kdpHxX&Fo~)J&u&UHph)+v`JG1YbKSb`yY9)X%vDZR?(liEG2~A+42! zzcdQNBL;P}MQ+0DmT6IS1M>;@SqwwHfL_=S43gky<>Dp|^Mi8Wd70fiGjC@GtzW&%;8~Ko?Srj~1vH2u@vWCv> zkPoTcw~uPeAyD17cXA^rwyWud7ipVoGc1fiYdBUKP@x<0j;)Qk zG7l+)%Et=W_CDqQLkuA-ang{MBDkp4V98xmTSY#Wq@rD;@A(G%OW3A zxvyz&T$^IquA?g8Dk@w^t9KURL#mk|YI8K$ahxq(8-n*I=-LphG@yd64fP99D4qVA zJo({-VZCV_Ri;V%tg|Kzr1W2MqwWMkd(TSxz!RkR-{o# zR&kK>RVL!N1!&BpcgMJ>yj<1GouQ#qu)m+U1p2~)}Y@1x0?Lx)`*HK*1y|uYE+g9XLBQUUn z>hw&ywwKoJAD_eE|Jqj}T^s4z+VkOc_p!OQp~ugAU9``C^+7*d034&^+9cyklCDke zJ0Q3_cC*Uz)Qdt0Jn0->Hc+G?)4ukQ|^(jZ5t8#ObCnzf$F|B)E1A9Ym?NqVc+eBI<8f_NY_>&L4OH>T?B}0Bd%?C zR?v}`5V%;b4O@qXZBH`{1OlWPdQ@Xz7Q1~-OL6vo&v?$4;17hfjfQJ;4Db{M|54o$ z?xNK@OTo3NK(^_crDzIh66XPrzClR0hbyR26_Wr9tf2aQYMP3wle?Mx`U%H-v;+yb zu{_B9RZwN7l)z3@g$c&f*hBLo#n!d`t#bODJwF7Dr-k#UWS{%1UX1qBbdn)m0(-Q{ zXAx;&$jqBW0O5wzD=yev8}|CrYv(V(IB{(kFv!%!&ImN?)#b)(>kJ-MyhsH8K8sh$ zOsJYarFHH1>DtsQ-82-%RZYu<3FYnxxEIF0uF@cYr7Q>@P}nwD^AMRfhSH`6J@A@Z z%`$L%fe==$5NsJ_9bF&wc`}bUL}LS)8x!H!cnsm#K)@CzK+M4{CJy~v1ZZPX7=uTR zFeP%9io~YuUUWVWd2(IG$`Tn*r_b!Y7tO^DqvrXF;BM_!9ooNv;l6!80k#c?ZO4AM zZu=}kr$RsUBQJwscZWJe#|eQ8Lg3Q4Htf5}1x=M+x=LJIFR+r)H-bQWy}DGkZJQW* zBSTBw=m~-C`69TsE<{>S&FjxBwhj&3-q3Ypz}KKsA=o30&5dE(>xpYqt_;Y50OXlr z>xR9-wJC5I*l%0cEePe-(zU4uq)d>;Us%BuqG_5b->6ovZYICo#Na|YZ5@-@z4cOH zfo{EcisMC!&9xQw1brpb$yVJDiy+Mzn*u-fqRXh@&uTWVqS|~fopRx9+Pm}Uy2WC& zKTe(=av1SeCpzyoJ`*^3{n}g`&jDHnzO}$cg02ym$!W;zkH-Kwt_4 zmf?sq#I-eZuhSlJZIfD!)=oT1T-!0ziMY07P@*!Iia>SW8fyQsb*|03>zd1#?D&p7 z;@VC^l(@E&;G)78hCs8v5Z6{Y{9V`-pzfZC0C8AkM6I|PD@&GwB zakf9hJP#7iV}|3cIdmV=uZu8&%%js_CVWV%cx*+;we<%GHkLuqPKe0%JvR-(&d2#OeZztkfmVU4O10l!k2sJ7A!S{71<)vOvKNNgkgkY)uZ7JFG1J_ZoIE%PD~ z(f=OKW!KGT$a~}ap;r{H3qA74_Yy(Lfum``4At@61Q&PBy!d%O^CBL?KWkg1WyXDf zl|tSGwpuPBxY^Q6^NjKPhN{~N1SdxO&Vbwq8P1Rc6S|poHVgi>3KBnH-B!WPJft-+ zHQJ}{lo+2{>u~vHHQI-Z&>2FYHv}$?Yuj00B$3^*C%Il>-|Zy3*L!truI;0j&I4Ah z`pc9*hH&%9%Acx4V(;v`m&R}h6&GeVt1M5wNMxG(0J(yW=bPdA3TPc(Y_WYlvF&NLiW(Xo z8&qf?w%aFa6S285Y+SK z2;TnkG9YL|Al`m%hi|LXB(UPWB3VJEQt?s%@~a>fX-SLJqFYRv1dQvd#c2Pt80~v` zYm7C&Olt)mw1v-2)dKCV9Fl$mFh9sHb;V0~cQJ z(zv#r^+j_0ckD^7SJ-zu$?o-D9h+-Al0xgfUvvRN00V($eZk?{Fxqr(8{2s$&wYKm z-nbCBSX~=--cW3Nnqi_+4cOBX+BfLuVQg*;+g^WMo2r{OsMkzLh9GhvlpEwmP;?iP zBDj{KskYVT+Ehz*t8(3^iLfHCj%%)O7*;pQP-9|a?`D!sK7aYg#xa=>U%3Ae1(1U@ zi9%3X&C?)A3awVw%_>YV!Pud(Z8$Nuo4(uyvoH>l+)JNX3Vmm6mCVA&5Ijn+ft>K8 z)eL^8=TSK4kWW)^ZUwov3}Rx&N$U31O|Wracu(_gdUr)y@qYwqffr_t(VNb zl!Hq7Q_AN-^32|;mZPx=6YtLK*8BOIZO*_#hHbAWu1&czK+t9y zx})d@xQbSDNN{bM3tvb@QBBv};@T`#-d$6*EL5c@GIruH=2XssB(&2^GAZ41OJ#f$i0}S%LNCj>Es5SRAJ~` z+oq1Hg6pI%Qei^i%_30Ux87_Aw>q}<954OL&6aukcp?JCwVepqR`Kok)UDI>kq{U? z0^7XE=sQFm*g#-=4cod$ZQKxwfJiu5G9-A5zs;RCz_3u!1Q>Bd+c66VwXSKM!}na9gF{}B|30BGpEM78sD{FsOXd4jsNL~DgW&Uc002Z zQx}Wl7-Q$Qs7j2-&V$vfXMwfHe>h%r|L3#pYt>H($n?YsPMx(#Mk}8Ddu%wD9ZR{Fqz+(=H*RUF>6V?@tMA_h8W6X;)0H;M~dfmcx?% z47$_%g2^SYkH@08jvxko!a`0X0?fYlc7yj)i@|zFcv@70F`gQd(W2n^O{)wX(Vwpt zF#nFDEz!o;1a&d~?6&O^UaN8p<7@8i>&A=o+4p5QaOn=;znI-VG@A$i{QiBPC2AUZ zIq{}+<@^p77P}qnYqNQ$y|Tp|T>Hf$LxoVZdk0fUYA*PsmjN>h_jS!c;6L2K z`+P?~)`aT<&+*MTesg7{Nf^f9L3v-}Nw7$hmVv>wf7-f^G3resG(tE;0Xizp&Q?d(N}x5^H@5{I_4&_r;!97QG?qf5umoxQ{@kZ^y6WT-!9o<9^r&yt74KS z^Y?e8pUGj+-TvO5Y4~h{er4&$w+ycdhtvG-zPq#a9rsLgI;h` zVnP>e(v#hm`IJFh2CRDK3f8Jo{q-du&yzn|GWWpGH|qplf6kzjMeS}Df@?$W86GNN z&>0%?9?jsy1>5%P65Kfs1H6Yuz#a+mGz=u=Fh*6250j$7HYcQNsK@7_c>30j#B|k|2Wp>D9UW z4VqxS-*SS{n2(#6v^BbdLlyhnbTD74-kur1238fgBy}8n-*Wf;`yYb~p3F(Eh+)v% zB;)N~g1MA^U9006wC(BC=FebV-lVUelNogOSy2xGwC%`x;k(qS z2>mps80oPdT$ZU9`3d7T{g=Rto#1cDOu;&gv(ap>7xv()5Ke`v@_+7?eebom{5W>t z_)Lqk)0(xoAM+dQHi35@vyq>HIo2IC{RIQuJUPg$L_aTIJC|xi`|c<@>+oY-YM1c0 zz6HyExK~$@$DmU^c50P?vz4lSjWNz`J|?XN;F48i9G@RCXd4xS=c!=-wcb1V((oJ# zY`f(KRy?p(Y!dyEcydB>1^D39Jcj)Z20h#KsQV&tu}=J>VXW`69j_W^flq9nawGK$ zo)7z8H%?$I6@FDYYQS#T@qznnCD{1+orLMw7d5K2Odo*R)8v zXxA6}(+z20{jZPH20R&bUP@ZSP4I$N4jUIdrz#yHZkz+N+r|EFHnaB$avvf08FC*Y z_o@H2j}iMExet>2}1yqYxC6vFxoqdP5sVV-tZoN%84X7?3)Kd_lC_EqvB zgU)*WOS%&CD45C6vrlEvS}Gh9N#Jnz)6GBe%qZvh?Bfkyu|U1&D)!xx_@q2(Fi*P6 znQrXkQ!Fwxx>@~~R?%07anF)_qQRPXWdCDWU~6;!$Nn2Sz!#UoFZ;i2>R2DcurmMu zS?>$~JC4-rq;FF5|2cQg(t!Xqu%uL~xH0VUt+~>St_8BReAw&kTCi|($d(6S>04sq zTIXR`onO244$i+}@x&+z?6x?k;36eU`>nNJDe7ZNd+G_^I8wEuH5`iODS2p->YOrC3w!Q zcFj0^{`Y9}ij5ac>2$q&o~-NLI8`_D87#S@x^dSWS^9L)g{-X?P3gSIV`I}KW$FD> zjm2()Kef+^NuMc87pDiDIOJza|6M;f>g6<9`s~8SVR7`$ixZO;T0*k)s2T6oFfh9> z*?p(adq?9l>-^cD9vb2JdGQ{fC{bD3d|Tsi6|VP?rzPh!zE3|qcvl2ieD!5sWwifk zsQpz%uw3Js`jhC7>nlcnRHHrK?m;8c=&$^UIU{ahd8bSh6Ioe$eXRLU9&qXL#gkfc zvb1Pvs>=@CpZE*6#Afiv<5?@dp+8ogGXKx`D4$+q6bYWHe~Q;|p)B1zr_o3o%w^!_ zDkm>X?{V(P?nHlIms_M+2VI9DFVF^W^GMw5N1+H_NWmD&?uzQbo}VZC_dM2kUNM zd4W+&(I1!IEN0mg%ha2TI6hMJU$dvh*TCGrW8(B1DSGo>`R;tMA=mVhnih&~xx9pb z1iZY<-go`B)Ix|bnfAA6uoLazik0{<{Y!u%ixM-!%X=UQ@W$& zZ=xO8VELe*Gx$oNQlB_je9(ef1}++Gjs1x0MeW;@I4{+do_|hH_X^l2mB;@mxbVyA zrSoup>+LbYWog(qM86j|fq$Ai+`kImqobi11NM=3O`VZ$N+0GlQcS?-|E|d0q6bbp zk))7`_E+uCv3CGJ%g_m*+D*|5c#`^3!H(Y_jX1PZwDjT*`#$h3g}0t+Z4{lEd9Z&G z;t&Su%406z*T$Wx7T|5A6`pB0|I|#y%U8fIhxZ0VcTjZE&NRi_;N(q@B3aiz%c1q{ z9+={E==e^9~Pe`8m?f7$(=qL)oGT2>D35Zd>j;~|?7lr)CrEcuiSSA{Kxjpw|iifw-+|=0mtqUDqM@} zr;jR^4q!fxYjdTW<9-{S+)26u{(R{tk1N`@`lcDb9@uF1A;XzqFFz(z930K{&ioPX z*P*)4uN3p$P=9?=<`6}Hv+GNo4^B?AKj{Z9sXUNai}iEl&;v~qoUd|6Twe!#^znv= z3&7E{|M+o(s>v3S7Ic1^U7E+dF%(N77jM1U|qfGvj^+3Ke->(699icz2{`sdx}o(yT$h% z`%zg^c)WQvMdymeSpNdgemW%9SwYd)4#r*_0$WcW+hbcs(Z6Pu={A8+J@a^Zzl5T< z>Ri8f53FW0v*{T2zX-L1C9}Y_+f(e$Vt;I%wNtMN`=_3JT;bst6umvXKRFYu{-m-a z_8CQg%3Eb)1rC2^m+zKG(f4P)?H<8?uW6uR>GzbP6YWhC%fY_a-Sy^y*E(%r7J}C_ zj_8_$g>`k95#T_HF~xrD--)w06nw!mZl_uLV!vlUi^%5<9z()#p-NcKtu;A0J_lGf z`A<0w^$1fsMR1|@S*&IDT4g=ABv}80Kk_m+-{`ji-#ceVyU*Ywica_` zm$V!#>>|M{nN88N&vo!~fxnF|yB+wDqBYB+lR6Tx531JITRouY5IqzAB5;9Tqe)i= zMZ2CFvk3#ANdGB0nNHC^cHhv~0zd8=sv8Ao_cbS$$77$HFq>Ties*hSk3X328kg}E z@P?&X`a8e_o44x*rcw092N|}{;!Np5&JfexV1>q;J;%W7V_PS#rea@T?W(^7d||db zV|NP1mG=!(0=)0d9J|BG6y2Te{iZV(?GdMLNhD#OD!sD34&Hd`m&?FCiq;WIwOs=) zKWVRe^e#nceAMeLw6dfR7)m@G_)*Agg zo}5Q1x})cC;zJgvY}Yh9LeW8YcKcli=RHb~cxp}28V2k6{lG`InT=Liz%E!R+jRu| zEHHmi&kWD;p9y?Bz!&6}iMi~f=$A#)x;BD+xATuF?uH$6qgq}QywY`>TaO{e`v+@ig$h1fDS8h7>AQB|!+O@1e%r8*B<|{Do$s}#k;iQlMR)N#c5Vjql-bAV>QZ#* zqL;7Dz;g!Dz3kS*Zn{vpF%Ue@W4c$g21Oq)N_)*}|Kb4gJ=N;?yjbsoIB<(3m)G%? zm@i!7|6xnjt?!hVW1p=(_2n*(S5My`oTW_BwNvCf9)r(pavO49M$uodQkL(*uC8Yz zHz-mx|DTJmTEMauYA02fQgr>`+5hm)c{YoR7gO}m{EoOP9Czw}q4H=E?5t;t|8uu7DEi#VlzCUcy9^myZ_mQ`<@Xf$ zfM0$%WZFIx>!Bu&>lj!)`037{Z zzk*gTgEqMuDdP$D`*W0Lwm>#Z6X?Ve)=U3?ctwf$<=lhU-5swky zl*=^D1pj>WE=T}zugnI$yj5Utsb9x7;CuzYvcF;2TkHCsTK>lIJ*yY|Jqk8p=EjC0 zp7J%d)pQ$}>(i$)JzS5y7w0eX-nbx(LFf1{ioS;9!7c%g+3>SfYJ5#v!R8A)&i#g8 zEpBpV&<5Yf-pl)!_qck3Gi$P9LlK|KuJax%0RM3k;KD{l|dI4JI4%w2d+I$^}&2HU!XDh9lTa0(_rfloFBY_ zA&mWLs6u+j+24puEEImB2-eo9QZeJi{`7XdW&%kgWH$_|T z5Ad=F2fq4SpfrV|r`^(*JqgzA`pDzTkMkexJLd{+Qxx-66{cw2WCIIFFm-cJM5qY% z>!sg0kAa28gtpqw!G7v0t9=5T86-}1U_It+7X9oDR$D5)W&1q%T}KudI)G>TuUB}6 z^?NFKULmVJ?}IjmJ1(GTS>yWotmDH`j#r+{r*OY>C6mF)1;-3yWhpv5c2N5}m}Bba z_1-vtb#j6BF!-3=*7@(SU&I($y&41`JpDs%0rsEj$dC=6!9VD`sovPHRIe|7-3k^G zDRkvqP0__m@)Lf6+hqeC9%28JD(dSP13UA#yFJGK_%`Wz$0+!~3T>+y8*sn->Ujn5 zJYoJYYS<5Ue`tSqDmZY4?!iquxPQB2Q!;V>Ow(GsG3>VnPdM(q0M~2FwN_(4HWcK! z*aKEvclyZrE%0l-$8~t{`8yH^i+^B05C6$iECQ}=>e-!PK+$VnMC!7*VsvuJ>75kq zxMHG{58RYfS7*PMq7_RXUu?qlT0(^9EZU%@|l9M$#BDZ1xlvix7LmTmQ| zm_x9m1N3cH;`4U;UwjT&V*k6(rEnH(a8Q=xJy_lEy?5 zDjnd{cSMrpz(J`F6_!>M%@A&5>VpGLm(DB0^KR8>V2>S`uiQ8BtSv>`TVK}q10U82 znKQ+nqAkZeZ6d(}Ll0AL<9W+Dk)UuNJdaO0Sn3q^=fMX(@4%ZyI2oVtybj+I@P-HW zfnVlKuRZP*{oW(6XE%65_6nwv7e!muDkNVA_pDsr_S>7HMIL`=z66IdRBxGHfL)&A z+cOpRN7C=-lg0tKK6keNUU0?V<6HHD;fDsE&@Tj+kEp2fg;4Y(ZcqPj;2p1@cZ7xE z`I~XS#}xL?HuWzWmUj3EdPP(8w!j+#N5G-$4pwx+J{p!UOiBSOe7xNy5BsTh({+VO z@UZQjEpNeMSr-*n!2Wxhb>Um~9gOEzPTN56*3?v$SFse`wkWNq87#o*B9;;dyZ>fN z(h}I04u9ep@8c;tWnCA)57?gTinYaET<`9f-aGKov*yfQi4-lb^^1Qt?Ai58!d@D{ zUKCrIEMNz2jJ5T8luFTpRShNK;5R4FCY^*m+WyR2fw2-L%;2L7Dc;k=J3x1Z=3qv^#$y2yNiGH`oOD>b!o1Hy)Kn&o;V+V zkW$>k$vyBBteaA z{u;2FNTb2LVu}vlzb}ywepyOZ^!nRxD4NSvPfrtU`0TdANICo>ll0>4;IE@)IT>#$ z+W%I6rvdoos=3v??wT~!g_vrs@o78q)}I()r`1=%QL>a;5SXFd&=Pl?r(Co=>R`% zPR?1|hWYSp7ynfFrRwh7owlFhAL!fiuK^E;2Y4;`g1D0PF26HiK7WhJL*T|^PcHrh z2k0!dTa5G1_kQ}h=eyZkE(N5G|Sopoo>-Y3#K zdhdcA&b~L@@d5Vv3B%rnEWbWQh;sq%_gByFWOH!3cIMSZ?=fHcn+0RQI!h+$@EYvz z+@F$5z){=om{e53K67sE9Rs%{C&`RfV0_-*y2Og(+`jDVV}bF=`E4t78m!vkZo!Jf z^`Dv{914Ek(x`U&B}Gr2qh5X={EX{jr#}2rU+x9=tT;=;3y%hs=ZN#v7xcA)b51Ym zrZN6(U(i(Ral0n^gUw4e?;JhTPSKxdSZ)7-^Dk#yd@zpwc%}4k+W^@5;9u89-2a1t z`IRj8_*88fhVhJ(kT}*0K6B{TY(eyg+s@v=MsVkG{q_&FugtgobW^%ssWa;@9Mio0BuT@!N{*E8jEyp#q073w4Ls)3@p zryLOq0duOhY>vSA?Z!9%_-iuPo@>{|hT#;>g&2JYcvrYr*YPUkCs4c_OazC{k4pLIRwD|qK- z`7Ki515VINOQE zT>h;2rv8{Cej|>nOdjbj!F){hU$|(vm!hc$Jxtboo#Lro>2lb@uf3+nH=C#u?6|f_l^ly>tF7u3SqyP)tlQ@2A-?!6CaE? zONVRBy%cb~^Qd$H=I@G&rh2yE-Q3Z$=VHFzTr$$F1D=yHo^w8rqVL_^+Re)UtBG$@ z-GTkilp1Fq!v6QugtNTx5&SEI0seX5)s%g)JdP*xIoXV0KlK>gCKrX{Q}Yh$6@ya} zK6|~!{;4H-_9-h){^>z@zzOh$pJv_3V71uE-P^F=O4+vfd4ON0MSU&DejL_vxI`PQ z8)WZ$4V-eL*{=)x|LcSqTdso5i@6ojz+^lHAr?ruc)b3duQW*upQ40w|4!FGQ6los z+GB-eg7A;^N9O0^h}1 zYe7EnjfQMGm`mx8jA}cB_IP_pwiX;|Q);OMzwE<)t8=XQT1NYaoHt$2w;cM$DT{GC zdd^Q=5`J81+JJ=(_?z^psBZXq`o8HeSkIlE=C?Mo{J!ru?yOscdGt>GzFq-%=THR4 z5pWle<%mu{{EBR^b3FOq}x+^A3LWo8dCXu0=+x6gsAx8#4_F$}-Z z_JVmm__k+%eIL=6|9ph`bmeS;72@JulGC)eDITTWIfJ5HP_bD4(pPqq4e|a zUkrNvw?3^TaEEDm;rvlt|I}@cE-+v4lVjh<81y}(S#5Gy*X?4Zl<7Ewej#CFVGs6p z+CKRUd|&NtK{z<^X=LRXSn_qlof2@%`@JbEC*g;kS{Jtg`-oS*SFZ+mo2cTU>tL4y zU1yRe7_>$1tGEx~WBr2d?f87nvdF`d*e8vJc&#JB;e{>Sn&58biz?5+VgrZg90qGP zpQ+l4>xJ+KA9ez3d|qMd4!`Hu$)FAP;FT*}CZBL2&eor?fOY=dM4!Pw+!Sr$e$2`p z9Jgv^$5z%Pr+$poW>fw6fMM+S^N!b_<6%(DKPuFLC)pr+S3l6$J}7@CsA`=O{3_8KSP8vz_$grsxO&}ePPC@BN5=pN4Mh+31D9o zJ7{AIp4<0bMvD)2-nEe8wct&ci>>xdp=du^zwQ#gJIyK zt|c2;@%hrK1?DpNe8HjQ1RpRp^}^3>@au+dpU`mvYnDl!Y(pHR?%?6OTftv)!+$ml zQ*@+Iez7{(LUHN*^Jw3(N=e;WU_*av^B}O)){9f>FrK=C8pgvoUvi~WQ5M+$TilN2 zxL%Wx@J3(oncfhqbGV;-`4Mt7nA3~jd=J{Q*n4AP5ayFZ|2@w&Xm7A#Yk@5I(wp3T zU-a*UQ7z{y%ukJ@o7eYXJo=_yIA{U>D&{nQ1mmU0Ur-5JExfj4 z@k`7Ew1R$#{lKPjl=%?rH|W<*E;n$K_RDk`>W=it*qy`3kz^uih6gfY;!&GhLo+t7g#D`4DqIuu^aI^^1u6eEZH=$Fj>sk^&-!5x1CX-Em+ym~-ms zg6+`7?6vw4BM*MK!LM)#aS)%Gub;_+d!KI3anE4TGrvx{Ph#&5nVS*x6*?fXBY&HI zfIZ&76aN{*pacGT=eK~ZC{=6{)i#by%cMi_~#(6FM(a z2ezEhiIF-oQfEf$&~6htHTHP2bxv$%>x<+F{gED_Pa^e8T!g-f)Zge4`W$XTzeDPK zt`hnmQXfR>he-PcCJiLSTolU=Vr!Au+M`af5M9nP#{d5;Vc9`6{;~eZ>##q|)-jPf zC$pATk zMDSLw^58A$47xyIpL7*CLdn>0F4jHA>=vmpFmGQNuN(OI+)GyCtbMY_z-tnpZ{p#~ zR{~$tU3N4H@!T@GcY-W`_}r3@=NIDqZSg6+j^NXmRo~4;yp>ZuSmP2{ufJNM1YG{G z=0+a)l-sZFbI@Tae=@oB13WcNEWZbA{iDTk5BBLn&OISK_`ZS?t8u?u_k4-j0&dOTDRmQkw_+l}6P&#C=&!lBpHP`D|8HQ0$B|;FRo_!9PHz4Hk&;Zu{xhY7k~bv)`s&1K5FskK7$@nJowjDaQs=xatp+> z)BGFj_kq{S&N+7ttZKn6D-B-1KmM;7cw5YMzxQa*%lq<|4!lBtul+MO47|v5Q+-@1 z`ZwBOpE8&|mwkCZJSoEcxi;>KKZfIUT^J(_obtQm+8=!W2uJC1X>fEzA@eSDvI5s% z-FuJzRQz>)_{(z!y-H*8#B%VD)=Mh2Xz#9u868a+ub73qgy-Xa(zf2@a{?zkFg)Od ze1mxQo1xj@$Zx(2b#ec}5g|%pm>=8w*4EeK_~kttHSEAG0?f2W;QWN!XZgVgo^;lh zg72#>|NDY9|D5JNZU?`8XWf(vCUv`{uD6cR{gS$1Qa4QMib>rusY@o;KemMFY~3Kc zZEOv-;H3`BJFwx|W6#=0*t$W|zhQrtt*L7m0dpG+qtfVqq6o z4nDs;E?Nuo!04!21@#g32UVvL3+{~F^wJsjjF6_t>N_&X-t{xBTD$@;cX(DV9I7A?7=O+#vL{t;sL$=EZOJKXH93NKVe4Pl+b!uRB<=Zd4zA@-S zoCjh);(l+B>VJHI_W0&>N}mNE_*u%#?1ug4B4D-FB?j%Up(+ zU_TZaewbjbi?vfl(xY*|b_Wv7{L%mIUD{W~nz0X5pJ}PZ_*v&AOrAu4ivH2pG6MI_ z*(QCtjzQlTtMq8YzMs8-ArM#to%F`P2abcu`Glz^f`2xw=i6#p`H{tP_)hv~(=VfM z`scfw*sS}gyb>he;s3=yM47$a1%b8@5tY_5XV>NNmj9RQ7-Ufr0hdRg{(D>)OVsm~pl&(U2%eQYk>wI%h!FYLf=+=hE-{ykl@BgtXT#0zm z@!xN=!R)rO$DM2sn#w9!SjW1r>T4;})-9!I{olsRhVfaB+J$SKRR7=mVx9M&KjPT6 zmE3zwkQZlvwpjVT>r&*w7l)P%{KU1`*Jihayf&^m6DGty*7e!85nIE_)_Af%%PK4@ zVRbV8F5$m96+djc5hG)tA8<0nqzkNTrDdda9$K9lVJ1&#tlfVNlwl%N8TZ5aO`@u6z*BQizJ|hQ;8X zH`W!J9}!~@|C=cn0AJ)i=d)16Xdm_LRsIN;u)gBF{vBebbE?`#z$!=2|Jhj$tzu-D z2z3e1n8iFv5{PL>Ec~%d3H<$MHcwC`a%R%w4(q@bMvqNymmx-Xvt_yiScIo1qV^SH zgi@gyZ^1&LFB`fGkfYQ2v1|-%c4LN~{R_mzpXh8C4MhLMXM0K^rq@!pAw3zaUuWqg zm4W_$*_{3dtlQ))E|fyiu}%*Z=LMP4d)-@IXC@-2L)}+g1+L)pN>YtS>>!LUZ7+D! zMNRR~G4Q3QS8NFaU#?aO-w=)d=zneh68tWjv6Yq6azDIOnDa7XL2jig16Pp)OkmVW-KT?Bq6Y}b^q5wVf5?6?^r z$mxF;9okJJwv(lJ-WjaA@?m0%xMZvUKJ2rgZA{QW^y=zua2G8HIhY)cu4ckDX@rQh=sT`#xLmd zy0hTgnOu42=0MA;5Kgg<8-A{f3z9%i=k&ANYH06en<15PXma%We=ru}ergUYhdUv5 zc6X=$?GHFUJK=3L$26>;qYG}QgEOL?U!E6)#_ZfzPfIZWZz=5<#FDZ{9+}C2-+vu% z&_`^q{rLXLd-y&-x9arLNyJY2(mhS`Usf>8_jYg&Nt!m{W@23++S7U)>NJ8(Mr_1k8SZwrB)0xcr{A({&BFmf3Fl z8ttETP;K9JaQyLLgBIMcn62UJVXzkG@Qdb4u$8-wKP`pEDaouy$Nw^e)~l9y@*T&= z9`5zgxyqo=o^E~;3vQa0EcOune^R_IY8lw1KGrnuHiPcH*zVeb_EeoeqiP=qTRb%X zQ#Y%<)(fK;7|)Dsr-o@b-?5-eQ{yg!Zv5-gumybgmbu_Cc`HP@sjo78OoFHJY0`6e~tq-LDdl#`lsQj<<<)=5n}sd*z&XKisWbGYUn@85}k+pqf?H?JBVYBmU&R@mQY!`(x#SECT^a8&P z>CeH{oqG+X8L~8w^P|flU{&6PVmH)O7nkNMo&vA>IN`W&7HS38n{Q{;aCpvAxurZE zHOn_&R82s${^Q-n@Rg{6zI5b!dOg^xDJuLuY7K82e@=f2-uY#R$q^1&`na2@h&8zH z*OcjcW6;ED-4+>zJvYH2c}d_8{N3Hxl~|hkq&2#Rn!liRI6m?19Qe@Guziyedplvkk25ZoG&H@$HH{_)w5=Y7Cd1+PT45O2D;T%Uz@Z+)0=L>IdiV26Mb`D5QT7!JO(QKtrEj3Alw2X~2e#^eX3z5)H9DnJ$}Pb& zBO;g+&nbEb_sPwg;7?_b47u{*kMHtctN^}jJiVm(3C1I*T~HEy+Ox@WEEigh{Dwv1 z-~-1FT5U$WCr_|Q@DJ?uC0da(-49U9DBRxr7;N}jK>8IlxJY(wasr=~w(#OZ{AXHk zkpEI}ap%fqQTL!p&butY4_;reFHSKIpHB%B5Ci{^j$F@;_)oK%w}K+LW#?5di5nFC zLFK!R5?CT!%Xc5*Kea;FljealH$Jxfjrh;Vy)Px4;6%Q0?)RaHzkHbC-voaoXxZ@k zWmgd2S;%dB4b0s!+f@HD;thk%dON_E&7}`Y1YrKG`(Bb5Zc4lR^Z2?VUi3(Sw)p|( zS!`x~%!i_v|2Fpf2v(92X>9jK{Hsi~=P~%pn3-S);z>VNOcjU*t2~cAxy=Ldytzm8 zJiw=?Y#&+d22EXRW1>0uPyAML!*xZ#a~e`7XV_tW^TzT0LVTjt<6ac+5yY=X0*h#@U_pLPQB z3&FXEkB#Yqr5z6yF~FhQH7rICLDQ%j)Abkei)QgRt<~Vj^ZB~@;NL47eMeZw(>tfI z;xjecmg|))p{bv@kWT^3*D9L-3g6eRDWLlW@rWkZC6OP{KBK?Vd`aMw-)HV;-QU)f z`MTC%o;z`|Gtpn|g<-`q;Lx&yd0Ob-s^{u{KM}83C%JNtJH}(W`!Abh@ZNmg04`7T zXJ@Ld7g)Ogd*XJCe}VB$0Uhwk_i|efV16)vIhW1_cZH0n4h2#4)3dZ|FDf-1dZjlpUt)i!%K9y)s z-S4-fh|hlNFv;JB{UnDu^GhDMTc#&32m6tIh|$nVFhg-)=lL&)uR4u~cq9J%z+po5 zB=*Oji2@HC5pOP=F<21y8}ZolMIjT2XD6OGHoAV4HUC#PH!O~#cD>}3!+Q4QpX)W3l9mZ#WygG*#|Tt+Znhhje^Z%x7TCiZ4o z4#unFv(=_VaG2_;R8frAscU6NhQNp7FD>%}H!V;-BA<%**s6m`OIhQ0K5k1gSXXB#jZCt# zmrKyT0Qb?LUL1cn{5@bJ?#H%CFpc%@hoL)dUN6CN`LBgp?}q4&DhObm@9S@{iS_+8 zTmv%Pn4hZGFZlC=U;Q#nvPS>;7EYGrqy2?N@`B$nzs0K>n08=KPmzE$@S)!?Y%cATF#;{<&mQGFH9NpIr?2{-04Mp}cbozH=hCBafjQt$ z=ZcQp$M}5_yry4(@i}(2KY;b_fT>fn1VX_dYqfZ<525BXecXmsQ&KDb+_wkziiBuO z_ge6d9mWTFF<(rs>?m0P&h}~HX+{2Ocm|jL46vV$RB>uI?15Sxe%8Cq;tTJN`oo^- z+wi^Q19+ibRNi;kGd<;pna9D>>cX1(&Dc*DIVY-u69UFAM>aq!dhyOhVesYY_oq46 z<9TOhV)G31Id^i4oet)Ie&=#O8?cPv@-Mt#ciF=J1K|0lsza>xbK;V)|5vP!jqMWU zS>@Odw2YbC!G8i0CB3o!q$KJSd$8VA-^`yqqXhT+JwaeUI9jjvKio3+)|;+)tex&uQ%oQ|CPIB=Nj3$3|88*k(Ic_ADKrX^DJZ@hRoBDc^op& zL*{|VJQ0~kBJ)gS9*WFUk$EgK&qe0J$UGUDMeZmdw+Vd0aBjOXh*eJTaL^CiBc>9-7QklX+}1 z&rRmR$viokM`OKF9fM+=oIOpbNNpZhN&1SXJIQ zvm2b>vC**O6W(1AwbseR7j~5WmH1XXr_DUWs|a?*uF8s2a0)({xJN2yC!=yWaC^yo*C_Q^*^zW$}XY zM(lgFH93v~ez4oC{mcDOt0S46lW`CneQjEaD)zBk@k8me{E^3&7|&~mAGctmRu@CM(z z$eRvHRi=SY>(`lP$KqWH7N=5wgZnhQ+f;7Bj#k;ebVDGtgX{UnVk6<#7RK101HW-> z`+D>$_W9S6!Y{$|-UC7yTI?2P8eFdQ}p2eKm0CW?A9S7Qb^w9y?&8 z%^UErdHjQw4)D`Uv-w8BPA~1$y^dghm_~FlFT?K8Sf}P=!K(dqzH1KlQR)6VYK)qN zPz~Q(;2GNM%I_Jm{7AV4Przwya~x+G;yK7|?b8Lml8~Yzxe2wa(k-85VCSd&*qD1> z7dn>mzWHk4WoI%pXKg^ih5wgTVsFj!@{4!V*yr9um^S_0!C z|60t4;Q(((u=lB83s-fDKCB%&kEJ7+Y!$d*wF2X7R$q_@-d1<9aDxio1;f#2@fqx) zK1GIa8SekbFefkkmZ1CrBd^8qTSBxrSAwf9ten1bA!?^yX`G7zpPHC*_1ApVYStN_ z%LH2%Mx{NP2fH-JobwmByMOi9^$hrhvyEgg!>{_V%XVnjT)a!<&Xd+-;9qHzgWlq( zH4J)Wz6PA<)(}k1#P~lL(h>vf+t@I^OoNW2`0#wbxqCw;NDjMT(Kz>tzF<0;|{K}GE{iLjrHSrzu6U>{<-^WHU~bhlf2d(YUN9@enj~tM=Wj1?z-WjCevb7k%zz{&TR~ajOds(5M+Fy5#$T zT~zf1vyn%ialbWM6xW+S(%MTQkDh+G(DVzAOD+5$UklCP0$r7}Uf{nJZ+le*YA76} zG-5FR8@`Ih-hj4LOE2~Y4bGTCFIrxLcbSH7i{=3DUDjh@jXZn6ufVYnn17Ef+pcFp zdpc9U-Zc*#OC1os`GP?|i*;@A0P8P4#KC~JbkVx1)my+jG*$-h!n-R1Hn0CA1>VW& z)<2MgcjKCvtmX#~R7%M>fcMV&^JD_+>FH)EpBZ?!#czG7sKq$GPHIdz724#+%-BC2 zIKE0k*fQikw5Y*n_PT&C%$pUr4|&*zqhYhTS;vKZK2ad0lX!k|~y{gjXZD?>7LItaQ0ivto5u%DS8 z9~WJZymNuW#cO-OMNWSn&BHsmYuCPTX~llH?Vez6NDT|AA|Thb!2$w&P_K?r+3tikAnTCuz;YAaw<#?ts)Kkh%p@*FfqXNL>V}n;>-+r0#;$ zWstfJQrAK1K1f{%sT(16C8X|z)TNNR6;jti>Rw1)45^zTbv2~!hScSdx*by2L+XA= zT@a}oB6UTi?ugVSk-8;P*F@@`NL>`En<8~pr0$B;Ws$nA|E239bYG+{jMR;hx-wFC zM(WZ?-5RNDBmFhFaANTLBw2nRdoN+HWw!0eezvjvz8Cn%`XisKQmfg0wqGyM(k`NV|r#dq}&8w3|q~inO~(yNtBkNV|@-`$)Txv>QpglC(QX zyOgwBNxPP`dr7;Pw3|u0nzXw~yPUM!NxPo3`$@lm^czUOg7iB`zl8K#NWX^kdq}^C z^qbgggY8$5ei!MNO80Y6B&>eshuJjdr9)rn`tTZ2tSlzj2rH$Hwk<_h9NZxh7^ z--6>y>Xf4{&%u$6 zcM1k69jk<$6297{fgADm(k~(dXJOYwk4{83#vy+g;2ks*cF%`e-S{t2$WKkY4GaaB zSPXb*fo-xxg(JZ?rGDgC!_OI7r{9+VE>cJ+xDG#uH%YbcJ^1kGwuamO@N@L03ax=% z6(28@Tjm8nNNZ`SFPO*0I%$C`Y6@N-e^UUS`6S586MoA*fpEV6=%<4@)}KXw=XmP$ zi!!j|vQ1*^Par=sn@d*y4ES_WQ9*$j-qA3UurU=J)m1u}whMN0qjUTOICepXUBnjX zt@97?=)ew@3_QELNDuMl#lBw-v-t7+riB|RddklahupzlrH5u)z%MZ=G(N=AN7tOR z%yrSE=&Tj`+_~VtTtBz4Y8q0Pxg6{UTk_e*g{;Kqiw4d~!!F+7E2NnOyiluK|xVC1bT@3Fa;axMi3_Lg6&rTbD4Oh4M^Ec>^gAK7q zI(d*U36Szw2Hwb@Taf_0@1Kfq<3BMT#nTo^cl|(ZP+nLQ%P&1Bo8TsbTAk$lwzG;j z|AWtfWeaL+S~+X}4&%5>FOTYI7yN>cHv7}Sp2}xGxT2P5v%8W8H~6RA@*PW2J9DXF zb6zv%Pr$Qf)RHE=D}Qvp#uM<1Bf0zbH{u=t>rXaZ0l!WERjviT^5)E%yz5{;B z{1rvB9$`I8eG1o;fZlYU^^JOMaL~-Y*pNF6`rPEm?x)xfRA?I;P3ToQiYIq#fwksu zj(ik@-ws$BdFmtfkJUCC>VJcKq68w1!Dr{alVITU%PjBCF2erfX;nAe6pMFF#DqOv z39cSJl=BeZS7CGYMilldt_No))$}yoP~_TkZ~F^jzh+I$T$!gCnDoWWSohNLy>VRGLA*YxyU#e z87Cv{eYgPPhG@;Bqi-;^VNvrUrtO*is4 z-y9G19~XnJ-*V(sgDG@Y8S0eGMbt$!3;x|V2;C8>n1*JZGoJ-!%v3A|e>g5_Td z1`=Goz^WIk7MFDKu&B|6ny}W%w8e4 z0KZ`}&3UzI99Ztk8o5H~^*H^aj%R>1Pp)ek6_cfd$^&Xx%)Q;tYiI`a5&iKhm%ygs zZS~$$W$D&~LOV8qBMV=zr|>%)&WAgyCBTc^>wN^F=le1y^L+&Rsp|PW7E$=^fMwQA zGg`s6SN$DNAs-)7sgs$_;{1e%Q;?6ptM_pz7`#_!`J_Jb=Z5@BA?v{-b_+~YknfHS zHy>hv`2EGN#^RKg znbRFV!vFXkmsSJzdG?_E_dCS#>sr#v!0(59*93vXo?R_uar=w+TMofLTA0S6SPu46 z&B^&xi5lUc*~i|1t=?9tHJ3r3Xy)2~7o2{p!bPtH`W+9Ssm|a{GtXvO!M|y0=?&Zn z7RroK%zc44|M84+IdGWA6}4-~1JS&OTV{gm65qT$@dSB@!^@V;0RMcmH@Fpfpu_=> z`%A#jCpXI7%R>LkUN18Q=Z1&}gk{2i87SX$1pNC0SH5r>^dS26rN_aOD*W27lcDcw zm`FYb4!T~=ymk+I3M;W*1F+VOUR~h?_(yv`1t^0z4nDUCjlnxFy_Ax>pg%J>c7Vqr z8vf5yrP7CBrbOERN7jFU_4tPW|9GLDvT0CRm6R40tygGiOIAf88d@q!R1z(P(4f-N zpfpgC(9n=nC?k?eLsUk{==Z$e|4+x~^FO~G2gmU^?$@|q_kG>hb)D;Z1{hpK-0g+w zKpgn;->NBbu&1Qj6vF+%Zr8WyMaRKji@WD|9=v<4#{%E;$RE_u9XJDyi%@i#bq;YV zuPEsNu)?7@Hp>{izvThXR`9E<%^JUu52$@scVHQ~>fh_u0dVmr4X1y|PkXPC&S3+d z8Zk5H5jgixUi{iK$mh{j;!Ou#Ma)SqXdZ`tD$97jWjOFLq8R5l2ahk+uRm1wBX#34#40n%l1cRIZX z_eG&e{~|bCM=wDM=i@H&v-TeN;ZONwew?qa?v?Bo@XnL-D$-9d`A~^GL*QHH=eMhe zqTX}fVaLh1zEO#NX_ol?oH9eT{NN+c17DBg`ULd4h6#eLHP4>g8wq^uLc9gw1=Tn2uEhQO>z!QJ1b)YREz1P=ceiCn?PsuwzV!Q-7#~`*&N^~p ze2xSb>TSgMTEU}}GY_mB^Lx7}#-qr?RpD~rO-s8HZ(zKd@LJ@Uf*bs~0*sSzy_Mw* zkAbh=PTiw;8ToQXN9(e|e4FKS&!=KMd+Irrfz_-mc(bp-o=&u^e+ZUwNn86j8{_e$ z8ZR>+Xuk^1o(}(mMgFJyYVgvMQ~~~c%opFQa*DvCVa-DpMR;C4U)86B&qz9bKXV7Y zF9sYR#e-#k-cNW8|Az7vwdDn1|G{swx-lPQuDCsT2Yj?i->4e%h4QLLkxc$7@19Pc z_8R2V-0WCB1M_QnxU7>r0rg+$y!?oWa8K zpR~_lJ}bMvU)~Np{rryNh0k$3bBWwm@Mpe-KOA2m-&yh3Co8aI#6jb4ov6=s3cS7@ zyl}Z!TgV64~AbpH1rMgUu3-d0dRQV&yr7AKYqoh44(ymY={&5^%MSw zqZPxI;O{oul}`V`?~_a$o{#k`*k#mh-#@HR^DYeQf?1nS1@tia>CM&(yTDcTgFAVU zFF0wF$H)=z>F(u4V_5H|mW_`jf!A?GpPPyG@PNpAg%4nd>)Eom!HT(jUpTN{Z}+eD zt_9zyQZQcu4*X)(X^UPw38#Ne7XyD=ll58*>uYM^5``_`r2(v+df?9mo16W?PYdRK zUjhEor7}_o&Z`wpeu4Lo@3I+r1Ln3a++2?JyJ?=ac_Y~4?u*iKtoI3H0%!BVLRwcA zH=&1^iO{2hNU%!Z+68OT^X%U3x{(BM+9b~GesF5?!|A2q{ROMiCW%R~eog8xcmY1= zHoVvb`-@L8h6SI&1&{ru?=F&H1-tzyn1Q_v-^-$%cIfGrHuCSxe6alN*nuQT~U)G9FhCJp94`~?~g_Y9!-ShIe- zJX2@z+BQI71n1EmpWMXsEq=+t<#HasBk|jxC(pr44W%FVqK?5Hx(AJ5XvYdJnj^vb zwo|kz2YilYA3uMZ1WQ&wJl_eNCGw#&k6(g?#wtw)U>*0}$qu;gzr}*hRKU^aPgmq{ zq8FQor@S2a_stb|pJH5`3r?K21Z$1D& zWpgX`Mae6|&)9c-Aa1f47Z-0C^!1eRz_MlFq>tjU| z(_4XC-Baf*de3c+Q}=iVUQ+quV*$p^%OMBz=ioDCk#;|Ey>!kL=J$fPS3Tg^hU+;Le?IH2Q%jw3qG z=s2X~l#XLM&gpYNpA-5V(dUdlhx9q6&oO<@={!K^2|AC^d4|qIbe^K~7@go{HK z={|t&6X-sI?lb5cj*j0~TD##gwyVO=&PsYA3UgEwSql0l(y>jO*?9&3HUER#U`+1KDZNk3mhkSZa z7X1Fp=bieGSI+(s%rNYGKSk`rO83c}W^{8DoJ~ zcY|L)GU!ElF!oI=b`2@)f?s}StdL1BzjeY7A9=Py^&$A8OO5wIu-ubvtt;{RqVW3Ja^ROLb| zp0~p<|7XUp#AftXjq=;ODiqvlKl$)uAqm#L(>>31!5@9iDvPn+x#_y!%7x!Oe`M{VzT39U1sbttq>h1RjqIu}|8 zL+fN{9SyCsp>;U4PKVa<&^jMl2Sn?HXdMx)Gop1!v`&fEG0{3FS_ehzq-Y%#t+S$a zShP-y)^X7~FIopi>%?ds8Lcy;b!fCsjn=WzIyYJeN9*Kh9o@uUdZNyb*5T1QJzB>{ z>-=aPAgvRmb%eCekk%p6Iz?K?Nb4MF9VD%jq;-_E&XU$)(mG9A$4TovX&or76Qy;e zw9b^)q0%~4TE|N3TxlIFt&^p7w6xBa*5T4RU0TOW>wIY)Fs&1&b;PvJnARcFI%QhN zOzWI!9WiTh$4%?JX&rb*NKOgU8{|RC5kV2yLm#5j1TOYt{pR&v z@e}r$k93mMDy+ALn|+>ifxTnt-1$wfl&LFB?T~e~0w)#TDM`Zq z;E4M}A%@qTkuo-b-)8&N-TOCzZ&b-DB{ahCXzXWf1nyB*Z!w3TdCzI@sVl)EoyUf` z;0LNmzkN*{th0M+cuzHYd;FGhVF&*doDou4fjpM02c3uE=UBaxv%uh!&fow=Z!H<@3N;(U?>d6LOBkafK#Qx;=2Xn@H>+XYJ zP%7kWDC76iEq_6KCNyYMx69E zuz*z6csBBoss(Nt90mUo8SUAch&nr~!2US!g|z8nci@+-eB-jTb#XtbrpVp=LPk?V3AiH&8_fDme)!0GJd~Xf5sYq;QlPx zt6aYa+;FHo_hk{rmv@TuKJdkA`6;!CE2zHg$zl9%hx^-$VoKqc7dZB@9BlhNFxMO7 z<<9$+gUtPIu-?6==mC0@?vz;W3ick6DXfK`(oXeOB;#kxag%c~twnE5@ybXauwl}Y zkur?eM=KvRc!EWWO9h>tpjV&pA~}Y4q{`REVf>qTWiIvw7iXl!eZ%uo6P7J-416F| zSw#naNcGQWoQKJaSpwhY5T=uR|Dvtq{o zkG!`RqKBYkDEC!#499)`+8IX=!;kzdf^RvF?;I9ev~~n_o0qMmcY}w<%m%-L?|Q3G za|J)RG87hw*Qcxv8!7?c9n<_6_XTmNN?!M`V5(z5buOq52Gz-+IvP}GgX(Zloerww zL3KW;4hYo=p*kWH{+|io-2|IVjTGsCKZyG8;L@9i_~{nL4>)0qz22r8o(J1%;=L0- zL&gOdihs=C#IcPDzO#66UyMpWPWc1BV8Sl%|G2@nty32MNRNSg8-HRPhOax+-UL6E z)Srh+?cg8FuAFm#op8)*fzo~Ob(39fnuw1!Dlhtv=e-&d8h~GrOFr@JZ5%gN<~+O) z@zLGYv4z=SnMSS!Rq*q51~!??fkm~GV*KIf+orBxzz_CO3rq`wAFo-p-JE&O&y6p( zxiW_5W|{N!a_}A(p32kTQFkP8%AL`VSomQ@J3GdWv`We82(aH~(H7N@s9#<+=oSce ztX44!?}h(c8IvB7UR(b~zJ&oYQM-l8WbktTX2$G6tuBf5=Oj zJ7S)&IX-RpFXA_;+KY^jm*f0Z+Xwr=4OQj*lffI!O3wC!d*AaLFGRf4P+~?T6Yr@J zY-20Hyd>t4tnveF*`?F5_Xg^jpQOB-gwOx9{bs5r)*-LukrBM$YYo%h_ha3;k^eB9 z;kTR?Y2Mcm|2y+J=NI1Z7m{Plj`-&#{We2Jet5fV~83bTv{C|15f*!|0=`2{(IWCnNsdt7&KtZg^hDyAt#Ef$R#WmEd=OjU1L^ z9lL#NXZ>Pu(xC$Z{g~$qbIo&1!JAK-C2vK1bX&Y~&P=fQ9=qRdSl0&nLL4eb;c_wB-;nPzI<6*>KTm3xw($c;A<9T3*KYh%J;D}GzHr( z7VFK#x|ZfEQf~m>TRyJ3F#_}Urj$BSut#vc&|nyJro$F3`wc%xWUADx5cok(#bii# zfFHK8b8a|^eMDWfBctyzy5XPt$O**bM>f}A1c&y7C{}@UJ3^)Xz&D@1jcLZ~4;z|< z3xWfhKk012=cQ!8tbw;=e(?XFeL2dAFM{=km&JIS*ucm?={ zUaj9rKg8`tGmfc)HE&;Wc!A&h<7vWW1u&1!(#{*+$O}28VK)t|yX3`U=L688zqLcF z1O8a$gnK2+Jy93pTxMGaUK3k-_pm$m4Y6gSF<{q>TeJ4Kq8?fI^`)cWB@f$#bX>3w zhPC}+>OJ4~dRa9)L03y^)ujk<%h#7P2OVL5vng920m~bOl&FEvcZYAc2ZveR7#-Y) zynyzBF>Ub84V~?u_9AZ}@SGYa*yB=m(;WxcfBQ6E)ZyM9^7LulwFmchZ(ea6_*Qp+ zbI&gPUe~l(rk*r0KGiG8{(r~91^om86+85{RT`ah2FKU_;{3RNC-OkrM_1K=Bh|_q zW`YA{ZnZJrci;Tw(0;IVrPx*nFn3nrWqUAx@yylC^^uf{wn*Or-I_eZu{pSY(zpJ} zzO=>ln& ztSH6xztfm|SqH2UB^W5^hCCG!>rPd$d7~ZgSKPnE8BIYe!JpTtd*rslA%2NxXQHk^U$KQsQVrc;eaKcYD~z56ls^B3BEquhM>A&cr+P zKigCeTf;syoGQ-5N3vEZD&N8NHMu$OvLK%QS4jp&Z*l)7=7Qz_be_(}czSy9BWnz= z-?(f0?*$ltw-w$7F?}M2?e#;pIzzYV>Z{7j;8*nQ*{WdvXL0?9!LBz>I|YOHU%bh)8(h6*tw$jIM0qAx>$Jg% zzT)G|{(XJ!YyE}biSHhcVs0Zth#)-esIZ zGT=!!|At-zA86d%u8Ys_eeCBL16JN#_HoWF*!j0lWQT%9=}43;g$|i%{it_@#8N@XP|=&^$KF7JiXYG+}&t)tYs?HQme!_Q4UEP@V*^ z^33cK0r30A}k_~-9-lYTl;6Ky%TRqLhyssh~ zE&>kL?^o~2z__*zGY|$J+#fpaWg2?gstPUR0qg9o&z_QmxM2%=%;%s7&EjRXRTp8u zh5jDU1ncQ+H1SF&~fttEvyZdIMhQ zbTaH-7UuE!f7LU=Kc5!x*nrR2ZW$Sez46mJAWRDUrFZ^s>sXx6+K9Rt;K%8)nvY;_ z*u+K}tOdL637h3`8uf4utLlTmdkRh-t__FZE=$X>7yR(;F4?8}pu}KIbHGQPHv~yR@(m5IwMTDwuEZ+{bkquotyW9X5gQa`4_AUMp6NUfASz}QxIPOGeyxV-Nb21H;7s37P zwiVWR{rTR$$)?~10cR^_;P^VR>P1FigV$b_|E6Qz`<$br2^Lw+xAzhc{HRwl9?F7u z+G;NPz=rFkbn>z&*dgMQ`o*8fz`v)D!NyF=OXyu+wr$Tf0 z(xH(z(X(x6XvF$O_M8qU*qqysu1hg04upu$nb99(GFzRaE1+$)z=a zT)?MvJldY2-$m(@%0H*Um-HgU*F(4E-kyfCVsONKo{JlCKX|M*9%%y4G+$I~U4i;U z(@f27uuN*soRiRwum5l-2k#{^BAw+c|-+sJ6TLb78%2w30XrR`M<^ zdN~96&dWpI9|y~twkGAyfo@f1-lPcdSN7Dvm9w#4AJdG#Bca~ZSf2j)pNqg{!JY zO$+vkKZ-1!?t$gD>l(kr{QJIht>IHJ&(X~0J1$sXB;PIL#(KcF;EBT)%;%;z^q1L! zHCgtXuOGzzqE3tF%xJIgzR+7Xy(Y5^>**=iAuA!QKPS3RevAPpus=1^@WK4*zGko=+);P` zA#?uY>my|ty;Hs~XTLK#TsOv=2ZO*{&n(|RhV|&+BQd*Dur}+Q@m28oIn^$A!C#vS z1Q@%+;Z(ovT44FB-pig)_3Oq+ODr{{D{vVuTbZ!V8QM@E+kL| zZXHZ0&;i#5SGkvghhO?TuxlWW_-l^IVQ}*sb{%Vsui~fQA8UcT`NTD(S3v)^%e)Zw##aK_4e;>%&1K#$S&3^>r|C{U`N#=SE8oFyp;rWq& zw2!k7*ZcfcePI&;tlw(WGvdIWAD+I;;>7(weQfbj@GqyOcINDOean-s!(dUNExGM~ z(a$HyIM@wrQN7#$Xb*IgbRPtXgX4v7>zj0;mtyTFmN59+!Ypg^PBGTCEt^?$!5RZI zuwzF5C5^5icJS;;2bFm-f8@Jd(`LqtUyAOnr>IB%=_e4_f$`M)gX4@K<|8q#>zen$ zai5gj1^Qa8r)#C&ZU5@^}-f4Ejfy{s6ok8rE@ zID`3rFHhTleEdiAH?tGyE!k38)`H`k=AJq}4Z2V+oF(ml!B<~L^ESnxKK%Z+H%j<@ zjf+-}HztX()|qlIH~>zK-Yq&A>zB3*cakMo%B!wI7X7X?pKRZu3;wz9yWiK_Vywz# z*2!DJuIBI2B^a+?Y`S{~_()h6rxfbpAJyguhk!$cMz;5%p5FaHOke_7ZN$8l?-kZh zb&m{2Pk8y=+3ary#8~_%PapdTF6>gO9D+Uo-=!R#OSsr#a)>nymYC&!KNAJ)_T8!C^1drXoyYJv|JbWc73uH8QDG#&H#i}9gF=fE2c z6g$^q{N(OgRapdXS7~g|o&;UPe{GfBVEqi_K~9Fwo#mm$pTQ!J|AYr{K!3tWZv8i~ zT+RN$MOY6)5=1h%G2Z1wxks$H@w|QSY4C@*gw8!Q(~2hnMn+v$lF< zmD~ec7KuML1Q$-{UeEN37w2ovbejrYyqoTvU%{@gBE8k7VZT@BT<6Sx{wkiwg898U4&Z_nB9&fPZx)=9;Xew7Jyytz^(Om_%<`MyYbP9e z>#$!GFU%eM3r;`q(tMXJULU^WlR50E-#JMw>EQJ*Tx5g6#!^OY4&VzxzOs4XLjw2P ze3s+-)JinAfls$6wsGO}lttD&=7Rn9a_*64!)4;EKgLrdRlwrkpDb1apTBd>#T?wP zeON~W%y#hU@~z-t|6lH#!CFFlWOsr&w!hAc17~lFmfZ(lYJ0(31)qQUP3JYH=k>QI zJJf5L^SL}vAOO7a(wl%ySU->45otUQ_8K-?{2B9CM#=Q3i{N`-SdXLSaXnKnMWurI z3nz`_DTuR-l#*Su!M~jn?lxjS-MY2@`c?4#e=BunVg23Gqw|F6S)AOZzf&K)pvZox z0Bo7dUbzc=;n$k$CE(@U;r4sM+I7mN55VF#cE)W5Z~JW#T@U^=)9<(_xFuRA`W1MW z)50WQe4p$d^A#iDw!9?KczTg@2&%K({tGUjK1JqU&~W_m+s{E%-d+=u3^4z|Yt$ zwtIp5MN*^Mz^8j_$L{0xOMY9J^1|P7=D_DH1!d?YhH^}k2QT0j-8_30_BVVL`6^)N z@(dLpRjfybi-xCyCHF5?ex(M#lX<*5^M2W_uQwlny~D@OnLiI466r3N1#UP#8odFW z^JkZl4S3R#$@1>t==j)=nOM&ngvy62!Nc-rrNUuf8TD|wKLy7rx{unx{>qyxYSs>B z^NQn0H9#JQ=?AyhV81ktnlD&y*DYZ;YXdLbCBEkh*5lA53sdIxa{F?V%HaoNC2owa z0+(O0Pj|M5A9ZN+ihJNgWAm){?tz~#H%qPwtWkDcN*L>X)QS5h{CTh+(zI$>;7M$o zhUCGlkqn)Fu;q!HuKU2p?tiT~-~{`6XqIa{SYzqU);as(=d&w}x(Rm8`tdIY`wfB1 z9iN|pwL0#UsCZ(3IAc=dU+`$WM}jH*WTz{p<;h%!za>O7&f6RHvJUKW8^QT9rRnWp zSFYKgJ;8iS-vlo65ogU<-|{&T+)}gMYCX7F^@Yhju#~s*6kTwx^9{KcaJu-J?RT+X zGt&7Xy9mBBOX;i+p4dOFe#0SW2$qyw93+bU)~V5%p98_%9g`z?1OM08w!-XO`p;u{ z{SLiTry1XycE!R}#82BNAC+a^pQkUQdO8g8>4PD;eBg_Tzs)_PkazNGjQ=^lf2C~p z_HAbok8XWoR1FS!|Lf?z3)ru#FNwSh#{D|a_G21EWtW5{^Cu%7d}3pyHh7M6 ziJV$0_U}pE{L)~nzicmBGof!jcEDK(d?4Waw>8)Be)Uk9F2K_f!X; z>f}=$eX6rhb@-`HKh^Q4I{&l}0PPb%`v}lJ1GEpp#8{i?Q$YI|&^`yW4+8CzK>H}r zJ`1!D1MSm5`#8`(53~;i?Gr)!NYFkLv=0UCQ$hP!&^{Nm4+ia%LHlUXJ{$D;#Um}s z#x_}>iC0u-OgB1L1Rt(qFSix;q_Q)=)Vvw`|L=wFz)S-8ll$nWlJOb!%fq%Ke=%Q* zbGPJv|A_r$=IYTar znUBq`EByBEguislhg>^wlJt`R6>vd;{ikJM_b<1;yW#coWqMo&F<+MF>z-#mf60{W z!CY{jSGIJ)AokZb#V&5(-Rzs>nDhIyvcyOqyjfRWE*Zb&xTIh1bnwJ)8{O%4mFZtm z*?mZMA&$Sh{F|5QU$H1iU+*;De>`_e*nZ5P4~O5Sz62+1+nmM3Q%*<956;HxXAj#+ zJ;MH@r{hnSGuTXdIP?H`hOq6sDDdMIU!^3$&9RqCc7xS)xE&v0zmwwA_uU_#m(npU z+Xp;&MZv=g=YQqIpp+2!_)_<=@P4d!Z+_NRVt>{zcun~J2dua8D*J-K%DpZjrd?RS z`L=T_gUu%i*0ywD{f}DC{T=(^$v>0R4BDZ8u~o9~E_j9N+lOo~aXoDB)La1TnXWn^ z_8j>{vWYc*V6FNokByp;H|ton*9Lr{plf_}J?!!N)qT?7PdDGspH~BalZIj+vtM^! zb79r}D)?(IXiG8ihBFGXV@oQrUe@lC@&c=GwNFrbfV^Ol2YXk6eFR?&v)_Z>M#@(G zN#OA3b8k$(i|b=$-}egkiOh@o!j{{JPxR|&G4Y3UvMOuS!E7siwF<$aE|=3bmg0Q! z&GpZMZ5O}!P+9^V_Q$PKUf|{%|7QAu4_U9&cL6_f+8NEPf96kyYE8jBuO7{pV%~pz zZXM%K5O!_JS+&5))%uEpo8q>fveARZ>_*b#aZ{HpMLxEI#z6Qj0$IpAv(|hs$H%ckt+T_J&Fv|7iA^8?FYH%4`!Q z3mmr=|0}f%ES&$hIJgY)fh)#+GjPAGCSMW~!SzmD{=H8ZoPJsH`v;9dJ(@{xu$jw#{1?5=guaK_qTtoq#VHqQy<)YI{^D@M~i+Tcy~Qt=T6Ko zvNHyx*zi1E<6XMfAFO7gBee?58L+m-9lZP|OWz!v8@6;AQ&%8f*e~S^RxM>qUysjM z9Czxg1%Kd;oK=VU!%JpT*a1Axc~VPe>EgJYp5`((Jpb&Qp2Zvo&)mTIXb;%)t6~@z zIBc>1hDTu3y%&GP`ET4HX(WpIWI&Lsp&Gm`P}OKLxSV^fY7Dr4lGL?j;E{m`MO@$j zv5LW!;BD8N6#wFS_;s7|>w;}84XoL_VSil?cisvv`&u?Q1@qmbf+d5y!R$&M8{*z# zzKzt23A)LC0}I1@bV2C$9!@AUDF**zk#i_ zO5bbX_f0RjyZaw_=9vPC>zMy0n64*SV&+3<_z$x#8S|~z>P4((qDJsul+*BubkuD0 z=PzJ-GEBy6Ci=@T>(h1o`;uQ2@D2W$3zt5(Cx>Fa(Q@_qGzsyx-11AUVDu$p`@{kJ zBga|e4|wOAd3wuvVZWc;V51g>@fvb)P+}&2kErwA3*bFGyB-F~{O@;mEM3u}2R?Xl zTgPP_7Yegf(N;y?jHz+Q4RGE@)$rTvV6VDPyWa<%!+%RP$PD&*W`OoEIC=VN_ph+$ zY?9fJrGwcn&v~=M5&jIDW8FKz@4~fD)VgAR5WUhP558=n%gf~PJP|&m>jO@o;=9NT z_7ST&Me-(i>70%jU-*~5&UW2b1AcvI@@P#Ep4aNNIR)U|dbeZpVed3OEO%o1UbHCQ zY}*@-JQ+{j0V(j{7YnN!r!n8kb49$x^_YD&yxrm)p8qNFSFeG0dmWp#7xqrXKetB* z!EP!2iqGPZmvP?ekq+2sastoEOV~Tep1nG767izWIlN6N@V5u`3>bmeg~}8)r{aG9 ztK<~`hgwRFd`-jiH=}ES(WN)+&$hgG1mJs+~qF)vk**Mxq0w=I&Y5)v2ZafP<>k+ppq!w>VvO+zUSIxQK(%!_D|s zP!|riSr{QF3VXDFTd;H*xbo`7fSI^|OK->4m4eSbty$!G3jW21DgBM$&8O?!Cr7}3 zmGbI;0d61FklPr>jK}f5H(;+CgNDy1QRgz6U)KgM&Ah4<3jSBY7WM%Aog>gX%Tslk59<}cH5RsP@4=Hx zR&hKI#(c%cvavXizA?Le)}05xon>p+58kO>UFpmmPvkkxjQ68WAJTgfw9MU zK8za{Wy}KC-qf7MjISl0i!&H~(#FshodXzuuZvzTQ3v0Ae#7iBSX_5W^%n581EFH< z$#~vDhnMZn%ACb}WuEPj(#hBw!3I1vM$rf@poCAc?do}@QeFk5A$7UTax zgSUeQSi(K)+;Ti0b#GrS;Q~LI*3`Qe95!B^QGX2m$zngXvcU3NuPnI$K2~t6WE}55 za*%(?CUD;Rmc>uO6LYe4X|0^-l>g;M)xj(-%(o%_BU>u*dXHVL;R~D-JxnsP{m^tUnTCj;q`|W4oi8Ui>@Z@0&j7PI;d95ipzS=Qr&3*7O zH}pTn_g(ya^|dVKBfmVuQ#)`z4+8eQX6oz<7vzf8kBhM!1C+`pVSc0Q3C-)Id7m^d zl;(}nyi%HXO7l|xpS)F)*GltVX*9xyh)l@N%JmgUM9`kq#nQZ4npaEnZfRaF&D*7Uy)^Ha<^|KdVVYM=^Nwj=GR<42dCfHM zndU{)ylI+OP4lj4UN+6!rg_~o@0;d@)4Xvy|JWZ$J3F=&>xpvA4@=CC7q~424{t-f zCG<|Q7g(UUPqqvEHE_M^4X}_7w{$r;GS}iz3pk0#I4=|LPafV|@*TX~t$o44E!Z!P zURIle`E-l)O81b>@c(yZZDsh|xrog%R`6eUzB=>^ubED*!xDob^f}!7Su+~udcQ+SDH z=4>(>uwP$)RJjYhbipT~mH7Vkm)vGO0>5ERwNt|H6_2r0eh5z7`0)E~rt~TGf_Xju#c1O9&)DR7V*$RuaQ#X5y&(^b!l zz-x>GVlr_5&dv&6^$|RFIneJ2#*6BD<5fKPy=Gap-mVy5+=<+6i@zt-I{XY;?-P3s4yoA{AC@AwgwL4$QPlr0k9sR!$NUw4c=WIvc>xozl}y_5Eu&eC zrMtX++ZLR6p1tMm70AO_c~<&%6xbuGd|*?(7_0i`3gI{4fUM?=tE$CV1IS=89uJY1pmD8xHM1{$wmLyQ_cih3_MOF@1yT$Ur7(RGJ#)mb}8}} zv_=oD#XjWtEFbp<S-z*nn?YI`FHV^xf6j9&zYwjbD?%9Q{$HAWa zW=|4?uKly8Cw=?C_KVX^AA)m)75wC2=lyiGdY*?oMfN#sUWI}WKQRmOL!R63_obzE z;Ikj{H`d~Mw9lM;kqfpp``XKc^0;0_*Siv&!4+YBY5$s#N1^&t_!fAP_$o2OH^|F- zCh4CQfjFe?26Y4E!E|a#SdV}ucdmUx*^5{Y?SRtf&sXSbgJs~D?Fe3Q**@C<`PPf|yb=f`{7U+@(C2K|eqo5zsXCv;7H z8CZz@Q2qz(+ZLAlB(4Hmd~vV}XNO%Y9IjypHhkvb{d_X^_1-Ma^WdJ^S#7^y_n-EZ zT5ty}n`=~)&5b$9K&O@!c+0i>0)g}KTveabwgZQKSrBC<4nOIgf8$KwaskJY`P>U(Hx$pjp8|%S zhKB8u{~h~W{xEZ;}g$Dm$JUZ74Uehw08(zf5cun#S_fA&TZ5bpQk^g zSB&Ya;q-24r3$`JE$!aUU-*483w_l- z{DwP!?zjvt{hq3nKLdGL=~iNAz{P93huH;TS3KQt&jk#T;Dm>oSJLF7Aa3# zmvSOb?kQ%w5PYwqtYrXp!tyOXN7%tb+`?aQEBUr&oSP-j(69KU2Wme=^xr`Z4~ahSjfO{CP$c`%Xb#X5)jE z{`%l+1`9))-iony&I$Sb1mk!6hwX8}$QzPb?|0Y}T(mGkdjaMTos8r=kMTS>US9aF z_%ZT=zq3}mgU7-SzGuht(RF0QHpXwH*!^PZY|IyDY>S0h;8hl@c_Q$&kN71 z?4@yCH9ViUkBI)73l=OAY5j-i*J9O0-$Xp`r$f0vz5#!X((_~btd9y@i08uVZ)%?w zdV%@E&1Rn8Uwq!o(d?H;!D}wA(_4h^GuBzLZ3cMhiNVr$cz(4$t-qas`AAACt8@X* zH#dIQD@E{r_2PHSF#kEoDGR^G{N*NcTk`TVG1h_*fo&JSpV!nDAAE`agex|VG5TCW zYtJ=?B5y4(U4BduyzfmBw{nLVYhRY?SO?}uE}x2bQ<3+!SM*!S8SphezNWW$z9;5e zI{#uyhLr!LKxQ7F(Egav`k2sa;P!tRd>q;@Oq|&m|CqnGP16pi4ny~XgQv+H$A%PA zqY_6D&tR>*&H_*S`litI3!cNnv!|^EYy8;qhW9J{PmZ_b8NJkr&%W+EJAVb3>O@c- z392(ebtrs@P6gGmpgI>+2P2c{WKbOqsV!}o5vnsnbx5dA z3Dq&7Iww>Ih3ceG9TlpxLUmZEP7Bp>p*k;A2ZrjzP#qbnGedP~s7}qqSf0?ap*lBI z2Z!q9P#qnrvqN=ws7?>n@u50DR0oLa1W_F!sxw4&h^S5x)iI(vM^p!i>LgJeC91PT zb(pA56V-8|I!{ywit0pB9Vx0aMRlmCP8HR$dKX?M*!~&&+5ef#==_B#O`MI5`@j9H z*8lWQnwT>udc95b?qcRaA?82)(Y{->FBk3GMf-ZuzF)L280{NI`-;)NW3(?B?OR6s zn$f;zv@aU%n@0Pp(Y|Z6FWVu~w~h97(<6P~XkR$mH;(p|qkZRSUpm^ij`p>qeeY;r zJlZ#p_SK_(_h?@}+P9DP^`m|NXkS3uH<0!fqpw|g1p9-sBPP~Y8^l@m$6ie>$9`fZ>w63fJi?ZqXN~HN3uJX-AX>_A{^b zlRsM5A@8C5zQ8beuL8euc`f`FW_E5`*dJ}P4R~;~26=^Nt^ebzqc%Kq!KlVPWNA!mxu3blnCSvtVEulK;UC$f7Bpxx>XY#cBbBi zu}k}=tSjOH7vyZ2%ESrA{+hQ8R*17MUR`g*_}%}7^9xPJaaT{pi4H{0hytveg_d3 zcsGsbljTnFwq)=(S9`~P};UFruOb($@90GzTfp|=3s*s&{V6mgBm zbJy=11GX|hi1BaR3pxGBIXR= z2G|=ZYvOvW5w}wLbWWxSJb0_wxa$n!C|{d%M!-{^d*nStoG(@I+(#Ddmoqbi^e!Q; zXCIv;a}exj*y^<*1a+$18x2#y0g2ki;z97MNoYD%f=id1+L<1~{o{BqH3{~SCRcGt znlJX<(_Tm|248ZVvSRE2{CFvSS5|>L6i?(9IKuB!c&}O$e7;joW9Ay^!7=wA- zt_iN)i9S0wswaDc=M+x)9SpzXspk)ZZ-aMn)U?cjAIQwGv8x6gl;5`VAN)@7!m&Cp z!4BsRdBwmFrgdCm@^|nW->`I@^@!t_oUmUA`>uZ1bB%c{#Iej{>@C4fHQW_DG!Pfe zi_|#`_SAZjuB3+Nt-6uL_!nB71UMw+VW0lVWitos+RcsFxeRsm(xvy=zz6o2)ohYN zUE3GI`yKeccW+PgdBC2Q)85!o3iiFK7#O)2*SB@%{W!2z*{zCi3*qO2y?-3s!@WAZ zL;~~I92XEN(pWkfi&;b`4pXe!qKfua3_pK;6@yeBtuCSLku$pXZas4^F zl-|sRz5MB3t!5DTt=hS@!u*IkALX&t0FNxbz9<^@v)o*7o2R(n(MhK&1YtirPTD=+ z7d(F0C4D90ZnMYq?k)h+JO>)LqH!%6_o8tz8aJbHH5zxLaXA{dqj5bN_oHz^8aJeI zMH+Xc_9(SisXa^WU1|?gdzsqP)ZV7{IJMWQJx}d@>JOm)0_sno{s!ugp#BQ#&!GMe z>JOp*66#N({ub(wq5c}`&!PSv>JOs+BI-|~{wC^=qW&uC&!YY=>JOv-GU`vG{x<55 zqy9ST&!hf6>JOy;Lh4VX{zmGLr2b0k&!qlN>JO#JO&= zV(L$({$}crrv7T`&!+xv>JO*>a_Uc~{&wn*r~Z2C&!_%=8V{iH0vb=C@dg@?pz#VC z&!F)R8V{lI5*kmT{yy4Qm-gMIeR*l$UfS39|LOZn`U2Cw!L+Y1?K@2S64SoL6Snz8 zUt`+$nD#}ceUoWlW!iU{_GPAhn`vKX+V`3Eg{FO@X*9A2w! zX}R+~_U)&60*!Zpbpl?0NQ#CISDy1HF|hGTS&qUB(2>6EF!ej~d0hfO2Jpj<;1T3- zX#)FqwZ4l?h5tKo(clSij{J1LTbYQ{Y?VVD8RGLk_QR}P?91oui_ieinJ!htQ;2#) z`O^{nV8t7Xc~@^D|8I%$z^y#kZByKDTrWj@T|~{0sTWLK6)(H*4)W2tlj`2(!cI`! zJ94TF`&##C=`&!lzNb#Q<WqTq>Zb133l=+kS^@t&?E z?SkVUgJXHZ!F_L@6_?}l#FeKR>;R8nTl3*|3HHG^_#KtOGOq``G;d*_``~212>6qi zg2v7w#LZ^d40KEsNHt zW|%VdI&&{)$aeH(>`Z{E}%<0iGi9TyhQA@PqBPo8WvE z3q`d!)Ncj|@dkj^jZa3s$9PaqNX=dYw*ET1<w(^7Q+N6NT|DS;=;k2q_`V0`G^4EvLgdIj;%9~qtXl336F zE8sQvpS!DLJa(5W<|u+wBVq&jz0@z;1V`oAH@;h&b zX2+!>Kj2AZO(({;Rce;wVsLt&uatBs@<%PVhSx%`Bela&>P8^yO+9|}@q)!THizFi zjC@dm^U~L$50ddTQ0Ju=zE3}nhXpP@$YH?gfw-$rqVycF=bIlD2F|Dth%c{efd0tg zvo)oedtvv=o|Mi5j~gkxVeP{AKNjZ60{5E9G$-#s{_C}~j<>;g7WHlqu|a<7rn&ze zPnBwVd~rMKLHOCTcjLHwUx7d-@@3wgZQy3~Q$*OblJ9Q8^(?cJ%7wm)ZFp5%%tq*$ zSv}&?0tY?*q2Yx5iA#l9Tms;KJ;6ModdSyI6_k7f{g~GeJ{phcAYXNSeoZ}i$O)ceD5@_-^{1#l71ghz`c_o`it1xg{Vb}lMfJC+J{Q&RqWWG`|BLE_QT;HgFJ?mY z$EZFT)i0y^X8(`=8PP|h`e{^Ojq0yaeKxA!M)lpO{u|YYqxx}FUyka}QGGh9Uq|)r zsQw+*$D{grR9}zk?@@g|s^3TT{iyyQ)d!^dfmC0R>JL(VLaJX#^$n^1A=O8u`iWFu zk?Jo}eMYL^NcA15{v*|gr23ImUy|xiQhiFQUrF^Xss1I^$E5n1R9}35h{j~m{>H|>y0IDxQ^#`av0o5;{`UX`0fa)Vq{RFD7K=l`>J_FTnp!yC} z|AFd5Q2hw1FG2Mus6GYNub}!CRR4nNV^IAJs;@!yH>f@b)$gGC9#sE>>Vr`I5UMXi z^+%{a3Dqy5`X*HWgzBSE{S>ONLiJauJ`2@vq53XV|Ap$qQ2iLHFGKZbs6GwVuc7)j zRR4zR<52w^s;@)!cc?xO)$gJDK2-mQ>H|^zAgV7!^@pfF5!ElE`bJd$i0UIz{UoZd zMD>@bJ`>e%qWVr$|B32DQT-^YFGcmIs6G|ducG=^RR4Vr}JFsd&`^~b0_8PzYN`es!BjOwFN{WPktM)lXIJ{#3a9|z;pZ7I_Q}$nH<;vKE{9M!TdHcmKkdmrgGSS66|H;}FT9e-8#S80U{?+JhS3b;?d<3e6VZ~!*vo?UdwIWbpuY0!rYVfxfOfjS znFr!(HbKTe89g=L%mrhiywI0?KioYX_PWc-k>R_@1FpDowOR-K#Na?f$V~VXbfvih z!JbcaIhP~PS5)_~K_&Q0tV+fSapXDJit_e>XFTty2}c}J=1}5QPWT6&rUnW!_SlWY z;}I%gcHYz^32EdZ$CMc{`qFlrA3OAb3+!$zXX@|gKfj;N99Kw_iByHZAz$@v+A!jl zFW2+&GyVwM`%5jdVeftNww_uFKC{6p{iH1PlyiilCc)p)RPB{)1s;!75eNhGnOvB; zUk3gPXM@MH;ZGSI2z8%>_rGsUA3OpcbJa}_#`iraZMpUmEVOU0&MO?}Fmv9&68@YE zD>V}O@p*dXYfNInJ+X_O0~N(t`<_m9Zv>|g4+!w9!d@-NHX8@Ou5O-U2K!aAY>FA< zPqH|X`dEJ*>f5#X|Klw2_7BT8K>s3%y-6R(J!8%Ggjpgl`Lj~AI=Cn1w)0MF_`l41 zqxXXq`Shiv?4Xy>p}V3I?7oMi!Q26Q0rJmH6yVQ#$C+`r!V&kc_p*Q$IPS*EL8blB zD_c_R>IzofscF>d20iArg;5#cxnC{z4tt{B!ES{s7yNDe%8n-|dc(gkzoe1ThgvkJ z@qrfN;>|y2<{buSj4o{G^oKvpoGt1KnELnN`di4x#{OR)u8czS9UiC=crNugbK}064r;cwfulCBS9b=-4}~n%!*diJcSb%2JaI0wZ`O?n;l3s< zwd7xfdWJmXL@p2Tw|5bW&ykO8`FeWK8?fw;*stC9#8_(TGXIouA1}pKN?k!c1W#;L z<}>iMpS2f$p`O5rtzLOP?t@>uWx6Z!qgkynVlm(kv(D(TL$@)6;BPbDthum5kVd_q%KU5{Uup*rfllMgFr%a%|``u%?7?&;_uP-DHXRxX$VR^BU~H zd138s+rT_8wWjFde80|){=xWBZG=YRR^#{7bp2B41P4rdD_M-+KXI+;wU0e`qcb1% z3Vp2t--M4&nQcMNdwvNB;B8z{57|;CaQ-Qg-0kJUiz%F~2X> zu;Kvn#}#znS_^m+J!CN)I1Nqzy z53MxVzGAvbW>@`;e>v8$+6$#A>ioVscL_5J`>l@ylb6| z=>NHv6W5k+QbElSUZ1A6D*hSrI`;Zc(Ypf{$vQs#YqB`Y?ue_y5%8*&-s86?VLtqn zzhpO9y=$fQ5v*@Nd`_)%0c*VVZ89E5T>^LXZVzyu!b!z))XB{Hyjz%g{T4osz474l zl^v5cz;2&--@ga9tcc&uoWJ*uUf=A0Vl2CoslcW*D1BQUdKF4$A5sYsyumc9{he*y|^_P@drB>Fn9EA*Dt@t zSecvVb}k0@ST|HILS4_|gO}NQaep{o*LPX{Kpn==pjI~6{?(TFHr$VcB8~yh;L`@# zyG3#RZ|msayMgK<#pjzo)8K^&bpE-+UyF%?Z45XVUs1@Ry4z z+RMO0{~ud#9!_P{_WhG7Lx!R<6EY@4N`=~_5QRde6hcIyNGOENL@87hrOYZtC9_CW zrY4jjGF8SV%I|yiKKs4z-+Erh;g8R8?S1XN*R{@Zo$FkpCsGE%hM@%&EttBIiK)W|oPKF9*$$3x zv-b=7hiWx8g89#|)NqEO*P^=+^{7^lvJ zb&_({!)K{|teIWQyjEOBJ=c6Tr(*EiiSQ@OsdbZ!(rmJ!R3E4_zMUA058GVi_JQeR zlhw)=(Fo>9<2CQW_01x`=YIf`eL2X!9b{h*vhN4k7lf==BFw zdhIN|_LX{${P?H-$k&o}r(|6!S+`2owUTwOWL+#-H%r#ll6ALaT`pO-OV;(0b-!d? zFj+TD9~*jIF@a>~=tQ~GmX;+Iu@7vz}CGComG`(!j z`+PVXk{b6Q{8N8kH{Ha~EtkZ4Bdf*_oa4BhwNvh{f+X!Zdu4kknEu|jpS}M-4%AjP zbHnvJx;h-pczyWD6M=(Z#&xV)QspFROj+*}_k&v!7@Y!e&g$l(u7AwPwc2(+KF{5< z(aRp!%N515eoITzG`4nlxq>$;osrwVOp(5Fvccq_ zgd{ClSJ5j9yvmG0P9NvCX6A$zMS{cUxr%j(O46b>skMiK#TS-!_=rf-425l0odR3E z*Qgd1mZW*Eww`hacQ4=`@mwrPJGwt&)jIHo3BR;jKAh8?QkzZ>Vej<~#Q zWdJWwx_j&Cd`VjN@@Kd`l$1N?nB%o#yPeD zTsJ#iS=fy}c}@bn%HZU#sFMCJ)Svgtm~wzGUp*n%`W}5%H%xq+#P?my5_iI<6@7+7 z#Et90Ii6YnIHY81JHd(!JW4D1j)1tnc)?%G8aPbjQaDUI8!@f1x zPsuLwq8;aWx-R-#d>q$zec^p^8NBeOUveAn$D&sLVmmndj;{T$-vGkoRF_y%S?S->mRx}P5Z0R25s{i-t#e#O;UM}-lbv*f42rvR=r zTU2*t41JV#yjE?Gh2Jp3+O*-f1ns1$UyC1j&q@cO0L+J`^U*D?;H7>!BeH+dhn)GQ znhiMZxZZf}JW1M=sg)P7usjam1MIVEYrd_(GAA>01yH_pGn z{8TFTUey5>U>??VUkG`6et2ggIQGFo@0W`p&+9G}WrAO}ZM&x-AW6%05Y@O2wwpDP zAHw{9qNr$*27bYoET$$XNlW7W(MDZ2+W%CI3-V($e&>D`SaxlSc`fAe;~Q>^a`30w zXC2})lC<|H6Ym|9O>{|Ha~OUM|}S{>IM1`y29q>fYeR&ER7eg<3&klK)p% zB@Qd{lq{8`*&hDQzY*8vmIyvhf;@_yytrU5SbZ+^*BF;1jnVhZjw9g4@muUZ7$27w z%N;e~Y##l%12foP?2?sIdTIVrR`5o)VZ*p3w{lbI6^&Iyn zBxuai7Z+{^qi^QObL@LMRoBXwK=H#(^4suyjhWVu=YrW+w6FvZqEDXtzJJg6$fwm( zERerkp2ey>c>ewe6taVQk!Q%47+(+Oj+|S@19>~o(qE7cc6fJLijucgH(19Sz`818 zcJnabCU&zFeg(6MJ*7pqVVzraY`Zk(+o{}TqmG!Lr*=Nq(gvrS7b@`OX%C%tMS&282ke4jZim&6~rMtFzk5r?N_LBu) zS3y2f)>xVqRH84&!$(K_!H2VV$GKIYUcKcW_hT^KmKbMyF8B-PoLeZg9M^F!*$V~8 ztITKCE%D$+X{CSiyzKVdS5CNJ(VE$R^6nS@`l>Dpx7JTs# zET;V7^}c7Q1KC`{-3$K8`+AKO=o{d@pv3t z!6*PuG7VTX1NQ1aQLqFo)1@bL=`H%S-Lj372Y>#}Y`quq+raZ_r6t&=rRMLwJ)cmQ^I_^}7`SF-qo&?x^f8=QQ5Xw8XwawD3jOQ*C!042%$S)qXEhH0 zOvmuf5%9a?;ab!=-M8L88{Y#C>YO;=4f)Qg3w~w>Uj5xi$`a>(mkJ$HGX~3lT|Ldi zB1zl6E68&zm_P38)Jk?qnvnC*qy<<~IXdJ4`*f98##q@Bs4V`TjURCv5+Z zh~cLWkYBnlK-zz}1hViW{>97mGc_~?u5XJ+jfI@)U%@}e(1Y}`)C=m27RryfL^%=o zqrcXEzSaV7OfPHuiql5jwq3CTPsJbBeh?sz^BR>)4}&i*UzcnXhPd84WM2?i zH@j)-aSYy-QTmcH9%Af@dWj15z1VU60n?4G!5F{9GtX`pA)lGe(q%M<=O^utTH+R4w_67mED1UKAMV=>%p_-6V^%?FR{BP z4Es^P)x2w_y>AP~Z)$Y@1+Y-9S(7&MyPS?PK1~trPYOqW8jJFiQhHf?nDk|6~v}Jbre6YvD3*8zk(cdomP^KKXWnF}r zGxF0H*iH!nZ+skyAS3VfwuSLJd+=+$3M)qY^k z4PAmg{HP~%5$tmSC#~vJlUa!I_?*y7afSzHrycVDiHXX=)Omczt91{5VnbgM!Ru!z z<{1_IuFinCQ_hek4W1vhawHb{9=mlJ43nt;^08Jlf88NYJF?9_jXIC3e=N62v=RO9 zYHDomfPZOMs#u{<%+>PXTu}zW{Qd7woWWCWHs`pmArHIngs49Fn0@L*sUANKM>}%9@ zHhlh0E*-Ta&|e46g!)nEpIzB?kz>ml^l>rqUq6HQXBe$Dbq@#2$j$x%8@MokRF8xH z&{}$B5nk_^y;PhZzh}E?n}{MfMMe^|ScCTno4RQ|F(l3dcCu z;r(CCnie;K|McB&GEWkxu_&r{j)ChYUhZ|vKp!6_lT(6t9+!_?>e0$9;0m#Ow8Z$O_7N2j7P7x2d2d5iYo`DnUT*qjG%`rB%H7 z!QEfY=06=k-;r2932J_^t&tZmh5j?hR%Dz5zm)yWvXT}1!M)#I*T8&y9+%&8ZvpzE zaHpnI%sx8z-Ua&Y;O%>T)?ltC_0DMMv-WLs2k(LJw=b!-kVU`8Q2(bdz)UgBz8|6A z=INc<+y>Usk+dDfeBW3p%{C3bZI>uVt>@T(Sr{e;`T7{fX2J}4c{O=6%mu96!2Z^A z73xoUZfwZ~@0h5JlUaxQ!XJi%@4#DbspUCC{`8{`$_;^c&)KVSLqAT%S^KT}?JLuqzXy!-!VcuDeO zS<`lH$NCr}jl&80=E^QJ@c)*PdJE8xg4Hfdi zi%i~Yhg(2@?R9(J0bV;#iqqc``}i)!Ki?1icgCoWxf1=!t|qhPUI9N@ACp1ptGk2F zQEgx=-kE?nYxs?^{)*IjRT-a_gfKwA$=kW)*g>DJKGWSUg7u^eW$Mvs;HNqleSLA= zwsOPhG`Qk>&6@Kzc%Jh(r>CYE zzm+PZYw-FKB>{gA6ZnxWy8o^p$#d@bY=pRO>hr(*v6`8`@!pL3@VNTMF-pG={nGBD zpe2nKi*`Q&>2DVO>;1H9Cec4o)^#Q+cI2z(-scxoTs+#WL=GN{v_Rh?sIMC;Tvt2N_Gj<8ksP&hxM@^GQm2GsZU| za5#M(`Y!dxc<7dZH?U}xXkvX;``tiLBzR54llAZ559A%usHFTA+a9Lpm$0soJ3UrT z*^`EOLDP?laUR%%Rr{&=kiM)X!#q=*cH{7~zg^(&st$E-=(C%dKSgW7ScJ+AO~AU# zszaxO!HU7>zb}s#r=4pH|8oEw?KE`qSt$C@lvZeM1@}gtmt#ML^T2uqN;SZAeYEX! zk&fbjG2Op{%Y`L2rUO*IhyGf+E}=hr`m>_^G-v!%fArVV?@N9z`Mnf~?@j)$kHqgL z$Aug>a$L!AC!Y)X+{oujK6i3nkn@I|SLD1S=OsCB$$3rAdr~e)xgq81f5{yom!#a% zpDkUkNx3KW0;xAhy+Z08QZJEui_~kR-XrxQsW(ZzO6pxwFOzzk)a#_)C+z}hH%Ple z+8xp^k#>u;Yoy&H?ILM6NxMqgUD7U-cAK>8q}{(l_ywfjK>8J=-?54COZsafM0F6S z9f&Xyq5QzZA^Y}jIFEJ6iAQ@WKcjF7l1LUmZ~Vtt=;rt2}dOZF3o z64tAgEGu=|!Q!D@{65v6>>&L6`RFLt<;EWzu!J8_ zP_6H+0l#$7g@K+taN%0RC;sqbgR(}etHI}U#n$eWz&YXvRca+*=dT*oZ21d6+2`*5 z%y6*Qs%O)4E0Aw3$o#w*di2wo;>Hu|=qnl@bjTjO-0G-%!&<~`dhhFBfdzLgdc&*- zzk0-?Apm+mN`0u=4u1aImxC^q;H=PT_5pC_yEiV2D7&Qp)P(Ab&nNei!vg$n$K^HQ zc>nsbKMjH4(Qv(@5Gegyn{;S1|zE`wEB zf5ZfFQRgN!*&hV!3G|E3XFz`U>mFS>aJ1rfQz5M9%S*&|Q-01R>tn->cd)M4mQ{8P z%$8x$(|j9#eTd^6b*}87b^EH>RB_rRV+I>laOFcUhr<`)hbty@EdcMn**a_rKlbKi zpB5D-RbLDm*>)O!sY=(CQZU`#gvAmCg(jV7o^;faz)A=IX^LF73lR-g_;s#VTfhFtQztS zKrcVd8uye+M)a7`+f;cAXNgnuoL)c0AidpeB1fFYF{CMSHU;`%dGVI4TZm&HJh@^G z{^h~Rkb}5zV~|j%DELlGQF;R8_5SNKGnKfvR&jC2e&o&QpGE)d4;-_5#&F%h*~eNE zadu?n_4O=x|E26AZSFLjgUcq>=>s-A93DFVCi=#I3lS~`znuvcFhkyLwchf*!{Bwi zIq80d;$J9jAMGXC zGMf!R}bbzM*Em6{EnMgPk`AML^8O`_)hKVjdNb5$sVb=2;dnoJ=N7`I8B z3;gZk=(Es;Pjr9R@B{`xcy-g9DLkqD!d7G9PZAdAHd-W+FIW+ zKElFkHWgsDXKw6j7{8N8kDZDJKRNVHE94RCe$)fIEWv5d+d`Hh57XT6WbbnD^K);S zOrh`7Z6{~H;Q8@37T!#Sz3$GI6nO+D=NqOXR4yX}rJvpHzj*Q^=R%)5^szVKeIPc1 zv7qKH4!QaFw|ec>q-xZ4-SMmz>%_H4EyH1^*T^UQ&udSG?y5HI5~o$|aF~n5b^5*N z&yjr2-%6YlY3QfAg3AgHkbYdul*9;Ck(l^kxhxf&(p%yMFUvs-qFpDn?>X%hb61YS5&KKc#w zhAnxSSPsPlCM8roa^OaevS(oO`Qf1|VICw=^FYIO{;Kj2^o0JO-y#-}P_+;~NlW}L z@g(HJsCl6BZx%2xSPYs~`y$Rg^ChX{6Mh%{y#qT&AB}+>Ox@f!MvBwCt)_(d@V$>T zPn@hzLS1x>?!Vu8b9LmLRXXNB&-Tt2xE`lN3%v{d_hwP)Y#X>h>;pd|es@EGZ`TO; zTa2Vu7xa_iY1z`{_#GQwde5DJ{)uJu7u^BQuW6Q5y@$Fh_qy2(uz9RSjW+a=w_R}8 zC$QMBfs73JtINNQcg=ylo^zWyVjbc7iPay3@Y^nz?yM4RM}4HjUORm-o879jOHlU| zCu_Ce5$vKQYh&~Ub!Y!G7M?kNaedGi_C*U1tiyH3s4H5Rp-&1O`3|5r7wv$Jj?@7B zGa2=;Ya(Fbb(~IDu#TXadGqNCuwlpX>c4#0=leBVR|iZV>rLXdQpVs#&#p^FFGb$G zO}g(dzV8b4#JV}W{;t>GpkALB=TX{!y0qnJ$bWk0nUsi%f5zH*`Z8X54y<)j zaNJej@7zkQzKTL+Fk*>NF%2p>&asCw$gkE0`o{?;@_X=taVg?Ptr#Mcqnzh}rq)5y<}?pR?V7I)dLe zEEmp$XLW^Bs<wWj1;Fonh0+QT+SL_A9=8ui+>v}_h8ydQ`xfltzZ!;hWb@|E zokEf{pO~{nCg1{tTLH|dySaSg^>p7^tdFGjYG;6d{{En$58fgzHQ<0cf-iyf8ZScO zXN0NC)nT2oQNFyL>I1#v=o;x+aK|G3`@-PoH`4s5`^{rAeNY>M^&Z2R9vap$eFGl} zID`EZiz~xW2NbU3w0aTPAXvxbx)eU&_K`(aF!FEN@y2&CwxX&0iBunG`dG`Juf0$8 z{})>EHF6DJzf>eofa;?!QJeKm1WfO1sIJF#(F>nX=L6=F@@K)%miRn)hu0U5FPM3Q z-(&mP?g4fGk&~v?#rXb5H@iQ`1NYc5`8A=A;?TMN_RaWyvkQM+ZN~V;`Z}!+2ftf0 zx7limB+a9{w7nCo{BqB04Xo?li);{3#_x|#sM36jI*rS&bzUdHJVQcqf54&h-c9v@ z^OCHeMB(*!I`4VOV|Z=N&E+d;9@-K(N;<5_K2Kdp@={f=i$AOIPto(wsUY zcTR#M-S)KH#C+A|kh?^03EX{epL8b~m^+$TM|i zIMxek8zUW1_du6jx~xWN)zVCGUEIpMuLjqToE}Y!jzFKsJ(`jE;KWV4S>MDWPIokD zpz4%9Xt<_UB%wdYHn**LxWCNoTJG1^&FBPE-by`UGl58SDIGls$)S$}oyX?~gx|=4qH)^co&psSlIR7ypl5y8J(S ziGGd#+GTQzx`U|yE4lhy<160lZG1-PELbpj&ui*Dh_Am)IiAZ)(j@&^tn$EEFAA6k zHh7vTK+QwF!zO_{P%rqqrBq`p=9Nuk#uh=;3o_JIdgX#22&)UUK#qs1N)tJtheqFo z2zp3M(lQ)7rmewq75cwrA-4}EuT2MoS1j2(!-D#T#K5=ruY$YdtJLgKA9$>C)%1O^ zxH|WY6Xe*a@onNq@X1)?)U%LVgF3$ZJebD^XDm~bAh#Uc>i4&Rc{f+9=|ir9KM5v= zfZOf`2Wmo22IUgB&&LuJ>XXL%&Fe;pQQ*et&$w->3vF|LIxt6_Asy zeiCLKuoJlq6=vpOFYcXHtewi$f`(`BJ&&Gtx5*yPDmk)6@FzRiCvM<6(XpQX$R z_fz}6!l(v(e{?RDAr9;QMFq~&;O!g2ip$QS{^*XO0x#-sHgF2w@K3x-DjDGY9t>|{9)z4m3%UzcZiJ#$;*NJJXWyZF%S@&#=3cQ1<%^o+lN zngUh-Gn%eYzcd}^po(rbQUPa2PsXX-K;6b6)kq8K{_#q-wOJUiw_?Nj;7;wv*oS$j zdr*Fy-3U%PBNDgz4*C`?2;po42bW)W(JDfpou?wX)O8m(p^;O?Scm0X+4uxpZ?c@l zrwnxsvpbt8)-Uj2cZDANTh^&m3vQAx9{F8^{=>KLMR$PLUsG#d2)!Qp*xhXy%$6`C z*YylOQ`zAKOA?_6_dKyOc!B(=(*8U>aL0wyV<({Z9QB1_&A~MS{a+)X_wM&q@tc89 zcIufozrnec>_=8^1#e_r(HZvveScnUE#LvGdOj`@Lw(=<$g(jRaF}h}>xgf7-V)8{ z#K5(m8|J1aC1^Ce6!#Ti*6MTG2Y$lNv}W<{0eia6lq{Xac-_`)J^}ubRCAyicJM*f z*E?syOu8e@5lpDBv^h4B1HR(w==G3Il4i$rQmq+mdaRIPhC`AT;L+dw2OM&Ccd`QP ztWRg1rvh|uRz_gzP1xazwFc@2;IV26>HY8n&hFPgrUm9ar!1%szv!=C>G}O&%dLWC zTTwqv*WDLxMU_^g-cr%0XUA?_r|a*YeasTu|BC~DT;+!SigsP_~VFecz@nrvvf4Wn%mGOkloGJ~fQ%eu@1S zN7}ht!R1>zJKmvwxYftfEgyX5f>&~MIo6pUS;Pc_8-M1-ij-oX-6t&`q4kMIF`ejnVHQ|AODT zncZ{H&)Ysk@dB7P==a=FFb9tqmkxO7^QG8&@Y5U1T+5+v9Qda1M&kXSnnrRrf={w8 zCC=&m(@7Nj?`?$+qBhC-~{K*E- zf5)l30B_}UbuH+^KH$JJW4FPJZ~Qz+)obpqlaD_I-tjt1ekJCI<I+Tft#-JB2l zA&(`iRH^fejGmQd{DFNFI;EjH2zw^;BC?kW^1VgOd7P?O{*u%6I^rw7uk0uvRo^_g z$8byR1nl9N_U2UZZ(7v3MLR&(}0J}LdZJ5o;avtRl@ex4WM=h5{9 zsYggXL+T+?Pmy|z)N`aBB=sbzM@c

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 zcmeHQ!EPiq5KY7ZaX?6%5U2hCwCZx%ZufN&2QDlAAPPG}GVE@$$|k@f@$a}BR&j>u zu8v$r{j8otGBZ8xiJmI2s-COLpMCuNi{H-9&d$&OI*-5p^Vl8Ue|qoL@Z{@Hum1i0 z%l`8I;ch>?`hNKF)9@pCe7L`Rczyrz{r+(J`sJU8cMtd1Cf+ix|IJs)&GG5sX1D+2 z;_h&D{m=E|@4tHY_TsP`?>YJXw?F^7e|byC7pNKL0G=!8o{?1;8SQN+Hrkq`JOo2I>~!dV6W-F!()KIfb(5Q!Pg*+fp*L^ zbop0MIKLAuACU7=q8%@SPpQMT%zd_Hl4V=g;WC87WtsCSp`Ffk`Pes|3q`SST)+U| z2hrgs;Ohj5i2l1BvtNlovp>9a&xgOtLx*@NFx{ z`H8&H2T$2yeiK(In&37L1N^8KNTH@NiM(LhE2x@r6@_$R+6q88zfSNCMzljtw0!EG zSU%+^r%_%Y1@@Y@e8!+1Yeet?lPuFiWVl9-b`Ze_p&b$}ACPvoXlI&|Bj<0?PK2k< zXvc%-`A*b!EGN;9>ouwuTwk_&!C+`R)QaGnBRxMqNF4P1{B8JYT5AkJL_2ksj-Vu2 za%*IU zb9u9CV%7Ha-}ZlAi3+hKDFez2W6=}<_*GV{HG`@N0KRPb#wAIXO0C&IJNend3_*;N z^7_GVdajUmKrEl+>IFbmbJX+KZ~5d!%LmkU+)ysKe(+hgd_c|zp`E4|g*8f5O^gz8 zV=*khC(+?1r6bO@3So-QDl^dzh~={!=L6EtD6FlY^U3#;2)GSw1P0^*!g$ppLutoIkr{F0}^%-^!gSc!MuM>4RHYA8*u|25PXXvFR(o33$PUwB*R?*J|O3hBVX1}I~^h^FT0Hh66Y_OpPz)918n(cKEzeZ9?yr;f!>Kp79j0_ z?BUXa4EHJRDA?>^`9(ubeTQ4-40L%X3sBG3ceq8sry%P2E{XGXT&0fZnDg!G_4#GM zud*j=oS&~6=NAEg!Mq*7>IMC1rwI5YS1AKVJNm*}5%5i&=VYlj#FYWR6-&ecyAj93 zhygfXmKOjyKd5h}6tVmTMN^1zzIv@@5%8DP=checo-|GiiU-!@Z^_KW`6wB#d|D8o zs)+)AmGXkeh<3OWH_!o(;RZ>vSgu|G3^nyPQ;IlT_F_*KV9^u`=d(yVfR?X6MZAdR z8xk{IL~RE{Qsz*j48bP`m&Ge!T{jv}-}RZbE2>UBFsz>ihOVo#QRtWxemr_hrX zS5as#n*Psf^veZ{fNw}{f=>2ubq?InS!D)%6iGRV!;SzYWf1U59&qQ95^*2v`DZL{ z;U%3_X1)vr`~{&Vpylh==NI98YileQ1c`Qx+#F%?q8Cbx9<$@NG%ZgHH79a1yo5>lgFB9&XHq*$TcFL4se?IdH6 zS`%ZcX*Erpa=IAZW~k&o>Z4N2kwoL$y?dVTvQ{g9_~ZQ6v)^a$_u0?$%E;)?mHFh~ ztg7d7GBUEnM#kN&$C}u<{EiJLJ;?`a)L!XtTf)IVDEB^zIOl^u8UFu2eq3hm$AA3e z5d|~L5qADDvq?RdiJbvh+5c*x0{#;mZd{m^GEa3W9tG z1C}AYW}&9|W;SE5y(kzUy z!m9&=B3koWU>QYWQu%y@GZfX&hn!+^Sp=4dl6>YDA-tthS<)zCj18u}Mt4FS*xj086w;-{M$0Inz7|zAz!N1 z_-#fwa`a+sHZ@ByAKge#VI;zbC;rvm`U4YZ1*VdI;~tCby-?V+!Rs_*e6VX}y~c5i z5w?_;%o(AzWDVvtIA$5-h4A9#1)<)wCkwzbxaoGUN&k#F5|uGcd)*2!g;o9o6N(7` z-X~WaL3`^;uyos4g`Cp}59V!cK1_S-DzF>l>(u6p5pL2*YS~EViVc{JApQ%%D}=Mp z-u8x zNPFEnu%$h3o+b_rZ8}POohO)g#6yj~ z4um^<*P5=Qv%?Dv^$xCyB)JS{jg+3ExoiZRT;j8GGy>u2R_Ak9>HYHtlg`wd7fSBt z-X^I-K^Ajno519K`&7kO$c(e9zdVoTvKh=b#+MgDzWeI>c)wNBv;Gc@A9Pb`JLv`I z&9jefpuNBctkS;coC@h#-MY{I+(B!13)m<2w-rp_bm(9^d4I9@R~0DGdFu;S8Jtw% z*?@3a=IGIWde(kmZ8AJ#)kg?hXSoG9(KGW0OPy=0_1FdB()i)z26|Tlz=X0695XUI zoJ|gUhS7Jl4eYrU>q0{XSeSgOxd!=Wa1ia^=}n&(4EBjV>;U^q*|?weh>dss_Cec^&eWY?{mJc; z=^lhlLJxIw>Al?rR{VR7z744}hnq#K+i1OnfUUiMC|7WRymz;@f=PNVp|97$n4)o!VC!YAG*k3Qok@nfp3yps0+XAunsva5)C5n{ zVu0on4c4^CS?3ISxAA$b_<8iK_ki7bzHGld={5J$u2%x=ZZ$Z$nsPSH6xM YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 2488fabe502b5f4a437f90a23d6c843074a96d6a Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Sat, 6 Sep 2025 14:05:46 +0800 Subject: [PATCH 065/135] add validation in sampler --- m2l/processing/algorithms/sampler.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 9e4ec6e..b98b1ad 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -87,6 +87,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -149,11 +150,20 @@ def processAlgorithm( spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type is "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") From 38a552a54e2cfa575bce5282ec07d3ecb8c638f0 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 12:37:06 +0800 Subject: [PATCH 066/135] spacing decimator test --- m2l/processing/algorithms/sampler.py | 12 +++- tests/sampler_decimator_test.py | 98 ++++++++++++++++++++++++++++ tests/sampler_spacing_test.py | 80 +++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 tests/sampler_decimator_test.py create mode 100644 tests/sampler_spacing_test.py diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 9e4ec6e..b98b1ad 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -87,6 +87,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -149,11 +150,20 @@ def processAlgorithm( spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type is "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") diff --git a/tests/sampler_decimator_test.py b/tests/sampler_decimator_test.py new file mode 100644 index 0000000..8106f88 --- /dev/null +++ b/tests/sampler_decimator_test.py @@ -0,0 +1,98 @@ +""" +qgis python console: + ``` + import sys + dir = your_directory_to_the_plugin_code + sys.path.append(dir) + import unittest + from tests.sampler_decimator_test import TestSamplerDecimator + suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerDecimator) + unittest.TextTestRunner(verbosity=2).run(suite) + ``` +""" + +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from m2l.processing.algorithms.sampler import SamplerAlgorithm + +class TestSamplerDecimator(unittest.TestCase): + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.structure_file = self.input_dir / "structure_clip.shp" + self.dtm_file = self.input_dir / "dtm_rp.tif" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + self.assertTrue(self.structure_file.exists(), f"structure not found: {self.structure_file}") + self.assertTrue(self.dtm_file.exists(), f"dtm not found: {self.dtm_file}") + + def test_decimator_1_with_structure(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + structure_layer = QgsVectorLayer(str(self.structure_file), "structure", "ogr") + dtm_layer = QgsRasterLayer(str(self.dtm_file), "dtm") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") + + QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': dtm_layer, + 'GEOLOGY': geology_layer, + 'SPATIAL_DATA': structure_layer, + 'SAMPLER_TYPE': 0, + 'DECIMATION': 1, + 'SPACING': 200.0, + 'SAMPLED_CONTACTS': 'memory:decimated_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + + try: + QgsMessageLog.logMessage("Starting decimator sampler algorithm...", "TestDecimator", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestDecimator", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Decimator sampler test completed successfully!", "TestDecimator", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Decimator sampler test error: {str(e)}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestDecimator", Qgis.Critical) + + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestDecimator", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/sampler_spacing_test.py b/tests/sampler_spacing_test.py new file mode 100644 index 0000000..bd667fb --- /dev/null +++ b/tests/sampler_spacing_test.py @@ -0,0 +1,80 @@ +""" +qgis python console: + ``` + import sys + dir = your_directory_to_the_plugin_code + sys.path.append(dir) + import unittest + from tests.sampler_spacing_test import TestSamplerSpacing + suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerSpacing) + unittest.TextTestRunner(verbosity=2).run(suite) +``` +""" + +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from m2l.processing.algorithms.sampler import SamplerAlgorithm + +class TestSamplerSpacing(unittest.TestCase): + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + def test_spacing_50_with_geology(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- geology layer", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Spacing", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spacing: 50", "TestSampler", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': None, + 'GEOLOGY': None, + 'SPATIAL_DATA': geology_layer, + 'SAMPLER_TYPE': 1, + 'DECIMATION': 1, + 'SPACING': 50.0, + 'SAMPLED_CONTACTS': 'memory:sampled_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting spacing sampler algorithm...", "TestSampler", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestSampler", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Spacing sampler test completed successfully!", "TestSampler", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Spacing sampler test error: {str(e)}", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestSampler", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestSampler", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + +if __name__ == '__main__': + unittest.main() From 9341a8ecb5d24c56515a207c85fa4d75352056ae Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:26:05 +0800 Subject: [PATCH 067/135] refactor sampler tests for cicd compatibility --- .github/workflows/tester.yml | 110 +++++++++--------- tests/{ => qgis}/input/dtm_rp.tif | Bin tests/{ => qgis}/input/dtm_rp.tif.aux.xml | 0 tests/{ => qgis}/input/faults_clip.cpg | 0 tests/{ => qgis}/input/faults_clip.dbf | Bin tests/{ => qgis}/input/faults_clip.prj | 0 tests/{ => qgis}/input/faults_clip.shp | Bin tests/{ => qgis}/input/faults_clip.shx | Bin tests/{ => qgis}/input/folds_clip.cpg | 0 tests/{ => qgis}/input/folds_clip.dbf | Bin tests/{ => qgis}/input/folds_clip.prj | 0 tests/{ => qgis}/input/folds_clip.shp | Bin tests/{ => qgis}/input/folds_clip.shx | Bin tests/{ => qgis}/input/geol_clip_no_gaps.cpg | 0 tests/{ => qgis}/input/geol_clip_no_gaps.dbf | Bin tests/{ => qgis}/input/geol_clip_no_gaps.prj | 0 tests/{ => qgis}/input/geol_clip_no_gaps.shp | Bin tests/{ => qgis}/input/geol_clip_no_gaps.shx | Bin tests/{ => qgis}/input/structure_clip.cpg | 0 tests/{ => qgis}/input/structure_clip.dbf | Bin tests/{ => qgis}/input/structure_clip.prj | 0 tests/{ => qgis}/input/structure_clip.shp | Bin tests/{ => qgis}/input/structure_clip.shx | Bin .../test_sampler_decimator.py} | 28 ++--- .../test_sampler_spacing.py} | 28 ++--- 25 files changed, 86 insertions(+), 80 deletions(-) rename tests/{ => qgis}/input/dtm_rp.tif (100%) rename tests/{ => qgis}/input/dtm_rp.tif.aux.xml (100%) rename tests/{ => qgis}/input/faults_clip.cpg (100%) rename tests/{ => qgis}/input/faults_clip.dbf (100%) rename tests/{ => qgis}/input/faults_clip.prj (100%) rename tests/{ => qgis}/input/faults_clip.shp (100%) rename tests/{ => qgis}/input/faults_clip.shx (100%) rename tests/{ => qgis}/input/folds_clip.cpg (100%) rename tests/{ => qgis}/input/folds_clip.dbf (100%) rename tests/{ => qgis}/input/folds_clip.prj (100%) rename tests/{ => qgis}/input/folds_clip.shp (100%) rename tests/{ => qgis}/input/folds_clip.shx (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.cpg (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.dbf (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.prj (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.shp (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.shx (100%) rename tests/{ => qgis}/input/structure_clip.cpg (100%) rename tests/{ => qgis}/input/structure_clip.dbf (100%) rename tests/{ => qgis}/input/structure_clip.prj (100%) rename tests/{ => qgis}/input/structure_clip.shp (100%) rename tests/{ => qgis}/input/structure_clip.shx (100%) rename tests/{sampler_decimator_test.py => qgis/test_sampler_decimator.py} (90%) rename tests/{sampler_spacing_test.py => qgis/test_sampler_spacing.py} (86%) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..9ea3818 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -45,55 +45,61 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:3.4 + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml diff --git a/tests/input/dtm_rp.tif b/tests/qgis/input/dtm_rp.tif similarity index 100% rename from tests/input/dtm_rp.tif rename to tests/qgis/input/dtm_rp.tif diff --git a/tests/input/dtm_rp.tif.aux.xml b/tests/qgis/input/dtm_rp.tif.aux.xml similarity index 100% rename from tests/input/dtm_rp.tif.aux.xml rename to tests/qgis/input/dtm_rp.tif.aux.xml diff --git a/tests/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg similarity index 100% rename from tests/input/faults_clip.cpg rename to tests/qgis/input/faults_clip.cpg diff --git a/tests/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf similarity index 100% rename from tests/input/faults_clip.dbf rename to tests/qgis/input/faults_clip.dbf diff --git a/tests/input/faults_clip.prj b/tests/qgis/input/faults_clip.prj similarity index 100% rename from tests/input/faults_clip.prj rename to tests/qgis/input/faults_clip.prj diff --git a/tests/input/faults_clip.shp b/tests/qgis/input/faults_clip.shp similarity index 100% rename from tests/input/faults_clip.shp rename to tests/qgis/input/faults_clip.shp diff --git a/tests/input/faults_clip.shx b/tests/qgis/input/faults_clip.shx similarity index 100% rename from tests/input/faults_clip.shx rename to tests/qgis/input/faults_clip.shx diff --git a/tests/input/folds_clip.cpg b/tests/qgis/input/folds_clip.cpg similarity index 100% rename from tests/input/folds_clip.cpg rename to tests/qgis/input/folds_clip.cpg diff --git a/tests/input/folds_clip.dbf b/tests/qgis/input/folds_clip.dbf similarity index 100% rename from tests/input/folds_clip.dbf rename to tests/qgis/input/folds_clip.dbf diff --git a/tests/input/folds_clip.prj b/tests/qgis/input/folds_clip.prj similarity index 100% rename from tests/input/folds_clip.prj rename to tests/qgis/input/folds_clip.prj diff --git a/tests/input/folds_clip.shp b/tests/qgis/input/folds_clip.shp similarity index 100% rename from tests/input/folds_clip.shp rename to tests/qgis/input/folds_clip.shp diff --git a/tests/input/folds_clip.shx b/tests/qgis/input/folds_clip.shx similarity index 100% rename from tests/input/folds_clip.shx rename to tests/qgis/input/folds_clip.shx diff --git a/tests/input/geol_clip_no_gaps.cpg b/tests/qgis/input/geol_clip_no_gaps.cpg similarity index 100% rename from tests/input/geol_clip_no_gaps.cpg rename to tests/qgis/input/geol_clip_no_gaps.cpg diff --git a/tests/input/geol_clip_no_gaps.dbf b/tests/qgis/input/geol_clip_no_gaps.dbf similarity index 100% rename from tests/input/geol_clip_no_gaps.dbf rename to tests/qgis/input/geol_clip_no_gaps.dbf diff --git a/tests/input/geol_clip_no_gaps.prj b/tests/qgis/input/geol_clip_no_gaps.prj similarity index 100% rename from tests/input/geol_clip_no_gaps.prj rename to tests/qgis/input/geol_clip_no_gaps.prj diff --git a/tests/input/geol_clip_no_gaps.shp b/tests/qgis/input/geol_clip_no_gaps.shp similarity index 100% rename from tests/input/geol_clip_no_gaps.shp rename to tests/qgis/input/geol_clip_no_gaps.shp diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx similarity index 100% rename from tests/input/geol_clip_no_gaps.shx rename to tests/qgis/input/geol_clip_no_gaps.shx diff --git a/tests/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg similarity index 100% rename from tests/input/structure_clip.cpg rename to tests/qgis/input/structure_clip.cpg diff --git a/tests/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf similarity index 100% rename from tests/input/structure_clip.dbf rename to tests/qgis/input/structure_clip.dbf diff --git a/tests/input/structure_clip.prj b/tests/qgis/input/structure_clip.prj similarity index 100% rename from tests/input/structure_clip.prj rename to tests/qgis/input/structure_clip.prj diff --git a/tests/input/structure_clip.shp b/tests/qgis/input/structure_clip.shp similarity index 100% rename from tests/input/structure_clip.shp rename to tests/qgis/input/structure_clip.shp diff --git a/tests/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx similarity index 100% rename from tests/input/structure_clip.shx rename to tests/qgis/input/structure_clip.shx diff --git a/tests/sampler_decimator_test.py b/tests/qgis/test_sampler_decimator.py similarity index 90% rename from tests/sampler_decimator_test.py rename to tests/qgis/test_sampler_decimator.py index 8106f88..088fd29 100644 --- a/tests/sampler_decimator_test.py +++ b/tests/qgis/test_sampler_decimator.py @@ -1,23 +1,19 @@ -""" -qgis python console: - ``` - import sys - dir = your_directory_to_the_plugin_code - sys.path.append(dir) - import unittest - from tests.sampler_decimator_test import TestSamplerDecimator - suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerDecimator) - unittest.TextTestRunner(verbosity=2).run(suite) - ``` -""" - import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis,QgsApplication +from qgis.testing import start_app from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider class TestSamplerDecimator(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + def setUp(self): self.test_dir = Path(__file__).parent self.input_dir = self.test_dir / "input" @@ -93,6 +89,10 @@ def test_decimator_1_with_structure(self): finally: QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() diff --git a/tests/sampler_spacing_test.py b/tests/qgis/test_sampler_spacing.py similarity index 86% rename from tests/sampler_spacing_test.py rename to tests/qgis/test_sampler_spacing.py index bd667fb..a542b85 100644 --- a/tests/sampler_spacing_test.py +++ b/tests/qgis/test_sampler_spacing.py @@ -1,23 +1,19 @@ -""" -qgis python console: - ``` - import sys - dir = your_directory_to_the_plugin_code - sys.path.append(dir) - import unittest - from tests.sampler_spacing_test import TestSamplerSpacing - suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerSpacing) - unittest.TextTestRunner(verbosity=2).run(suite) -``` -""" - import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider class TestSamplerSpacing(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + def setUp(self): self.test_dir = Path(__file__).parent self.input_dir = self.test_dir / "input" @@ -76,5 +72,9 @@ def test_spacing_50_with_geology(self): finally: QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + if __name__ == '__main__': unittest.main() From 096a13e95f566efc9ee6a3d00ec70ff8adc7ad47 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:34:47 +0800 Subject: [PATCH 068/135] update tester.yml workflow for all branches --- .github/workflows/tester.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 9ea3818..a169c27 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "๐ŸŽณ Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml From 5fa83adae59e42025c44ba4dbe99cd0bedc494f4 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:54:03 +0800 Subject: [PATCH 069/135] change image --- .github/workflows/tester.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index a169c27..8e3ff71 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest container: - image: qgis/qgis:3.4 + image: qgis/qgis:latest env: CI: true DISPLAY: ":1" From a6c14b2e5bedc09050d89b8084e171ab3b41819b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 14:12:55 +0800 Subject: [PATCH 070/135] update testing.txt --- requirements/testing.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/testing.txt b/requirements/testing.txt index 1940035..d238575 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -3,3 +3,5 @@ pytest-cov>=4 packaging>=23 +shapely +geopandas \ No newline at end of file From 378f9aef78c3c523fbcea0e6cce8a8863bbec12d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 14:40:44 +0800 Subject: [PATCH 071/135] install map2loop in tester.yml --- .github/workflows/tester.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 8e3ff71..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -83,6 +83,7 @@ jobs: . .venv/bin/activate # Install the requirements python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor - name: verify input data run: | From 06fc7ec2afac1328e641da46f6e770e18b097f57 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:15:54 +0800 Subject: [PATCH 072/135] tester.yml --- .github/workflows/tester.yml | 115 ++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "๐ŸŽณ Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml @@ -45,55 +41,62 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:latest + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml From a2b96f6af09684f86d132223fd94fa489f360111 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:19:38 +0800 Subject: [PATCH 073/135] input file --- tests/qgis/input/faults_clip.cpg | 1 + tests/qgis/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/qgis/input/faults_clip.prj | 1 + tests/qgis/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/qgis/input/faults_clip.shx | Bin 0 -> 380 bytes tests/qgis/input/folds_clip.cpg | 1 + tests/qgis/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/qgis/input/folds_clip.prj | 1 + tests/qgis/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/qgis/input/folds_clip.shx | Bin 0 -> 156 bytes tests/qgis/input/geol_clip_no_gaps.cpg | 1 + tests/qgis/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/qgis/input/geol_clip_no_gaps.prj | 1 + tests/qgis/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/qgis/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/qgis/input/structure_clip.cpg | 1 + tests/qgis/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/qgis/input/structure_clip.prj | 1 + tests/qgis/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/qgis/input/structure_clip.shx | Bin 0 -> 1044 bytes 20 files changed, 8 insertions(+) create mode 100644 tests/qgis/input/faults_clip.cpg create mode 100644 tests/qgis/input/faults_clip.dbf create mode 100644 tests/qgis/input/faults_clip.prj create mode 100644 tests/qgis/input/faults_clip.shp create mode 100644 tests/qgis/input/faults_clip.shx create mode 100644 tests/qgis/input/folds_clip.cpg create mode 100644 tests/qgis/input/folds_clip.dbf create mode 100644 tests/qgis/input/folds_clip.prj create mode 100644 tests/qgis/input/folds_clip.shp create mode 100644 tests/qgis/input/folds_clip.shx create mode 100644 tests/qgis/input/geol_clip_no_gaps.cpg create mode 100644 tests/qgis/input/geol_clip_no_gaps.dbf create mode 100644 tests/qgis/input/geol_clip_no_gaps.prj create mode 100644 tests/qgis/input/geol_clip_no_gaps.shp create mode 100644 tests/qgis/input/geol_clip_no_gaps.shx create mode 100644 tests/qgis/input/structure_clip.cpg create mode 100644 tests/qgis/input/structure_clip.dbf create mode 100644 tests/qgis/input/structure_clip.prj create mode 100644 tests/qgis/input/structure_clip.shp create mode 100644 tests/qgis/input/structure_clip.shx diff --git a/tests/qgis/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/qgis/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 79b28ad572db08758e9f3c832889b13d3394303d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:41:20 +0800 Subject: [PATCH 074/135] test_basal_contacts --- tests/qgis/test_basal_contacts.py | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/qgis/test_basal_contacts.py diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py new file mode 100644 index 0000000..9131188 --- /dev/null +++ b/tests/qgis/test_basal_contacts.py @@ -0,0 +1,116 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.basal_contacts import BasalContactsAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestBasalContacts(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.faults_file = self.input_dir / "faults_clip.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + if not self.faults_file.exists(): + QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) + + def test_basal_contacts_extraction(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + faults_layer = None + if self.faults_file.exists(): + faults_layer = QgsVectorLayer(str(self.faults_file), "faults", "ogr") + self.assertTrue(faults_layer.isValid(), "faults layer should be valid") + self.assertGreater(faults_layer.featureCount(), 0, "faults layer should have features") + QgsMessageLog.logMessage(f"faults layer: {faults_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + strati_column = [ + ["Turee Creek Group"], + ["Boolgeeda Iron Formation"], + ["Woongarra Rhyolite"], + ["Weeli Wolli Formation"], + ["Brockman Iron Formation"], + ["Mount McRae Shale and Mount Sylvia Formation"], + ["Wittenoom Formation"], + ["Marra Mamba Iron Formation"], + ["Jeerinah Formation"], + ["Bunjinah Formation"], + ["Pyradie Formation"], + ["Fortescue Group"], + ["Hardey Formation"], + ["Boongal Formation"], + ["Mount Roe Basalt"], + ["Rocklea Inlier greenstones"], + ["Rocklea Inlier metagranitic unit"] + ] + + algorithm = BasalContactsAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'UNIT_NAME_FIELD': 'unitname', + 'FORMATION_FIELD': 'formation', + 'FAULTS': faults_layer, + 'STRATIGRAPHIC_COLUMN': strati_column, + 'IGNORE_UNITS': [], + 'BASAL_CONTACTS': 'memory:basal_contacts' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting basal contacts algorithm...", "TestBasalContacts", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestBasalContacts", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + + basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) + self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") + self.assertTrue(basal_contacts_layer.isValid(), "basal contacts layer should be valid") + self.assertGreater(basal_contacts_layer.featureCount(), 0, "basal contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", + "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Basal contacts test error: {str(e)}", "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestBasalContacts", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestBasalContacts", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestBasalContacts", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 82532dc43a4d7408229a2d8a3575db3c5f30a065 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:47:23 +0800 Subject: [PATCH 075/135] fix import in test_basal_contacts.py --- tests/qgis/test_basal_contacts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 9131188..1e0642e 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -2,7 +2,7 @@ from pathlib import Path from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication from qgis.testing import start_app -from m2l.processing.algorithms.basal_contacts import BasalContactsAlgorithm +from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider class TestBasalContacts(unittest.TestCase): @@ -61,7 +61,7 @@ def test_basal_contacts_extraction(self): ["Rocklea Inlier greenstones"], ["Rocklea Inlier metagranitic unit"] ] - + algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() From 02f9cb99617def5ee8863d7423eec3bf11a0dab7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:59:56 +0800 Subject: [PATCH 076/135] fix strati_column in test_basal_contacts --- tests/qgis/test_basal_contacts.py | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 1e0642e..19c56dc 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -43,23 +43,23 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) strati_column = [ - ["Turee Creek Group"], - ["Boolgeeda Iron Formation"], - ["Woongarra Rhyolite"], - ["Weeli Wolli Formation"], - ["Brockman Iron Formation"], - ["Mount McRae Shale and Mount Sylvia Formation"], - ["Wittenoom Formation"], - ["Marra Mamba Iron Formation"], - ["Jeerinah Formation"], - ["Bunjinah Formation"], - ["Pyradie Formation"], - ["Fortescue Group"], - ["Hardey Formation"], - ["Boongal Formation"], - ["Mount Roe Basalt"], - ["Rocklea Inlier greenstones"], - ["Rocklea Inlier metagranitic unit"] + "Turee Creek Group", + "Boolgeeda Iron Formation", + "Woongarra Rhyolite", + "Weeli Wolli Formation", + "Brockman Iron Formation", + "Mount McRae Shale and Mount Sylvia Formation", + "Wittenoom Formation", + "Marra Mamba Iron Formation", + "Jeerinah Formation", + "Bunjinah Formation", + "Pyradie Formation", + "Fortescue Group", + "Hardey Formation", + "Boongal Formation", + "Mount Roe Basalt", + "Rocklea Inlier greenstones", + "Rocklea Inlier metagranitic unit" ] algorithm = BasalContactsAlgorithm() From 91bcc09cfee6e450000beacb5c30ee1eee902740 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 10 Sep 2025 21:15:56 +0800 Subject: [PATCH 077/135] add structure and dtm parameters for SorterObservationProjections and column mapping --- m2l/processing/algorithms/sorter.py | 119 +++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ee081ab..5187d30 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,5 +1,7 @@ from typing import Any, Optional +from osgeo import gdal +from PyQt5.QtCore import QMetaType from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -7,6 +9,7 @@ QgsField, QgsFeature, QgsGeometry, + QgsRasterLayer, QgsProcessing, QgsProcessingAlgorithm, QgsProcessingContext, @@ -15,6 +18,8 @@ QgsProcessingParameterEnum, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, QgsVectorLayer, QgsWkbTypes ) @@ -48,6 +53,8 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): """ METHOD = "METHOD" INPUT_GEOLOGY = "INPUT_GEOLOGY" + INPUT_STRUCTURE = "INPUT_STRUCTURE" + INPUT_DTM = "INPUT_DTM" INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" @@ -100,12 +107,62 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRUCTURE, + "Structure", + [QgsProcessing.TypeVectorPoint], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT_DTM, + "DTM", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'MIN_AGE_FIELD', + 'Minimum Age Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='MIN_AGE', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'MAX_AGE_FIELD', + 'Maximum Age Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='MAX_AGE', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'GROUP_FIELD', + 'Group Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='GROUP', + optional=True + ) + ) # enum so the user can pick the strategy from a dropdown self.addParameter( QgsProcessingParameterEnum( - self.ALGO, + self.SORTING_ALGORITHM, "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default @@ -115,7 +172,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -130,8 +187,10 @@ def processAlgorithm( ) -> dict[str, Any]: # 1 โ–บ fetch user selections - in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context) - algo_index: int = self.parameterAsEnum(parameters, self.ALGO, context) + in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") @@ -150,21 +209,34 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries โ€“ only attribute values. # -------------------------------------------------- - units_df, relationships_df, contacts_df, map_data = build_input_frames(in_layer, feedback) + units_df, relationships_df, contacts_df = build_input_frames(in_layer, feedback,parameters) # 3 โ–บ run the sorter sorter = sorter_cls() # instantiation is always zero-argument - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - map_data, - ) + if sorter_cls == SorterObservationProjections: + from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame + geology_gdf = qgsLayerToGeoDataFrame(in_layer) + structure_gdf = qgsLayerToGeoDataFrame(structure) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) + else: + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + ) # 4 โ–บ write an in-memory table with the result sink_fields = QgsFields() - sink_fields.append(QgsField("strat_pos", int)) - sink_fields.append(QgsField("unit_name", str)) + sink_fields.append(QgsField("strat_pos", QMetaType.Type.Int)) + sink_fields.append(QgsField("unit_name", QMetaType.Type.QString)) (sink, dest_id) = self.parameterAsSink( parameters, @@ -190,17 +262,21 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: +def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - (units_df, relationships_df, contacts_df, map_data) + (units_df, relationships_df, contacts_df) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' + max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' + group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' # Example: convert the geology layer to a very small units_df units_records = [] @@ -208,10 +284,10 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: units_records.append( dict( layerId=f.id(), - name=f["UNITNAME"], # attribute names โ†’ your schema - minAge=f.attribute("MIN_AGE"), - maxAge=f.attribute("MAX_AGE"), - group=f["GROUP"], + name=f[unit_name_field], # attribute names โ†’ your schema + minAge=float(f[min_age_field]), + maxAge=float(f[max_age_field]), + group=f[group_field], ) ) units_df = pd.DataFrame.from_records(units_records) @@ -221,8 +297,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) # map_data can be mocked if you only use Age-based sorter - map_data = MapData() # or MapData.from_project(โ€ฆ) / MapData.from_files(โ€ฆ) feedback.pushInfo(f"Units โ†’ {len(units_df)} records") - return units_df, relationships_df, contacts_df, map_data + return units_df, relationships_df, contacts_df From 9273c176c79a0bfcf961d6a0f7fda289d4e4978f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 20:13:21 +0800 Subject: [PATCH 078/135] add contact as output layer --- .../algorithms/extract_basal_contacts.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..6223c10 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -41,6 +41,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" + ALL_CONTACTS = "ALL_CONTACTS" def name(self) -> str: """Return the algorithm name.""" @@ -116,6 +117,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "ALL_CONTACTS", + "All Contacts", + ) + ) + def processAlgorithm( self, parameters: dict[str, Any], @@ -149,6 +157,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) + all_contacts = contact_extractor.extract_all_contacts() basal_contacts = contact_extractor.extract_basal_contacts(strati_column) feedback.pushInfo("Exporting Basal Contacts Layer...") @@ -160,7 +169,15 @@ def processAlgorithm( output_key=self.OUTPUT, feedback=feedback, ) - return {self.OUTPUT: basal_contacts} + contacts_layer = GeoDataFrameToQgsLayer( + self, + all_contacts, + parameters=parameters, + context=context, + output_key=self.ALL_CONTACTS, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts, self.ALL_CONTACTS: contacts_layer} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 3576bd757cd367416fb5cd2d8e6503f501efa828 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 20:20:40 +0800 Subject: [PATCH 079/135] change contacts_df and relationships_df --- m2l/processing/algorithms/sorter.py | 61 ++++++++++++++++------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5187d30..2fff928 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,5 +1,6 @@ from typing import Any, Optional from osgeo import gdal +import pandas as pd from PyQt5.QtCore import QMetaType from qgis import processing @@ -35,6 +36,8 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) +from map2loop.contact_extractor import ContactExtractor +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame # a lookup so we donโ€™t need a giant if/else block SORTER_LIST = { @@ -125,6 +128,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFeatureSource( + "CONTACTS_LAYER", + "Contacts Layer", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + self.addParameter( QgsProcessingParameterField( 'MIN_AGE_FIELD', @@ -190,6 +202,7 @@ def processAlgorithm( in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] @@ -209,29 +222,27 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries โ€“ only attribute values. # -------------------------------------------------- - units_df, relationships_df, contacts_df = build_input_frames(in_layer, feedback,parameters) + units_df= build_input_frames(in_layer, feedback,parameters) # 3 โ–บ run the sorter sorter = sorter_cls() # instantiation is always zero-argument - if sorter_cls == SorterObservationProjections: - from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame - geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - geology_gdf, - structure_gdf, - dtm_gdal - ) - else: - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - ) + geology_gdf = qgsLayerToGeoDataFrame(in_layer) + structure_gdf = qgsLayerToGeoDataFrame(structure) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + contacts_df = qgsLayerToGeoDataFrame(contacts_layer) + relationships_df = contacts_df.copy() + if 'length' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['length']) + if 'geometry' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['geometry']) + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) # 4 โ–บ write an in-memory table with the result sink_fields = QgsFields() @@ -262,14 +273,14 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: +def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFrame: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - (units_df, relationships_df, contacts_df) + units_df """ import pandas as pd @@ -292,12 +303,8 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: ) units_df = pd.DataFrame.from_records(units_records) - # relationships_df and contacts_df are domain-specific โ”€ fill them here - relationships_df = pd.DataFrame(columns=["Index1", "UNITNAME_1", "Index2", "UNITNAME_2"]) - contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) - # map_data can be mocked if you only use Age-based sorter feedback.pushInfo(f"Units โ†’ {len(units_df)} records") - return units_df, relationships_df, contacts_df + return units_df From fb873f333b0e5a342e10ae2ba0dfc16cd218733f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 21:37:00 +0800 Subject: [PATCH 080/135] add contact layer in sorter --- m2l/processing/algorithms/sorter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 8e16f42..ca79b66 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -61,6 +61,7 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" + CONTACTS_LAYER = "CONTACTS_LAYER" # ---------------------------------------------------------- # Metadata From a9092f455d1f75d431c1161e45787e642530e87d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 15 Sep 2025 18:52:52 +0800 Subject: [PATCH 081/135] handle dip, dipdir, orientation type for structure in sorter.py --- m2l/processing/algorithms/sorter.py | 113 +++++++++++++++++++++------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ca79b66..ed402cb 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -36,7 +36,6 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) -from map2loop.contact_extractor import ContactExtractor from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame # a lookup so we donโ€™t need a giant if/else block @@ -113,28 +112,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRUCTURE, - "Structure", - [QgsProcessing.TypeVectorPoint], - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_DTM, - "DTM", - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - "CONTACTS_LAYER", - "Contacts Layer", - [QgsProcessing.TypeVectorLine], - optional=True, + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.Any, + defaultValue='UNITNAME', + optional=True ) ) @@ -143,7 +127,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'MIN_AGE_FIELD', 'Minimum Age Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='MIN_AGE', optional=True ) @@ -154,7 +138,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'MAX_AGE_FIELD', 'Maximum Age Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='MAX_AGE', optional=True ) @@ -165,11 +149,67 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'GROUP_FIELD', 'Group Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='GROUP', optional=True ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRUCTURE, + "Structure", + [QgsProcessing.TypeVectorPoint], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'DIP_FIELD', + 'Dip Field', + parentLayerParameterName=self.INPUT_STRUCTURE, + type=QgsProcessingParameterField.Any, + defaultValue='DIP', + optional=True + ) + ) + self.addParameter( + QgsProcessingParameterField( + 'DIPDIR_FIELD', + 'Dip Direction Field', + parentLayerParameterName=self.INPUT_STRUCTURE, + type=QgsProcessingParameterField.Any, + defaultValue='DIPDIR', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterEnum( + 'ORIENTATION_TYPE', + 'Orientation Type', + options=['Dip Direction', 'Strike'], + defaultValue=0 + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT_DTM, + "DTM", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + "CONTACTS_LAYER", + "Contacts Layer", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) # enum so the user can pick the strategy from a dropdown @@ -236,6 +276,27 @@ def processAlgorithm( relationships_df = relationships_df.drop(columns=['length']) if 'geometry' in contacts_df.columns: relationships_df = relationships_df.drop(columns=['geometry']) + + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: + geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) + + dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if dip_field != 'DIP' and dip_field in structure_gdf.columns: + structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) + + orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) + orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if dipdir_field in structure_gdf.columns: + if orientation_type_name == 'Strike': + structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( + lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val + ) + else: + structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + + order = sorter.sort( units_df, relationships_df, From cfd09ecbaae191a9b7d23478975d8f77094a0a89 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:38:11 +0800 Subject: [PATCH 082/135] fix syntax in sampler --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index b98b1ad..726d44a 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -154,7 +154,7 @@ def processAlgorithm( if spatial_data is None: raise QgsProcessingException("Spatial data is required") - if sampler_type is "Decimator": + if sampler_type == "Decimator": if geology is None: raise QgsProcessingException("Geology is required") if dtm is None: From 1e28eb702badf49b417c79ffbb99620058485609 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:47:08 +0800 Subject: [PATCH 083/135] update basal contact test --- tests/qgis/test_basal_contacts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 19c56dc..aeb208a 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -72,7 +72,8 @@ def test_basal_contacts_extraction(self): 'FAULTS': faults_layer, 'STRATIGRAPHIC_COLUMN': strati_column, 'IGNORE_UNITS': [], - 'BASAL_CONTACTS': 'memory:basal_contacts' + 'BASAL_CONTACTS': 'memory:basal_contacts', + 'ALL_CONTACTS': 'memory:all_contacts' } context = QgsProcessingContext() From 856cb423b6c6c82cad3953e15364d802c353c1ca Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:50:41 +0800 Subject: [PATCH 084/135] update basal contact test to include contacts layer --- tests/qgis/test_basal_contacts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index aeb208a..0aae78e 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -88,6 +88,7 @@ def test_basal_contacts_extraction(self): self.assertIsNotNone(result, "result should not be None") self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + self.assertIn('ALL_CONTACTS', result, "Result should contain ALL_CONTACTS key") basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") @@ -97,6 +98,14 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", "TestBasalContacts", Qgis.Critical) + all_contacts_layer = context.takeResultLayer(result['ALL_CONTACTS']) + self.assertIsNotNone(all_contacts_layer, "all contacts layer should not be None") + self.assertTrue(all_contacts_layer.isValid(), "all contacts layer should be valid") + self.assertGreater(all_contacts_layer.featureCount(), 0, "all contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {all_contacts_layer.featureCount()} total contacts", + "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) except Exception as e: From 73b6e2263915fbbb4dfddb285f8f8ab702281069 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:55:33 +0930 Subject: [PATCH 085/135] feat: thickness calculator tool --- .../algorithms/thickness_calculator.py | 151 +++++++++++++----- 1 file changed, 115 insertions(+), 36 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 71f72eb..e10604d 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -25,10 +25,20 @@ QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsProcessingParameterField, - QgsProcessingParameterMatrix + QgsProcessingParameterMatrix, + QgsSettings, + QgsProcessingParameterRasterLayer, ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer +from ...main.vectorLayerWrapper import ( + qgsLayerToGeoDataFrame, + GeoDataFrameToQgsLayer, + qgsLayerToDataFrame, + dataframeToQgsLayer, + qgsRasterToGdalDataset, + matrixToDict, + dataframeToQgsTable + ) from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -39,11 +49,13 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' - INPUT_UNITS = 'UNITS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' + INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' OUTPUT = "THICKNESS" @@ -73,21 +85,27 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "Thickness Calculator Type", options=['InterpolatedStructure','StructuralPoint'], allowMultiple=False, + defaultValue='InterpolatedStructure' ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, - "DTM", + "DTM (InterpolatedStructure)", [QgsProcessing.TypeRaster], + optional=True, ) ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") self.addParameter( - QgsProcessingParameterEnum( + QgsProcessingParameterMatrix( self.INPUT_BOUNDING_BOX, - "Bounding Box", - options=['minx','miny','maxx','maxy'], - allowMultiple=True, + description="Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox ) ) self.addParameter( @@ -98,18 +116,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=1000 ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_UNITS, - "Units", - [QgsProcessing.TypeVectorLine], - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_BASAL_CONTACTS, "Basal Contacts", [QgsProcessing.TypeVectorLine], + defaultValue='Basal Contacts', ) ) self.addParameter( @@ -119,29 +131,60 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field e.g. Formation', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='Formation' + ) + ) + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( QgsProcessingParameterMatrix( name=self.INPUT_STRATI_COLUMN, description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=[] + defaultValue=last_strati_column ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_SAMPLED_CONTACTS, - "SAMPLED_CONTACTS", + "Sampled Contacts", [QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_STRUCTURE_DATA, - "STRUCTURE_DATA", + "Orientation Data", [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIPDIR_FIELD, + "Dip Direction Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIPDIR' + ) + ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIP_FIELD, + "Dip Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIP' + ) + ) self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -157,32 +200,63 @@ def processAlgorithm( ) -> dict[str, Any]: feedback.pushInfo("Initialising Thickness Calculation Algorithm...") - thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) - dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) - max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) - units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] + dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) + structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", bounding_box) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) # convert layers to dataframe or geodataframe + units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) - units = qgsLayerToDataFrame(units) basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) structure_data = qgsLayerToDataFrame(structure_data) + rename_map = {} + missing_fields = [] + if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: + geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) + units = units.rename(columns={'UNITNAME': 'name'}) + if structure_data is not None: + if structure_dipdir_field: + if structure_dipdir_field in structure_data.columns: + rename_map[structure_dipdir_field] = 'DIPDIR' + else: + missing_fields.append(structure_dipdir_field) + if structure_dip_field: + if structure_dip_field in structure_data.columns: + rename_map[structure_dip_field] = 'DIP' + else: + missing_fields.append(structure_dip_field) + if missing_fields: + raise QgsProcessingException( + f"Orientation data missing required field(s): {', '.join(missing_fields)}" + ) + if rename_map: + structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) - + dtm_data = qgsRasterToGdalDataset(dtm_data) + bounding_box = matrixToDict(bounding_box) feedback.pushInfo("Calculating unit thicknesses...") - if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, ) - thickness_calculator.compute( + thicknesses = thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -197,7 +271,7 @@ def processAlgorithm( bounding_box=bounding_box, max_line_length=max_line_length, ) - thickness_calculator.compute( + thicknesses =thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -206,17 +280,22 @@ def processAlgorithm( sampled_contacts ) - #TODO: convert thicknesses dataframe to qgs layer - thicknesses = dataframeToQgsLayer( - self, - # contact_extractor.basal_contacts, + thicknesses = thicknesses[ + ["name","ThicknessMean","ThicknessMedian", "ThicknessStdDev"] + ].copy() + + feedback.pushInfo("Exporting Thickness Table...") + thicknesses = dataframeToQgsTable( + self, + thicknesses, parameters=parameters, context=context, feedback=feedback, - ) - + param_name=self.OUTPUT + ) + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # ThicknessCalculatorAlgorithm() From b483f006e335e735800e971824a4bd53d1aa7069 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:56:00 +0930 Subject: [PATCH 086/135] feat: raster and dataframe handling --- m2l/main/vectorLayerWrapper.py | 356 +++++++++++++++++++++++++++------ 1 file changed, 298 insertions(+), 58 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 72ff281..00c6193 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -1,5 +1,5 @@ # PyQGIS / PyQt imports - +from osgeo import gdal from qgis.core import ( QgsRaster, QgsFields, @@ -12,17 +12,79 @@ QgsProcessingException, QgsPoint, QgsPointXY, + QgsProject, + QgsCoordinateTransform, + QgsRasterLayer ) -from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant - +from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis import processing from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np - +import tempfile +import os + +def qgsRasterToGdalDataset(rlayer: QgsRasterLayer): + """ + Convert a QgsRasterLayer to an osgeo.gdal.Dataset (read-only). + If the raster is non-file-based (e.g. WMS/WCS/virtual), we create a temp GeoTIFF via gdal:translate. + Returns a gdal.Dataset or None. + """ + if rlayer is None or not rlayer.isValid(): + return None + + # Try direct open on file-backed layers + candidates = [] + try: + candidates.append(rlayer.source()) + except Exception: + pass + try: + if rlayer.dataProvider(): + candidates.append(rlayer.dataProvider().dataSourceUri()) + except Exception: + pass + tried = set() + for uri in candidates: + if not uri: + continue + if uri in tried: + continue + tried.add(uri) + + # Strip QGIS pipe options: "path.tif|layername=..." โ†’ "path.tif" + base_uri = uri.split("|")[0] + + # Some providers store โ€œSUBDATASET:โ€ URIs; gdal.OpenEx can usually handle them directly. + ds = gdal.OpenEx(base_uri, gdal.OF_RASTER | gdal.OF_READONLY) + if ds is not None: + return ds + + # If weโ€™re here, itโ€™s likely non-file-backed. Export to a temp GeoTIFF. + tmpdir = tempfile.gettempdir() + tmp_path = os.path.join(tmpdir, f"m2l_dtm_{rlayer.id()}.tif") + + # Use GDAL Translate via QGIS processing (avoids CRS pitfalls) + processing.run( + "gdal:translate", + { + "INPUT": rlayer, # QGIS accepts the layer object here + "TARGET_CRS": None, + "NODATA": None, + "COPY_SUBDATASETS": False, + "OPTIONS": "", + "EXTRA": "", + "DATA_TYPE": 0, # Use input data type + "OUTPUT": tmp_path, + } + ) + + ds = gdal.OpenEx(tmp_path, gdal.OF_RASTER | gdal.OF_READONLY) + return ds def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: if layer is None: @@ -42,63 +104,147 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: data[f.name()].append(str(feature[f.name()])) else: data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - -def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: - """Convert a vector layer to a pandas DataFrame - samples the geometry using either points or the vertices of the lines - - :param layer: _description_ - :type layer: _type_ - :param dtm: Digital Terrain Model to evaluate Z values - :type dtm: _type_ or None - :return: the dataframe object - :rtype: pd.DataFrame + return gpd.GeoDataFrame(data, crs=layer.sourceCrs().authid()) + +def qgsLayerToDataFrame(src, dtm=None) -> pd.DataFrame: """ - if layer is None: + Convert a vector layer or processing feature source to a pandas DataFrame. + Samples geometry using points or vertices of lines/polygons. + Optionally samples Z from a DTM raster. + + :param src: QgsVectorLayer or QgsProcessingFeatureSource + :param dtm: QgsRasterLayer or None + :return: pd.DataFrame with columns: X, Y, Z, and all layer fields + """ + + if src is None: return None - fields = layer.fields() - data = {} - data['X'] = [] - data['Y'] = [] - data['Z'] = [] - - for field in fields: - data[field.name()] = [] - for feature in layer.getFeatures(): - geom = feature.geometry() - points = [] - if geom.isMultipart(): - if geom.type() == QgsWkbTypes.PointGeometry: - points = geom.asMultiPoint() - elif geom.type() == QgsWkbTypes.LineGeometry: + + # --- Resolve fields and source CRS (works for both layer and feature source) --- + fields = src.fields() if hasattr(src, "fields") else None + if fields is None: + # Fallback: take fields from first feature if needed + feat_iter = src.getFeatures() + try: + first = next(feat_iter) + except StopIteration: + return pd.DataFrame(columns=["X", "Y", "Z"]) + fields = first.fields() + # Rewind iterator by building a new one + feats = [first] + list(src.getFeatures()) + else: + feats = src.getFeatures() + + # Get source CRS + if hasattr(src, "crs"): + src_crs = src.crs() + elif hasattr(src, "sourceCrs"): + src_crs = src.sourceCrs() + else: + src_crs = None + + # --- Prepare optional transform to DTM CRS for sampling --- + to_dtm = None + if dtm is not None and src_crs is not None and dtm.crs().isValid() and src_crs.isValid(): + if src_crs != dtm.crs(): + to_dtm = QgsCoordinateTransform(src_crs, dtm.crs(), QgsProject.instance()) + + # --- Helper: sample Z from DTM (returns float or -9999) --- + def sample_dtm_xy(x, y): + if dtm is None: + return 0.0 + # Transform coordinate if needed + if to_dtm is not None: + try: + from qgis.core import QgsPointXY + x, y = to_dtm.transform(QgsPointXY(x, y)) + except Exception: + return -9999.0 + from qgis.core import QgsPointXY + ident = dtm.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue) + if not ident.isValid(): + return -9999.0 + res = ident.results() + if not res: + return -9999.0 + # take first band value (band keys are 1-based) + try: + # Prefer band 1 if present + return float(res.get(1, next(iter(res.values())))) + except Exception: + return -9999.0 + + # --- Geometry -> list of vertices (QgsPoint or QgsPointXY) --- + def vertices_from_geometry(geom): + if geom is None or geom.isEmpty(): + return [] + gtype = QgsWkbTypes.geometryType(geom.wkbType()) + is_multi = QgsWkbTypes.isMultiType(geom.wkbType()) + + if gtype == QgsWkbTypes.PointGeometry: + if is_multi: + return list(geom.asMultiPoint()) + else: + return [geom.asPoint()] + + elif gtype == QgsWkbTypes.LineGeometry: + pts = [] + if is_multi: for line in geom.asMultiPolyline(): - points.extend(line) - # points = geom.asMultiPolyline()[0] - else: - if geom.type() == QgsWkbTypes.PointGeometry: - points = [geom.asPoint()] - elif geom.type() == QgsWkbTypes.LineGeometry: - points = geom.asPolyline() - - for p in points: - data['X'].append(p.x()) - data['Y'].append(p.y()) - if dtm is not None: - # Replace with your coordinates - - # Extract the value at the point - z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) - if z_value.isValid(): - z_value = z_value.results()[1] - else: - z_value = -9999 - data['Z'].append(z_value) - if dtm is None: - data['Z'].append(0) - for field in fields: - data[field.name()].append(feature[field.name()]) - return pd.DataFrame(data) + pts.extend(line) + else: + pts.extend(geom.asPolyline()) + return pts + + elif gtype == QgsWkbTypes.PolygonGeometry: + pts = [] + if is_multi: + mpoly = geom.asMultiPolygon() + for poly in mpoly: + for ring in poly: # exterior + interior rings + pts.extend(ring) + else: + poly = geom.asPolygon() + for ring in poly: + pts.extend(ring) + return pts + + # Other geometry types not handled + return [] + + # --- Build rows safely (one dict per sampled point) --- + rows = [] + field_names = [f.name() for f in fields] + + for f in feats: + geom = f.geometry() + pts = vertices_from_geometry(geom) + + if not pts: + # If you want to keep attribute rows even when no vertices: uncomment below + # row = {name: f[name] for name in field_names} + # row.update({"X": None, "Y": None, "Z": None}) + # rows.append(row) + continue + + # Cache attributes once per feature and reuse for each sampled point + base_attrs = {name: f[name] for name in field_names} + + for p in pts: + # QgsPoint vs QgsPointXY both have x()/y() + x, y = float(p.x()), float(p.y()) + z = sample_dtm_xy(x, y) + + row = {"X": x, "Y": y, "Z": z} + row.update(base_attrs) + rows.append(row) + + # Create DataFrame; if empty, return with expected columns + if not rows: + cols = ["X", "Y", "Z"] + field_names + return pd.DataFrame(columns=cols) + + return pd.DataFrame.from_records(rows) def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ @@ -454,3 +600,97 @@ def dataframeToQgsLayer( feedback.pushInfo("Done.") feedback.setProgress(100) return sink, sink_id + + +def matrixToDict(matrix, headers=("minx", "miny", "maxx", "maxy")) -> dict: + """ + Convert a QgsProcessingParameterMatrix value to a dict with float values. + Accepts: [[minx,miny,maxx,maxy]] or [minx,miny,maxx,maxy]. + Raises a clear error if an enum index (int) was passed by mistake. + """ + # Guard: common mistake โ†’ using parameterAsEnum + if isinstance(matrix, int): + raise QgsProcessingException( + "Bounding Box was read with parameterAsEnum (got an int). " + "Use parameterAsMatrix for QgsProcessingParameterMatrix." + ) + + if matrix is None: + raise QgsProcessingException("Bounding box matrix is None.") + + # Allow empty string from settings/defaults + if isinstance(matrix, str) and not matrix.strip(): + raise QgsProcessingException("Bounding box matrix is empty.") + + # Accept single-row matrix or flat list + if isinstance(matrix, (list, tuple)): + if matrix and isinstance(matrix[0], (list, tuple)): + row = matrix[0] + else: + row = matrix + else: + # last resort: try comma-separated string "minx,miny,maxx,maxy" + if isinstance(matrix, str) and "," in matrix: + row = [v.strip() for v in matrix.split(",")] + else: + raise QgsProcessingException(f"Unrecognized bounding box value: {type(matrix)}") + + if len(row) < 4: + raise QgsProcessingException(f"Bounding box needs 4 numbers, got {len(row)}: {row}") + + def _to_float(v): + if isinstance(v, str): + v = v.strip() + return float(v) + + vals = list(map(_to_float, row[:4])) + bbox = dict(zip(headers, vals)) + + if not (bbox["minx"] < bbox["maxx"] and bbox["miny"] < bbox["maxy"]): + raise QgsProcessingException(f"Invalid bounding box: {bbox} (expect minx Date: Wed, 17 Sep 2025 14:08:26 +0800 Subject: [PATCH 087/135] fix clean up process for tests --- tests/qgis/test_basal_contacts.py | 6 +++++- tests/qgis/test_sampler_decimator.py | 6 +++++- tests/qgis/test_sampler_spacing.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 0aae78e..c6f9256 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -120,7 +120,11 @@ def test_basal_contacts_extraction(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index 088fd29..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -92,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From beef36db179bf4067cbabad07d3e4a26df9f7397 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 18 Sep 2025 14:36:36 +0800 Subject: [PATCH 088/135] fix remove duplicated units_df in sorter --- m2l/processing/algorithms/sorter.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ed402cb..ac5be49 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -364,8 +364,14 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFr ) units_df = pd.DataFrame.from_records(units_records) + total_num_of_units = len(units_df) + units_df = units_df.drop_duplicates(subset=['name']) + unique_num_of_units = len(units_df) + + feedback.pushInfo(f"Removed duplicated units: {total_num_of_units - unique_num_of_units}") + # map_data can be mocked if you only use Age-based sorter - feedback.pushInfo(f"Units โ†’ {len(units_df)} records") + feedback.pushInfo(f"Units โ†’ {unique_num_of_units} records") return units_df From 81f267055550eda6d44ed76e09ffb2a8cd9d70e8 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 18 Sep 2025 16:41:37 +0800 Subject: [PATCH 089/135] fix build_input_frames in sorter --- m2l/processing/algorithms/sorter.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ac5be49..ee37b32 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -263,19 +263,13 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries โ€“ only attribute values. # -------------------------------------------------- - units_df= build_input_frames(in_layer, feedback,parameters) + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) # 3 โ–บ run the sorter sorter = sorter_cls() # instantiation is always zero-argument geology_gdf = qgsLayerToGeoDataFrame(in_layer) structure_gdf = qgsLayerToGeoDataFrame(structure) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - contacts_df = qgsLayerToGeoDataFrame(contacts_layer) - relationships_df = contacts_df.copy() - if 'length' in contacts_df.columns: - relationships_df = relationships_df.drop(columns=['length']) - if 'geometry' in contacts_df.columns: - relationships_df = relationships_df.drop(columns=['geometry']) unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: @@ -335,14 +329,14 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFrame: +def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - units_df + (units_df, relationships_df, contacts_df) """ unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' @@ -374,4 +368,16 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFr feedback.pushInfo(f"Units โ†’ {unique_num_of_units} records") - return units_df + contacts_df = qgsLayerToGeoDataFrame(contacts_layer) if contacts_layer else pd.DataFrame() + if not contacts_df.empty: + relationships_df = contacts_df.copy() + if 'length' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['length']) + if 'geometry' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['geometry']) + feedback.pushInfo(f"Contacts โ†’ {len(contacts_df)} records") + feedback.pushInfo(f"Relationships โ†’ {len(relationships_df)} records") + else: + relationships_df = pd.DataFrame() + + return units_df, relationships_df, contacts_df \ No newline at end of file From a3a7082d9876fee419a9ca37e6c433f9ec83859b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 13:19:15 +0800 Subject: [PATCH 090/135] add contacts and basal contacts data for testing --- tests/qgis/input/all_contacts.gpkg | Bin 0 -> 176128 bytes tests/qgis/input/basal_contact.gpkg | Bin 0 -> 167936 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/qgis/input/all_contacts.gpkg create mode 100644 tests/qgis/input/basal_contact.gpkg diff --git a/tests/qgis/input/all_contacts.gpkg b/tests/qgis/input/all_contacts.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..d116bff4aba6685c7ffeb21cc3abe3395e3d3049 GIT binary patch literal 176128 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

C|3QNdSyAAf1zd?V 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 YVDz6}WTrd(%aPc>Js6evn}gB+19}Be4gdfE literal 0 HcmV?d00001 diff --git a/tests/qgis/input/basal_contact.gpkg b/tests/qgis/input/basal_contact.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..107a6899bc51e4a950df60fc17ef519820d62dc0 GIT binary patch literal 167936 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%0A&G*9b<+Y?`36AFI0ujytA{5dn(7b4Uf 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 af7|f;$B&t5R{p-h^KTn|(*LUA_rCz>Iv}b5 literal 0 HcmV?d00001 From a3fe1de3290a32cfa3d220ebde47fd3e94b61749 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 13:19:32 +0800 Subject: [PATCH 091/135] add validation in sorter --- m2l/processing/algorithms/sorter.py | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ee37b32..dd30da3 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -79,6 +79,8 @@ def groupId(self) -> str: def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) + selected_algorithm = parameters.get(self.SORTING_ALGORITHM, 0) + if selected_method == 0: # User-Defined selected self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': True}}) self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': False}}) @@ -88,6 +90,14 @@ def updateParameters(self, parameters): self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': True}}) self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': False}}) + # observation projects + is_observation_projections = selected_algorithm == 5 + self.parameterDefinition(self.INPUT_STRUCTURE).setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition(self.INPUT_DTM).setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('DIP_FIELD').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('DIPDIR_FIELD').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('ORIENTATION_TYPE').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + return super().updateParameters(parameters) # ---------------------------------------------------------- @@ -241,11 +251,11 @@ def processAlgorithm( # 1 โ–บ fetch user selections in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) - dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) + method = self.parameterAsEnum(parameters, self.METHOD, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] + is_observation_projections = (method == 1) and (sorter_cls == SorterObservationProjections) feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") @@ -263,33 +273,44 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries โ€“ only attribute values. # -------------------------------------------------- + + structure = None + dtm = None + if is_observation_projections: + structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + if not structure or not structure.isValid() or not dtm or not dtm.isValid(): + raise QgsProcessingException("Structure and DTM layer are required for observation projections") + else: + structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) # 3 โ–บ run the sorter sorter = sorter_cls() # instantiation is always zero-argument geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) + structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' - if dip_field != 'DIP' and dip_field in structure_gdf.columns: - structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) - - orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) - orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] - dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' - if dipdir_field in structure_gdf.columns: - if orientation_type_name == 'Strike': - structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( - lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val - ) - else: - structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) - + if structure_gdf: + dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if dip_field != 'DIP' and dip_field in structure_gdf.columns: + structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) + orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) + orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if dipdir_field in structure_gdf.columns: + if orientation_type_name == 'Strike': + structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( + lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val + ) + else: + structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) order = sorter.sort( units_df, From 170c1b83b559e76674ac242e8f9cdb9b4610cca4 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 15:15:08 +0800 Subject: [PATCH 092/135] handle user defined strati column and add validation --- m2l/processing/algorithms/sorter.py | 177 ++++++++++++++++------------ 1 file changed, 104 insertions(+), 73 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index dd30da3..174d8ee 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -21,8 +21,10 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterField, QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, QgsVectorLayer, - QgsWkbTypes + QgsWkbTypes, + QgsSettings ) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -113,11 +115,25 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=0 ) ) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") + + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["layerId", "name", "minAge", "maxAge", "group"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, "Geology polygons", [QgsProcessing.TypeVectorPolygon], + optional=True ) ) @@ -199,7 +215,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterEnum( 'ORIENTATION_TYPE', 'Orientation Type', - options=['Dip Direction', 'Strike'], + options=['','Dip Direction', 'Strike'], defaultValue=0 ) ) @@ -217,7 +233,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "CONTACTS_LAYER", "Contacts Layer", [QgsProcessing.TypeVectorLine], - optional=True, + optional=False, ) ) @@ -249,69 +265,64 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - # 1 โ–บ fetch user selections - in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) method = self.parameterAsEnum(parameters, self.METHOD, context) - algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) - sorter_cls = list(SORTER_LIST.values())[algo_index] - is_observation_projections = (method == 1) and (sorter_cls == SorterObservationProjections) - - feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") - - # 2 โ–บ convert QGIS layers / tables to pandas - # -------------------------------------------------- - # You must supply these three DataFrames: - # units_df โ€” required (layerId, name, minAge, maxAge, group) - # relationships_df โ€” required (Index1 / Unitname1, Index2 / Unitname2 โ€ฆ) - # contacts_df โ€” required for all but Ageโ€based - # - # Typical workflow: - # โ€ข iterate over in_layer.getFeatures() - # โ€ข build dicts/lists - # โ€ข pd.DataFrame(โ€ฆ) - # - # NB: map2loop does *not* need geometries โ€“ only attribute values. - # -------------------------------------------------- - - structure = None - dtm = None - if is_observation_projections: - structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) - dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) - if not structure or not structure.isValid() or not dtm or not dtm.isValid(): - raise QgsProcessingException("Structure and DTM layer are required for observation projections") + algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) + sorter_cls = list(SORTER_LIST.values())[algo_index] + contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) + in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + + if method == 0: # User-Defined + strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column_matrix) + if not strati_column_matrix or len(strati_column_matrix) == 0 or not strati_column_matrix[0]: + raise QgsProcessingException("No stratigraphic column provided") + + + units_df, relationships_df, contacts_df= build_input_frames(None,contacts_layer, feedback,parameters, strati_column_matrix) + else: + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + + if sorter_cls == SorterObservationProjections: + geology_gdf = qgsLayerToGeoDataFrame(in_layer) structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + if geology_gdf is None or geology_gdf.empty or not structure or not structure.isValid() or not dtm or not dtm.isValid(): + raise QgsProcessingException("Structure and DTM layer are required for observation projections") - units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - # 3 โ–บ run the sorter - sorter = sorter_cls() # instantiation is always zero-argument - geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: + geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' - if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: - geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - - if structure_gdf: dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if not dip_field: + raise QgsProcessingException("Dip Field is required") if dip_field != 'DIP' and dip_field in structure_gdf.columns: structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) - orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + orientation_type_name = ['','Dip Direction', 'Strike'][orientation_type] + if not orientation_type_name: + raise QgsProcessingException("Orientation Type is required") dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if not dipdir_field: + raise QgsProcessingException("Dip Direction Field is required") if dipdir_field in structure_gdf.columns: if orientation_type_name == 'Strike': structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val ) - else: + elif orientation_type_name == 'Dip Direction': structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + else: + geology_gdf = None + structure_gdf = None + dtm_gdal = None + sorter = sorter_cls() order = sorter.sort( units_df, relationships_df, @@ -332,7 +343,7 @@ def processAlgorithm( context, sink_fields, QgsWkbTypes.NoGeometry, - in_layer.sourceCrs(), + in_layer.sourceCrs() if in_layer else None, ) for pos, name in enumerate(order, start=1): @@ -350,7 +361,7 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters) -> tuple: +def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters, user_defined_units=None) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. @@ -360,34 +371,54 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee (units_df, relationships_df, contacts_df) """ - unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' - min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' - max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' - group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' - - # Example: convert the geology layer to a very small units_df - units_records = [] - for f in layer.getFeatures(): - units_records.append( - dict( - layerId=f.id(), - name=f[unit_name_field], # attribute names โ†’ your schema - minAge=float(f[min_age_field]), - maxAge=float(f[max_age_field]), - group=f[group_field], + if user_defined_units: + units_record = [] + for i, row in enumerate(user_defined_units): + units_record.append( + dict( + layerId=i, + name=row[1], + minAge=row[2], + maxAge=row[3], + group=row[4] + ) ) - ) - units_df = pd.DataFrame.from_records(units_records) - - total_num_of_units = len(units_df) - units_df = units_df.drop_duplicates(subset=['name']) - unique_num_of_units = len(units_df) - - feedback.pushInfo(f"Removed duplicated units: {total_num_of_units - unique_num_of_units}") + units_df = pd.DataFrame.from_records(units_record) + else: + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' + max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' + group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' + + if not layer or not layer.isValid(): + raise QgsProcessingException("No geology layer provided") + if not unit_name_field: + raise QgsProcessingException("Unit Name Field is required") + if not min_age_field: + raise QgsProcessingException("Minimum Age Field is required") + if not max_age_field: + raise QgsProcessingException("Maximum Age Field is required") + if not group_field: + raise QgsProcessingException("Group Field is required") + + units_records = [] + for f in layer.getFeatures(): + units_records.append( + dict( + layerId=f.id(), + name=f[unit_name_field], # attribute names โ†’ your schema + minAge=float(f[min_age_field]), + maxAge=float(f[max_age_field]), + group=f[group_field], + ) + ) + units_df = pd.DataFrame.from_records(units_records) + feedback.pushInfo(f"Units โ†’ {len(units_df)} records") # map_data can be mocked if you only use Age-based sorter - feedback.pushInfo(f"Units โ†’ {unique_num_of_units} records") + if not contacts_layer or not contacts_layer.isValid(): + raise QgsProcessingException("No contacts layer provided") contacts_df = qgsLayerToGeoDataFrame(contacts_layer) if contacts_layer else pd.DataFrame() if not contacts_df.empty: From 569f39bb0f4c5fc5bf0f0fddbc614ab51a18e216 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 15:36:40 +0800 Subject: [PATCH 093/135] add json file output for sorter --- m2l/processing/algorithms/sorter.py | 47 ++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 174d8ee..93a2726 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,6 +1,7 @@ from typing import Any, Optional from osgeo import gdal import pandas as pd +import json from PyQt5.QtCore import QMetaType from qgis import processing @@ -17,6 +18,7 @@ QgsProcessingException, QgsProcessingFeedback, QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterField, @@ -255,6 +257,14 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFileDestination( + "JSON_OUTPUT", + "Stratigraphic column json", + fileFilter="JSON files (*.json)" + ) + ) + # ---------------------------------------------------------- # Core # ---------------------------------------------------------- @@ -270,6 +280,7 @@ def processAlgorithm( sorter_cls = list(SORTER_LIST.values())[algo_index] contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + output_file = self.parameterAsFileOutput(parameters, 'JSON_OUTPUT', context) if method == 0: # User-Defined strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) @@ -317,20 +328,20 @@ def processAlgorithm( ) elif orientation_type_name == 'Dip Direction': structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + order = sorter_cls().sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) else: - geology_gdf = None - structure_gdf = None - dtm_gdal = None - - sorter = sorter_cls() - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - geology_gdf, - structure_gdf, - dtm_gdal - ) + order = sorter_cls().sort( + units_df, + relationships_df, + contacts_df + ) # 4 โ–บ write an in-memory table with the result sink_fields = QgsFields() @@ -350,8 +361,14 @@ def processAlgorithm( f = QgsFeature(sink_fields) f.setAttributes([pos, name]) sink.addFeature(f, QgsFeatureSink.FastInsert) - - return {self.OUTPUT: dest_id} + try: + with open(output_file, 'w') as f: + json.dump(order, f) + except Exception as e: + with open(output_file, 'w') as f: + json.dump([], f) + + return {self.OUTPUT: dest_id, 'JSON_OUTPUT': output_file} # ---------------------------------------------------------- def createInstance(self) -> QgsProcessingAlgorithm: From 9435579cde827bbd0a4bc54098b975963799b870 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:43:20 +0930 Subject: [PATCH 094/135] refactor: remove user defined column --- m2l/processing/algorithms/sorter.py | 60 +++++++---------------------- 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 93a2726..20d3cd7 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -3,7 +3,7 @@ import pandas as pd import json -from PyQt5.QtCore import QMetaType +from PyQt5.QtCore import QVariant from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -40,7 +40,7 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, qvariantToFloat # a lookup so we donโ€™t need a giant if/else block SORTER_LIST = { @@ -109,26 +109,17 @@ def updateParameters(self, parameters): # ---------------------------------------------------------- def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + # enum so the user can pick the strategy from a dropdown self.addParameter( QgsProcessingParameterEnum( - name=self.METHOD, - description='Select Method', - options=['User-Defined', 'Automatic'], - defaultValue=0 + self.SORTING_ALGORITHM, + "Sorting strategy", + options=list(SORTER_LIST.keys()), + defaultValue="Observation projections", # Age-based is safest default ) ) strati_settings = QgsSettings() last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") - - self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["layerId", "name", "minAge", "maxAge", "group"], - numberRows=0, - defaultValue=last_strati_column - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( @@ -239,17 +230,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - - # enum so the user can pick the strategy from a dropdown - self.addParameter( - QgsProcessingParameterEnum( - self.SORTING_ALGORITHM, - "Sorting strategy", - options=list(SORTER_LIST.keys()), - defaultValue=0, # Age-based is safest default - ) - ) #:contentReference[oaicite:0]{index=0} - self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -275,25 +255,13 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - method = self.parameterAsEnum(parameters, self.METHOD, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) output_file = self.parameterAsFileOutput(parameters, 'JSON_OUTPUT', context) - - if method == 0: # User-Defined - strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column_matrix) - if not strati_column_matrix or len(strati_column_matrix) == 0 or not strati_column_matrix[0]: - raise QgsProcessingException("No stratigraphic column provided") - - - units_df, relationships_df, contacts_df= build_input_frames(None,contacts_layer, feedback,parameters, strati_column_matrix) - - else: - units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) if sorter_cls == SorterObservationProjections: geology_gdf = qgsLayerToGeoDataFrame(in_layer) @@ -345,8 +313,8 @@ def processAlgorithm( # 4 โ–บ write an in-memory table with the result sink_fields = QgsFields() - sink_fields.append(QgsField("strat_pos", QMetaType.Type.Int)) - sink_fields.append(QgsField("unit_name", QMetaType.Type.QString)) + sink_fields.append(QgsField("order", QVariant.Int)) + sink_fields.append(QgsField("unit_name", QVariant.String)) (sink, dest_id) = self.parameterAsSink( parameters, @@ -423,9 +391,9 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee units_records.append( dict( layerId=f.id(), - name=f[unit_name_field], # attribute names โ†’ your schema - minAge=float(f[min_age_field]), - maxAge=float(f[max_age_field]), + name=f[unit_name_field], + minAge=qvariantToFloat(f, min_age_field), + maxAge=qvariantToFloat(f, max_age_field), group=f[group_field], ) ) From a660002ee17d02e18b28cf5e2d50f2955503b1e5 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:43:35 +0930 Subject: [PATCH 095/135] feat: add qvariantToFloat function --- m2l/main/vectorLayerWrapper.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 72ff281..5bf2da0 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -454,3 +454,35 @@ def dataframeToQgsLayer( feedback.pushInfo("Done.") feedback.setProgress(100) return sink, sink_id + +from qgis.core import NULL +from PyQt5.QtCore import QVariant + +def qvariantToFloat(f, field_name): + val = f.attribute(field_name) # usually returns a native Python type + # null / empty values + if val in (None, NULL, ''): + return None + # strings with decimal comma (depending on locale) + if isinstance(val, str): + val = val.strip() + if val == '': + return None + val = val.replace(',', '.') # replace comma with dot if present + try: + return float(val) + except ValueError: + pass + # residual QVariant + if isinstance(val, QVariant): + # toDouble() -> (value, ok) + d, ok = val.toDouble() + return float(d) if ok else None + # native int/float + if isinstance(val, (int, float)): + return float(val) + # fallback conversion attempt + try: + return float(val) + except Exception: + return None From b74bc79480048e0d2f77f45beafb74ca7d5d9a27 Mon Sep 17 00:00:00 2001 From: noellehmcheng <143368485+noellehmcheng@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:13:46 +0800 Subject: [PATCH 096/135] feat: Processing/processing tools sampler (#19) * sampler * fix sorter * fix data type of spacing and decimator in sampler * rename unused loop idx in sampler * add dtm to input * add validation in sampler * spacing decimator test * refactor sampler tests for cicd compatibility * update tester.yml workflow for all branches * change image * update testing.txt * install map2loop in tester.yml * fix: update field types to use QVariant * fix optional dtm parameter in decimator --------- Co-authored-by: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> --- .github/workflows/tester.yml | 115 +++++++++++----------- m2l/processing/algorithms/sampler.py | 128 +++++++++++++++---------- m2l/processing/algorithms/sorter.py | 4 +- requirements/testing.txt | 2 + tests/qgis/input/dtm_rp.tif | Bin 0 -> 161316 bytes tests/qgis/input/dtm_rp.tif.aux.xml | 11 +++ tests/qgis/input/faults_clip.cpg | 1 + tests/qgis/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/qgis/input/faults_clip.prj | 1 + tests/qgis/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/qgis/input/faults_clip.shx | Bin 0 -> 380 bytes tests/qgis/input/folds_clip.cpg | 1 + tests/qgis/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/qgis/input/folds_clip.prj | 1 + tests/qgis/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/qgis/input/folds_clip.shx | Bin 0 -> 156 bytes tests/qgis/input/geol_clip_no_gaps.cpg | 1 + tests/qgis/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/qgis/input/geol_clip_no_gaps.prj | 1 + tests/qgis/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/qgis/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/qgis/input/structure_clip.cpg | 1 + tests/qgis/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/qgis/input/structure_clip.prj | 1 + tests/qgis/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/qgis/input/structure_clip.shx | Bin 0 -> 1044 bytes tests/qgis/test_sampler_decimator.py | 93 ++++++++++++++++++ tests/qgis/test_sampler_spacing.py | 80 ++++++++++++++++ 28 files changed, 334 insertions(+), 107 deletions(-) create mode 100644 tests/qgis/input/dtm_rp.tif create mode 100644 tests/qgis/input/dtm_rp.tif.aux.xml create mode 100644 tests/qgis/input/faults_clip.cpg create mode 100644 tests/qgis/input/faults_clip.dbf create mode 100644 tests/qgis/input/faults_clip.prj create mode 100644 tests/qgis/input/faults_clip.shp create mode 100644 tests/qgis/input/faults_clip.shx create mode 100644 tests/qgis/input/folds_clip.cpg create mode 100644 tests/qgis/input/folds_clip.dbf create mode 100644 tests/qgis/input/folds_clip.prj create mode 100644 tests/qgis/input/folds_clip.shp create mode 100644 tests/qgis/input/folds_clip.shx create mode 100644 tests/qgis/input/geol_clip_no_gaps.cpg create mode 100644 tests/qgis/input/geol_clip_no_gaps.dbf create mode 100644 tests/qgis/input/geol_clip_no_gaps.prj create mode 100644 tests/qgis/input/geol_clip_no_gaps.shp create mode 100644 tests/qgis/input/geol_clip_no_gaps.shx create mode 100644 tests/qgis/input/structure_clip.cpg create mode 100644 tests/qgis/input/structure_clip.dbf create mode 100644 tests/qgis/input/structure_clip.prj create mode 100644 tests/qgis/input/structure_clip.shp create mode 100644 tests/qgis/input/structure_clip.shx create mode 100644 tests/qgis/test_sampler_decimator.py create mode 100644 tests/qgis/test_sampler_spacing.py diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "๐ŸŽณ Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml @@ -45,55 +41,62 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:latest + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index a4b61f6..28e5184 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,9 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QVariant +from osgeo import gdal +import pandas as pd # QGIS imports from qgis.core import ( @@ -22,13 +24,17 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterString, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterEnum, QgsProcessingParameterNumber, + QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, - QgsVectorLayer + QgsVectorLayer, + QgsWkbTypes, + QgsCoordinateReferenceSystem ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame @@ -68,17 +74,20 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterString( + QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", + ["Decimator", "Spacing"], + defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -104,6 +113,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + QgsProcessingParameterNumber.Integer, + defaultValue=1, optional=True, ) ) @@ -112,6 +123,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + QgsProcessingParameterNumber.Double, + defaultValue=200.0, optional=True, ) ) @@ -119,7 +132,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Sampled Contacts", + "Sampled Points", ) ) @@ -130,60 +143,77 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) - spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) - sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsInt(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) + sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) + sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type == "Decimator" and geology is None: + raise QgsProcessingException("Geology is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) - spatial_data = qgsLayerToGeoDataFrame(spatial_data) + spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - if sampler_type == "decimator": + if sampler_type == "Decimator": feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) - if sampler_type == "spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") - sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - - # create layer - vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") - provider = vector_layer.dataProvider() - - # add fields - provider.addAttributes([QgsField("ID", QMetaType.Type.QString), - QgsField("X", QMetaType.Type.Float), - QgsField("Y", QMetaType.Type.Float), - QgsField("Z", QMetaType.Type.Float), - QgsField("featureId", QMetaType.Type.QString) - ]) - vector_layer.updateFields() # tell the vector layer to fetch changes from the provider - - # add a feature - for i in range(len(samples)): - feature = QgsFeature() - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) - feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) - provider.addFeatures([feature]) - - # update layer's extent when new features have been added - # because change of extent in provider is not propagated to the layer - vector_layer.updateExtents() - # --- create sink + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) + + fields = QgsFields() + fields.append(QgsField("ID", QVariant.String)) + fields.append(QgsField("X", QVariant.Double)) + fields.append(QgsField("Y", QVariant.Double)) + fields.append(QgsField("Z", QVariant.Double)) + fields.append(QgsField("featureId", QVariant.String)) + + crs = None + if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: + crs = QgsCoordinateReferenceSystem.fromWkt(spatial_data_gdf.crs.to_wkt()) + sink, dest_id = self.parameterAsSink( parameters, self.OUTPUT, context, - vector_layer.fields(), - QgsGeometry.Type.Point, - spatial_data.crs, - ) + fields, + QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, + crs + ) + + if samples is not None and not samples.empty: + for _index, row in samples.iterrows(): + feature = QgsFeature(fields) + + # decimator has z values + if 'Z' in samples.columns and pd.notna(row.get('Z')): + wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" + feature.setGeometry(QgsGeometry.fromWkt(wkt)) + else: + #spacing has no z values + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) + + feature.setAttributes([ + str(row.get('ID', '')), + float(row.get('X', 0)), + float(row.get('Y', 0)), + float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, + str(row.get('featureId', '')) + ]) + + sink.addFeature(feature) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 215e79a..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + from map2loop.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/requirements/testing.txt b/requirements/testing.txt index 1940035..d238575 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -3,3 +3,5 @@ pytest-cov>=4 packaging>=23 +shapely +geopandas \ No newline at end of file diff --git a/tests/qgis/input/dtm_rp.tif b/tests/qgis/input/dtm_rp.tif new file mode 100644 index 0000000000000000000000000000000000000000..a9ba843ebb8ff37ef232f9f26cbed3d5e2c1cdd4 GIT binary patch literal 161316 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ℜ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 u`9Q?|`E=UPCFX@l`u8i5mu_PcJXSd3UBZ!R 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

J@~-neKlvhm@zs1gyjwY}`PBS(6$ABM$z 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≪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 + + + 1175 + 561.72162965205 + 297 + 107.52060579962 + 99.73 + + + diff --git a/tests/qgis/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 zcmeHQOK%*x5jGMmf*gWegPa=3HE!_zK#l=&T;v}Jv)0(wMlVS6!p?2~dA`V~ zzc@ZVoqfJVetGq9R(^l|>Gs1&{Kx_+AO9;U~Sc+Tm+celsCPdA6} z@y@GW$Ith7kH^bb=}Jo9;(?_1_wrsf+dj$LTI-e;L)_BZG;RFf-S**rgVxT^-+xT6 znTyWY^wy-eZqx6=$;_!HlS8(~;g2yR<~5$(&&5S!ohi!}zWo}fzhr%cqh1=XSEdBqiH zDdb>I33&C-6%g=h8}u9CyaVZ{zP_yl{PkE#pVSqwgo?g*4uSLYcV`5Q-~OYkDFnCh z?}!z!k1pgnms(wkCB^E!iHv~pBfy>m@@wDU!Ui2na3B?Tx0QgqA!7@?mz{{$4_6B=>!RMH2jIpp4FhbC) z_oTP5L47JXkczw8O2FNaF$E=H28VVU2nh3cp9E~H#nxEO(NsGo28`{uZ0EWpSu9)+ zvUMe&5O63BJey+Y*4vFoQ!o z4FrVwyH5hPNI=UbMB_sm6oOcedCVplhovx%OH_xvG)BN485j*bo8m2O&_M(TQgL@% z3Ah_Frl17O;LuJ30b%~`lYpId2+tx5Ef(y-jmQIb&g3#19QCll7Ml{r;(5Rz&($&D zEo@Mq3J#>=?zR$eH)Kpf37Em5odyEJ{M{!3+mLMV7Q15+bs0twFfxFPoe|?exH(|Y z&qgE!vlVc}nz(^yQ@n)@I*8yvD(-G80e3^j6qJA&9NK9hAk5!=60q?eXVgHywT3!` zfF~bHn7zx%6$Bun%Q%p6Lcr@+z&OsgdKFE5DmajeyW2{@-H^LM8N zoO5vn0RmghAOfC>Lmn_jK_&!_V)$Y+Pea2o;?;S;TiBpJ6&y&#-EAe{ZpfH|5-@{9 zI}HSc`MXmB#_=?dQ^*bn)dn5JK$V|7I}A~g34tRYCcBdm1eVARJe%UUfeku{;6N(w zZYu$IL&g-8fEgUxX&@lX-<=XLD&%b5CTT{7quWzaLs_` zov*pZbV|Uh4`LvOz1jx#so+2=?rtjqcSFV$lz3Sw%q}+K7u55Mnkns!d=J@H}?QlHn`hY}Q{lvxN=nQ^A2$+}&0J?uLvh zC;>A#w9`O9n7=zEVC3i4<(dlo9h3!;a!7MP`Y(bH1y?I!EzD2C;YN#jOE{1`W(AC67H$sF;vjM=IF8|PGJ}O6 zP%L6~2x1Ex)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2ED@wRl{%cGLjaX+}EQN9OnGvuxI3?Erv?boc1|36iAQgAFm4Le;V+u;Z3=Zuy5D@0? zP6@bF7f{J&sT4VA6E1^vXOC)Y1!W;DA|B516|hA~wzWs$wy;5cDmajeyW2{@-H^LM8NjBA2WDjf@8>j$lXL#P3_A_vq#LkA#w9`O9n7{iZU>k6mk4H^*vt*$S2|-M`q#1DsoVfG200~vs z5>G>0$a8H$;4N%Wp9&77;_kK*a5rR3K?#__p`8W-!u;JQ0h@q}f@;PN9LklAA>a^G z!kutAR)irkHvF*G<}qLp@Y*Xe76M*tgZfl(AQgAFm4Le;V+u;Z3=Zuy5D@0?J_*=j zd5ThwzC>KuHiUpJE}e^W#GOY-b}Og}0S5ln3K;h!uX}-jSKFXZAUKeUyW2{@-H^LM8NjH@zHpw}Wh4E#GH1YskNIO9s9go~G#i-WKSSNNT9AkVZm5qJw5 z)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2CLbpbU>Q&W(xzgW}Wtd>;`mp0gFZai~5? zYe67BoL8@aH?To{DmajeyW2{@-H^LL*F?EF#-*jW?Gpj(lxL89?; zBL>bpN4r!+3+9A?V_d%pw}B1nQ^A2$+}&0J?uLvhC;>A#w9`O9n7{iZU<4m>j1l({ zSu=x81RGvBFa&EgACv$+Kju2PB!6EMtPofFC1@PZG%36;6N(w zZYu$IL&g-8fEgUxX&@lX-+dA=P649~k4J0(p@=~Qj5{~x6j27)mr_GO^-KfVcq zM|GlH8Db?iG{eM`N4#FH&wVysj)-RzZ2pnXBmepO|NbXGaOQuFrCw3oY#Pjvx2D7O zL%n!}6XfK$hxXBVLq=^kcz2W`PWDBy%8;dc(RcBR0NM-geKTFh{qF==V(!uQW{EG4 z=r@lOskp$BxYy|uZ36zDM+$P>LuI#IEsU)4q)>AG=XrwbBUe60xQ+7X9P>SIpI$$C6Lx zOB=6)Qep{$ ziLi}l>w0igfS%7iN0#WyB(}{2$1XDzQQpmxYUu-p`@t87xK7@?pC#n43kq3a`EiXQ zu5K(D80%<%7hHQl@bg^{jDK4{B?G+KC@&=4hb1+43b*e7SE^+_-x|P@qq_~?D1tkC zmaV)V#1g)l!;Uv#J;t66s9Sc1C5l7NU%mp~yw_7=>sgkZ{jsau1-$FRYqh6gEHMjR zS-udwL@->~Hsh`Q#qsIp?y@=YwT<9@G)}4)~xPh1>Q9odO^Eoq#W@bUz2Ht@Az(WxW!6T zjzl(>NuCFPSU)WO%5XVy)!Q{s6+u4E%bu4+jG&krPnDxDpWBr6B zY)(Yzj6fdIj^P)Ts$_}A=O5;J;FCfA`qKAUGTAOKF&liQORjQJ3G7h4KJ7cWWPF}; zcrHsWj!1NLJq7(28XTLH%96a=PfP2-Q~NbXR>iVpMv%@GqaYq>(AW`Ogmu4awsY}S zaHzp*q33}tS!rkdr+F}sI3DSYIpNQewl(ttQo+ZyZ%)5jce3Pk*w3xG;H%?uG*`hdx7xNEy#jw|7Ao9s z2YtWD4jUiBBfJ&I4?1pU$$1B11$nU5oA~B_9!nYy|Q+8_ig<%QUR)5ID{? zLBZ6BB~cp7FBX6wlpCiQEMUn8-+n>kGdv>D(=v)^Vtq;T!i`|dGzTr`*(^y}I7Qz9 z?31^8Q-U%}JX{kdT>RWKBA!vhf zs@77Izx*;oQmrZ?&tYxcb>Aw=RWRg~!uSY2eCw}qiuO_2fz-8*NAED?kUJ|=fOqLw z^j*v`6wR3PckP@U@AVN3NenK&S$d8~X2*KnT~@%5FY2!kx}X1h9wEqa5ACC}{53V@ z#y1!;{__;=d0{+Kpyat|>UD+``zuAB1)tg%;FNWhA!?P!);57{<=Z^IL@{LSTGwuS zj7{gJb72Z@s(GAVG`7;%>wC0jeCBC}+`Yc7dDpSO_2L$bduSh(Z4@aL z@e|i6%uo2fOXI^V!wLkWI<^3jDC&_RTgH$!cQAgv(8lW3a#4 z+haD!zF>&_wjt|E!Mg*`!1IT{FMhU^4asMScdZm}AN*Bm;LfS9Sq#~e=k$Fp{PhN(y8MwV3>jG9e6tU{ z_IVsHK9(V$d|w`HJj)~XBU26-dfftX^R=a$VAq|=u(krm`)Hc)> zu-S051DwCi@yJ=01mEf{`?$s@iAT1cd|%`EnjwqoThD*I^0&W?fE@SGJ}Mg&lX^O} zogoRXquv(b-5ZaaWF1==;^VtYPAHj2+I+otThuY6G+^-sUvPrSqd%)3Go(`8-re95 zkIc^~u6Di6kS4_eoe?QKvfp{ajh8nW^7zTt`m136j$yW|au_mGMlXNpWgeN8F<-L@ z`=tHT?8rsnKT;(MUS~37cW@Ur`ji-%HzvP zVoC1mh_^Ga4|fap?kv8@663N8(E@Od!hvmJs1@7Blq>0BU+$gWIC5VMO9XN(O_RV+ z=8kgI7N>7T%BgW}Ux)=mhB9G_b@@8C;fa`{xvJ^npF@ufth#?W$#p zIXHHpY)II7mdt)WE8q}#SDLlaDdcU-uv^ zmT1qLm3#>6e*C2Mgeug7uguQ*DR6dasXL^Jda*HL_ij7bt!V!I`k`R!l7?5#VAZf4 zO4Ih>`?R7vdcha^Hnl$31^K=5Mh@_U!=`cu0}fb^1UN@yI}C`u3Km0*098(C(|$&tXt9i&3+Z$*D-tb z1WavBZ3~l%!Vd=Xbi%fgVj7LuGnHQY`w`k+6_0lfd%Bm`nV^nNmDt<*vx5Lh;Q(hQbJE!n-0AAVQSxA@z5)Gy5+F7fMwRi@k3#!q2M(E08S z>hQDa0<#KsPGm`gdb`H~_{rq^UQz2Nu%u$UTk(7F@Ab@W^*KkI#^{fk$z_6mp2RTn4ZQ4@?@>4OzkJrt z=Lb8crS7y?CSuEwEH26}E%0__ z1sl_M$P>ZVOc;1$`MP&oTNxs8zOQj8e1rEbhQInHLykV!$BYM4U!uMPx8YJ&6Bp<2 z3)sK^slt+l>8ZwoSnJv5-Rpa>N0e*Ag)C$L_OtQ-_!;e^vJy)*o-e@u(Ji;|oqr zWkj&MH8!-p%mVxi;B?^dugX{(FI)JCu zBzwycmPF|9ymJ{m^@;4z1K0=77o~?)fHO0dhHTPhNsG)L`9H9(qMc{n8tAa3UC-Rk z3LN-u*`zT)@ZAVrG&aB61a-b} zr~92)u*psvflpB!u<)RObKk>_0bZiVY3eBGT9@k|l zUq>R=(lZ-9!~O9#znwcXar4p(9x6-wHV&Ee`77e~y=UH=g&}+EL|>_v6H8{56)2V> z-V{AsZZmTqOS;!(`T2rNohG%eKZLj}k(w+D7CoV`pb&ZG&)c(&?;&2@mt4*N9QO)7 zL+M zvmpU?x?GdHwvZ*C1ut%1ff|wfE_yDava~OHl|flh8uYL#y!#;$^CpNdIg`l}+q;6( zQo&s-4EUULa37)eYWPa9cx>vHV>ehbM8V2W0Ziwjb7LxTG;40XX)Qs|< zt|;an+DByrUVL&<$>z?>cU3ZxA8wye`)Y6vdGo@2mC48tx({DIkVq6ePwa{jAItkJSnLJwoXR5z*%R2YpG%jx}Q zj79ZZoc-zD!M}G2|I`ee-rPrW&%b^D+%r_)TK_-;^R_&Es-h0Pl2;gA=|%lyHQBe5 zyJzSe)iFmfLmRwJ;4j&4?dph;f z&T`a4GLMQ(W`h?WiQwCHk|q2215LcZyalJvY(0Vdx_Cd6X7J%>M{7SHXNmhONB>dq zb9e3Xj_0VCuIZQg#e(VDye=>`nS*%zdp(6E8H47;t)qjHs>RhYENO~U>l}?(s<}ws zVu>QYttD9h6Ji;+jwntBAKFJ{pPpP4>NFKO!}|H;U3m9nxKmKxWccOO2~sKGlwwEA zI60Pt#&zrWfLkuv4>4j{A`;OaKMis9HNQ;>k3s*d4ncEpm{0ThU9v1mp8lXT0^I0( zyJUt8^09-?xz-a*=b~d{$Z=?c zdT8wMeLf(?k~5~cj&|uhA~e%rhPN{N?;ZKCdQe$W1rjE>1?j=t6aa&fHbb;J%_c zQ99wF+hX{encA@9Ebdagwv~)^IrYE$iuO_2g5#PJwy=5dqqcAUc(+IHP4@M2hHM=d zaWfnI{*lbYT{!=5lKh(Y1O9Y#v5dDk&i`LbWWuMTj&BvWo%%Ti_aJjb)=pJ+CHjGB}F z#^W9QrllXu=|$@zDogvQEPWTE6VL|Z$nP=g_u7#gm&TFbWqm|dhYnLfo~~;1;_A## z(wUN{lvol_6nl!}i-zJ3ZK(UM`Q^uOoD^pSl^GaoSefeVaWc(N6pr-saOuLK}ZcXw0Y8kR=msUN$YzmOZx(}P2LDrF~nFb zd*WNf?Zd~cOHJw-QnGx@&p_~UjqPjK;NEb=yPAon;C)L^_wK;GqgAtgV-wCrnN^od zBYGH86q8w)2Ch)vY_e_uchBx$V&C8#HLS2sIg+0xnsavQy+Zx}W0E=1mWKi*0%5HWR2 zX?SY(B#a+p%ikBoBQ8IL&h#OVEvmBi$N)R8bCU`|p3AG>POQN`ZrUGYkt3^rCb2KD zKST^KO^pJFU)Vb87Fcgw+^*1REE)cDvs@tfn9p_XsjzcVRezH+c*mP{T3>PYYG>+5 z_b;}gwt{K6tDZqSu1Z>&6rIYFz=YM4hvMwB^PcVa>kTaV>#@YObGSSF)s9q__NjCY zJ=v1Ml8JwcbjU(h$*kbJMizb_8JB$j8fwm(YoP&q!9JgDI#R%ox7(h2f;!Ujao0U_ zuy9SetRm{jVsFX&L%_$*9X6hH6aMRLpje8!a(>+>BU`Yfk)dMPf7F?N#l52wQRBWB ztPHt>npb~g<~`FQoS*p?Y#sqNRCnTQ1AjW=_d@$3;#AgJ@k?NF_51Sb@jS9Y?VHW2 zTP%5^BC~cm&OynYDUZ@I-f{Cqo3(Ln8a0Bo*$@7lQvYrT^8dBF+I@zQzr6CH^JDn< ze7B>Pq9w>XW?xGWf=7-}yjc#m2{$U;1U~iTjn6o22*HBs7*LFiItsM8(rCuI4Z$NgIk4c{~ z&Qp+KjGy#lPb_kM}WHHMfxUc259JhRHR`v~rzN1pzxXgrweN_B^-4B8j2uA;gM zpYiI?!`|(PS~8^$zZLm5h)M+gbC19UCGMfJw9olPVqu;uL&gO9M;wQ&yA)r51HSXc zf*HI6;G4~Ft5b01Tzp6=e+}&2R^~6ez?UJm{!3-1fDKfQ=lA2jUvjkhA+G+*Xp!~_ zJ;{)Qn|zm|!QMxd;@iM`H%!qE13RZkF%tOwz}(`g%yjU9$cc{!z}_#V^TolTi^^WV zJ;#u~7!jEw_{V0s@g=vBH_amF_wN9g?@JWej{D;K(waQ3Uj3fidFNvSL&^<}WwPKO zwVMKc4k7Qx9x2h*0(YI1)2;`vIHHg*4^ErdAi-tf53MYv5TY{5+Y&>#4jUi!zRoZG`EwRgSZm2_>O7mq# zfy;xQKeWhTNZf{x{qGQW#U7+5M8mFcQ{-gwz{{TouL;BbbnTQ3UIyZDU-pxe!UYV; zs5Q>l1bfQ#IT{o(WSU=je+c3-wI#JJOvs`Q-Ve|mu;Gwhj0wK&_q#35pXvR;?|t+7 zR?U_~eppkuqmjE8yV?0=YZ!i8p<~gpX@0>_a%jn%ztQ-YVO;&;6mrA1f`m+=pZ^~p zxp}!mBKOcfD(k*>QF+BTmJB;>^Pv^*TB>FBEcn6_wT`1=dhp%iB^P4lds(vSb7v4& zBXoVU==}JBB?bGm#T>C7#fe@1N4r^aLEpbC7i?EtHb%D-zaJ~VFjL0*g%ABc#`g`* zC%J3Px!)tHt~4I39ldh;msZ?62dnZQfIV~-_B^t9&64=043VMWrhxoWb?=ck4pn=3 zfML|Fnt+4C$HnZ9Vt9*~M>%mO}R)UI65>m1c_r)Xp;y! z_&*5YBu>)M+$6ofI0+v2@Ve*weSVyjiEIBeT?Zsy9R{Ax-)p7|)PA!5-2dU0_{`6_L5I7ren=>`QTzH>8BwcZtlCi>BYWLh7vxq|W+6=2#nK mPcn=2C0n?Z4(_+6%s%#}O#Po^e@5lj%(3ppSt#U^^URp}WSf_pv)5;f#p2_Ocdy#N!7s@~n8y9>&)xM}d7`-e!K0>sVidpQ ztXWmO^BTpQP1%Fn@VlI4PY;axM@78TKTgZv5s&(}c*yH!YkVCZr`1-x6stUWj9`@i z5$}iCg?B@Y;u%|qEa4&lxdl)2op`;;ZVS$0b8d`fG$-oV>&j=n>6Z@{s8#8ENUL> zusr*GE20O%8Te9U#<4xK+OwcNs{ev%Zuw4qQq=yv1ZEwGA@VMucY&$Z%;C}_PCzIO zeB@&Y;orjOxQ?+fTHugVRtqa*I(EIuZR#TUR1_lNJZ9kLiCHJA{rSvIDCU;${@xrr z%xeE$0<#X>n=leW3B;6O>e4Y`bPVf(T46NhkV8-lD`Psg*|ZEtJPG5lCda&U62@az ze>k?pK}+rs#LRpq>zs4BJvnwwC} zEnl4;IHZr-zmpY4(w;6lZhOp#sIP9Nsb{_mw!tA!oLX2J)3MEtesxAXuFu?rVs7~! z_wD-k=xYB?^y3R!V(266Qsi{9sIC|hg{B{SkOdC8+-hNEOvkppV$_M@kUn!0in--` b$ahI+EVX|pyJExvbb@v#OW1Y-9rn5;d?Q#kUk6XE%jx?I$jj~bgD5slEl89)uso1RP zHr1+jrczB?thHlnT}ow*OJP(P_dS~^jA-|n><`=NJ@fwYnfH00dA{#+8HTYP#tgdj z6Cdd_41Sn#`tdH=+qkg$GsB9dyD22xU4P=5Jr71!oDS$Kr!eUC|9`267&MqK7tfBU zy}1M=WlwV?i4=5(Y(VVqvS)5*Ok6ffxG+6q&f4W@>$uY7{*X>Cm&3a`LR- z8ZG?@#M@W57$Ki9=r|UWL}1FXxBlZ3D6p}z?|`8WVHmysH`>Wz#G)83w9cJMqoov5 zICuG-dOYyXYD)ru{>hm#0*<$Aqzv-whuelV93^o7>+8=;ODQ<_ z)prdS69_pSI%*;Ejg6Nw?BWRQ?=kvASwi9QicyoTw-Q*VO|o!6cH%sl7#xc6%=elW zAg7>Ob!uaph(MKQR8E(S!bD-MIwFX`mPOv?k;s;nCr5_)6FAbIa@qMO3a{>ZOn>A{ z;GBG4?}TCsI}V-SI|*5As|fq02=ABdD6qqKPkZg~U=nhKrboi~5KuoYOI=b(p}URK zJw`yF)xu%X++!5HzNqCZR}knb^z;`0Na0E>^C)uxfya5z%HI@Fs4e);a;PH#x9A9a z5i)Q5#l?EI1a{dUYW*jlg4GIxr5U3MXu1XIQsg|Ts#&T>Apf|Ha3pd8&mb%N0T&`> z=Uh9TMPNqUej3+@>F@oTp`C8Aw(ME`7$3csT?7&hE=27? z+jCteaU~f98fV^E=76k?81m|3F@ZOxe&4=Jqac#`91J-_V4g~0ehRro5ca3(S?sUr zj@DJkcYYnA4!B4lBTwlrLH;H@Id{ty?9Z*e#_`B64R5WzbB{pa3|Z`A#Uxi4ET z?5rT5a+R5SW@0~XkM}UGB~b0It&7RV{_to#xZx53qp@ptF3hDMcvxa~6?J#44wyF< zYiMo0Ji#1w3k9Og9;{d2&732X-{QL^%)7S``$1)H+9l-kK+*O2VHGl)e^v`h8Hzhq zm+zV?N$=tNT(C;{-L46HZ+-5ULAH71!z|b79)Vl8*fZ8P-}>F0A_;-lS1L?=v9CL) z$+rg|!hC4P-~J1^M^d+pgSvzBqvI4f2R2`*qPy@BKrD1(~0Q%RE`z|36#(TY71b3>-8gbzIRr|C9=d1P|I$ziqjX2Q0 z`uu_RiP1|vG0OYfpRJZbnytb-ICNf|Dz?4&7xm{UUoH37FHU`Xss6OAeE^?(p2l7t z@@oEUe?Q_No(Ab*`_rKOazTmx?T5MjQvbfgX+M7fzXH2&Q0Rbt0c~-5|9L<4a?!bi z?GxjZ2WjfPR39#o?ms`{u@@%A0POAmJPm>*gC-mtf4KhC%fe*8{qodH?VnafitE>- zQB?Wqr)*W8|90T|JmvG?1$>q%e+(1<;QA%bf6M=UuzzB7zd<8r;r{-K>wkkucxZg8 z^Vw>DeZZct@(31~{pW*6SpKpGzyG_&rTXK|gZq@n;^W@&gY!8r3-3K2dr#k>%(wW! z{DJc;fG^FT_uG%5125JOlb7qSuRl9<{k8o==YuC$zF>$BozE8DGWZq+$^5531PuS> zKbK%s|CZT({@}YyMYx~l{M0;uW6>!`Y647Gx_Z%p7`NZ z+EQuY)`P`~-ForVW51_7VZT;OcDD-@)uV28_tlbY&W_0s3;4D$HuaKOFk>O~cmnG~ zd6Qx+Ud@WtlrcEZvM|bvkJk*oNIYe}mri*C_Y^n~{+n_Q%Q7Lk=U3 zagE>LSt`w<;F`pwZ7EkltE$^%^-AxrWckD^7 zSJ-zu$?o-D{m&qScZ-DGti*d{6nc_Z6!scp3HwmI|4168>V&{6A<(QZ@5M{$88K#j9i%dae4ZqFGSHHI20aFPVEOZ(l;S91V)7mg<&ewr4FgLv>VJl(B8C zEc0o($QND|Z(K~%RMA>ZHO#Wq9WXo#_Bo8E9=r+Pt)^Kx3xHH%?=;I5P1lUF=pO<1 zQh1?(rlV}w_B2bA=RZ(-w%>?my7vAv-Hw{wsvWHLdvXsec0=OWqGJsA_%26%>k=tuDLVCVIv9+O`h*7Jx# z9c__Y@mnuq>xa&LB6f8YrZDd$eRTNZxVC~@#Jk(|F(#NciEG0|lWe^;1c+;6Y@4-s zYhz6P!9{?$wxM!urh%yv!ksl>O*552w-23bL+0*!Q5_4!w+uEK`Mk5W$ zuVE5Q<|(L!1<7#_f1+m=d7xW^pKD$cy45sLsd+2tx!#$#VzmD`dGf=_Jq7^Hv*lXX zwpxOirMqR~cDiFnHQzo0^7o^DF&Z7$Cb?c=-|hMx9eL}-waH^h7Tzub&GQiH+A5*Z z+Z}D{83qCu%e8$5B?<>mOOW;PfAh|If_ewr&i&eeylrw1BNc!{+Y# z)&rEIy>9Ydd|W zldkP_%*IrMxHh@bYTku$eBQBD+;Y9^zcz7g{bNj57%u|N`a)bAac$$B$LDPM#d2-f zKA+h3G}5(6eGjm?F>HIiaczS0GHk_C9SdB&t9O>dhg8!w;X>+KE_hyV#A|cq6=@<~ z8>SEq;yfRYUYfr@x-m1}6> zIJ^D;KUhGRKyapI-yvG;I#Sy-^8AO&*K~cl74xkXW2^}D2LG(%z8wVWs1sxIA+3Yi z9o4h_g~SjP!2ME>%>IIOln@vJ0@ZzMs7)Q6u1zvt8}{99sN-6-i@3H53HnP2>>@y1 z8~Kp#&I&s65(31vA^BM0b8W+phXy{Rn9RycbF|aBi`BJhmTG9O;;Im~ZHsGz22U6rg${;<0cMojaBaeJ9ft~WGAkfP62=_sh?VP`c90MXBKtngJjjaR0ts}kx z(s>{vZ0@c zyD(t~og3kAJSr)#+4Dmnl4T0&t4l}EQl}X0r|INaZth2o!XE8SZYNCgbd`mVAPMZa zftsxxosRXX8X+(w1c+-J(r8>>TZn7ByfLR9ACExuJcPrw9e-siNeGM+fgy5j*RA*1 zFX4S2CUf@p#{hDy*Q}gv7THN9FvsS`uXKwR5~uC2?EwCWJo z)&hfe34tvHh-=#dgg%}X0pi-u3O8MBWC#$~HnIVu=Ds=t#I+68hg4|&2D}m1CO0_! z>Dp}7avj@tm7REPplNg9KU-5kg4V=q1C|WI9T>+-11fQC4HjMYh-;gWuC2?&p*n=X zt0O>M+p7=3MK^%Bwu>Hq>iiE7Ag=8P0MTheU<(1_+K6l0ni9u-B(Ci^oDP-hk83j> zM**Tu)gVK{rmjubTvbtBkh|G!U7KSl2(&I(L5TX+z(u(c8Z5f(VRLPi8zIf2;JJ#| zCK+qI9j~nkR@Y^m%Q_+>0_8^Njvv(=76O;XwKcKE*Lg!FhhxW{ zQhVdtOv7>vTQ^+AP&c_Y4JbI%byO45k+yRqsD`e~heiV{2IdEJ z!;fkY3xQ^R!Qt8{H^Q*Sms*35zz}t9_`Td?5j(ybwmr&?uxHRtIE2lOVRLu=aBZq_ zWviNPK(s#~*2K1kOu14V+cZs8cPvAYa4jEF;@VD_S9MYBw~s=HhL~)iu!k70jkq@A z+Um*w>N~`>z4{RJ+JNf5)$2thj1$);0p19zk`L(!28o*ZY6uY5_G-}6g-412acx7@ zwNV~Ysjn1qZN#;0Odye&@~_ELHkoc5?)-tcwiwdYca;iBFHsD=J3&&cqw;=Gk(s zJCW3hkgl!s7q6&DT$>zzL$6I-o7@OtuUFW2JGtKVUz@nL{xPO2j2D4seIZ@jct??1 zeoh34Ya_0$(|p5@*M<-=Y@;85+5+K6lGG&hNB>jYa}iF`Mv9N7$$y@mC1je9|D$r_tF^dphpVe zn)^{{`SyW^{T!QdHl>ZKmLTTLhML<5g|v=IVt1p(sP zkiO4<1l&vEmC-}8r`f7Aq>XoZ0re#~Mt+--a zH&?mROxspnO*2fzQ51#E10H=d73JZ|HtnjIMCWYNL4832uC3c^!GS&q_GLMl9-GMW zfR*O(=jYU$g#i}TR@xad*Y=$9ryzm1f@PS5_rcR751#Us_)64dTEVy#?++zc!y@m| zW8Xt~jM~gn8hM#u=tR=d4Z5wUTc##`;3C{ymVjm}G8(K$k3 zUxHK)_cyAImoZzsRJbkL1AaeTwA4d0!)N`BFDr$^CI$IIhkkIX1pNnvPB=}vwiCdjqUZ=z_bqh8 zNCwBtwRKysugVv2{ z_OQ9Y3+39>E8Er-OSfIebT_#+#c?#(uq;JUEs&r!x;E8tH2HTMVFifEGF1SS-l}8I=E=+jJ?t>@;DO>GFTAaUTrHIY<(kV~8 zJSe1YV61Q|Wu=P^kAf8RZGy6k;M7rD!L^x_CvYJ>s{D@%HP4pYTwAwQx{V+7tA*^3 zT0-Xq0u-<9ZO;Sw`%%C6jF)Tcwq8+g1iATiq-&_$MW{9*a3%zr=OG-f?GK*j;1rhS zyMCnCtT_2S*ng5(x-$(qT}BcC;@SqwwW-~Hfs~vO$5y9!ZO0 zo*3bz8HJu>lZwOo(*S531NFuvqPjbD&zS~K5ulMTMx;En4 zdLJ^nz!?!}))ySE?Tk%FyVM77nT9@36ZR?P^8krRKx-Zu8W;kfK}(Qk@SI*wmuL3Q z(oHsDrlG)lde19Ai*Je#wo0%t)RQ=UD7s=M{T)hY#ttHucEYBaC{tbkjZ*`Bq~ z4AoI>Q3hl>WtmUQMZWN&IE093b#pWgqJ~YFrh*8pXzmWnorSXiqL8w8n&pb7YercV z+vgM89yXVSJ@0_Au`z9S`?>XS3($3K8PE9=ggHoCe_WesLQFQxH4S5%Ytt0n1UFLG z0v2vV*QQ(Yv=*c?un@w&)$o$8t%jo0PQl&fA*{PN8|m7D%wGi+*OmqUS`{~zZVOkN zYa?CT=`)mcZKq>4rW%*VwKY1CHJ2|rSUdJ4*DKPs$qfi`ZF2Zg?O`F%tS>lR8|m7H zHNMmud<2dl%kYS>D$U4;)Pp2+4C48C53426V76T_W@{P@zn6O~Vsm5I_8=Y_g4k$J z$B_XGSgjA{}BuZ+N@aczxG1cz(eu_vi(!@k?S^7Q)Q371SXwhiZ`scE8v&tFT|C@`Yc!!edk}>x})BV zBN)c!#<1=6#?#fe|CntS_W%t72M>csQx)XF%X$b!`PVwoEyMtKn5ewuJRx~GX)fukUU35Kd`E4Vwvcx`{H(t*yN z9|94;AHs~GiSSzEgtJ?W_S1CIQDnH&2<*|`)U}04p02X+5!`P(Zs0juCTs0jpQ;f8 zLqg!vxHjzdWk_Q=#c^QfNOL(7$ z$(;TDF-Y+Z0M-DuY>?`Z73e3 zj1kxNN;CMtWwE(2YH@s&S00tI*w{P?!YmU~ zU{JTk;apI9B6QmZ1j54>@8Tz05~Gbc*bIBL1y+-aaJvs80jG?*t)6=2;GB#XRUib0 zguu|bHf*f0*B9*eU79`O+T=!jJJyk$HL6Vr3=e_r_*=tkA+<;bf$cSH>mGfw>02+E zdnpI?!>5$bgG9t@vmA|0n0R+)x8Bd!YHVaG$$PM#VsSAh1R`j8?h zt2P>po*Mx<*FtYxn`^nY0eKM&a2eguwW+oVY?x}=hVE|okgBHZ$fx`iRxq7DAt^CJ zcGPB0lwO5=;x&h)_L^VzF2jfPqxT%75OOW{?tvE)2@+NjqzEWK3V%{ae&9m{%q*Ay z@e?Lcbord}rvP|I@Hu@9(g=>S=`)LhmCt>VUBG8^Z#qr6AnhtI$ZHmHA5uh2MJ@)G z1?dtL&7m-IN^*ru^C6X*$|J0N8$O|i|Ep-v)|{51K1Yb?u$nw{t_?e08}{8~Sfe?< z))3b=zA>W~pAUg%y~5G8-SJhvU=_P@x0+7i`41i!X(I&Q3IapawVef9Y#kc5J@O%y zn+DiAG;Djlac#O|8=7eWW3GyTZEF;ZJfw=Dsg|pOb1CHAXnaT^Z7D)%K(KahJ;8hT)?pv>N@gpuuT-y;KjkpMLZ6h8$YI^SoU~_G~H<&I! z2)r@^L*&{hUK>mb;@afC8Q9zyw!PlCHq9}03;ai2TPboNlrR=~NNp2>xEU@a9niEE z*9Hn!M_xiEbg-CjNRZtDQzlpBMp#UqGMv1WjQ!@Nv*5W*ABqtct>DHGlMP^VZFj*e zj6q`NrO$<`ZSDB@$Ri&-XMHA|MnT$EiqaZQP;pr9tK6lw(6!xZc*EU1Tdq}%S@5q_ zkoW=XCKzt#Aq7!j=!bsfWkTsk+c60ArEA-s)yK4j%HSh_c_+bd9b-f1+OXFv?7Lkb zV}fZDn``^%rSpJQ>RHIi_87v=BP;HtMY5snyO+jr{}dNyH>(UJ;}EkUVIM&My(i{s zGrpze-ywDYUuEp`+A<~#brZt1X)Yv309jl0&H`MUrU=EGs<^fVDG^#+n`tTt?!Y)!nn?ut9T3!Z zjlG-6Z#OF-U!sX0Hb=Yfm2X?O{ueJ`cMA_56og-8dGzi%dJMhOcsL+)QRB72(-5kz z?RGWG;He)%zyUXuuXp`LkV0g-DRdoN`ofealcE}J9=hyx|sj?Il>+tW+~fdJ`mGHNIU zO@Hmf)}dkB>q*z9UO^x|TXSvO1#)eJYlF-Px^7vzp&2%OyBi-;$8r!}ZNLiZge4zR zxi1Gc*G73rDG#aKjHKFxz_AD*+>nmFRHt&-TpO&`Ip+G(sZ&&`Mu51s8VcGW1a=T; z)~n0q+IB#p1BAc}2pmC{ZPT}1QzKm0TF=D;Cd74tZwk}hL>JS32i~w}$j z*R|O;wp-=S;L9A)EDhC%RHiNa>SA+a*!Ftk+BDU$Ekh(nfQW5vA5ujD=@~?8Q*_nV z)fU<DC1ONfTBOD7$(rA*!5wqiSoch}V`*rmZ7#yQQ+>3l~8qAY&$YQp#-wH#XpS zZDlVZpx8!)wkkkdb#NVYOn8rOh9Nt1+_oZKn+j|j65ZB3TW-f|>o!TN$US8m*9twk z8AP=SfwzJHacy!ViM?K7-|ggj*MDtnu8rcg^*?5Gh4UfMtS>mawv*$vov*8OB|-oN zfgy5jC>{He5o~S@+a9*AO+|ish4yue3VvW

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/qgis/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py new file mode 100644 index 0000000..b12f56a --- /dev/null +++ b/tests/qgis/test_sampler_decimator.py @@ -0,0 +1,93 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis,QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestSamplerDecimator(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.structure_file = self.input_dir / "structure_clip.shp" + self.dtm_file = self.input_dir / "dtm_rp.tif" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + self.assertTrue(self.structure_file.exists(), f"structure not found: {self.structure_file}") + self.assertTrue(self.dtm_file.exists(), f"dtm not found: {self.dtm_file}") + + def test_decimator_1_with_structure(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + structure_layer = QgsVectorLayer(str(self.structure_file), "structure", "ogr") + dtm_layer = QgsRasterLayer(str(self.dtm_file), "dtm") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") + + QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'SPATIAL_DATA': structure_layer, + 'SAMPLER_TYPE': 0, + 'DECIMATION': 1, + 'SPACING': 200.0, + 'SAMPLED_CONTACTS': 'memory:decimated_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + + try: + QgsMessageLog.logMessage("Starting decimator sampler algorithm...", "TestDecimator", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestDecimator", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Decimator sampler test completed successfully!", "TestDecimator", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Decimator sampler test error: {str(e)}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestDecimator", Qgis.Critical) + + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestDecimator", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py new file mode 100644 index 0000000..a542b85 --- /dev/null +++ b/tests/qgis/test_sampler_spacing.py @@ -0,0 +1,80 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestSamplerSpacing(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + def test_spacing_50_with_geology(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- geology layer", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Spacing", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spacing: 50", "TestSampler", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': None, + 'GEOLOGY': None, + 'SPATIAL_DATA': geology_layer, + 'SAMPLER_TYPE': 1, + 'DECIMATION': 1, + 'SPACING': 50.0, + 'SAMPLED_CONTACTS': 'memory:sampled_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting spacing sampler algorithm...", "TestSampler", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestSampler", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Spacing sampler test completed successfully!", "TestSampler", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Spacing sampler test error: {str(e)}", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestSampler", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestSampler", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() From 26399b2307a225aca2cb5d3fd4c1b392b0e699ac Mon Sep 17 00:00:00 2001 From: noellehmcheng <143368485+noellehmcheng@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:15:11 +0800 Subject: [PATCH 097/135] Processing/processing tools basal contacts test (#21) * input files * fix: change stratigraphic column QgsProcessingParameterFeatureSink description parameter to string * feat add formation column mapping and validation for strat column * tester.yml * input file * test_basal_contacts * fix import in test_basal_contacts.py * fix strati_column in test_basal_contacts * removed duplicated input files --- .../algorithms/extract_basal_contacts.py | 31 ++++- tests/qgis/test_basal_contacts.py | 116 ++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 tests/qgis/test_basal_contacts.py diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..c78653e 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -78,6 +78,17 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterField( + 'FORMATION_FIELD', + 'Formation Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='formation', + optional=True + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -127,7 +138,14 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") + + if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): + feedback.pushInfo("no units to ignore specified") + # if strati_column and strati_column.strip(): # strati_column = [unit.strip() for unit in strati_column.split(',')] # Save stratigraphic column settings @@ -138,10 +156,15 @@ def processAlgorithm( ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) - geology = geology[mask].reset_index(drop=True) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + feedback.pushInfo(f"filtered by formation field: {formation_field}") + else: + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -154,7 +177,7 @@ def processAlgorithm( feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, - contact_extractor.basal_contacts, + basal_contacts, parameters=parameters, context=context, output_key=self.OUTPUT, diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py new file mode 100644 index 0000000..19c56dc --- /dev/null +++ b/tests/qgis/test_basal_contacts.py @@ -0,0 +1,116 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestBasalContacts(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.faults_file = self.input_dir / "faults_clip.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + if not self.faults_file.exists(): + QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) + + def test_basal_contacts_extraction(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + faults_layer = None + if self.faults_file.exists(): + faults_layer = QgsVectorLayer(str(self.faults_file), "faults", "ogr") + self.assertTrue(faults_layer.isValid(), "faults layer should be valid") + self.assertGreater(faults_layer.featureCount(), 0, "faults layer should have features") + QgsMessageLog.logMessage(f"faults layer: {faults_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + strati_column = [ + "Turee Creek Group", + "Boolgeeda Iron Formation", + "Woongarra Rhyolite", + "Weeli Wolli Formation", + "Brockman Iron Formation", + "Mount McRae Shale and Mount Sylvia Formation", + "Wittenoom Formation", + "Marra Mamba Iron Formation", + "Jeerinah Formation", + "Bunjinah Formation", + "Pyradie Formation", + "Fortescue Group", + "Hardey Formation", + "Boongal Formation", + "Mount Roe Basalt", + "Rocklea Inlier greenstones", + "Rocklea Inlier metagranitic unit" + ] + + algorithm = BasalContactsAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'UNIT_NAME_FIELD': 'unitname', + 'FORMATION_FIELD': 'formation', + 'FAULTS': faults_layer, + 'STRATIGRAPHIC_COLUMN': strati_column, + 'IGNORE_UNITS': [], + 'BASAL_CONTACTS': 'memory:basal_contacts' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting basal contacts algorithm...", "TestBasalContacts", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestBasalContacts", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + + basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) + self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") + self.assertTrue(basal_contacts_layer.isValid(), "basal contacts layer should be valid") + self.assertGreater(basal_contacts_layer.featureCount(), 0, "basal contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", + "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Basal contacts test error: {str(e)}", "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestBasalContacts", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestBasalContacts", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestBasalContacts", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 72f59498754e2febae672f2f1bf819f38a94a62c Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:59:25 +0930 Subject: [PATCH 098/135] fix: fix syntac error --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index a76218a..2cdc5a3 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -37,7 +37,7 @@ QgsPointXY, QgsVectorLayer, QgsWkbTypes, - QgsCoordinateReferenceSystem + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsCoordinateReferenceSystem From 30c7e2047c63dd5f4648fb8771c3d8bee68f47cb Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:03:23 +0930 Subject: [PATCH 099/135] fix: remove redundant parameters in SamplerAlgorithm --- m2l/processing/algorithms/sampler.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 2cdc5a3..3f4c5cf 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -80,25 +80,20 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterEnum( QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", ["Decimator", "Spacing"], defaultValue=0 - ["Decimator", "Spacing"], - defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterRasterLayer( QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], optional=True, - optional=True, ) ) @@ -126,8 +121,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "DECIMATION", QgsProcessingParameterNumber.Integer, defaultValue=1, - QgsProcessingParameterNumber.Integer, - defaultValue=1, optional=True, ) ) @@ -138,8 +131,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "SPACING", QgsProcessingParameterNumber.Double, defaultValue=200.0, - QgsProcessingParameterNumber.Double, - defaultValue=200.0, optional=True, ) ) @@ -148,7 +139,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSink( self.OUTPUT, "Sampled Points", - "Sampled Points", ) ) @@ -180,7 +170,6 @@ def processAlgorithm( spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - if sampler_type == "Decimator": if sampler_type == "Decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) @@ -188,7 +177,7 @@ def processAlgorithm( sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) samples = sampler.sample(spatial_data_gdf) - if sampler_type == "Spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) From 5dbcfca11be21d3718a9f8ae62a5ef95552876eb Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:04:39 +0930 Subject: [PATCH 100/135] fix: remove redundant imports --- m2l/processing/algorithms/sampler.py | 38 ---------------------------- 1 file changed, 38 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3f4c5cf..28e5184 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -26,20 +26,14 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterRasterLayer, QgsProcessingParameterEnum, - QgsProcessingParameterRasterLayer, - QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsFields, - QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, QgsVectorLayer, QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsVectorLayer, - QgsWkbTypes, QgsCoordinateReferenceSystem ) # Internal imports @@ -167,17 +161,12 @@ def processAlgorithm( geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) samples = sampler.sample(spatial_data_gdf) - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) - samples = sampler.sample(spatial_data_gdf) - if sampler_type == "Spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) @@ -203,33 +192,6 @@ def processAlgorithm( crs ) - if samples is not None and not samples.empty: - for _index, row in samples.iterrows(): - feature = QgsFeature(fields) - - # decimator has z values - if 'Z' in samples.columns and pd.notna(row.get('Z')): - wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" - feature.setGeometry(QgsGeometry.fromWkt(wkt)) - else: - #spacing has no z values - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) - - feature.setAttributes([ - str(row.get('ID', '')), - float(row.get('X', 0)), - float(row.get('Y', 0)), - float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, - str(row.get('featureId', '')) - ]) - - sink.addFeature(feature) - - fields, - QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, - crs - ) - if samples is not None and not samples.empty: for _index, row in samples.iterrows(): feature = QgsFeature(fields) From 90663cf2ce192a087d56a79488a6abd62bd6dc5f Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:11:23 +0930 Subject: [PATCH 101/135] fix: remove unused QgsSettings --- m2l/processing/algorithms/sorter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 20d3cd7..f80cfe0 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -118,8 +118,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue="Observation projections", # Age-based is safest default ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") self.addParameter( QgsProcessingParameterFeatureSource( From 4ff75469fea8ce5d46db503e2f71e7ef29113ea5 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:59:22 +0930 Subject: [PATCH 102/135] fix: updated tearDownClass --- tests/qgis/test_sampler_decimator.py | 2 +- tests/qgis/test_sampler_spacing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..13e7228 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -87,7 +87,7 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + # QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..fc3a523 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,7 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + # QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() From 14db8d05abc7bb12983083001f5e52ce0e87bda1 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:00:52 +0930 Subject: [PATCH 103/135] fix: add pass statement --- tests/qgis/test_sampler_decimator.py | 1 + tests/qgis/test_sampler_spacing.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index 13e7228..15782bc 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -88,6 +88,7 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): # QgsApplication.processingRegistry().removeProvider(cls.provider) + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index fc3a523..4016d5f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -75,6 +75,6 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): # QgsApplication.processingRegistry().removeProvider(cls.provider) - + pass if __name__ == '__main__': unittest.main() From ec993bb982116e28936bfa9b2f184475c182b952 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 22 Sep 2025 13:22:54 +0800 Subject: [PATCH 104/135] update sampler tests --- tests/qgis/test_sampler_decimator.py | 11 ++++++++++- tests/qgis/test_sampler_spacing.py | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -34,22 +34,27 @@ def test_decimator_1_with_structure(self): self.assertTrue(geology_layer.isValid(), "geology layer should be valid") self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) algorithm = SamplerAlgorithm() algorithm.initAlgorithm() parameters = { + 'DTM': dtm_layer, 'GEOLOGY': geology_layer, 'SPATIAL_DATA': structure_layer, 'SAMPLER_TYPE': 0, @@ -87,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From 83c170376803fef39e6ba36dfc7f15c255b5a44d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 22 Sep 2025 13:24:13 +0800 Subject: [PATCH 105/135] update sampler --- m2l/processing/algorithms/sampler.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 28e5184..726d44a 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QVariant +from qgis.PyQt.QtCore import QMetaType from osgeo import gdal import pandas as pd @@ -154,8 +154,11 @@ def processAlgorithm( if spatial_data is None: raise QgsProcessingException("Spatial data is required") - if sampler_type == "Decimator" and geology is None: - raise QgsProcessingException("Geology is required") + if sampler_type == "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) @@ -173,11 +176,11 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QVariant.String)) - fields.append(QgsField("X", QVariant.Double)) - fields.append(QgsField("Y", QVariant.Double)) - fields.append(QgsField("Z", QVariant.Double)) - fields.append(QgsField("featureId", QVariant.String)) + fields.append(QgsField("ID", QMetaType.Type.QString)) + fields.append(QgsField("X", QMetaType.Type.Float)) + fields.append(QgsField("Y", QMetaType.Type.Float)) + fields.append(QgsField("Z", QMetaType.Type.Float)) + fields.append(QgsField("featureId", QMetaType.Type.QString)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: From 5205cf562e1e42d4b8f3b1ff607175f21d30c901 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:26:48 +0800 Subject: [PATCH 106/135] update strati column and bounding box --- .../algorithms/thickness_calculator.py | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index e10604d..6172a94 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -57,6 +57,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' OUTPUT = "THICKNESS" @@ -97,17 +98,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - bbox_settings = QgsSettings() - last_bbox = bbox_settings.value("m2l/bounding_box", "") - self.addParameter( - QgsProcessingParameterMatrix( - self.INPUT_BOUNDING_BOX, - description="Bounding Box", - headers=['minx','miny','maxx','maxy'], - numberRows=1, - defaultValue=last_bbox - ) - ) self.addParameter( QgsProcessingParameterNumber( self.INPUT_MAX_LINE_LENGTH, @@ -141,6 +131,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue='Formation' ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + 'STRATIGRAPHIC_COLUMN_LAYER', + 'Stratigraphic Column Layer (from sorter)', + [QgsProcessing.TypeVector], + optional=True + ) + ) strati_settings = QgsSettings() last_strati_column = strati_settings.value("m2l/strati_column", "") @@ -150,7 +149,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=last_strati_column + defaultValue=last_strati_column, + optional=True ) ) self.addParameter( @@ -207,17 +207,39 @@ def processAlgorithm( max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) - bbox_settings = QgsSettings() - bbox_settings.setValue("m2l/bounding_box", bounding_box) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) + geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + extent = geology_layer.extent() + bounding_box = { + 'minx': extent.xMinimum(), + 'miny': extent.yMinimum(), + 'maxx': extent.xMaximum(), + 'maxy': extent.yMaximum() + } + stratigraphic_column_layer = self.parameterAsVectorLayer(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) + stratigraphic_order = None + if stratigraphic_column_layer is not None and stratigraphic_column_layer.isValid(): + stratigraphic_order=[] + stratigraphic_order_df = qgsLayerToDataFrame(stratigraphic_column_layer) + stratigraphic_order_df = stratigraphic_order_df.sort_values('order') + for _, row in stratigraphic_order_df.iterrows(): + stratigraphic_order.append(row['unit_name']) + + else: + matrix_stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + if matrix_stratigraphic_order: + stratigraphic_order = [row[0] for row in matrix_stratigraphic_order if row and len(row) > 0] + else: + raise QgsProcessingException("Stratigraphic column layer is required") + if stratigraphic_order: + matrix = [[unit] for unit in stratigraphic_order] + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', matrix) # convert layers to dataframe or geodataframe units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) @@ -249,8 +271,6 @@ def processAlgorithm( structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) dtm_data = qgsRasterToGdalDataset(dtm_data) - bounding_box = matrixToDict(bounding_box) - feedback.pushInfo("Calculating unit thicknesses...") if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, From a259caef14bfb67736a65de6e2581ccd33803a7c Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:27:29 +0800 Subject: [PATCH 107/135] fix sample contacts data type --- m2l/processing/algorithms/thickness_calculator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 6172a94..afba1e8 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -270,6 +270,9 @@ def processAlgorithm( if rename_map: structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) + sampled_contacts['X'] = sampled_contacts['X'].astype(float) + sampled_contacts['Y'] = sampled_contacts['Y'].astype(float) + sampled_contacts['Z'] = sampled_contacts['Z'].astype(float) dtm_data = qgsRasterToGdalDataset(dtm_data) if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( From 00d4bb82b4b6a9dfdd21264cdafc75dc8d71990b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:47:40 +0800 Subject: [PATCH 108/135] add orientation type for structure data --- m2l/processing/algorithms/thickness_calculator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index afba1e8..0210f68 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -55,6 +55,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_THICKNESS_ORIENTATION_TYPE = 'THICKNESS_ORIENTATION_TYPE' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' @@ -167,6 +168,14 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterEnum( + 'THICKNESS_ORIENTATION_TYPE', + 'Thickness Orientation Type', + options=['Dip Direction', 'Strike'], + defaultValue=0 # Default to Dip Direction + ) + ) self.addParameter( QgsProcessingParameterField( self.INPUT_DIPDIR_FIELD, @@ -208,6 +217,8 @@ def processAlgorithm( basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + thickness_orientation_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_ORIENTATION_TYPE, context) + is_strike = (thickness_orientation_type == 1) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) @@ -269,6 +280,7 @@ def processAlgorithm( ) if rename_map: structure_data = structure_data.rename(columns=rename_map) + sampled_contacts = qgsLayerToDataFrame(sampled_contacts) sampled_contacts['X'] = sampled_contacts['X'].astype(float) sampled_contacts['Y'] = sampled_contacts['Y'].astype(float) @@ -278,6 +290,7 @@ def processAlgorithm( thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, + is_strike=is_strike ) thicknesses = thickness_calculator.compute( units, @@ -293,6 +306,7 @@ def processAlgorithm( dtm_data=dtm_data, bounding_box=bounding_box, max_line_length=max_line_length, + is_strike=is_strike ) thicknesses =thickness_calculator.compute( units, From d2d292545e25acd7893c11764b01491699aae3c4 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:56:04 +0930 Subject: [PATCH 109/135] Processing/thickness calculator (#23) * feat: thickness calculator tool * feat: raster and dataframe handling * fix: remove unused QgsSettings * fix: updated tearDownClass * fix: add pass statement * update sampler tests * update sampler --------- Co-authored-by: Noelle Cheng --- m2l/main/vectorLayerWrapper.py | 355 +++++++++++++++--- m2l/processing/algorithms/sampler.py | 19 +- m2l/processing/algorithms/sorter.py | 2 - .../algorithms/thickness_calculator.py | 151 ++++++-- tests/qgis/test_sampler_decimator.py | 11 +- tests/qgis/test_sampler_spacing.py | 6 +- 6 files changed, 438 insertions(+), 106 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 5bf2da0..4476e6a 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -1,5 +1,5 @@ # PyQGIS / PyQt imports - +from osgeo import gdal from qgis.core import ( QgsRaster, QgsFields, @@ -12,17 +12,79 @@ QgsProcessingException, QgsPoint, QgsPointXY, + QgsProject, + QgsCoordinateTransform, + QgsRasterLayer ) -from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant - +from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis import processing from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np - +import tempfile +import os + +def qgsRasterToGdalDataset(rlayer: QgsRasterLayer): + """ + Convert a QgsRasterLayer to an osgeo.gdal.Dataset (read-only). + If the raster is non-file-based (e.g. WMS/WCS/virtual), we create a temp GeoTIFF via gdal:translate. + Returns a gdal.Dataset or None. + """ + if rlayer is None or not rlayer.isValid(): + return None + + # Try direct open on file-backed layers + candidates = [] + try: + candidates.append(rlayer.source()) + except Exception: + pass + try: + if rlayer.dataProvider(): + candidates.append(rlayer.dataProvider().dataSourceUri()) + except Exception: + pass + tried = set() + for uri in candidates: + if not uri: + continue + if uri in tried: + continue + tried.add(uri) + + # Strip QGIS pipe options: "path.tif|layername=..." โ†’ "path.tif" + base_uri = uri.split("|")[0] + + # Some providers store โ€œSUBDATASET:โ€ URIs; gdal.OpenEx can usually handle them directly. + ds = gdal.OpenEx(base_uri, gdal.OF_RASTER | gdal.OF_READONLY) + if ds is not None: + return ds + + # If weโ€™re here, itโ€™s likely non-file-backed. Export to a temp GeoTIFF. + tmpdir = tempfile.gettempdir() + tmp_path = os.path.join(tmpdir, f"m2l_dtm_{rlayer.id()}.tif") + + # Use GDAL Translate via QGIS processing (avoids CRS pitfalls) + processing.run( + "gdal:translate", + { + "INPUT": rlayer, # QGIS accepts the layer object here + "TARGET_CRS": None, + "NODATA": None, + "COPY_SUBDATASETS": False, + "OPTIONS": "", + "EXTRA": "", + "DATA_TYPE": 0, # Use input data type + "OUTPUT": tmp_path, + } + ) + + ds = gdal.OpenEx(tmp_path, gdal.OF_RASTER | gdal.OF_READONLY) + return ds def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: if layer is None: @@ -42,63 +104,147 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: data[f.name()].append(str(feature[f.name()])) else: data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - -def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: - """Convert a vector layer to a pandas DataFrame - samples the geometry using either points or the vertices of the lines - - :param layer: _description_ - :type layer: _type_ - :param dtm: Digital Terrain Model to evaluate Z values - :type dtm: _type_ or None - :return: the dataframe object - :rtype: pd.DataFrame + return gpd.GeoDataFrame(data, crs=layer.sourceCrs().authid()) + +def qgsLayerToDataFrame(src, dtm=None) -> pd.DataFrame: """ - if layer is None: + Convert a vector layer or processing feature source to a pandas DataFrame. + Samples geometry using points or vertices of lines/polygons. + Optionally samples Z from a DTM raster. + + :param src: QgsVectorLayer or QgsProcessingFeatureSource + :param dtm: QgsRasterLayer or None + :return: pd.DataFrame with columns: X, Y, Z, and all layer fields + """ + + if src is None: return None - fields = layer.fields() - data = {} - data['X'] = [] - data['Y'] = [] - data['Z'] = [] - - for field in fields: - data[field.name()] = [] - for feature in layer.getFeatures(): - geom = feature.geometry() - points = [] - if geom.isMultipart(): - if geom.type() == QgsWkbTypes.PointGeometry: - points = geom.asMultiPoint() - elif geom.type() == QgsWkbTypes.LineGeometry: + + # --- Resolve fields and source CRS (works for both layer and feature source) --- + fields = src.fields() if hasattr(src, "fields") else None + if fields is None: + # Fallback: take fields from first feature if needed + feat_iter = src.getFeatures() + try: + first = next(feat_iter) + except StopIteration: + return pd.DataFrame(columns=["X", "Y", "Z"]) + fields = first.fields() + # Rewind iterator by building a new one + feats = [first] + list(src.getFeatures()) + else: + feats = src.getFeatures() + + # Get source CRS + if hasattr(src, "crs"): + src_crs = src.crs() + elif hasattr(src, "sourceCrs"): + src_crs = src.sourceCrs() + else: + src_crs = None + + # --- Prepare optional transform to DTM CRS for sampling --- + to_dtm = None + if dtm is not None and src_crs is not None and dtm.crs().isValid() and src_crs.isValid(): + if src_crs != dtm.crs(): + to_dtm = QgsCoordinateTransform(src_crs, dtm.crs(), QgsProject.instance()) + + # --- Helper: sample Z from DTM (returns float or -9999) --- + def sample_dtm_xy(x, y): + if dtm is None: + return 0.0 + # Transform coordinate if needed + if to_dtm is not None: + try: + from qgis.core import QgsPointXY + x, y = to_dtm.transform(QgsPointXY(x, y)) + except Exception: + return -9999.0 + from qgis.core import QgsPointXY + ident = dtm.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue) + if not ident.isValid(): + return -9999.0 + res = ident.results() + if not res: + return -9999.0 + # take first band value (band keys are 1-based) + try: + # Prefer band 1 if present + return float(res.get(1, next(iter(res.values())))) + except Exception: + return -9999.0 + + # --- Geometry -> list of vertices (QgsPoint or QgsPointXY) --- + def vertices_from_geometry(geom): + if geom is None or geom.isEmpty(): + return [] + gtype = QgsWkbTypes.geometryType(geom.wkbType()) + is_multi = QgsWkbTypes.isMultiType(geom.wkbType()) + + if gtype == QgsWkbTypes.PointGeometry: + if is_multi: + return list(geom.asMultiPoint()) + else: + return [geom.asPoint()] + + elif gtype == QgsWkbTypes.LineGeometry: + pts = [] + if is_multi: for line in geom.asMultiPolyline(): - points.extend(line) - # points = geom.asMultiPolyline()[0] - else: - if geom.type() == QgsWkbTypes.PointGeometry: - points = [geom.asPoint()] - elif geom.type() == QgsWkbTypes.LineGeometry: - points = geom.asPolyline() - - for p in points: - data['X'].append(p.x()) - data['Y'].append(p.y()) - if dtm is not None: - # Replace with your coordinates - - # Extract the value at the point - z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) - if z_value.isValid(): - z_value = z_value.results()[1] - else: - z_value = -9999 - data['Z'].append(z_value) - if dtm is None: - data['Z'].append(0) - for field in fields: - data[field.name()].append(feature[field.name()]) - return pd.DataFrame(data) + pts.extend(line) + else: + pts.extend(geom.asPolyline()) + return pts + + elif gtype == QgsWkbTypes.PolygonGeometry: + pts = [] + if is_multi: + mpoly = geom.asMultiPolygon() + for poly in mpoly: + for ring in poly: # exterior + interior rings + pts.extend(ring) + else: + poly = geom.asPolygon() + for ring in poly: + pts.extend(ring) + return pts + + # Other geometry types not handled + return [] + + # --- Build rows safely (one dict per sampled point) --- + rows = [] + field_names = [f.name() for f in fields] + + for f in feats: + geom = f.geometry() + pts = vertices_from_geometry(geom) + + if not pts: + # If you want to keep attribute rows even when no vertices: uncomment below + # row = {name: f[name] for name in field_names} + # row.update({"X": None, "Y": None, "Z": None}) + # rows.append(row) + continue + + # Cache attributes once per feature and reuse for each sampled point + base_attrs = {name: f[name] for name in field_names} + + for p in pts: + # QgsPoint vs QgsPointXY both have x()/y() + x, y = float(p.x()), float(p.y()) + z = sample_dtm_xy(x, y) + + row = {"X": x, "Y": y, "Z": z} + row.update(base_attrs) + rows.append(row) + + # Create DataFrame; if empty, return with expected columns + if not rows: + cols = ["X", "Y", "Z"] + field_names + return pd.DataFrame(columns=cols) + + return pd.DataFrame.from_records(rows) def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ @@ -455,6 +601,98 @@ def dataframeToQgsLayer( feedback.setProgress(100) return sink, sink_id +def matrixToDict(matrix, headers=("minx", "miny", "maxx", "maxy")) -> dict: + """ + Convert a QgsProcessingParameterMatrix value to a dict with float values. + Accepts: [[minx,miny,maxx,maxy]] or [minx,miny,maxx,maxy]. + Raises a clear error if an enum index (int) was passed by mistake. + """ + # Guard: common mistake โ†’ using parameterAsEnum + if isinstance(matrix, int): + raise QgsProcessingException( + "Bounding Box was read with parameterAsEnum (got an int). " + "Use parameterAsMatrix for QgsProcessingParameterMatrix." + ) + + if matrix is None: + raise QgsProcessingException("Bounding box matrix is None.") + + # Allow empty string from settings/defaults + if isinstance(matrix, str) and not matrix.strip(): + raise QgsProcessingException("Bounding box matrix is empty.") + + # Accept single-row matrix or flat list + if isinstance(matrix, (list, tuple)): + if matrix and isinstance(matrix[0], (list, tuple)): + row = matrix[0] + else: + row = matrix + else: + # last resort: try comma-separated string "minx,miny,maxx,maxy" + if isinstance(matrix, str) and "," in matrix: + row = [v.strip() for v in matrix.split(",")] + else: + raise QgsProcessingException(f"Unrecognized bounding box value: {type(matrix)}") + + if len(row) < 4: + raise QgsProcessingException(f"Bounding box needs 4 numbers, got {len(row)}: {row}") + + def _to_float(v): + if isinstance(v, str): + v = v.strip() + return float(v) + + vals = list(map(_to_float, row[:4])) + bbox = dict(zip(headers, vals)) + + if not (bbox["minx"] < bbox["maxx"] and bbox["miny"] < bbox["maxy"]): + raise QgsProcessingException(f"Invalid bounding box: {bbox} (expect minx None: defaultValue="Observation projections", # Age-based is safest default ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") self.addParameter( QgsProcessingParameterFeatureSource( diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 71f72eb..e10604d 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -25,10 +25,20 @@ QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsProcessingParameterField, - QgsProcessingParameterMatrix + QgsProcessingParameterMatrix, + QgsSettings, + QgsProcessingParameterRasterLayer, ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer +from ...main.vectorLayerWrapper import ( + qgsLayerToGeoDataFrame, + GeoDataFrameToQgsLayer, + qgsLayerToDataFrame, + dataframeToQgsLayer, + qgsRasterToGdalDataset, + matrixToDict, + dataframeToQgsTable + ) from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -39,11 +49,13 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' - INPUT_UNITS = 'UNITS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' + INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' OUTPUT = "THICKNESS" @@ -73,21 +85,27 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "Thickness Calculator Type", options=['InterpolatedStructure','StructuralPoint'], allowMultiple=False, + defaultValue='InterpolatedStructure' ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, - "DTM", + "DTM (InterpolatedStructure)", [QgsProcessing.TypeRaster], + optional=True, ) ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") self.addParameter( - QgsProcessingParameterEnum( + QgsProcessingParameterMatrix( self.INPUT_BOUNDING_BOX, - "Bounding Box", - options=['minx','miny','maxx','maxy'], - allowMultiple=True, + description="Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox ) ) self.addParameter( @@ -98,18 +116,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=1000 ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_UNITS, - "Units", - [QgsProcessing.TypeVectorLine], - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_BASAL_CONTACTS, "Basal Contacts", [QgsProcessing.TypeVectorLine], + defaultValue='Basal Contacts', ) ) self.addParameter( @@ -119,29 +131,60 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field e.g. Formation', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='Formation' + ) + ) + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( QgsProcessingParameterMatrix( name=self.INPUT_STRATI_COLUMN, description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=[] + defaultValue=last_strati_column ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_SAMPLED_CONTACTS, - "SAMPLED_CONTACTS", + "Sampled Contacts", [QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_STRUCTURE_DATA, - "STRUCTURE_DATA", + "Orientation Data", [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIPDIR_FIELD, + "Dip Direction Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIPDIR' + ) + ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIP_FIELD, + "Dip Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIP' + ) + ) self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -157,32 +200,63 @@ def processAlgorithm( ) -> dict[str, Any]: feedback.pushInfo("Initialising Thickness Calculation Algorithm...") - thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) - dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) - max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) - units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] + dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) + structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", bounding_box) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) # convert layers to dataframe or geodataframe + units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) - units = qgsLayerToDataFrame(units) basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) structure_data = qgsLayerToDataFrame(structure_data) + rename_map = {} + missing_fields = [] + if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: + geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) + units = units.rename(columns={'UNITNAME': 'name'}) + if structure_data is not None: + if structure_dipdir_field: + if structure_dipdir_field in structure_data.columns: + rename_map[structure_dipdir_field] = 'DIPDIR' + else: + missing_fields.append(structure_dipdir_field) + if structure_dip_field: + if structure_dip_field in structure_data.columns: + rename_map[structure_dip_field] = 'DIP' + else: + missing_fields.append(structure_dip_field) + if missing_fields: + raise QgsProcessingException( + f"Orientation data missing required field(s): {', '.join(missing_fields)}" + ) + if rename_map: + structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) - + dtm_data = qgsRasterToGdalDataset(dtm_data) + bounding_box = matrixToDict(bounding_box) feedback.pushInfo("Calculating unit thicknesses...") - if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, ) - thickness_calculator.compute( + thicknesses = thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -197,7 +271,7 @@ def processAlgorithm( bounding_box=bounding_box, max_line_length=max_line_length, ) - thickness_calculator.compute( + thicknesses =thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -206,17 +280,22 @@ def processAlgorithm( sampled_contacts ) - #TODO: convert thicknesses dataframe to qgs layer - thicknesses = dataframeToQgsLayer( - self, - # contact_extractor.basal_contacts, + thicknesses = thicknesses[ + ["name","ThicknessMean","ThicknessMedian", "ThicknessStdDev"] + ].copy() + + feedback.pushInfo("Exporting Thickness Table...") + thicknesses = dataframeToQgsTable( + self, + thicknesses, parameters=parameters, context=context, feedback=feedback, - ) - + param_name=self.OUTPUT + ) + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # ThicknessCalculatorAlgorithm() diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -34,22 +34,27 @@ def test_decimator_1_with_structure(self): self.assertTrue(geology_layer.isValid(), "geology layer should be valid") self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) algorithm = SamplerAlgorithm() algorithm.initAlgorithm() parameters = { + 'DTM': dtm_layer, 'GEOLOGY': geology_layer, 'SPATIAL_DATA': structure_layer, 'SAMPLER_TYPE': 0, @@ -87,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From 17cd45b4efdca18df0da25048b43b64ff1ad8d64 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:46:30 +0930 Subject: [PATCH 110/135] feat: add UserDefinedStratigraphyAlgorithm to provider --- m2l/processing/provider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 318e944..9616d9b 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -19,6 +19,7 @@ from .algorithms import ( BasalContactsAlgorithm, StratigraphySorterAlgorithm, + UserDefinedStratigraphyAlgorithm, ThicknessCalculatorAlgorithm, SamplerAlgorithm ) @@ -35,6 +36,7 @@ def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(UserDefinedStratigraphyAlgorithm()) self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) From 439588045b7e7e3fd62769bb8b4d8102d69108a2 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:46:46 +0930 Subject: [PATCH 111/135] feat: import UserDefinedStratigraphyAlgorithm --- m2l/processing/algorithms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f0aaedb..08d76a0 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,4 +1,5 @@ from .extract_basal_contacts import BasalContactsAlgorithm from .sorter import StratigraphySorterAlgorithm +from .user_defined_sorter import UserDefinedStratigraphyAlgorithm from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm From 2d4eff65b83c200ccd7cb8ebf9a345086a9e524f Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:47:09 +0930 Subject: [PATCH 112/135] refactor: update BasalContactsAlgorithm --- .../algorithms/extract_basal_contacts.py | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( From 09034f1d47fdd596eced678e3c4e0632eff75a31 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:47:21 +0930 Subject: [PATCH 113/135] refactor: update SamplerAlgorithm --- m2l/processing/algorithms/sampler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 726d44a..c2b0e35 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QMetaType, QVariant from osgeo import gdal import pandas as pd @@ -59,15 +59,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Sampler" + return "Spacing-Decimator Samplers" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Samplers" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Samplers" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -176,11 +176,11 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QMetaType.Type.QString)) - fields.append(QgsField("X", QMetaType.Type.Float)) - fields.append(QgsField("Y", QMetaType.Type.Float)) - fields.append(QgsField("Z", QMetaType.Type.Float)) - fields.append(QgsField("featureId", QMetaType.Type.QString)) + fields.append(QgsField("ID", QVariant.String)) + fields.append(QgsField("X", QVariant.Double)) + fields.append(QgsField("Y", QVariant.Double)) + fields.append(QgsField("Z", QVariant.Double)) + fields.append(QgsField("featureId", QVariant.String)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: From 932b95cf2654b02a44adc383960ac027253747a8 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:04 +0930 Subject: [PATCH 114/135] refactor: update sorter --- m2l/processing/algorithms/sorter.py | 92 +++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index f80cfe0..e3772f9 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -24,6 +24,7 @@ QgsProcessingParameterField, QgsProcessingParameterRasterLayer, QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsSettings @@ -73,13 +74,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "Loop3d: Stratigraphic sorter" + return "Automatic Stratigraphic Column" def group(self) -> str: - return "Loop3d" + return "Stratigraphy" def groupId(self) -> str: - return "Loop3d" + return "Stratigraphy_Column" def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) @@ -341,6 +342,89 @@ def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + """ + Creates a one-column โ€˜stratigraphic columnโ€™ table ordered + by the selected map2loop sorter. + """ + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + # ---------------------------------------------------------- + # Metadata + # ---------------------------------------------------------- + def name(self) -> str: + return "loop_sorter" + + def displayName(self) -> str: + return "Stratigraphy: User-Defined Stratigraphic Column" + + def group(self) -> str: + return "Stratigraphy" + + def groupId(self) -> str: + return "Stratigraphy_Column" + + # ---------------------------------------------------------- + # Parameters + # ---------------------------------------------------------- + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + # ---------------------------------------------------------- + # Core + # ---------------------------------------------------------- + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + + # 4 โ–บ write an in-memory table with the result + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) + sink_fields.append(QgsField("unit_name", QVariant.String)) + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + sink_fields, + QgsWkbTypes.NoGeometry, + crs, + ) + + + return {self.OUTPUT: dest_id} + + # ---------------------------------------------------------- + def createInstance(self) -> QgsProcessingAlgorithm: + return __class__() + # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- @@ -415,4 +499,4 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee else: relationships_df = pd.DataFrame() - return units_df, relationships_df, contacts_df \ No newline at end of file + return units_df, relationships_df, contacts_df From 476eadba13a1a8c03cb97e3a98696a8f745e037b Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:18 +0930 Subject: [PATCH 115/135] refactor: update ThicknessCalculatorAlgorithm --- .../algorithms/thickness_calculator.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index e10604d..53d7624 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -28,6 +28,7 @@ QgsProcessingParameterMatrix, QgsSettings, QgsProcessingParameterRasterLayer, + QgsProcessingParameterMapLayer ) # Internal imports from ...main.vectorLayerWrapper import ( @@ -66,15 +67,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" + return "Thickness Calculator" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Thickness Calculators" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Thickness_Calculators" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -142,15 +143,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) self.addParameter( @@ -207,7 +205,7 @@ def processAlgorithm( max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + stratigraphic_order = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) @@ -216,8 +214,14 @@ def processAlgorithm( bbox_settings = QgsSettings() bbox_settings.setValue("m2l/bounding_box", bounding_box) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) + + if isinstance(stratigraphic_order, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + if stratigraphic_order is not None: + # extract unit names from stratigraphic_order + field_name = "unit_name" + stratigraphic_order = [f[field_name] for f in stratigraphic_order.getFeatures()] # convert layers to dataframe or geodataframe units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) From 9e890d57d758232b0375fe2055291eba7119db89 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:29 +0930 Subject: [PATCH 116/135] feat: implement UserDefinedStratigraphyAlgorithm --- .../algorithms/user_defined_sorter.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 m2l/processing/algorithms/user_defined_sorter.py diff --git a/m2l/processing/algorithms/user_defined_sorter.py b/m2l/processing/algorithms/user_defined_sorter.py new file mode 100644 index 0000000..23857a3 --- /dev/null +++ b/m2l/processing/algorithms/user_defined_sorter.py @@ -0,0 +1,112 @@ +from typing import Any, Optional +from osgeo import gdal +import numpy as np +import json + +from PyQt5.QtCore import QVariant +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsRasterLayer, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, + QgsVectorLayer, + QgsWkbTypes, + QgsSettings +) + + +from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsFeatureSink, QgsWkbTypes, + QgsCoordinateReferenceSystem, QgsProcessingAlgorithm, QgsProcessingContext, + QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterMatrix, + QgsSettings +) +from PyQt5.QtCore import QVariant +import numpy as np + +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + def name(self): return "loop_sorter_2" + def displayName(self): return "User-Defined Stratigraphic Column" + def group(self): return "Stratigraphy" + def groupId(self): return "Stratigraphy_Column" + + def initAlgorithm(self, config=None): + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + # 1) Read the matrix; it may be a list of lists (rows) or a flat list depending on input source. + matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + + # Normalize to a list of unit strings (one column: "Unit") + units = [] + for row in matrix: + if isinstance(row, (list, tuple)): + unit = row[0] if row else "" + else: + unit = row + unit = (unit or "").strip() + if unit: # skip empty rows to avoid writing "" into fields + units.append(unit) + + # 2) Build sequential order (1-based), cast to native int + order_vals = [int(i) for i in (np.arange(len(units)) + 1)] + + # 3) Prepare sink + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) # or QVariant.LongLong + sink_fields.append(QgsField("unit_name", QVariant.String)) + + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + sink, dest_id = self.parameterAsSink( + parameters, self.OUTPUT, context, + sink_fields, QgsWkbTypes.NoGeometry, crs + ) + + # 4) Insert features + for pos, unit_name in zip(order_vals, units): + f = QgsFeature(sink_fields) + # Ensure correct types: int for "order", str for "unit_name" + f.setAttributes([int(pos), str(unit_name)]) + ok = sink.addFeature(f, QgsFeatureSink.FastInsert) + if not ok: + feedback.reportError(f"Failed to add feature for unit '{unit_name}' (order={pos}).") + + return {self.OUTPUT: dest_id} + + def createInstance(self): + return __class__() From 3b61ed6f071af71091fea5d6a7b4024d4570b1b4 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:51:56 +0930 Subject: [PATCH 117/135] refactor: remove redundant algo --- m2l/processing/algorithms/sorter.py | 84 ----------------------------- 1 file changed, 84 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index e3772f9..55a179c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -341,90 +341,6 @@ def processAlgorithm( def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() - -class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): - """ - Creates a one-column โ€˜stratigraphic columnโ€™ table ordered - by the selected map2loop sorter. - """ - INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" - OUTPUT = "OUTPUT" - - # ---------------------------------------------------------- - # Metadata - # ---------------------------------------------------------- - def name(self) -> str: - return "loop_sorter" - - def displayName(self) -> str: - return "Stratigraphy: User-Defined Stratigraphic Column" - - def group(self) -> str: - return "Stratigraphy" - - def groupId(self) -> str: - return "Stratigraphy_Column" - - # ---------------------------------------------------------- - # Parameters - # ---------------------------------------------------------- - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") - self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Stratigraphic column", - ) - ) - - # ---------------------------------------------------------- - # Core - # ---------------------------------------------------------- - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") - - # 4 โ–บ write an in-memory table with the result - sink_fields = QgsFields() - sink_fields.append(QgsField("order", QVariant.Int)) - sink_fields.append(QgsField("unit_name", QVariant.String)) - crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() - - (sink, dest_id) = self.parameterAsSink( - parameters, - self.OUTPUT, - context, - sink_fields, - QgsWkbTypes.NoGeometry, - crs, - ) - - - return {self.OUTPUT: dest_id} - - # ---------------------------------------------------------- - def createInstance(self) -> QgsProcessingAlgorithm: - return __class__() - # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- From 248040d22a76436b0ee0a4ffd173296ca718e425 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:06:21 +0930 Subject: [PATCH 118/135] fix: use correct strati table --- tests/qgis/test_basal_contacts.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..f7565cf 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -61,7 +62,23 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] + strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") + # define the single field + provider = strati_table.dataProvider() + provider.addAttributes([QgsField("unit_name", vtype)]) + strati_table.updateFields() + vtype=QVariant.String + # add features (one row per value) + feats = [] + fields = strati_table.fields() + for val in strati_column: + f = QgsFeature(fields) + f.setAttributes([val]) + feats.append(f) + if feats: + provider.addFeatures(feats) + strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +87,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From 9cacb9bca59df8254304eb696fc0603c7ed5cad9 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:43:13 +0930 Subject: [PATCH 119/135] fix: correct variable assignment --- tests/qgis/test_basal_contacts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index f7565cf..e49ba85 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -65,9 +65,10 @@ def test_basal_contacts_extraction(self): strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") # define the single field provider = strati_table.dataProvider() + vtype=QVariant.String provider.addAttributes([QgsField("unit_name", vtype)]) strati_table.updateFields() - vtype=QVariant.String + # add features (one row per value) feats = [] fields = strati_table.fields() From f59f4f8d19fadc506de5a6e23de01dbba1813fe1 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 12:42:28 +0800 Subject: [PATCH 120/135] fix units in ThicknessCalculatorAlgorithm --- m2l/processing/algorithms/thickness_calculator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 0210f68..7bab4f4 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -10,6 +10,7 @@ """ # Python imports from typing import Any, Optional +import pandas as pd # QGIS imports from qgis import processing @@ -260,9 +261,8 @@ def processAlgorithm( missing_fields = [] if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) - units = units.rename(columns={unit_name_field: 'UNITNAME'}) - units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) - units = units.rename(columns={'UNITNAME': 'name'}) + units_unique = units.drop_duplicates(subset=[unit_name_field]).reset_index(drop=True) + units = pd.DataFrame({'name': units_unique[unit_name_field]}) if structure_data is not None: if structure_dipdir_field: if structure_dipdir_field in structure_data.columns: From b1bb23fd3c2e8e966ba8160cb3b07ac280c92d32 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 13:13:38 +0800 Subject: [PATCH 121/135] update extract_basal_contacts and test_basal_contacts --- .../algorithms/extract_basal_contacts.py | 50 ++++++++++--------- tests/qgis/test_basal_contacts.py | 22 +------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 4d3ba5f..5903d82 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,11 +22,9 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, - QgsVectorLayer, QgsSettings ) # Internal imports @@ -51,15 +49,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Basal Contacts" + return "Loop3d: Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Contact Extractors" + return "Loop3d" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Contact_Extractors" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -100,12 +98,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "Stratigraphic Order", - [QgsProcessing.TypeVector], - defaultValue='formation', + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column ) ) ignore_settings = QgsSettings() @@ -144,33 +145,34 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - - if isinstance(strati_column, QgsProcessingParameterMapLayer) : - raise QgsProcessingException("Invalid stratigraphic column layer") - - elif strati_column is not None: - # extract unit names from strati_column - field_name = "unit_name" - strati_order = [f[field_name] for f in strati_column.getFeatures()] + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") + + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if unit_name_field and unit_name_field in geology.columns: - mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") + feedback.pushInfo(f"filtered by formation field: {formation_field}") else: - feedback.pushInfo(f"no unit name field found: {unit_name_field}") + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -179,7 +181,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_order) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index e49ba85..c6f9256 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,7 +1,6 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField -from qgis.PyQt.QtCore import QVariant +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -62,24 +61,7 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] - strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") - # define the single field - provider = strati_table.dataProvider() - vtype=QVariant.String - provider.addAttributes([QgsField("unit_name", vtype)]) - strati_table.updateFields() - - # add features (one row per value) - feats = [] - fields = strati_table.fields() - for val in strati_column: - f = QgsFeature(fields) - f.setAttributes([val]) - feats.append(f) - if feats: - provider.addFeatures(feats) - strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -88,7 +70,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_table, + 'STRATIGRAPHIC_COLUMN': strati_column, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From ec389b37b34ea8bbc5490be52d4145e2152919e9 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 14:21:21 +0800 Subject: [PATCH 122/135] Revert "update extract_basal_contacts and test_basal_contacts" This reverts commit b1bb23fd3c2e8e966ba8160cb3b07ac280c92d32. --- .../algorithms/extract_basal_contacts.py | 50 +++++++++---------- tests/qgis/test_basal_contacts.py | 22 +++++++- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..e49ba85 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -61,7 +62,24 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] + strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") + # define the single field + provider = strati_table.dataProvider() + vtype=QVariant.String + provider.addAttributes([QgsField("unit_name", vtype)]) + strati_table.updateFields() + + # add features (one row per value) + feats = [] + fields = strati_table.fields() + for val in strati_column: + f = QgsFeature(fields) + f.setAttributes([val]) + feats.append(f) + if feats: + provider.addFeatures(feats) + strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +88,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From e6fa63c9b66d8363bf02496a61d80d9123fbde65 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 15:14:59 +0800 Subject: [PATCH 123/135] fix test_basal_contacts --- .../input/stratigraphic_column_testing.gpkg | Bin 0 -> 73728 bytes tests/qgis/test_basal_contacts.py | 41 ++---------------- 2 files changed, 3 insertions(+), 38 deletions(-) create mode 100644 tests/qgis/input/stratigraphic_column_testing.gpkg diff --git a/tests/qgis/input/stratigraphic_column_testing.gpkg b/tests/qgis/input/stratigraphic_column_testing.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..bb782ef5b2f52fba0d364aa506a7e5bdf571a79c GIT binary patch literal 73728 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 b00bZa0SG_<0uVS00+X)(fXnvO#pM41;`-|; literal 0 HcmV?d00001 diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index e49ba85..2155444 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -21,9 +21,10 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" + self.strati_file = self.input_dir / "stratigraphic_column.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") - + self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") if not self.faults_file.exists(): QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) @@ -43,43 +44,7 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) - strati_column = [ - "Turee Creek Group", - "Boolgeeda Iron Formation", - "Woongarra Rhyolite", - "Weeli Wolli Formation", - "Brockman Iron Formation", - "Mount McRae Shale and Mount Sylvia Formation", - "Wittenoom Formation", - "Marra Mamba Iron Formation", - "Jeerinah Formation", - "Bunjinah Formation", - "Pyradie Formation", - "Fortescue Group", - "Hardey Formation", - "Boongal Formation", - "Mount Roe Basalt", - "Rocklea Inlier greenstones", - "Rocklea Inlier metagranitic unit" - ] - strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") - # define the single field - provider = strati_table.dataProvider() - vtype=QVariant.String - provider.addAttributes([QgsField("unit_name", vtype)]) - strati_table.updateFields() - - # add features (one row per value) - feats = [] - fields = strati_table.fields() - for val in strati_column: - f = QgsFeature(fields) - f.setAttributes([val]) - feats.append(f) - - if feats: - provider.addFeatures(feats) - strati_table.updateExtents() + strati_table = QgsVectorLayer(str(self.strati_file), "strati", "ogr") algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() From d4d6d8358c6f416ec6f9e085f31134ef6d102ee7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 15:23:36 +0800 Subject: [PATCH 124/135] fix input file name in test_basal_contacts --- tests/qgis/test_basal_contacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 2155444..bb90de8 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -21,7 +21,7 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" - self.strati_file = self.input_dir / "stratigraphic_column.gpkg" + self.strati_file = self.input_dir / "stratigraphic_column_testing.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") From 2cd4c536f392f8bb6cb988e465ff53f39fa616bc Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 6 Oct 2025 14:06:04 +0800 Subject: [PATCH 125/135] feat dynamic field handling in Sampler --- m2l/processing/algorithms/sampler.py | 40 +++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 726d44a..cafd4b2 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -176,11 +176,19 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QMetaType.Type.QString)) - fields.append(QgsField("X", QMetaType.Type.Float)) - fields.append(QgsField("Y", QMetaType.Type.Float)) - fields.append(QgsField("Z", QMetaType.Type.Float)) - fields.append(QgsField("featureId", QMetaType.Type.QString)) + if samples is not None and not samples.empty: + for column_name in samples.columns: + dtype = samples[column_name].dtype + dtype_str = str(dtype) + + if dtype_str in ['float16', 'float32', 'float64']: + field_type = QMetaType.Type.Double + elif dtype_str in ['int8', 'int16', 'int32', 'int64']: + field_type = QMetaType.Type.Int + else: + field_type = QMetaType.Type.QString + + fields.append(QgsField(column_name, field_type)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: @@ -207,13 +215,21 @@ def processAlgorithm( #spacing has no z values feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) - feature.setAttributes([ - str(row.get('ID', '')), - float(row.get('X', 0)), - float(row.get('Y', 0)), - float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, - str(row.get('featureId', '')) - ]) + attributes = [] + for column_name in samples.columns: + value = row.get(column_name) + dtype = samples[column_name].dtype + + if pd.isna(value): + attributes.append(None) + elif dtype in ['float16', 'float32', 'float64']: + attributes.append(float(value)) + elif dtype in ['int8', 'int16', 'int32', 'int64']: + attributes.append(int(value)) + else: + attributes.append(str(value)) + + feature.setAttributes(attributes) sink.addFeature(feature) From 97c527446c1813599fa7873de4748b93d6eac539 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 6 Oct 2025 15:17:12 +0800 Subject: [PATCH 126/135] fix strat column data in ThicknessCalculator --- .../algorithms/thickness_calculator.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 7bab4f4..51d6f91 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -233,19 +233,22 @@ def processAlgorithm( 'maxx': extent.xMaximum(), 'maxy': extent.yMaximum() } - stratigraphic_column_layer = self.parameterAsVectorLayer(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) - stratigraphic_order = None - if stratigraphic_column_layer is not None and stratigraphic_column_layer.isValid(): - stratigraphic_order=[] - stratigraphic_order_df = qgsLayerToDataFrame(stratigraphic_column_layer) - stratigraphic_order_df = stratigraphic_order_df.sort_values('order') - for _, row in stratigraphic_order_df.iterrows(): - stratigraphic_order.append(row['unit_name']) + stratigraphic_column_source = self.parameterAsSource(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) + stratigraphic_order = [] + if stratigraphic_column_source is not None: + ordered_pairs =[] + for feature in stratigraphic_column_source.getFeatures(): + order = feature.attribute('order') + unit_name = feature.attribute('unit_name') + ordered_pairs.append((order, unit_name)) + ordered_pairs.sort(key=lambda x: x[0]) + stratigraphic_order = [pair[1] for pair in ordered_pairs] + feedback.pushInfo(f"DEBUG: parameterAsVectorLayer Stratigraphic order: {stratigraphic_order}") else: matrix_stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) if matrix_stratigraphic_order: - stratigraphic_order = [row[0] for row in matrix_stratigraphic_order if row and len(row) > 0] + stratigraphic_order = [str(row) for row in matrix_stratigraphic_order if row] else: raise QgsProcessingException("Stratigraphic column layer is required") if stratigraphic_order: From fe429df9cde758877301b8308afbf737204feafc Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 7 Oct 2025 11:08:39 +0800 Subject: [PATCH 127/135] merge with processing/processing_tools --- m2l/processing/algorithms/__init__.py | 1 + .../algorithms/extract_basal_contacts.py | 50 ++++---- m2l/processing/algorithms/sorter.py | 10 +- .../algorithms/user_defined_sorter.py | 112 ++++++++++++++++++ m2l/processing/provider.py | 2 + .../input/stratigraphic_column_testing.gpkg | Bin 0 -> 73728 bytes tests/qgis/test_basal_contacts.py | 29 +---- 7 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 m2l/processing/algorithms/user_defined_sorter.py create mode 100644 tests/qgis/input/stratigraphic_column_testing.gpkg diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f0aaedb..08d76a0 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,4 +1,5 @@ from .extract_basal_contacts import BasalContactsAlgorithm from .sorter import StratigraphySorterAlgorithm +from .user_defined_sorter import UserDefinedStratigraphyAlgorithm from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index f80cfe0..55a179c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -24,6 +24,7 @@ QgsProcessingParameterField, QgsProcessingParameterRasterLayer, QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsSettings @@ -73,13 +74,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "Loop3d: Stratigraphic sorter" + return "Automatic Stratigraphic Column" def group(self) -> str: - return "Loop3d" + return "Stratigraphy" def groupId(self) -> str: - return "Loop3d" + return "Stratigraphy_Column" def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) @@ -340,7 +341,6 @@ def processAlgorithm( def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() - # ------------------------------------------------------------------------- # Helper stub โ€“ you must replace with *your* conversion logic # ------------------------------------------------------------------------- @@ -415,4 +415,4 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee else: relationships_df = pd.DataFrame() - return units_df, relationships_df, contacts_df \ No newline at end of file + return units_df, relationships_df, contacts_df diff --git a/m2l/processing/algorithms/user_defined_sorter.py b/m2l/processing/algorithms/user_defined_sorter.py new file mode 100644 index 0000000..23857a3 --- /dev/null +++ b/m2l/processing/algorithms/user_defined_sorter.py @@ -0,0 +1,112 @@ +from typing import Any, Optional +from osgeo import gdal +import numpy as np +import json + +from PyQt5.QtCore import QVariant +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsRasterLayer, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, + QgsVectorLayer, + QgsWkbTypes, + QgsSettings +) + + +from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsFeatureSink, QgsWkbTypes, + QgsCoordinateReferenceSystem, QgsProcessingAlgorithm, QgsProcessingContext, + QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterMatrix, + QgsSettings +) +from PyQt5.QtCore import QVariant +import numpy as np + +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + def name(self): return "loop_sorter_2" + def displayName(self): return "User-Defined Stratigraphic Column" + def group(self): return "Stratigraphy" + def groupId(self): return "Stratigraphy_Column" + + def initAlgorithm(self, config=None): + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + # 1) Read the matrix; it may be a list of lists (rows) or a flat list depending on input source. + matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + + # Normalize to a list of unit strings (one column: "Unit") + units = [] + for row in matrix: + if isinstance(row, (list, tuple)): + unit = row[0] if row else "" + else: + unit = row + unit = (unit or "").strip() + if unit: # skip empty rows to avoid writing "" into fields + units.append(unit) + + # 2) Build sequential order (1-based), cast to native int + order_vals = [int(i) for i in (np.arange(len(units)) + 1)] + + # 3) Prepare sink + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) # or QVariant.LongLong + sink_fields.append(QgsField("unit_name", QVariant.String)) + + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + sink, dest_id = self.parameterAsSink( + parameters, self.OUTPUT, context, + sink_fields, QgsWkbTypes.NoGeometry, crs + ) + + # 4) Insert features + for pos, unit_name in zip(order_vals, units): + f = QgsFeature(sink_fields) + # Ensure correct types: int for "order", str for "unit_name" + f.setAttributes([int(pos), str(unit_name)]) + ok = sink.addFeature(f, QgsFeatureSink.FastInsert) + if not ok: + feedback.reportError(f"Failed to add feature for unit '{unit_name}' (order={pos}).") + + return {self.OUTPUT: dest_id} + + def createInstance(self): + return __class__() diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 318e944..9616d9b 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -19,6 +19,7 @@ from .algorithms import ( BasalContactsAlgorithm, StratigraphySorterAlgorithm, + UserDefinedStratigraphyAlgorithm, ThicknessCalculatorAlgorithm, SamplerAlgorithm ) @@ -35,6 +36,7 @@ def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(UserDefinedStratigraphyAlgorithm()) self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) diff --git a/tests/qgis/input/stratigraphic_column_testing.gpkg b/tests/qgis/input/stratigraphic_column_testing.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..bb782ef5b2f52fba0d364aa506a7e5bdf571a79c GIT binary patch literal 73728 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 b00bZa0SG_<0uVS00+X)(fXnvO#pM41;`-|; literal 0 HcmV?d00001 diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..bb90de8 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -20,9 +21,10 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" + self.strati_file = self.input_dir / "stratigraphic_column_testing.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") - + self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") if not self.faults_file.exists(): QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) @@ -42,26 +44,7 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) - strati_column = [ - "Turee Creek Group", - "Boolgeeda Iron Formation", - "Woongarra Rhyolite", - "Weeli Wolli Formation", - "Brockman Iron Formation", - "Mount McRae Shale and Mount Sylvia Formation", - "Wittenoom Formation", - "Marra Mamba Iron Formation", - "Jeerinah Formation", - "Bunjinah Formation", - "Pyradie Formation", - "Fortescue Group", - "Hardey Formation", - "Boongal Formation", - "Mount Roe Basalt", - "Rocklea Inlier greenstones", - "Rocklea Inlier metagranitic unit" - ] - + strati_table = QgsVectorLayer(str(self.strati_file), "strati", "ogr") algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +53,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From 53917e3a0e47807e5d4d344f12c529d9304b373d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:17:34 +0800 Subject: [PATCH 128/135] add user defined boundingbox in ThicknessCalculatorAlgorithm --- .../algorithms/thickness_calculator.py | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 51d6f91..a88f387 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -48,6 +48,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_THICKNESS_CALCULATOR_TYPE = 'THICKNESS_CALCULATOR_TYPE' INPUT_DTM = 'DTM' + INPUT_BOUNDING_BOX_TYPE = 'BOUNDING_BOX_TYPE' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' @@ -100,6 +101,29 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterEnum( + self.INPUT_BOUNDING_BOX_TYPE, + "Bounding Box Type", + options=['Extract from geology layer', 'User defined'], + allowMultiple=False, + defaultValue=1 + ) + ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") + self.addParameter( + QgsProcessingParameterMatrix( + self.INPUT_BOUNDING_BOX, + description="Static Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox, + optional=True + ) + ) + self.addParameter( QgsProcessingParameterNumber( self.INPUT_MAX_LINE_LENGTH, @@ -213,7 +237,7 @@ def processAlgorithm( thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + bounding_box_type = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX_TYPE, context) max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) @@ -225,14 +249,26 @@ def processAlgorithm( sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) - geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - extent = geology_layer.extent() - bounding_box = { - 'minx': extent.xMinimum(), - 'miny': extent.yMinimum(), - 'maxx': extent.xMaximum(), - 'maxy': extent.yMaximum() - } + if bounding_box_type == 0: + geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + extent = geology_layer.extent() + bounding_box = { + 'minx': extent.xMinimum(), + 'miny': extent.yMinimum(), + 'maxx': extent.xMaximum(), + 'maxy': extent.yMaximum() + } + feedback.pushInfo("Using bounding box from geology layer") + else: + static_bbox_matrix = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + if not static_bbox_matrix or len(static_bbox_matrix) == 0: + raise QgsProcessingException("Bounding box is required") + + bounding_box = matrixToDict(static_bbox_matrix) + + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", static_bbox_matrix) + feedback.pushInfo("Using bounding box from user input") stratigraphic_column_source = self.parameterAsSource(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) stratigraphic_order = [] From c94b40cb706a5144f032ff8fd4869fcff25dea3e Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:48:43 +0800 Subject: [PATCH 129/135] replace QMetaType with QVariant in sampler --- m2l/processing/algorithms/sampler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index cafd4b2..a3f384e 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QVariant from osgeo import gdal import pandas as pd @@ -182,11 +182,11 @@ def processAlgorithm( dtype_str = str(dtype) if dtype_str in ['float16', 'float32', 'float64']: - field_type = QMetaType.Type.Double + field_type = QVariant.Double elif dtype_str in ['int8', 'int16', 'int32', 'int64']: - field_type = QMetaType.Type.Int + field_type = QVariant.Int else: - field_type = QMetaType.Type.QString + field_type = QVariant.String fields.append(QgsField(column_name, field_type)) From b32baec3fbd2695729aa22fe827543517a421677 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:56:35 +0800 Subject: [PATCH 130/135] change orientation type variable name in ThicknessCalculatorAlgorithm --- m2l/processing/algorithms/thickness_calculator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index a88f387..824fd34 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -57,7 +57,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' - INPUT_THICKNESS_ORIENTATION_TYPE = 'THICKNESS_ORIENTATION_TYPE' + INPUT_ORIENTATION_TYPE = 'ORIENTATION_TYPE' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' @@ -195,8 +195,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) self.addParameter( QgsProcessingParameterEnum( - 'THICKNESS_ORIENTATION_TYPE', - 'Thickness Orientation Type', + self.INPUT_ORIENTATION_TYPE, + 'Orientation Type', options=['Dip Direction', 'Strike'], defaultValue=0 # Default to Dip Direction ) @@ -242,8 +242,8 @@ def processAlgorithm( basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) - thickness_orientation_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_ORIENTATION_TYPE, context) - is_strike = (thickness_orientation_type == 1) + orientation_type = self.parameterAsEnum(parameters, self.INPUT_ORIENTATION_TYPE, context) + is_strike = (orientation_type == 1) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) From b34340c963bf4764c751f6ac8daf587446eba43e Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Thu, 16 Oct 2025 16:23:50 +1100 Subject: [PATCH 131/135] Initial commit From d8661755098b969ca00942a27aaff33e21ca47e2 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Thu, 16 Oct 2025 17:28:37 +1100 Subject: [PATCH 132/135] rename for folder for merge with loopstructural plugin --- docs/conf.py | 2 +- {m2l => loopstructural}/__about__.py | 0 {m2l => loopstructural}/__init__.py | 0 {m2l => loopstructural}/gui/__init__.py | 0 {m2l => loopstructural}/gui/dlg_settings.py | 6 +++--- {m2l => loopstructural}/gui/dlg_settings.ui | 0 {m2l => loopstructural}/main/vectorLayerWrapper.py | 0 {m2l => loopstructural}/metadata.txt | 0 {m2l => loopstructural}/plugin_main.py | 8 ++++---- {m2l => loopstructural}/processing/__init__.py | 0 .../processing/algorithms/__init__.py | 0 .../processing/algorithms/extract_basal_contacts.py | 0 .../processing/algorithms/sampler.py | 0 .../processing/algorithms/sorter.py | 0 .../processing/algorithms/thickness_calculator.py | 0 .../processing/algorithms/user_defined_sorter.py | 0 {m2l => loopstructural}/processing/provider.py | 2 +- .../resources/i18n/plugin_map2loop_en.ts | 0 .../resources/i18n/plugin_translation.pro | 0 .../resources/images/default_icon.png | Bin {m2l => loopstructural}/toolbelt/__init__.py | 0 {m2l => loopstructural}/toolbelt/env_var_parser.py | 0 {m2l => loopstructural}/toolbelt/log_handler.py | 4 ++-- {m2l => loopstructural}/toolbelt/preferences.py | 6 +++--- tests/qgis/test_basal_contacts.py | 4 ++-- tests/qgis/test_env_var_parser.py | 2 +- tests/qgis/test_plg_preferences.py | 4 ++-- tests/qgis/test_processing.py | 2 +- tests/qgis/test_sampler_decimator.py | 4 ++-- tests/qgis/test_sampler_spacing.py | 4 ++-- tests/unit/test_plg_metadata.py | 2 +- 31 files changed, 25 insertions(+), 25 deletions(-) rename {m2l => loopstructural}/__about__.py (100%) rename {m2l => loopstructural}/__init__.py (100%) rename {m2l => loopstructural}/gui/__init__.py (100%) rename {m2l => loopstructural}/gui/dlg_settings.py (96%) rename {m2l => loopstructural}/gui/dlg_settings.ui (100%) rename {m2l => loopstructural}/main/vectorLayerWrapper.py (100%) rename {m2l => loopstructural}/metadata.txt (100%) rename {m2l => loopstructural}/plugin_main.py (96%) rename {m2l => loopstructural}/processing/__init__.py (100%) rename {m2l => loopstructural}/processing/algorithms/__init__.py (100%) rename {m2l => loopstructural}/processing/algorithms/extract_basal_contacts.py (100%) rename {m2l => loopstructural}/processing/algorithms/sampler.py (100%) rename {m2l => loopstructural}/processing/algorithms/sorter.py (100%) rename {m2l => loopstructural}/processing/algorithms/thickness_calculator.py (100%) rename {m2l => loopstructural}/processing/algorithms/user_defined_sorter.py (100%) rename {m2l => loopstructural}/processing/provider.py (98%) rename {m2l => loopstructural}/resources/i18n/plugin_map2loop_en.ts (100%) rename {m2l => loopstructural}/resources/i18n/plugin_translation.pro (100%) rename {m2l => loopstructural}/resources/images/default_icon.png (100%) rename {m2l => loopstructural}/toolbelt/__init__.py (100%) rename {m2l => loopstructural}/toolbelt/env_var_parser.py (100%) rename {m2l => loopstructural}/toolbelt/log_handler.py (98%) rename {m2l => loopstructural}/toolbelt/preferences.py (96%) diff --git a/docs/conf.py b/docs/conf.py index d7cc5df..8f1fb8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ import sphinx_rtd_theme # noqa: F401 theme of Read the Docs # Package -from m2l import __about__ +from loopstructural import __about__ # -- Build environment ----------------------------------------------------- on_rtd = environ.get("READTHEDOCS", None) == "True" diff --git a/m2l/__about__.py b/loopstructural/__about__.py similarity index 100% rename from m2l/__about__.py rename to loopstructural/__about__.py diff --git a/m2l/__init__.py b/loopstructural/__init__.py similarity index 100% rename from m2l/__init__.py rename to loopstructural/__init__.py diff --git a/m2l/gui/__init__.py b/loopstructural/gui/__init__.py similarity index 100% rename from m2l/gui/__init__.py rename to loopstructural/gui/__init__.py diff --git a/m2l/gui/dlg_settings.py b/loopstructural/gui/dlg_settings.py similarity index 96% rename from m2l/gui/dlg_settings.py rename to loopstructural/gui/dlg_settings.py index 351a008..037226e 100644 --- a/m2l/gui/dlg_settings.py +++ b/loopstructural/gui/dlg_settings.py @@ -18,15 +18,15 @@ from qgis.PyQt.QtGui import QDesktopServices, QIcon # project -from m2l.__about__ import ( +from loopstructural.__about__ import ( __icon_path__, __title__, __uri_homepage__, __uri_tracker__, __version__, ) -from m2l.toolbelt import PlgLogger, PlgOptionsManager -from m2l.toolbelt.preferences import PlgSettingsStructure +from loopstructural.toolbelt import PlgLogger, PlgOptionsManager +from loopstructural.toolbelt.preferences import PlgSettingsStructure # ############################################################################ # ########## Globals ############### diff --git a/m2l/gui/dlg_settings.ui b/loopstructural/gui/dlg_settings.ui similarity index 100% rename from m2l/gui/dlg_settings.ui rename to loopstructural/gui/dlg_settings.ui diff --git a/m2l/main/vectorLayerWrapper.py b/loopstructural/main/vectorLayerWrapper.py similarity index 100% rename from m2l/main/vectorLayerWrapper.py rename to loopstructural/main/vectorLayerWrapper.py diff --git a/m2l/metadata.txt b/loopstructural/metadata.txt similarity index 100% rename from m2l/metadata.txt rename to loopstructural/metadata.txt diff --git a/m2l/plugin_main.py b/loopstructural/plugin_main.py similarity index 96% rename from m2l/plugin_main.py rename to loopstructural/plugin_main.py index a286f1e..bcc245f 100644 --- a/m2l/plugin_main.py +++ b/loopstructural/plugin_main.py @@ -15,17 +15,17 @@ from qgis.PyQt.QtWidgets import QAction # project -from m2l.__about__ import ( +from loopstructural.__about__ import ( DIR_PLUGIN_ROOT, __icon_path__, __title__, __uri_homepage__, ) -from m2l.gui.dlg_settings import PlgOptionsFactory -from m2l.processing import ( +from loopstructural.gui.dlg_settings import PlgOptionsFactory +from loopstructural.processing import ( Map2LoopProvider, ) -from m2l.toolbelt import PlgLogger +from loopstructural.toolbelt import PlgLogger # ############################################################################ # ########## Classes ############### diff --git a/m2l/processing/__init__.py b/loopstructural/processing/__init__.py similarity index 100% rename from m2l/processing/__init__.py rename to loopstructural/processing/__init__.py diff --git a/m2l/processing/algorithms/__init__.py b/loopstructural/processing/algorithms/__init__.py similarity index 100% rename from m2l/processing/algorithms/__init__.py rename to loopstructural/processing/algorithms/__init__.py diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/loopstructural/processing/algorithms/extract_basal_contacts.py similarity index 100% rename from m2l/processing/algorithms/extract_basal_contacts.py rename to loopstructural/processing/algorithms/extract_basal_contacts.py diff --git a/m2l/processing/algorithms/sampler.py b/loopstructural/processing/algorithms/sampler.py similarity index 100% rename from m2l/processing/algorithms/sampler.py rename to loopstructural/processing/algorithms/sampler.py diff --git a/m2l/processing/algorithms/sorter.py b/loopstructural/processing/algorithms/sorter.py similarity index 100% rename from m2l/processing/algorithms/sorter.py rename to loopstructural/processing/algorithms/sorter.py diff --git a/m2l/processing/algorithms/thickness_calculator.py b/loopstructural/processing/algorithms/thickness_calculator.py similarity index 100% rename from m2l/processing/algorithms/thickness_calculator.py rename to loopstructural/processing/algorithms/thickness_calculator.py diff --git a/m2l/processing/algorithms/user_defined_sorter.py b/loopstructural/processing/algorithms/user_defined_sorter.py similarity index 100% rename from m2l/processing/algorithms/user_defined_sorter.py rename to loopstructural/processing/algorithms/user_defined_sorter.py diff --git a/m2l/processing/provider.py b/loopstructural/processing/provider.py similarity index 98% rename from m2l/processing/provider.py rename to loopstructural/processing/provider.py index 9616d9b..62fa602 100644 --- a/m2l/processing/provider.py +++ b/loopstructural/processing/provider.py @@ -10,7 +10,7 @@ from qgis.PyQt.QtGui import QIcon # project -from m2l.__about__ import ( +from loopstructural.__about__ import ( __icon_path__, __title__, __version__, diff --git a/m2l/resources/i18n/plugin_map2loop_en.ts b/loopstructural/resources/i18n/plugin_map2loop_en.ts similarity index 100% rename from m2l/resources/i18n/plugin_map2loop_en.ts rename to loopstructural/resources/i18n/plugin_map2loop_en.ts diff --git a/m2l/resources/i18n/plugin_translation.pro b/loopstructural/resources/i18n/plugin_translation.pro similarity index 100% rename from m2l/resources/i18n/plugin_translation.pro rename to loopstructural/resources/i18n/plugin_translation.pro diff --git a/m2l/resources/images/default_icon.png b/loopstructural/resources/images/default_icon.png similarity index 100% rename from m2l/resources/images/default_icon.png rename to loopstructural/resources/images/default_icon.png diff --git a/m2l/toolbelt/__init__.py b/loopstructural/toolbelt/__init__.py similarity index 100% rename from m2l/toolbelt/__init__.py rename to loopstructural/toolbelt/__init__.py diff --git a/m2l/toolbelt/env_var_parser.py b/loopstructural/toolbelt/env_var_parser.py similarity index 100% rename from m2l/toolbelt/env_var_parser.py rename to loopstructural/toolbelt/env_var_parser.py diff --git a/m2l/toolbelt/log_handler.py b/loopstructural/toolbelt/log_handler.py similarity index 98% rename from m2l/toolbelt/log_handler.py rename to loopstructural/toolbelt/log_handler.py index 284365e..4226aa8 100644 --- a/m2l/toolbelt/log_handler.py +++ b/loopstructural/toolbelt/log_handler.py @@ -12,8 +12,8 @@ from qgis.utils import iface # project package -import m2l.toolbelt.preferences as plg_prefs_hdlr -from m2l.__about__ import __title__ +import loopstructural.toolbelt.preferences as plg_prefs_hdlr +from loopstructural.__about__ import __title__ # ############################################################################ # ########## Classes ############### diff --git a/m2l/toolbelt/preferences.py b/loopstructural/toolbelt/preferences.py similarity index 96% rename from m2l/toolbelt/preferences.py rename to loopstructural/toolbelt/preferences.py index 8742313..1bb2cc3 100644 --- a/m2l/toolbelt/preferences.py +++ b/loopstructural/toolbelt/preferences.py @@ -11,9 +11,9 @@ from qgis.core import QgsSettings # package -import m2l.toolbelt.log_handler as log_hdlr -from m2l.__about__ import __title__, __version__ -from m2l.toolbelt.env_var_parser import EnvVarParser +import loopstructural.toolbelt.log_handler as log_hdlr +from loopstructural.__about__ import __title__, __version__ +from loopstructural.toolbelt.env_var_parser import EnvVarParser # ############################################################################ # ########## Classes ############### diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index bb90de8..20d0276 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -3,8 +3,8 @@ from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app -from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm -from m2l.processing.provider import Map2LoopProvider +from loopstructural.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm +from loopstructural.processing.provider import Map2LoopProvider class TestBasalContacts(unittest.TestCase): diff --git a/tests/qgis/test_env_var_parser.py b/tests/qgis/test_env_var_parser.py index b968008..316b034 100644 --- a/tests/qgis/test_env_var_parser.py +++ b/tests/qgis/test_env_var_parser.py @@ -1,7 +1,7 @@ import os import unittest -from m2l.toolbelt.env_var_parser import EnvVarParser +from loopstructural.toolbelt.env_var_parser import EnvVarParser class TestEnvVarParser(unittest.TestCase): diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py index 8ac07a3..b930109 100644 --- a/tests/qgis/test_plg_preferences.py +++ b/tests/qgis/test_plg_preferences.py @@ -18,8 +18,8 @@ from qgis.testing import unittest # project -from m2l.__about__ import __version__ -from m2l.toolbelt.preferences import ( +from loopstructural.__about__ import __version__ +from loopstructural.toolbelt.preferences import ( PREFIX_ENV_VARIABLE, PlgOptionsManager, PlgSettingsStructure, diff --git a/tests/qgis/test_processing.py b/tests/qgis/test_processing.py index 6f5c719..ca1a94c 100644 --- a/tests/qgis/test_processing.py +++ b/tests/qgis/test_processing.py @@ -15,7 +15,7 @@ from qgis.core import QgsApplication from qgis.testing import start_app, unittest -from m2l.processing.provider import ( +from loopstructural.processing.provider import ( Map2LoopProvider, ) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index bb1864c..c329321 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -2,8 +2,8 @@ from pathlib import Path from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis,QgsApplication from qgis.testing import start_app -from m2l.processing.algorithms.sampler import SamplerAlgorithm -from m2l.processing.provider import Map2LoopProvider +from loopstructural.processing.algorithms.sampler import SamplerAlgorithm +from loopstructural.processing.provider import Map2LoopProvider class TestSamplerDecimator(unittest.TestCase): diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a49042f..e2f285d 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -2,8 +2,8 @@ from pathlib import Path from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication from qgis.testing import start_app -from m2l.processing.algorithms.sampler import SamplerAlgorithm -from m2l.processing.provider import Map2LoopProvider +from loopstructural.processing.algorithms.sampler import SamplerAlgorithm +from loopstructural.processing.provider import Map2LoopProvider class TestSamplerSpacing(unittest.TestCase): diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py index e6373a6..55beda2 100644 --- a/tests/unit/test_plg_metadata.py +++ b/tests/unit/test_plg_metadata.py @@ -18,7 +18,7 @@ from packaging.version import parse # project -from m2l import __about__ +from loopstructural import __about__ # ############################################################################ # ########## Classes ############# From 83c4dfae5a4d46b11d7bd63a62c305c927985c69 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Thu, 23 Oct 2025 09:07:40 +1100 Subject: [PATCH 133/135] add agent.md file with initial content. (#53) --- AGENTS.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..58b90f9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,93 @@ +# AGENTS.md + +## Purpose + +This document outlines the architectural and development principles for contributors to the `plugin_loopstructural` QGIS plugin. The plugin provides a thin, modular interface to the `map2loop` and `LoopStructural` libraries, enabling geological modeling workflows within QGIS. + +--- + +## Design Philosophy + +### ๐Ÿ”น Thin Interface Layer +- The plugin **must not** reimplement or duplicate functionality from `map2loop` or `LoopStructural`. +- All core logic and enhancements should be contributed upstream to the respective libraries. +- The plugin should focus on **UI integration**, **data flow orchestration**, and **user interaction**. + +### ๐Ÿ”น Modularity +- UI components (dialogs, panels, actions) live under the `loopstructural/gui/` package and should be encapsulated in their own classes. +- Business logic and orchestration are located in `loopstructural/main/` and `loopstructural/toolbelt/` where adapters and services wrap external libraries. +- Processing algorithms and QGIS provider integration are in `loopstructural/processing/` and should be isolated from UI code. +- Avoid tight coupling between components. Use signals/slots or event-driven patterns where appropriate. + +### ๐Ÿ”น Object-Oriented Design +- Use classes with clear responsibilities and interfaces. +- Prefer composition over inheritance unless subclassing is semantically appropriate. +- Encapsulate interactions with external libraries in dedicated adapter or service classes (e.g., `loopstructural.main.Map2LoopService`, `loopstructural.main.LoopStructuralRunner`). + +--- + +## Development Guidelines + +### โœ… Code Quality +- All code must pass the repository's pre-commit checks (formatting, linting, import sorting). +- Use type hints and docstrings for all public methods and classes. +- Follow PEP8 and QGIS plugin development best practices. + +### ๐Ÿงช Testing +- All new code must include **unit tests** and, where applicable, **integration tests**. +- Tests live under the `tests/` package and are runnable with `pytest`. +- Mock external dependencies (`map2loop`, `LoopStructural`) in unit tests. + +### ๐Ÿงฉ Current Plugin Structure + +``` +plugin_loopstructural/ +โ”œโ”€โ”€ loopstructural/ # plugin package +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ __about__.py +โ”‚ โ”œโ”€โ”€ plugin_main.py # QGIS plugin entry and bootstrap +โ”‚ โ”œโ”€โ”€ gui/ # UI dialogs, widgets, and panels +โ”‚ โ”œโ”€โ”€ main/ # controllers, managers, adapters (service layer) +โ”‚ โ”œโ”€โ”€ processing/ # QGIS processing provider and algorithms +โ”‚ โ”œโ”€โ”€ toolbelt/ # utilities, env parsing, preferences, logging +โ”‚ โ”œโ”€โ”€ resources/ # icons, translations, help files +โ”‚ โ””โ”€โ”€ requirements.txt +โ”œโ”€โ”€ docs/ +โ”œโ”€โ”€ requirements/ +โ”œโ”€โ”€ tests/ +โ””โ”€โ”€ README.md +``` + +Notes on mapping older concepts: +- What used to be called `services/` and `controllers/` is implemented across `loopstructural/main/` and `loopstructural/toolbelt/`. +- UI remains in `loopstructural/gui/` (dialogs, `.ui` files, widget classes). +- Processing-specific code and QGIS provider live under `loopstructural/processing/`. + +--- + +## Contribution Workflow + +1. Fork the repository and create a feature branch. +2. Implement changes following the design and code quality guidelines. +3. Add or update tests under `tests/` and ensure they run with `pytest`. +4. Run pre-commit hooks (e.g. `pre-commit run --all-files`) and ensure all checks pass. +5. Submit a pull request with a clear description of the changes and rationale. Link to upstream libraries if behavior is moved upstream. + +--- + +## Future Enhancements + +- Support for asynchronous or background processing of long-running tasks (consider using QGIS background task framework). +- Improved error handling and user feedback. +- Internationalization (i18n) support and keeping `.ts`/.qm translation files in `loopstructural/resources/i18n/`. +- Better separation of concerns between UI, processing algorithms and adapters to facilitate unit testing. + +--- + +## Contact + +For questions or contributions to the upstream libraries: +- `map2loop` +- `LoopStructural` + +--- From 6ae3bc16d00f78c61ac5f795a30645d99f32f0fc Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:38:03 +0930 Subject: [PATCH 134/135] Fix: revert building qgs Table --- .../processing/algorithms/sampler.py | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/loopstructural/processing/algorithms/sampler.py b/loopstructural/processing/algorithms/sampler.py index 3e134d4..66191c3 100644 --- a/loopstructural/processing/algorithms/sampler.py +++ b/loopstructural/processing/algorithms/sampler.py @@ -77,7 +77,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", - ["Decimator", "Spacing"], + ["Decimator (Point Geometry Data)", + "Spacing (Line Geometry Data)"], defaultValue=0 ) ) @@ -112,7 +113,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterNumber( self.INPUT_DECIMATION, - "DECIMATION", + "DECIMATION (Point Geometry Data)", QgsProcessingParameterNumber.Integer, defaultValue=1, optional=True, @@ -122,7 +123,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterNumber( self.INPUT_SPACING, - "SPACING", + "SPACING (Line Geometry Data)", QgsProcessingParameterNumber.Double, defaultValue=200.0, optional=True, @@ -176,19 +177,11 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - if samples is not None and not samples.empty: - for column_name in samples.columns: - dtype = samples[column_name].dtype - dtype_str = str(dtype) - - if dtype_str in ['float16', 'float32', 'float64']: - field_type = QVariant.Double - elif dtype_str in ['int8', 'int16', 'int32', 'int64']: - field_type = QVariant.Int - else: - field_type = QVariant.String - - fields.append(QgsField(column_name, field_type)) + fields.append(QgsField("ID", QVariant.String)) + fields.append(QgsField("X", QVariant.Double)) + fields.append(QgsField("Y", QVariant.Double)) + fields.append(QgsField("Z", QVariant.Double)) + fields.append(QgsField("featureId", QVariant.String)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: @@ -215,21 +208,13 @@ def processAlgorithm( #spacing has no z values feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) - attributes = [] - for column_name in samples.columns: - value = row.get(column_name) - dtype = samples[column_name].dtype - - if pd.isna(value): - attributes.append(None) - elif dtype in ['float16', 'float32', 'float64']: - attributes.append(float(value)) - elif dtype in ['int8', 'int16', 'int32', 'int64']: - attributes.append(int(value)) - else: - attributes.append(str(value)) - - feature.setAttributes(attributes) + feature.setAttributes([ + str(row.get('ID', '')), + float(row.get('X', 0)), + float(row.get('Y', 0)), + float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, + str(row.get('featureId', '')) + ]) sink.addFeature(feature) From 003d963a2d5b6b4437f7e3d8118b0bca77cfc814 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:41:40 +0930 Subject: [PATCH 135/135] Refactor: remove unnecessary formation field --- .../processing/algorithms/extract_basal_contacts.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/loopstructural/processing/algorithms/extract_basal_contacts.py b/loopstructural/processing/algorithms/extract_basal_contacts.py index 4d3ba5f..6446b13 100644 --- a/loopstructural/processing/algorithms/extract_basal_contacts.py +++ b/loopstructural/processing/algorithms/extract_basal_contacts.py @@ -81,17 +81,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - self.addParameter( - QgsProcessingParameterField( - 'FORMATION_FIELD', - 'Formation Field', - parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, - defaultValue='formation', - optional=True - ) - ) - self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS,