pax_global_header00006660000000000000000000000064145636651000014520gustar00rootroot0000000000000052 comment=76e77f00491490f031d6eb3e45059b907cc6b79c shapely-2.0.3/000077500000000000000000000000001456366510000131675ustar00rootroot00000000000000shapely-2.0.3/.circleci/000077500000000000000000000000001456366510000150225ustar00rootroot00000000000000shapely-2.0.3/.circleci/config.yml000066400000000000000000000026721456366510000170210ustar00rootroot00000000000000version: 2.1 jobs: linux-aarch64-wheels: working_directory: ~/linux-aarch64-wheels machine: image: ubuntu-2004:2022.04.1 # resource_class is what tells CircleCI to use an ARM worker for native arm builds # https://circleci.com/product/features/resource-classes/ resource_class: arm.medium environment: GEOS_VERSION: 3.11.3 CIBUILDWHEEL: 1 CIBW_BUILD: "cp*-manylinux_aarch64" CIBW_ENVIRONMENT_PASS_LINUX: "GEOS_VERSION GEOS_INSTALL GEOS_CONFIG LD_LIBRARY_PATH" CIBW_BEFORE_ALL: "./ci/install_geos.sh" CIBW_TEST_REQUIRES: "pytest" CIBW_TEST_COMMAND: "pytest --pyargs shapely.tests" steps: - checkout - run: name: Build the Linux aarch64 wheels. command: | python3 -m pip install --user cibuildwheel==2.16.5 echo 'export GEOS_INSTALL=~/linux-aarch64-wheels/geosinstall/geos-"$GEOS_VERSION"' >> "$BASH_ENV" echo 'export GEOS_CONFIG="$GEOS_INSTALL"/bin/geos-config' >> "$BASH_ENV" echo 'export LD_LIBRARY_PATH="$GEOS_INSTALL"/lib' >> "$BASH_ENV" source "$BASH_ENV" python3 -m cibuildwheel --output-dir wheelhouse - store_artifacts: path: wheelhouse/ workflows: wheel-build: jobs: - linux-aarch64-wheels: filters: branches: only: - main - maint-2.0 tags: only: /.*/ shapely-2.0.3/.clang-format000066400000000000000000000001041456366510000155350ustar00rootroot00000000000000BasedOnStyle: Google DerivePointerAlignment: false ColumnLimit: 90 shapely-2.0.3/.dockerignore000066400000000000000000000004551456366510000156470ustar00rootroot00000000000000# Ignore everything ** # Allow files and directories !/*.py !/*LICENSE* !/setup.cfg !/pyproject.toml !/shapely/** !/src/** !/tests/** # Ignore unnecessary files inside allowed directories # This should go after the allowed directories **/*~ **/*.log **/.DS_Store **/Thumbs.db **/*.pyc **/*.so **/.* shapely-2.0.3/.gitattributes000066400000000000000000000000411456366510000160550ustar00rootroot00000000000000shapely/_version.py export-subst shapely-2.0.3/.github/000077500000000000000000000000001456366510000145275ustar00rootroot00000000000000shapely-2.0.3/.github/dependabot.yml000066400000000000000000000003321456366510000173550ustar00rootroot00000000000000version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "weekly" shapely-2.0.3/.github/workflows/000077500000000000000000000000001456366510000165645ustar00rootroot00000000000000shapely-2.0.3/.github/workflows/lint.yml000066400000000000000000000010201456366510000202460ustar00rootroot00000000000000name: Lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.8 - name: Run black, flake8 and isort uses: pre-commit/action@v3.0.0 - name: Validate citation file shell: bash run: | python -m pip install cffconvert cffconvert --validate cffconvert -f bibtex cffconvert -f apalike shapely-2.0.3/.github/workflows/release.yml000066400000000000000000000142341456366510000207330ustar00rootroot00000000000000name: Build and publish on: push: branches: - main # just build the sdist & wheel, skip release tags: - "*" pull_request: # also build on PRs touching this file paths: - ".github/workflows/release.yml" - "ci/*" - "MANIFEST.in" - "pyproject.toml" - "setup.py" jobs: build_sdist: name: Build sdist runs-on: ubuntu-latest steps: - name: Checkout source uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - name: Build a source tarball run: | python -m pip install --upgrade pip python -m pip install build twine python -m build --sdist twine check --strict dist/* - uses: actions/upload-artifact@v3 with: path: ./dist/*.tar.gz retention-days: 30 build_wheels: name: Build ${{ matrix.arch }} wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} env: GEOS_VERSION: "3.11.3" strategy: fail-fast: false matrix: include: - os: ubuntu-20.04 arch: x86_64 # Numpy no longer builds i686 packages # - os: ubuntu-20.04 # arch: i686 # The aarch64 build has been transferred to Travis # - os: ubuntu-20.04 # arch: aarch64 # qemu_platform: arm64 # Note: Numpy doesn't have ppc64le & s390x wheels # Also, some GEOS tests fail on s390x. - os: windows-2019 arch: x86 msvc_arch: x86 - os: windows-2019 arch: AMD64 msvc_arch: x64 - os: macos-11 arch: x86_64 cmake_osx_architectures: x86_64 - os: macos-11 arch: arm64 cmake_osx_architectures: arm64 - os: macos-11 arch: universal2 cmake_osx_architectures: "x86_64;arm64" steps: - name: Checkout source uses: actions/checkout@v4 with: fetch-depth: 0 - name: Cache GEOS build uses: actions/cache@v3 with: path: ${{ runner.temp }}/geos-${{ env.GEOS_VERSION }} key: ${{ matrix.os }}-${{ matrix.arch }}-${{ env.GEOS_VERSION }}-${{ hashFiles('ci/*') }} - name: Add GEOS LICENSE run: | cp ci/wheelbuilder/LICENSE_GEOS . shell: bash - name: Add MSVC LICENSE run: | cp ci/wheelbuilder/LICENSE_win32 . shell: bash if: ${{ matrix.os == 'windows-2019' }} # - name: Set up QEMU # uses: docker/setup-qemu-action@v1 # with: # platforms: ${{ matrix.qemu_platform }} # if: ${{ matrix.qemu_platform }} - name: Activate MSVC uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ matrix.msvc_arch }} if: ${{ matrix.msvc_arch }} - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_SKIP: cp36-* pp* *musllinux* *-manylinux_i686 CIBW_ENVIRONMENT_LINUX: GEOS_VERSION=${{ env.GEOS_VERSION }} GEOS_INSTALL=/host${{ runner.temp }}/geos-${{ env.GEOS_VERSION }} GEOS_CONFIG=/host${{ runner.temp }}/geos-${{ env.GEOS_VERSION }}/bin/geos-config LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/host${{ runner.temp }}/geos-${{ env.GEOS_VERSION }}/lib CIBW_ENVIRONMENT_MACOS: GEOS_INSTALL=${{ runner.temp }}/geos-${{ env.GEOS_VERSION }} GEOS_CONFIG=${{ runner.temp }}/geos-${{ env.GEOS_VERSION }}/bin/geos-config LDFLAGS=-Wl,-rpath,${{ runner.temp }}/geos-${{ env.GEOS_VERSION }}/lib MACOSX_DEPLOYMENT_TARGET=10.9 CMAKE_OSX_ARCHITECTURES='${{ matrix.cmake_osx_architectures }}' CIBW_ENVIRONMENT_WINDOWS: GEOS_INSTALL='${{ runner.temp }}\geos-${{ env.GEOS_VERSION }}' GEOS_LIBRARY_PATH='${{ runner.temp }}\geos-${{ env.GEOS_VERSION }}\lib' GEOS_INCLUDE_PATH='${{ runner.temp }}\geos-${{ env.GEOS_VERSION }}\include' CIBW_BEFORE_ALL: ./ci/install_geos.sh CIBW_BEFORE_ALL_WINDOWS: ci\install_geos.cmd CIBW_BEFORE_BUILD_WINDOWS: pip install delvewheel CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path ${{ runner.temp }}\geos-${{ env.GEOS_VERSION }}\bin -w {dest_dir} {wheel} CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: pytest --pyargs shapely.tests - name: Upload artifacts uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl retention-days: 5 publish: name: Publish on GitHub and PyPI needs: [build_wheels, build_sdist] runs-on: ubuntu-latest # release on every tag if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') steps: - uses: actions/download-artifact@v3 with: name: artifact path: dist - name: Create GitHub Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: false prerelease: false - name: Get Asset name run: | export PKG=$(ls dist/ | grep tar) set -- $PKG echo "name=$1" >> $GITHUB_ENV - name: Upload Release Asset (sdist) to GitHub id: upload-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: dist/${{ env.name }} asset_name: ${{ env.name }} asset_content_type: application/zip - name: Upload Release Assets to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} skip_existing: true # To test: repository_url: https://test.pypi.org/legacy/ shapely-2.0.3/.github/workflows/tests.yml000066400000000000000000000157171456366510000204640ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: test: name: ${{ matrix.os }}-${{ matrix.architecture }} Py${{ matrix.python }} GEOS ${{ matrix.geos }} runs-on: ${{ matrix.os }} defaults: run: shell: bash strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-2019] architecture: [x64] geos: [3.6.6, 3.7.5, 3.8.4, 3.9.5, 3.10.6, 3.11.3, 3.12.1, main] include: # 2017 - python: 3.7 # 3.6 is dropped geos: 3.6.6 numpy: 1.14.6 # 2018 - python: 3.7 geos: 3.7.5 numpy: 1.15.4 # 2019 - python: 3.8 geos: 3.8.4 numpy: 1.16.2 # 2020 - python: 3.9 geos: 3.9.5 numpy: 1.19.5 # 2021 - python: "3.10" geos: 3.10.6 numpy: 1.21.3 # 2022 - python: "3.11" geos: 3.11.3 numpy: 1.23.4 matplotlib: true doctest: true extra_pytest_args: "-W error" # error on warnings # 2023 - python: "3.12" geos: 3.12.1 numpy: 1.26.0 # dev - python: "3.12" geos: main # extra ignore for dateutil Python 3.12 warning fixed upstream (waiting on release 2.8.3+) extra_pytest_args: "-W error -W ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning" # error on warnings # enable two 32-bit windows builds: - os: windows-2019 architecture: x86 python: 3.7 geos: 3.7.5 numpy: 1.15.4 - os: windows-2019 architecture: x86 python: 3.9 geos: 3.10.6 numpy: 1.19.5 # pypy (use explicit ubuntu version to not overwrite existing ubuntu-latest + geos 3.11.0 build) - os: ubuntu-20.04 python: "pypy3.8" geos: 3.11.3 numpy: 1.23.4 env: GEOS_VERSION: ${{ matrix.geos }} GEOS_VERSION_SPEC: ${{ matrix.geos }} GEOS_INSTALL: ${{ github.workspace }}/geosinstall/geos-${{ matrix.geos }} GEOS_BUILD: ${{ github.workspace }}/geosbuild steps: - name: Correct slashes in GEOS_INSTALL (Windows) run: | echo 'GEOS_INSTALL=${{ github.workspace }}\geosinstall\geos-${{ matrix.geos }}' >> $GITHUB_ENV echo 'GEOS_BUILD=${{ github.workspace }}\geosbuild' >> $GITHUB_ENV if: ${{ matrix.os == 'windows-2019' }} - name: Checkout Shapely uses: actions/checkout@v4 - name: Checkout GEOS (main) uses: actions/checkout@v4 with: repository: libgeos/geos ref: main path: ${{ env.GEOS_BUILD }} if: ${{ matrix.geos == 'main' }} - name: Put the latest commit hash in the cache token for GEOS main run: | echo "GEOS_VERSION_SPEC=$(git rev-parse HEAD)" >> $GITHUB_ENV working-directory: ${{ env.GEOS_BUILD }} if: ${{ matrix.geos == 'main' }} - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.architecture }} allow-prereleases: true - name: Cache GEOS and pip packages uses: actions/cache@v3 with: key: ${{ matrix.os }}-${{ matrix.architecture }}-geos-${{ env.GEOS_VERSION_SPEC }}-${{ hashFiles('ci/install_geos.sh') }} path: | ~/.cache/pip ${{ github.workspace }}/geosinstall - name: Activate MSVC uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ matrix.architecture }} if: ${{ matrix.os == 'windows-2019' }} - name: Install GEOS run: | bash ci/install_geos.sh - name: Install python dependencies run: | python -m pip install --disable-pip-version-check --upgrade pip pip install --upgrade wheel setuptools if [ -z "${{ matrix.numpy }}" ]; then pip install --upgrade --pre Cython numpy pytest pytest-cov coveralls; else pip install --upgrade Cython numpy==${{ matrix.numpy }} pytest pytest-cov coveralls; fi if [ -n "${{ matrix.matplotlib }}" ]; then pip install matplotlib fi pip list - name: Set environment variables (Linux) run: | echo "${{ env.GEOS_INSTALL }}/bin" >> $GITHUB_PATH echo "LD_LIBRARY_PATH=${{ env.GEOS_INSTALL }}/lib" >> $GITHUB_ENV if: ${{ startsWith(matrix.os, 'ubuntu') }} - name: Set environment variables (OSX) run: | echo "${{ env.GEOS_INSTALL }}/bin" >> $GITHUB_PATH echo "LDFLAGS=-Wl,-rpath,${{ env.GEOS_INSTALL }}/lib" >> $GITHUB_ENV if: ${{ matrix.os == 'macos-latest' }} # Windows requires special treatment: # - geos-config does not exist, so we specify include and library paths # - Python >=3.8 ignores the PATH for finding DLLs, so we copy them into the package - name: Set environment variables + copy DLLs (Windows) run: | cp geosinstall/geos-${{ matrix.geos }}/bin/*.dll shapely echo 'GEOS_LIBRARY_PATH=${{ env.GEOS_INSTALL }}\lib' >> $GITHUB_ENV echo 'GEOS_INCLUDE_PATH=${{ env.GEOS_INSTALL }}\include' >> $GITHUB_ENV if: ${{ matrix.os == 'windows-2019' }} - name: Build and install Shapely run: | pip install -e . - name: Overview of the Python environment (pip list) run: pip list - name: Run tests # Enable this if we have failures on GEOS main # continue-on-error: ${{ matrix.geos == 'main' }} run: | python -c "import shapely; print(f'GEOS version: {shapely.geos_version_string}')" pytest shapely/tests -r a --cov --cov-report term-missing ${{ matrix.extra_pytest_args }} # Only run doctests on 1 runner (because of typographic differences in doctest results) - name: Run doctests if: ${{ matrix.os == 'ubuntu-latest' && matrix.doctest }} run: | python -m pytest --doctest-modules docs/manual.rst - name: Run doctests (part 2) if: ${{ matrix.os == 'ubuntu-latest' && matrix.doctest }} run: | pytest --doctest-modules shapely --ignore=shapely/tests - name: Upload coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_PARALLEL: true shell: bash run: | coveralls --service=github || echo "!! intermittent coveralls failure" coveralls: name: Indicate completion to coveralls.io needs: test runs-on: ubuntu-latest container: python:3-slim steps: - name: Finished run: | pip3 install --upgrade coveralls coveralls --finish || echo "!! intermittent coveralls failure" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shapely-2.0.3/.gitignore000066400000000000000000000020531456366510000151570ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so *.dylib *.dll # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg *.log *.whl # to make binary distributions (that pack additional licenses) not 'dirty': /LICENSE_GEOS /LICENSE_win32 # 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/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # pyenv python configuration file .python-version # PyCharm .idea # VS Code .vscode # Pipenv .venv Pipfile Pipfile.lock # Documentation docs/_build/ docs/shapely.*.txt docs/shapely.txt docs/modules.txt docs/_reference.rst docs/reference/* # Benchmarks .asv # Cython C files shapely/*.c .ipynb_checkpoints .DS_Store shapely-2.0.3/.mailmap000066400000000000000000000016261456366510000146150ustar00rootroot00000000000000Allan Adair Aron Bierbaum Brendan Ward Casper van der Wel Casper van der Wel Filipe Fernandes Frédéric Junod Kai Lautaportti Kevin Wurster Konstantin Veretennicov Mike Taves Mike Taves Sean Gillies Sean Gillies Sean Gillies Sean Gillies shapely-2.0.3/.pre-commit-config.yaml000066400000000000000000000004671456366510000174570ustar00rootroot00000000000000files: 'shapely/' repos: - repo: https://github.com/psf/black rev: 22.3.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort exclude: shapely/__init__.py shapely-2.0.3/.readthedocs.yml000066400000000000000000000014251456366510000162570ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: "ubuntu-20.04" tools: python: "mambaforge-4.10" jobs: post_checkout: # we need the tags for versioneer to work - git fetch origin --depth 150 - git fetch --tags pre_install: # to avoid "dirty" version - git update-index --assume-unchanged docs/environment.yml docs/conf.py # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py fail_on_warning: false # Optionally build your docs in additional formats such as PDF and ePub formats: all conda: environment: docs/environment.yml python: install: - method: pip path: . shapely-2.0.3/.travis.yml000066400000000000000000000011321456366510000152750ustar00rootroot00000000000000os: linux dist: focal language: python python: '3.8' if: (branch = main OR tag IS present) AND (type = push) env: global: - GEOS_VERSION=3.11.3 cache: directories: - "$HOME/geosinstall" - "~/.cache/pip" jobs: include: - arch: ppc64le - arch: s390x - arch: arm64 install: - | export GEOS_INSTALL=$HOME/geosinstall/geos-$GEOS_VERSION ./ci/install_geos.sh export PATH=$HOME/geosinstall/geos-$GEOS_VERSION/bin:$PATH pip install .[test] script: - | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/geosinstall/geos-$GEOS_VERSION/lib cd .. pytest -v --pyargs shapely.tests shapely-2.0.3/CHANGES.txt000066400000000000000000000275101456366510000150050ustar00rootroot00000000000000Changes ======= 2.0.3 (2024-02-16) ------------------ - Fix regression in the ``oriented_envelope`` ufunc to accept array-like input in case of GEOS<3.12 (#1929). - The binary wheels are not yet compatible with a future NumPy 2.0 release, therefore a ``numpy<2`` upper pin was added to the requirements (#1972). - Upgraded the GEOS version in the binary wheel distributions to 3.11.3. 2.0.2 (2023-10-12) ------------------ Bug fixes: - Fix regression in the (in)equality comparison (``geom1 == geom2``) using ``__eq__`` to not ignore the z-coordinates (#1732). - Fix ``MultiPolygon()`` constructor to accept polygons without holes (#1850). - Fix ``minimum_rotated_rectangle`` (``oriented_envelope``) to always return the minimum area solution (instead of minimum width). In practice, it will use the GEOS implementation only for GEOS 3.12+, and for older GEOS versions fall back to the implementation that was included in Shapely < 2. Wheels are available for Python 3.12 (and still include GEOS 3.11.2). Building from source is now compatible with Cython 3. 2.0.1 (2023-01-30) ------------------ Bug fixes: - Fix regression in the ``Polygon()`` constructor taking a sequence of Points (#1662). - Fix regression in the geometry constructors when passing ``decimal.Decimal`` coordinate values (#1707). - Fix ``STRtree()`` to not make the passed geometry array immutable as side-effect of the constructor (#1714). - Fix the ``directed`` keyword in ``shapely.ops.linemerge()`` (#1695). Improvements: - Expose the function to get a matplotlib Patch object from a (Multi)Polygon (without already plotting it) publicly as :func:`shapely.plotting.patch_from_polygon` (#1704). For a full changelog, see https://shapely.readthedocs.io/en/latest/release/2.x.html#version-2-0-1 2.0.0 (2022-12-12) ------------------ Shapely version 2.0.0 is a major release featuring a complete refactor of the internals and new vectorized (element-wise) array operations providing considerable performance improvements. For a full changelog, see https://shapely.readthedocs.io/en/latest/release/2.x.html#version-2-0-0 Relevant changes in behaviour compared to 2.0rc3: - Added temporary support for unpickling shapely<2.0 geometries. 2.0rc1 (2022-11-26) ------------------- Relevant changes in behaviour compared to 2.0b2: - The ``Point(..)`` constructor no longer accepts a sequence of coordinates consisting of more than one coordinate pair (previously, subsequent coordinates were ignored) (#1600). - Fix performance regression in the ``LineString()`` constructor when passing a numpy array of coordinates (#1602). Wheels for 2.0rc1 published on PyPI include GEOS 3.11.1. 2.0b2 (2022-10-29) ------------------ Relevant changes in behaviour compared to 2.0b1: - Fix for compatibility with PyPy (#1577). - Fix to the ``Point()`` constructor to accept arrays of length 1 for the x and y coordinates (fix compatibility with Shapely 1.8). - Raise ValueError for non-finite distance in the ``buffer()`` and ``offset_curve()`` methods on the Geometry classes (consistent with Shapely 1.8). 2.0b1 (2022-10-17) ------------------ Relevant changes in behaviour compared to 2.0a1: - Renamed the ``tolerance`` keyword to ``max_segment_length`` in the ``segmentize`` function. - Renamed the ``quadsegs`` keyword in the top-level ``buffer`` and ``offset_curve`` functions and the ``resolution`` keyword in the Geometry class ``buffer`` and ``offset_curve`` methods all to ``quad_segs``. - Added use of ``GEOSGeom_getExtent`` to speed up bounds calculations for GEOS >= 3.11. - Restored the behaviour of ``unary_union`` to return an empty GeometryCollection for an empty or all-None sequence as input (and the same for ``intersection_all`` and ``symmetric_difference_all``). - Fixed the Geometry objects to be weakref-able again (#1535). - The ``.type`` attribute is deprecated, use ``.geom_type`` instead (which already existed before as well) (#1492). Wheels for 2.0b1 published on PyPI include GEOS 3.11.0. 2.0a1 (2022-08-03) ------------------ Shapely version 2.0 alpha 1 is the first of a major release featuring a complete refactor of the internals and new vectorized (element-wise) array operations providing considerable performance improvements. For a full changelog, see https://shapely.readthedocs.io/en/latest/release/2.x.html#version-2-0-0 Wheels for 2.0a1 published on PyPI include GEOS 3.10.3. 1.8.5.post1 (2022-10-13) ------------------------ Packaging: Wheels are provided for Python versions 3.6-3.11 and Cython 0.29.32 is used to generate C extension module code. 1.8.5 (2022-10-12) ------------------ Packaging: Python 3.11 wheels have been added to the matrix for all platforms. Bug fixes: - Assign _lgeos in the macos frozen app check, fixing a bug introduced in 1.8.2 (#1528). - An exception is now raised when nan is passed to buffer and parallel_offset, preventing segmentation faults (#1516). 1.8.4 (2022-08-17) ------------------ Bug fixes: - The new c_geom_p type caused a regression and has been removed (#1487). 1.8.3 (2022-08-16) ------------------ Deprecations: The STRtree class will be changed in 2.0.0 and will not be compatible with the class in versions 1.8.x. This change obsoletes the deprecation announcement in 1.8a3 (below). Packaging: Wheels for 1.8.3 published on PyPI include GEOS 3.10.3. Bug fixes: - The signature for GEOSMinimumClearance has been corrected, fixing an issue affecting aarch64-darwin (#1480) - Return and arg types have been corrected and made more strict for area, length, and distance properties. - A new c_geom_p type has been created to replace c_void_p when calling GEOS functions (#1479). - An incorrect polygon-line intersection (#1427) has been fixed in GEOS 3.10.3, which will be included in wheels published to PyPI. - GEOS buffer parameters are now destroyed, fixing a memory leak (#1440). 1.8.2 (2022-05-03) ------------------ - Make Polygons and MultiPolygons closed by definition, like LinearRings. Resolves #1246. - Perform frozen app check for GEOS before conda env check on macos as we already do on linux (#1301). - Fix leak of GEOS coordinate sequence in nearest_points reported in #1098. 1.8.1.post1 (2022-02-17) ------------------------ This post-release addresses a defect in the 1.8.1 source distribution. No .c files are included in the 1.8.1.post1 sdist and Cython is required to build and install from source. 1.8.1 (2022-02-16) ------------------ Packaging: Wheels for 1.8.1 published on PyPI include GEOS 3.10.2. This version is the best version of GEOS yet. Discrepancies in behavior compared to previous versions are considered to be improvements. For the first time, we will publish wheels for macos_arm64 (see PR #1310). Python version support: Shapely 1.8.1 works with Pythons 3.6-3.10. Bug fixes: - Require Cython >= 0.29.24 to support Python 3.10 (#1224). - Fix array_interface_base (#1235). 1.8.0 (2021-10-25) ------------------ This is the final 1.8.0 release. There have been no changes since 1.8rc2. 1.8rc2 (2021-10-19) ------------------- Build: A pyproject.toml file has been added to specify build dependencies for the _vectorized and _speedups modules (#1128). To install shapely without these build dependencies, use the features of your build tool that disable PEP 517 and 518 support. Bug fixes: - Part of PR #1042, which added a new primary GEOS library name to be searched for, has been reverted by PR #1201. 1.8rc1 (2021-10-04) ------------------- Deprecations: The almost_exact() method of BaseGeometry has been deprecated. It is confusing and will be removed in 2.0.0. The equals_exact() method is to be used instead. Bug fixes: - We ensure that the _speedups module is always imported before _vectorized to avoid an unexplained condition on Windows with Python 3.8 and 3.9 (#1184). 1.8a3 (2021-08-24) ------------------ Deprecations: The STRtree class deprecation warnings have been removed. The class in 2.0.0 will be backwards compatible with the class in 1.8.0. Bug fixes: - The __array_interface__ raises only AttributeError, all other exceptions are deprecated starting with Numpy 1.21 (#1173). - The STRtree class now uses a pair of item, geom sequences internally instead of a dict (#1177). 1.8a2 (2021-07-15) ------------------ Python version support: Shapely 1.8 will support only Python versions >= 3.6. New features: - The STRtree nearest*() methods now take an optional argument that specifies exclusion of the input geometry from results (#1115). - A GeometryTypeError has been added to shapely.errors and is consistently raised instead of TypeError or ValueError as in version 1.7. For backwards compatibility, the new exception will derive from TypeError and Value error until version 2.0 (#1099). - The STRtree class constructor now takes an optional second argument, a sequence of objects to be stored in the tree. If not provided, the sequence indices of the geometries will be stored, as before (#1112). - The STRtree class has new query_geoms(), query_items(), nearest_geom(), and nearest_item() methods (#1112). The query() and nearest() methods remain as aliases for query_geoms() and nearest_geom(). Bug fixes: - We no longer attempt to load libc to get the free function on Linux, but get it from the global symbol table. - GEOS error messages printed when GEOS_getCoordSeq() is passed an empty geometry are avoided by never passing an empty geometry (#1134). - Python's builtin super() is now used only as described in PEP 3135 (#1109). - Only load conda GEOS dll if it exists (on Windows) (#1108). - Add /opt/homebrew/lib to the list of directories to be searched for the GEOS shared library. - Added new library search path to assist app creation with cx_Freeze. 1.8a1 (2021-03-03) ------------------ Shapely 1.8.0 will be a transitional version. There are a few bug fixes and new features, but it is mainly about warning of the upcoming changes in 2.0.0. Several more pre-releases before 1.8.0 are expected. See the migration guide to Shapely 1.8 / 2.0 for more details on how to update your code (https://shapely.readthedocs.io/en/latest/migration.html). Python version support: Shapely 1.8 will support only Python versions >= 3.5 (#884). Deprecations: The following functions and geometry attributes and methods will be removed in version 2.0.0. - ops.cascaded_union - geometry .empty() - geometry .ctypes and .__array_interface__ - multi-part geometry .__len__ - setting custom attributes on geometry objects Geometry objects will become immutable in version 2.0.0. The STRtree class will be entirely changed in 2.0.0. The exact future API is not yet decided, but will be decided before 1.8.0 is released. Deprecation warnings will be emitted in 1.8a1 when any of these features are used. The deprecated .to_wkb() and .to_wkt() methods on the geometry objects have been removed. New features: - Add a normalize() method to geometry classes, exposing the GEOSNormalize algorithm (#1090). - Initialize STRtree with a capacity of 10 items per node (#1070). - Load libraries relocated to shapely/.libs by auditwheel versions < 3.1 or relocated to Shapely.libs by auditwheel versions >= 3.1. - shapely.ops.voronoi_diagram() computes the Voronoi Diagram of a geometry or geometry collection (#833, #851). - shapely.validation.make_valid() fixes invalid geometries (#883) Bug fixes: - For pyinstaller we now handle the case of more than one GEOS library in the environment, such as when fiona and rasterio wheels are co-installed with shapely (#1071). - The ops.split function now splits on touch to eliminate confusing discrepancies between results using multi and single part splitters (#1034). - Several issues with duplication and order of vertices in ops.substring have been fixed (#1008). Packaging: - The wheels uploaded to PyPI will include GEOS 3.9.1. Previous releases ----------------- For older releases in the 1.x line, see https://shapely.readthedocs.io/en/latest/release/1.x.html shapely-2.0.3/CITATION.cff000066400000000000000000000020321456366510000150560ustar00rootroot00000000000000cff-version: 1.2.0 message: "Please cite this software using these metadata." type: software title: Shapely version: "2.0.3" date-released: "2024-02-16" doi: 10.5281/zenodo.5597138 abstract: "Manipulation and analysis of geometric objects in the Cartesian plane." repository-artifact: https://pypi.org/project/Shapely repository-code: https://github.com/shapely/shapely license: "BSD-3-Clause" authors: - given-names: Sean family-names: Gillies orcid: https://orcid.org/0000-0002-8401-9184 - given-names: Casper family-names: "van der Wel" orcid: https://orcid.org/0000-0002-0488-2237 - given-names: Joris family-names: "Van den Bossche" orcid: https://orcid.org/0000-0003-3284-2977 - given-names: "Mike W." family-names: Taves orcid: https://orcid.org/0000-0003-3657-7963 - given-names: Joshua family-names: Arnott - given-names: "Brendan C." family-names: Ward orcid: https://orcid.org/0000-0002-0813-9774 - name: others keywords: - cartography - geometry - GEOS - GIS - topology shapely-2.0.3/CODE_OF_CONDUCT.md000066400000000000000000000037001456366510000157660ustar00rootroot00000000000000# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.2.0, available at [https://contributor-covenant.org/version/1/2/0/](https://contributor-covenant.org/version/1/2/0/) shapely-2.0.3/CREDITS.txt000066400000000000000000000136541456366510000150360ustar00rootroot00000000000000Credits ======= Shapely is written by: * Adi Shavit * Alan D. Snow * Alberto Rubiales * Allan Adair * Andrew Blakey * Andy Freeland * Ariel Kadouri * Aron Bierbaum * Bart Broere <2715782+bartbroere@users.noreply.github.com> * Bas Couwenberg * Ben Beasley * Benjamin Root * BertrandGervais * Bhavika Tekwani <4955119+bhavika@users.noreply.github.com> * Bi0T1N * Brad Hards * Brendan Ward * Brandon Wood * Casper van der Wel * Chad Hawkins * Christian Prior * Christian Quest * Christophe Pradal * Dan Baston * Dan Mahr * Daniele Esposti * Dave Collins * David Baumgold * David Swinkels * Denis Rykov * Enrico Ferreguti * Erwin Sterrenburg * Ewout ter Hoeven * Felix Divo <4403130+felixdivo@users.noreply.github.com> * Felix Yan * Filipe Fernandes * Frédéric Junod * Gabi Davar * Gerrit Holl * Hannes * Hao Zheng * Henry Walshaw * Howard Butler * Hugo * Idan Miara * Jacob Wasserman * Jaeha Lee * James Douglass * James Gaboardi * James Lamb * James McBride * James Spencer * Jamie Hall * Jason Sanford * Jeethu Rao * Jeremiah England <34973839+Jeremiah-England@users.noreply.github.com> * Jinkun Wang * Johan Euphrosine * Johannes Schönberger * Jonathan Schoonhoven * Joris Van den Bossche * Joshua Arnott * Juan Luis Cano Rodríguez * Justin Shenk * Kai Lautaportti * Kelsey Jordahl * Kevin Wurster * Konstantin Veretennicov * Koshy Thomas * Krishna Chaitanya * Kristian Evers * Kyle Barron * Leandro Lima * Lukasz * Luke Lee * Maarten Vermeyen * Marc Jansen * Marco De Nadai * Martin Fleischmann * Mathieu * Matt Amos * Matthias Cuntz * MejstrikRudolf <68251685+MejstrikRudolf@users.noreply.github.com> * Michael K * Michel Blancard * Mike Taves * Morris Tweed * Naveen Michaud-Agrawal * Oliver Tonnhofer * Paveł Tyślacki * Peter Sagerson * Phil Elson * Pierre PACI * Raja Gangopadhya * Ricardo Zilleruelo <51384295+zetaatlyft@users.noreply.github.com> * Rémy Phelipot * S Murthy * Sampo Syrjanen * Samuel Chin * Sean Gillies * Sobolev Nikita * Stephan Hügel * Steve M. Kim * Taro Matsuzawa aka. btm * Thibault Deutsch * Thomas Gratier * Thomas Kluyver * Tim Gates * Tobias Sauerwein * Tom Caruso * Tom Clancy <17627475+clncy@users.noreply.github.com> * WANG Aiyong * Will May * Zachary Ware * aharfoot * bstadlbauer <11799671+bstadlbauer@users.noreply.github.com> * cclauss * clefrks <33859587+clefrks@users.noreply.github.com> * davidh-ssec * georgeouzou * giumas * gpapadok <38889721+gpapadok@users.noreply.github.com> * joelostblom * ljwolf * mindw * rsmb * shongololo * solarjoe * sshuair * stephenworsley <49274989+stephenworsley@users.noreply.github.com> See also: https://github.com/shapely/shapely/graphs/contributors. Additional help from: * Justin Bronn (GeoDjango) for ctypes inspiration * Martin Davis (JTS) * Sandro Santilli, Mateusz Loskot, Paul Ramsey, et al (GEOS Project) Major portions of this work were supported by a grant (for Pleiades_) from the U.S. National Endowment for the Humanities (https://www.neh.gov). .. _Pleiades: https://pleiades.stoa.org shapely-2.0.3/FAQ.rst000066400000000000000000000027241456366510000143350ustar00rootroot00000000000000Frequently asked questions and answers ====================================== I installed shapely in a conda environment using pip. Why doesn't it work? -------------------------------------------------------------------------- Shapely versions < 2.0 load a GEOS shared library using ctypes. It's not uncommon for users to have multiple copies of GEOS libs on their system. Loading the correct one is complicated and shapely has a number of platform-dependent GEOS library loading bugs. The project has particularly poor support for finding the correct GEOS library for a shapely package installed from PyPI *into* a conda environment. We recommend that conda users always get shapely from conda-forge. Are there references for the algorithms used by shapely? -------------------------------------------------------- Generally speaking, shapely's predicates and operations are derived from methods of the same name from GEOS_ and the `JTS Topology Suite`_. See the `JTS FAQ`_ for references describing the JTS algorithms. I used .buffer() on a geometry with Z coordinates. Where did the Z coordinates go? ---------------------------------------------------------------------------------- The buffer algorithm in GEOS_ is purely two-dimensional and discards any Z coordinates. This is generally the case for the GEOS algorithms. .. _GEOS: https://libgeos.org/ .. _JTS Topology Suite: https://locationtech.github.io/jts/ .. _JTS FAQ: https://locationtech.github.io/jts/jts-faq.html#E1 shapely-2.0.3/ISSUE_TEMPLATE.md000066400000000000000000000013121456366510000156710ustar00rootroot00000000000000## Please note If you are reporting an installation or module import issue, please note that this project only accepts reports about problems with packages downloaded from the Python Package Index. Conda users should take issues to one of the following trackers: - https://github.com/ContinuumIO/anaconda-issues/issues - https://github.com/conda-forge/shapely-feedstock ## Expected behavior and actual behavior. (For example: the area of my geometry is `1.417` when it should be `1.414`.) ## Steps to reproduce the problem. (For example, a script with required data) ## Operating system (For example, Mac OS X 10.12.3) ## Shapely version and provenance (For example, 1.6b4 installed from PyPI using pip) shapely-2.0.3/LICENSE.txt000066400000000000000000000030571456366510000150170ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2007, Sean C. Gillies. 2019, Casper van der Wel. 2007-2022, Shapely Contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. shapely-2.0.3/MANIFEST.in000066400000000000000000000004501456366510000147240ustar00rootroot00000000000000prune docs exclude *.txt exclude MANIFEST.in include CITATION.cff CHANGES.txt CREDITS.txt LICENSE.txt README.rst include pyproject.toml versioneer.py recursive-include src *.c *.h recursive-include tests *.py recursive-include shapely *.pxd *.pyx recursive-exclude shapely *.c include docs/*.rst shapely-2.0.3/README.rst000066400000000000000000000124571456366510000146670ustar00rootroot00000000000000======= Shapely ======= .. Documentation at RTD — https://readthedocs.org .. image:: https://readthedocs.org/projects/shapely/badge/?version=stable :alt: Documentation Status :target: https://shapely.readthedocs.io/en/stable/ .. Github Actions status — https://github.com/shapely/shapely/actions .. |github-actions| image:: https://github.com/shapely/shapely/workflows/Tests/badge.svg?branch=main :alt: Github Actions status :target: https://github.com/shapely/shapely/actions?query=branch%3Amain .. Travis CI status -- https://travis-ci.com .. image:: https://travis-ci.com/shapely/shapely.svg?branch=main :alt: Travis CI status :target: https://travis-ci.com/github/shapely/shapely .. PyPI .. image:: https://img.shields.io/pypi/v/shapely.svg :alt: PyPI :target: https://pypi.org/project/shapely/ .. Anaconda .. image:: https://img.shields.io/conda/vn/conda-forge/shapely :alt: Anaconda :target: https://anaconda.org/conda-forge/shapely .. Coverage .. |coveralls| image:: https://coveralls.io/repos/github/shapely/shapely/badge.svg?branch=main :target: https://coveralls.io/github/shapely/shapely?branch=main .. Zenodo .. .. image:: https://zenodo.org/badge/191151963.svg .. :alt: Zenodo .. :target: https://zenodo.org/badge/latestdoi/191151963 Manipulation and analysis of geometric objects in the Cartesian plane. .. image:: https://c2.staticflickr.com/6/5560/31301790086_b3472ea4e9_c.jpg :width: 800 :height: 378 Shapely is a BSD-licensed Python package for manipulation and analysis of planar geometric objects. It is using the widely deployed open-source geometry library `GEOS `__ (the engine of `PostGIS `__, and a port of `JTS `__). Shapely wraps GEOS geometries and operations to provide both a feature rich `Geometry` interface for singular (scalar) geometries and higher-performance NumPy ufuncs for operations using arrays of geometries. Shapely is not primarily focused on data serialization formats or coordinate systems, but can be readily integrated with packages that are. What is a ufunc? ---------------- A universal function (or ufunc for short) is a function that operates on *n*-dimensional arrays on an element-by-element fashion and supports array broadcasting. The underlying ``for`` loops are implemented in C to reduce the overhead of the Python interpreter. Multithreading -------------- Shapely functions generally support multithreading by releasing the Global Interpreter Lock (GIL) during execution. Normally in Python, the GIL prevents multiple threads from computing at the same time. Shapely functions internally release this constraint so that the heavy lifting done by GEOS can be done in parallel, from a single Python process. Usage ===== Here is the canonical example of building an approximately circular patch by buffering a point, using the scalar Geometry interface: .. code-block:: pycon >>> from shapely import Point >>> patch = Point(0.0, 0.0).buffer(10.0) >>> patch >>> patch.area 313.6548490545941 Using the vectorized ufunc interface (instead of using a manual for loop), compare an array of points with a polygon: .. code:: python >>> import shapely >>> import numpy as np >>> geoms = np.array([Point(0, 0), Point(1, 1), Point(2, 2)]) >>> polygon = shapely.box(0, 0, 2, 2) >>> shapely.contains(polygon, geoms) array([False, True, False]) See the documentation for more examples and guidance: https://shapely.readthedocs.io Requirements ============ Shapely 2.0 requires * Python >=3.7 * GEOS >=3.5 * NumPy >=1.14 Installing Shapely ================== We recommend installing Shapely using one of the available built distributions, for example using ``pip`` or ``conda``: .. code-block:: console $ pip install shapely # or using conda $ conda install shapely --channel conda-forge See the `installation documentation `__ for more details and advanced installation instructions. Integration =========== Shapely does not read or write data files, but it can serialize and deserialize using several well known formats and protocols. The shapely.wkb and shapely.wkt modules provide dumpers and loaders inspired by Python's pickle module. .. code-block:: pycon >>> from shapely.wkt import dumps, loads >>> dumps(loads('POINT (0 0)')) 'POINT (0.0000000000000000 0.0000000000000000)' Shapely can also integrate with other Python GIS packages using GeoJSON-like dicts. .. code-block:: pycon >>> import json >>> from shapely.geometry import mapping, shape >>> s = shape(json.loads('{"type": "Point", "coordinates": [0.0, 0.0]}')) >>> s >>> print(json.dumps(mapping(s))) {"type": "Point", "coordinates": [0.0, 0.0]} Support ======= Questions about using Shapely may be asked on the `GIS StackExchange `__ using the "shapely" tag. Bugs may be reported at https://github.com/shapely/shapely/issues. Copyright & License =================== Shapely is licensed under BSD 3-Clause license. GEOS is available under the terms of GNU Lesser General Public License (LGPL) 2.1 at https://libgeos.org. shapely-2.0.3/asv.conf.json000066400000000000000000000150611456366510000156020ustar00rootroot00000000000000{ // The version of the config file format. Do not change, unless // you know what you are doing. "version": 1, // The name of the project being benchmarked "project": "shapely", // The project's homepage "project_url": "https://github.com/shapely/shapely", // The URL or local path of the source code repository for the // project being benchmarked "repo": ".", // The Python project's subdirectory in your repo. If missing or // the empty string, the project is assumed to be located at the root // of the repository. // "repo_subdir": "", // Customizable commands for building, installing, and // uninstalling the project. See asv.conf.json documentation. // // "install_command": ["in-dir={env_dir} python -mpip install {wheel_file}"], // "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], // "build_command": [ // "python setup.py build", // "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" // ], // List of branches to benchmark. If not provided, defaults to "master" // (for git) or "default" (for mercurial). // "branches": ["master"], // for git // "branches": ["default"], // for mercurial // The DVCS being used. If not set, it will be automatically // determined from "repo" by looking at the protocol in the URL // (if remote), or by looking for special directories, such as // ".git" (if local). // "dvcs": "git", // The tool to use to create environments. May be "conda", // "virtualenv" or other value depending on the plugins in use. // If missing or the empty string, the tool will be automatically // determined by looking for tools on the PATH environment // variable. "environment_type": "conda", // timeout in seconds for installing any dependencies in environment // defaults to 10 min //"install_timeout": 600, // the base URL to show a commit for the project. "show_commit_url": "http://github.com/shapely/shapely/commit/", // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. "pythons": ["3.8"], // The list of conda channel names to be searched for benchmark // dependency packages in the specified order "conda_channels": ["conda-forge"], // The matrix of dependencies to test. Each key is the name of a // package (in PyPI) and the values are version numbers. An empty // list or empty string indicates to just test against the default // (latest) version. null indicates that the package is to not be // installed. If the package to be tested is only available from // PyPi, and the 'environment_type' is conda, then you can preface // the package name by 'pip+', and the package will be installed via // pip (with all the conda available packages installed first, // followed by the pip installed packages). // "matrix": { "Cython": [], "numpy": [], "geos": [] }, // Combinations of libraries/python versions can be excluded/included // from the set to test. Each entry is a dictionary containing additional // key-value pairs to include/exclude. // // An exclude entry excludes entries where all values match. The // values are regexps that should match the whole string. // // An include entry adds an environment. Only the packages listed // are installed. The 'python' key is required. The exclude rules // do not apply to includes. // // In addition to package names, the following keys are available: // // - python // Python version, as in the *pythons* variable above. // - environment_type // Environment type, as above. // - sys_platform // Platform, as in sys.platform. Possible values for the common // cases: 'linux2', 'win32', 'cygwin', 'darwin'. // // "exclude": [ // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows // {"environment_type": "conda", "six": null}, // don't run without six on conda // ], // // "include": [ // // additional env for python2.7 // {"python": "2.7", "numpy": "1.8"}, // // additional env if run on windows+conda // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, // ], // The directory (relative to the current directory) that benchmarks are // stored in. If not provided, defaults to "benchmarks" // "benchmark_dir": "benchmarks", // The directory (relative to the current directory) to cache the Python // environments in. If not provided, defaults to "env" "env_dir": ".asv/env", // The directory (relative to the current directory) that raw benchmark // results are stored in. If not provided, defaults to "results". "results_dir": ".asv/results", // The directory (relative to the current directory) that the html tree // should be written to. If not provided, defaults to "html". "html_dir": ".asv/html" // The number of characters to retain in the commit hashes. // "hash_length": 8, // `asv` will cache results of the recent builds in each // environment, making them faster to install next time. This is // the number of builds to keep, per environment. // "build_cache_size": 2, // The commits after which the regression search in `asv publish` // should start looking for regressions. Dictionary whose keys are // regexps matching to benchmark names, and values corresponding to // the commit (exclusive) after which to start looking for // regressions. The default is to start from the first commit // with results. If the commit is `null`, regression detection is // skipped for the matching benchmark. // // "regressions_first_commits": { // "some_benchmark": "352cdf", // Consider regressions only after this commit // "another_benchmark": null, // Skip regression detection altogether // }, // The thresholds for relative change in results, after which `asv // publish` starts reporting regressions. Dictionary of the same // form as in ``regressions_first_commits``, with values // indicating the thresholds. If multiple entries match, the // maximum is taken. If no entry matches, the default is 5%. // // "regressions_thresholds": { // "some_benchmark": 0.01, // Threshold of 1% // "another_benchmark": 0.5, // Threshold of 50% // }, } shapely-2.0.3/benchmarks/000077500000000000000000000000001456366510000153045ustar00rootroot00000000000000shapely-2.0.3/benchmarks/__init__.py000066400000000000000000000000001456366510000174030ustar00rootroot00000000000000shapely-2.0.3/benchmarks/benchmarks.py000066400000000000000000000300441456366510000177740ustar00rootroot00000000000000""" Shapely benchmarks These are run using asv: "pip install asv" or "conda install -c conda-forge asv" To run a specific test within the existing environment, e.g., PointPolygonTimeSuite: $ asv run -b PointPolygonTimeSuite -E 'existing' """ import numpy as np import shapely # Seed the numpy random generator for more reproducible benchmarks np.random.seed(0) class PointPolygonTimeSuite: """Benchmarks running on 100000 points and one polygon""" def setup(self): self.points = shapely.points(np.random.random((100000, 2))) self.polygon = shapely.polygons(np.random.random((3, 2))) def time_contains(self): shapely.contains(self.points, self.polygon) def time_distance(self): shapely.distance(self.points, self.polygon) def time_intersection(self): shapely.intersection(self.points, self.polygon) class IOSuite: """Benchmarks I/O operations (WKT and WKB) on a set of 10000 polygons""" def setup(self): self.to_write = shapely.polygons(np.random.random((10000, 100, 2))) self.to_read_wkt = shapely.to_wkt(self.to_write) self.to_read_wkb = shapely.to_wkb(self.to_write) def time_write_to_wkt(self): shapely.to_wkt(self.to_write) def time_write_to_wkb(self): shapely.to_wkb(self.to_write) def time_read_from_wkt(self): shapely.from_wkt(self.to_read_wkt) def time_read_from_wkb(self): shapely.from_wkb(self.to_read_wkb) class ConstructorsSuite: """Microbenchmarks for the Geometry class constructors""" def setup(self): self.coords = np.random.random((1000, 2)) def time_point(self): shapely.Point(1.0, 2.0) def time_linestring_from_numpy(self): shapely.LineString(self.coords) def time_linearring_from_numpy(self): shapely.LinearRing(self.coords) class ConstructiveSuite: """Benchmarks constructive functions on a set of 10,000 points""" def setup(self): self.coords = np.random.random((10000, 2)) self.points = shapely.points(self.coords) def time_voronoi_polygons(self): shapely.voronoi_polygons(self.points) def time_envelope(self): shapely.envelope(self.points) def time_convex_hull(self): shapely.convex_hull(self.points) def time_concave_hull(self): shapely.concave_hull(self.points, ratio=0.2, allow_holes=False) def time_concave_hull_with_holes(self): shapely.concave_hull(self.points, ratio=0.2, allow_holes=True) def time_delaunay_triangles(self): shapely.delaunay_triangles(self.points) def time_box(self): shapely.box(*np.hstack([self.coords, self.coords + 100]).T) class ClipSuite: """Benchmarks for different methods of clipping geometries by boxes""" def setup(self): # create irregular polygons by merging overlapping point buffers self.polygon = shapely.union_all( shapely.buffer(shapely.points(np.random.random((1000, 2)) * 500), 10) ) xmin = np.random.random(100) * 100 xmax = xmin + 100 ymin = np.random.random(100) * 100 ymax = ymin + 100 self.bounds = np.array([xmin, ymin, xmax, ymax]).T self.boxes = shapely.box(xmin, ymin, xmax, ymax) def time_clip_by_box(self): shapely.intersection(self.polygon, self.boxes) def time_clip_by_rect(self): for bounds in self.bounds: shapely.clip_by_rect(self.polygon, *bounds) class GetParts: """Benchmarks for getting individual parts from 100 multipolygons of 100 polygons each""" def setup(self): self.multipolygons = np.array( [ shapely.multipolygons(shapely.polygons(np.random.random((2, 100, 2)))) for i in range(10000) ], dtype=object, ) def time_get_parts(self): """Cython implementation of get_parts""" shapely.get_parts(self.multipolygons) def time_get_parts_python(self): """Python / ufuncs version of get_parts""" parts = [] for i in range(len(self.multipolygons)): num_parts = shapely.get_num_geometries(self.multipolygons[i]) parts.append(shapely.get_geometry(self.multipolygons[i], range(num_parts))) parts = np.concatenate(parts) class OverlaySuite: """Benchmarks for different methods of overlaying geometries""" def setup(self): # create irregular polygons by merging overlapping point buffers self.left = shapely.union_all( shapely.buffer(shapely.points(np.random.random((500, 2)) * 500), 15) ) # shift this up and right self.right = shapely.transform(self.left, lambda x: x + 50) def time_difference(self): shapely.difference(self.left, self.right) def time_difference_prec1(self): shapely.difference(self.left, self.right, grid_size=1) def time_difference_prec2(self): shapely.difference(self.left, self.right, grid_size=2) def time_intersection(self): shapely.intersection(self.left, self.right) def time_intersection_prec1(self): shapely.intersection(self.left, self.right, grid_size=1) def time_intersection_prec2(self): shapely.intersection(self.left, self.right, grid_size=2) def time_symmetric_difference(self): shapely.symmetric_difference(self.left, self.right) def time_symmetric_difference_prec1(self): shapely.symmetric_difference(self.left, self.right, grid_size=1) def time_symmetric_difference_prec2(self): shapely.symmetric_difference(self.left, self.right, grid_size=2) def time_union(self): shapely.union(self.left, self.right) def time_union_prec1(self): shapely.union(self.left, self.right, grid_size=1) def time_union_prec2(self): shapely.union(self.left, self.right, grid_size=2) def time_union_all(self): shapely.union_all([self.left, self.right]) def time_union_all_prec1(self): shapely.union_all([self.left, self.right], grid_size=1) def time_union_all_prec2(self): shapely.union_all([self.left, self.right], grid_size=2) class STRtree: """Benchmarks queries against STRtree""" def setup(self): # create irregular polygons my merging overlapping point buffers self.polygons = shapely.get_parts( shapely.union_all( shapely.buffer(shapely.points(np.random.random((2000, 2)) * 500), 5) ) ) self.tree = shapely.STRtree(self.polygons) # initialize the tree by making a tiny query first self.tree.query(shapely.points(0, 0)) # create points that extend beyond the domain of the above polygons to ensure # some don't overlap self.points = shapely.points((np.random.random((2000, 2)) * 750) - 125) self.point_tree = shapely.STRtree( shapely.points(np.random.random((2000, 2)) * 750) ) self.point_tree.query(shapely.points(0, 0)) # create points on a grid for testing equidistant nearest neighbors # creates 2025 points grid_coords = np.mgrid[:45, :45].T.reshape(-1, 2) self.grid_point_tree = shapely.STRtree(shapely.points(grid_coords)) self.grid_points = shapely.points(grid_coords + 0.5) def time_tree_create(self): tree = shapely.STRtree(self.polygons) tree.query(shapely.points(0, 0)) def time_tree_query(self): self.tree.query(self.polygons) def time_tree_query_intersects(self): self.tree.query(self.polygons, predicate="intersects") def time_tree_query_within(self): self.tree.query(self.polygons, predicate="within") def time_tree_query_contains(self): self.tree.query(self.polygons, predicate="contains") def time_tree_query_overlaps(self): self.tree.query(self.polygons, predicate="overlaps") def time_tree_query_crosses(self): self.tree.query(self.polygons, predicate="crosses") def time_tree_query_touches(self): self.tree.query(self.polygons, predicate="touches") def time_tree_query_covers(self): self.tree.query(self.polygons, predicate="covers") def time_tree_query_covered_by(self): self.tree.query(self.polygons, predicate="covered_by") def time_tree_query_contains_properly(self): self.tree.query(self.polygons, predicate="contains_properly") def time_tree_nearest_points(self): self.point_tree.nearest(self.points) def time_tree_nearest_points_equidistant(self): self.grid_point_tree.nearest(self.grid_points) def time_tree_nearest_points_equidistant_manual_all(self): # This benchmark approximates query_nearest for equidistant results # starting from singular nearest neighbors and searching for more # within same distance. # try to find all equidistant neighbors ourselves given single nearest # result l, r = self.grid_point_tree.nearest(self.grid_points) # calculate distance to nearest neighbor dist = shapely.distance( self.grid_points.take(l), self.grid_point_tree.geometries.take(r) ) # include a slight epsilon to ensure nearest are within this radius b = shapely.buffer(self.grid_points, dist + 1e-8) # query the tree for others in the same buffer distance left, right = self.grid_point_tree.query(b, predicate="intersects") dist = shapely.distance( self.grid_points.take(left), self.grid_point_tree.geometries.take(right) ) # sort by left, distance ix = np.lexsort((right, dist, left)) left = left[ix] right = right[ix] dist = dist[ix] run_start = np.r_[True, left[:-1] != left[1:]] run_counts = np.diff(np.r_[np.nonzero(run_start)[0], left.shape[0]]) mins = dist[run_start] # spread to rest of array so we can extract out all within each group that match all_mins = np.repeat(mins, run_counts) ix = dist == all_mins left = left[ix] right = right[ix] dist = dist[ix] def time_tree_query_nearest_points(self): self.point_tree.query_nearest(self.points) def time_tree_query_nearest_points_equidistant(self): self.grid_point_tree.query_nearest(self.grid_points) def time_tree_query_nearest_points_small_max_distance(self): # returns >300 results self.point_tree.query_nearest(self.points, max_distance=5) def time_tree_query_nearest_points_large_max_distance(self): # measures the overhead of using a distance that would encompass all tree points self.point_tree.query_nearest(self.points, max_distance=1000) def time_tree_nearest_poly(self): self.tree.nearest(self.points) def time_tree_query_nearest_poly(self): self.tree.query_nearest(self.points) def time_tree_query_nearest_poly_small_max_distance(self): # returns >300 results self.tree.query_nearest(self.points, max_distance=5) def time_tree_query_nearest_poly_python(self): # returns all input points # use an arbitrary search tolerance that seems appropriate for the density of # geometries tolerance = 200 b = shapely.buffer(self.points, tolerance, quad_segs=1) left, right = self.tree.query(b) dist = shapely.distance(self.points.take(left), self.polygons.take(right)) # sort by left, distance ix = np.lexsort((right, dist, left)) left = left[ix] right = right[ix] dist = dist[ix] run_start = np.r_[True, left[:-1] != left[1:]] run_counts = np.diff(np.r_[np.nonzero(run_start)[0], left.shape[0]]) mins = dist[run_start] # spread to rest of array so we can extract out all within each group that match all_mins = np.repeat(mins, run_counts) ix = dist == all_mins left = left[ix] right = right[ix] dist = dist[ix] # arrays are now roughly representative of what tree.query_nearest would provide, though # some query_nearest neighbors may be missed if they are outside tolerance shapely-2.0.3/ci/000077500000000000000000000000001456366510000135625ustar00rootroot00000000000000shapely-2.0.3/ci/install_geos.cmd000066400000000000000000000014701456366510000167340ustar00rootroot00000000000000:: Build and install GEOS on Windows system, save cache for later :: :: This script requires environment variables to be set :: - set GEOS_INSTALL=C:\path\to\cached\prefix -- to build or use as cache :: - set GEOS_VERSION=3.7.3 -- to download and compile if exist %GEOS_INSTALL% ( echo Using cached %GEOS_INSTALL% ) else ( echo Building %GEOS_INSTALL% curl -fsSO http://download.osgeo.org/geos/geos-%GEOS_VERSION%.tar.bz2 7z x geos-%GEOS_VERSION%.tar.bz2 7z x geos-%GEOS_VERSION%.tar cd geos-%GEOS_VERSION% || exit /B 1 pip install ninja cmake cmake --version mkdir build cd build cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOS_INSTALL% .. || exit /B 2 cmake --build . || exit /B 3 ctest . || exit /B 4 cmake --install . || exit /B 5 cd .. ) shapely-2.0.3/ci/install_geos.sh000077500000000000000000000037141456366510000166110ustar00rootroot00000000000000#!/bin/bash # Build and install GEOS on a POSIX system, save cache for later # # This script requires environment variables to be set # - export GEOS_INSTALL=/path/to/cached/prefix -- to build or use as cache # - export GEOS_VERSION=3.7.3 or main -- to download and compile pushd . set -e if [ -z "$GEOS_INSTALL" ]; then echo "GEOS_INSTALL must be set" exit 1 elif [ -z "$GEOS_VERSION" ]; then echo "GEOS_VERSION must be set" exit 1 fi # Create directories, if they don't exist mkdir -p $GEOS_INSTALL # Download and build GEOS outside other source tree if [ -z "$GEOS_BUILD" ]; then GEOS_BUILD=$HOME/geosbuild fi prepare_geos_build_dir(){ rm -rf $GEOS_BUILD mkdir -p $GEOS_BUILD cd $GEOS_BUILD } build_geos(){ echo "Installing cmake" pip install cmake echo "Building geos-$GEOS_VERSION" rm -rf build mkdir build cd build # Use Ninja on Windows, otherwise, use the platform's default if [ "$RUNNER_OS" = "Windows" ]; then export CMAKE_GENERATOR=Ninja fi # Avoid building tests, depends on version case ${GEOS_VERSION} in 3.5.*|3.6.*|3.7.*) BUILD_TESTING="";; 3.8.*) BUILD_TESTING="-DBUILD_TESTING=ON";; *) BUILD_TESTING="-DBUILD_TESTING=OFF";; esac cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=${GEOS_INSTALL} \ -DCMAKE_INSTALL_LIBDIR=lib \ ${BUILD_TESTING} \ .. cmake --build . -j 4 cmake --install . } if [ -d "$GEOS_INSTALL/include/geos" ]; then echo "Using cached install $GEOS_INSTALL" else if [ "$GEOS_VERSION" = "main" ]; then # Expect the CI to have put the latest checkout in GEOS_BUILD cd $GEOS_BUILD build_geos else prepare_geos_build_dir curl -OL http://download.osgeo.org/geos/geos-$GEOS_VERSION.tar.bz2 tar xfj geos-$GEOS_VERSION.tar.bz2 cd geos-$GEOS_VERSION build_geos fi fi popd shapely-2.0.3/ci/wheelbuilder/000077500000000000000000000000001456366510000162355ustar00rootroot00000000000000shapely-2.0.3/ci/wheelbuilder/LICENSE_GEOS000066400000000000000000000642531456366510000200710ustar00rootroot00000000000000This binary distribution of pygeos also bundles the following software: Name: Geometry Engine Open Source (GEOS) Files: libgeos-*.so.*, libgeos_c-*.so.*, libgeos.dylib, libgeos_c.dylib, geos-*.dll, geos_c-*.dll Availability: https://libgeos.org License: LGPLv2.1 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 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. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, 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 library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete 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 distribute a copy of this License along with the Library. 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 Library or any portion of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, 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 Library, 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 Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you 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. If distribution of 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 satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be 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. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library 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. 9. 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 Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library 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 with this License. 11. 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 Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library 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 Library. 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. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library 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. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library 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 Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, 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 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. 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 LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. 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 library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; 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. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! shapely-2.0.3/ci/wheelbuilder/LICENSE_win32000066400000000000000000000022011456366510000202570ustar00rootroot00000000000000This binary distribution of pygeos also bundles the following software: Name: Microsoft Visual C++ Runtime Files Files: msvcp140.dll License: MSVC https://www.visualstudio.com/license-terms/distributable-code-microsoft-visual-studio-2015-rc-microsoft-visual-studio-2015-sdk-rc-includes-utilities-buildserver-files/#visual-c-runtime Subject to the License Terms for the software, you may copy and distribute with your program any of the files within the followng folder and its subfolders except as noted below. You may not modify these files. C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist You may not distribute the contents of the following folders: C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist\debug_nonredist C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist\onecore\debug_nonredist Subject to the License Terms for the software, you may copy and distribute the following files with your program in your program’s application local folder or by deploying them into the Global Assembly Cache (GAC): VC\atlmfc\lib\mfcmifc80.dll VC\atlmfc\lib\amd64\mfcmifc80.dll shapely-2.0.3/docker/000077500000000000000000000000001456366510000144365ustar00rootroot00000000000000shapely-2.0.3/docker/Dockerfile.arm64000066400000000000000000000010231456366510000173540ustar00rootroot00000000000000# This docker container is used for testing shapely in ARM64 emulation mode. # To build it: # docker build . -f ./docker/Dockerfile.arm64 -t shapely/arm64 # Then run the shapely test suite: # docker run --rm shapely/arm64:latest python3 -m pytest -vv FROM --platform=linux/arm64/v8 arm64v8/ubuntu:20.04 RUN apt-get update && apt-get install -y build-essential libgeos-dev python3-dev python3-pip --no-install-recommends RUN pip3 install numpy Cython pytest COPY . /code WORKDIR /code RUN python3 setup.py build_ext --inplace shapely-2.0.3/docker/Dockerfile.valgrind000066400000000000000000000022171456366510000202370ustar00rootroot00000000000000# This docker is used for memory leak testing of shapely. To use it, first build: # docker build . -f ./docker/Dockerfile.valgrind -t shapely/valgrind # Then run the pytest suite with valgrind enabled: # docker run --rm shapely/valgrind:latest valgrind --show-leak-kinds=definite --log-file=/tmp/valgrind-output python -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output > valgrind.log FROM python:3.9-slim-buster RUN apt-get update && apt-get install -y build-essential valgrind curl --no-install-recommends RUN pip install cmake numpy Cython pytest pytest-valgrind WORKDIR /code ENV PYTHONMALLOC malloc ENV LD_LIBRARY_PATH /usr/local/lib # Build GEOS RUN export GEOS_VERSION=3.10.3 && \ mkdir /code/geos && cd /code/geos && \ curl -OL http://download.osgeo.org/geos/geos-$GEOS_VERSION.tar.bz2 && \ tar xfj geos-$GEOS_VERSION.tar.bz2 && \ cd geos-$GEOS_VERSION && mkdir build && cd build && \ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib .. && \ cmake --build . -j 4 && \ cmake --install . && \ cd /code COPY . /code # Build shapely RUN python setup.py build_ext --inplace && python setup.py install shapely-2.0.3/docs/000077500000000000000000000000001456366510000141175ustar00rootroot00000000000000shapely-2.0.3/docs/Makefile000066400000000000000000000061551456366510000155660ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* build/plot_directive/* reference/* _reference.rst html: $(SPHINXBUILD) -n -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Shapely.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Shapely.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." shapely-2.0.3/docs/_templates/000077500000000000000000000000001456366510000162545ustar00rootroot00000000000000shapely-2.0.3/docs/_templates/autosummary/000077500000000000000000000000001456366510000206425ustar00rootroot00000000000000shapely-2.0.3/docs/_templates/autosummary/class.rst000066400000000000000000000002061456366510000224770ustar00rootroot00000000000000{{ fullname | escape | underline}} .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} :members: :inherited-members: shapely-2.0.3/docs/code/000077500000000000000000000000001456366510000150315ustar00rootroot00000000000000shapely-2.0.3/docs/code/buffer.py000066400000000000000000000015251456366510000166570ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LineString from shapely.plotting import plot_polygon, plot_line from figures import SIZE, BLUE, GRAY, set_limits line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) fig = plt.figure(1, figsize=SIZE, dpi=90) # 1 ax = fig.add_subplot(121) plot_line(line, ax=ax, add_points=False, color=GRAY, linewidth=3) dilated = line.buffer(0.5, cap_style=3) plot_polygon(dilated, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a) dilation, cap_style=3') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) plot_polygon(dilated, ax=ax, add_points=False, color=GRAY, alpha=0.5) eroded = dilated.buffer(-0.3) plot_polygon(eroded, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('b) erosion, join_style=1') set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/buffer_single_side.py000066400000000000000000000015761456366510000212320ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LineString from shapely.plotting import plot_polygon, plot_line from figures import SIZE, BLUE, GRAY, set_limits line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) fig = plt.figure(1, figsize=SIZE, dpi=90) # 1 ax = fig.add_subplot(121) plot_line(line, ax=ax, add_points=False, color=GRAY, linewidth=3) left_hand_side = line.buffer(0.5, single_sided=True) plot_polygon(left_hand_side, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a) left hand buffer') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) plot_line(line, ax=ax, add_points=False, color=GRAY, linewidth=3) right_hand_side = line.buffer(-0.3, single_sided=True) plot_polygon(right_hand_side, ax=ax, add_points=False, color=GRAY, alpha=0.5) ax.set_title('b) right hand buffer') set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/convex_hull.py000066400000000000000000000014471456366510000177370ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPoint from shapely.plotting import plot_polygon, plot_line, plot_points from figures import GRAY, BLUE, SIZE, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1 ax = fig.add_subplot(121) points2 = MultiPoint([(0, 0), (2, 2)]) plot_points(points2, ax=ax, color=GRAY) hull2 = points2.convex_hull plot_line(hull2, ax=ax, add_points=False, color=BLUE, zorder=3) ax.set_title('a) N = 2') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) points1 = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) plot_points(points1, ax=ax, color=GRAY) hull1 = points1.convex_hull plot_polygon(hull1, ax=ax, add_points=False, color=BLUE, zorder=3, alpha=0.5) ax.set_title('b) N > 2') set_limits(ax, -1, 4, -1, 3) plt.show()shapely-2.0.3/docs/code/difference.py000066400000000000000000000015661456366510000175050ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Point from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) a = Point(1, 1).buffer(1.5) b = Point(2, 1).buffer(1.5) # 1 ax = fig.add_subplot(121) plot_polygon(a, ax=ax, add_points=False, color=GRAY, alpha=0.2) plot_polygon(b, ax=ax, add_points=False, color=GRAY, alpha=0.2) c = a.difference(b) plot_polygon(c, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a.difference(b)') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) plot_polygon(a, ax=ax, add_points=False, color=GRAY, alpha=0.2) plot_polygon(b, ax=ax, add_points=False, color=GRAY, alpha=0.2) c = b.difference(a) plot_polygon(c, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('b.difference(a)') set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/figures.py000066400000000000000000000012241456366510000170460ustar00rootroot00000000000000from math import sqrt from shapely import affinity GM = (sqrt(5)-1.0)/2.0 W = 8.0 H = W*GM SIZE = (W, H) BLUE = '#6699cc' GRAY = '#999999' DARKGRAY = '#333333' YELLOW = '#ffcc33' GREEN = '#339933' RED = '#ff3333' BLACK = '#000000' def add_origin(ax, geom, origin): x, y = xy = affinity.interpret_origin(geom, origin, 2) ax.plot(x, y, 'o', color=GRAY, zorder=1) ax.annotate(str(xy), xy=xy, ha='center', textcoords='offset points', xytext=(0, 8)) def set_limits(ax, x0, xN, y0, yN): ax.set_xlim(x0, xN) ax.set_xticks(range(x0, xN+1)) ax.set_ylim(y0, yN) ax.set_yticks(range(y0, yN+1)) ax.set_aspect("equal") shapely-2.0.3/docs/code/geometrycollection.py000066400000000000000000000017141456366510000213150ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LineString from shapely.plotting import plot_line, plot_points from figures import BLUE, GRAY, YELLOW, GREEN, SIZE, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) a = LineString([(0, 0), (1, 1), (1,2), (2,2)]) b = LineString([(0, 0), (1, 1), (2,1), (2,2)]) # 1: disconnected multilinestring ax = fig.add_subplot(121) plot_line(a, ax, add_points=False, color=YELLOW, alpha=0.5) plot_line(b, ax, add_points=False, color=GREEN, alpha=0.5) plot_points(a, ax=ax, color=GRAY) plot_points(b, ax=ax, color=GRAY) ax.set_title('a) lines') set_limits(ax, -1, 3, -1, 3) #2: invalid self-touching ring ax = fig.add_subplot(122) x = a.intersection(b) plot_line(a, ax=ax, color=GRAY, add_points=False) plot_line(b, ax=ax, color=GRAY, add_points=False) plot_line(x.geoms[0], ax=ax, color=BLUE) plot_points(x.geoms[1], ax=ax, color=BLUE) ax.set_title('b) collection') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/intersection-sym-difference.py000066400000000000000000000016161456366510000230130ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Point from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) a = Point(1, 1).buffer(1.5) b = Point(2, 1).buffer(1.5) # 1 ax = fig.add_subplot(121) plot_polygon(a, ax=ax, add_points=False, color=GRAY, alpha=0.2) plot_polygon(b, ax=ax, add_points=False, color=GRAY, alpha=0.2) c = a.intersection(b) plot_polygon(c, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a.intersection(b)') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) plot_polygon(a, ax=ax, add_points=False, color=GRAY, alpha=0.2) plot_polygon(b, ax=ax, add_points=False, color=GRAY, alpha=0.2) c = a.symmetric_difference(b) plot_polygon(c, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a.symmetric_difference(b)') set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/linearring.py000066400000000000000000000014771456366510000175460ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LinearRing from shapely.plotting import plot_line, plot_points from figures import SIZE, BLUE, GRAY, RED, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1: valid ring ax = fig.add_subplot(121) ring = LinearRing([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 0.8), (0, 0)]) plot_line(ring, ax=ax, add_points=False, color=BLUE, alpha=0.7) plot_points(ring, ax=ax, color=GRAY, alpha=0.7) ax.set_title('a) valid') set_limits(ax, -1, 3, -1, 3) #2: invalid self-touching ring ax = fig.add_subplot(122) ring2 = LinearRing([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]) plot_line(ring2, ax=ax, add_points=False, color=RED, alpha=0.7) plot_points(ring2, ax=ax, color=GRAY, alpha=0.7) ax.set_title('b) invalid') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/linestring.py000066400000000000000000000016171456366510000175660ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LineString from shapely.plotting import plot_line, plot_points from figures import SIZE, BLACK, BLUE, GRAY, YELLOW, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1: simple line ax = fig.add_subplot(121) line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) plot_line(line, ax=ax, add_points=False, color=BLUE, alpha=0.7) plot_points(line, ax=ax, color=GRAY, alpha=0.7) plot_points(line.boundary, ax=ax, color=BLACK) ax.set_title('a) simple') set_limits(ax, -1, 4, -1, 3) # 2: complex line ax = fig.add_subplot(122) line2 = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (-1, 1), (1, 0)]) plot_line(line2, ax=ax, add_points=False, color=YELLOW, alpha=0.7) plot_points(line2, ax=ax, color=GRAY, alpha=0.7) plot_points(line2.boundary, ax=ax, color=BLACK) ax.set_title('b) complex') set_limits(ax, -2, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/make_valid_geometrycollection.py000066400000000000000000000013441456366510000234700ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Polygon from shapely.validation import make_valid from shapely.plotting import plot_polygon, plot_line from figures import SIZE, BLUE, RED, set_limits invalid_poly = Polygon([(0, 2), (0, 1), (2, 0), (0, 0), (0, 2)]) valid_poly = make_valid(invalid_poly) fig = plt.figure(1, figsize=SIZE, dpi=90) invalid_ax = fig.add_subplot(121) plot_polygon(invalid_poly, ax=invalid_ax, add_points=False, color=BLUE) set_limits(invalid_ax, -1, 3, -1, 3) valid_ax = fig.add_subplot(122) plot_polygon(valid_poly.geoms[0], ax=valid_ax, add_points=False, color=BLUE) plot_line(valid_poly.geoms[1], ax=valid_ax, add_points=False, color=RED) set_limits(valid_ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/make_valid_multipolygon.py000066400000000000000000000013531456366510000223230ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Polygon from shapely.validation import make_valid from shapely.plotting import plot_polygon from figures import SIZE, BLUE, RED, set_limits invalid_poly = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]) valid_poly = make_valid(invalid_poly) fig = plt.figure(1, figsize=SIZE, dpi=90) invalid_ax = fig.add_subplot(121) plot_polygon(invalid_poly, ax=invalid_ax, add_points=False, color=BLUE) set_limits(invalid_ax, -1, 3, -1, 3) valid_ax = fig.add_subplot(122) plot_polygon(valid_poly.geoms[0], ax=valid_ax, add_points=False, color=BLUE) plot_polygon(valid_poly.geoms[1], ax=valid_ax, add_points=False, color=RED) set_limits(valid_ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/minimum_rotated_rectangle.py000066400000000000000000000016341456366510000226300ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPoint, LineString from shapely.plotting import plot_polygon, plot_line, plot_points from figures import DARKGRAY, GRAY, BLUE, SIZE, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1 ax = fig.add_subplot(121) mp = MultiPoint([(0, 0), (0.5, 1.5), (1, 0.5), (0.5, 0.5)]) rect = mp.minimum_rotated_rectangle plot_points(mp, ax=ax, color=GRAY) plot_polygon(rect, ax=ax, add_points=False, color=BLUE, alpha=0.5, zorder=-1) ax.set_title('a) MultiPoint') set_limits(ax, -1, 2, -1, 2) # 2 ax = fig.add_subplot(122) ls = LineString([(-0.5, 1.2), (0.5, 0), (1, 1), (1.5, 0), (1.5, 0.5)]) rect = ls.minimum_rotated_rectangle plot_line(ls, ax=ax, add_points=False, color=DARKGRAY, linewidth=3, alpha=0.5) plot_polygon(rect, ax=ax, add_points=False, color=BLUE, alpha=0.5, zorder=-1) set_limits(ax, -1, 2, -1, 2) ax.set_title('b) LineString') plt.show() shapely-2.0.3/docs/code/multilinestring.py000066400000000000000000000016531456366510000206410ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiLineString from shapely.plotting import plot_line, plot_points from figures import SIZE, BLACK, BLUE, GRAY, YELLOW, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1: disconnected multilinestring ax = fig.add_subplot(121) mline1 = MultiLineString([((0, 0), (1, 1)), ((0, 2), (1, 1.5), (1.5, 1), (2, 0))]) plot_line(mline1, ax=ax, color=BLUE) plot_points(mline1, ax=ax, color=GRAY, alpha=0.7) plot_points(mline1.boundary, ax=ax, color=BLACK) ax.set_title('a) simple') set_limits(ax, -1, 3, -1, 3) #2: invalid self-touching ring ax = fig.add_subplot(122) mline2 = MultiLineString([((0, 0), (1, 1), (1.5, 1)), ((0, 2), (1, 1.5), (1.5, 1), (2, 0))]) plot_line(mline2, ax=ax, color=YELLOW) plot_points(mline2, ax=ax, color=GRAY, alpha=0.7) plot_points(mline2.boundary, ax=ax, color=BLACK) ax.set_title('b) complex') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/multipolygon.py000066400000000000000000000017021456366510000201450ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPolygon from shapely.plotting import plot_polygon, plot_points from figures import SIZE, BLUE, GRAY, RED, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1: valid multi-polygon ax = fig.add_subplot(121) a = [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)] b = [(1, 1), (1, 2), (2, 2), (2, 1), (1, 1)] multi1 = MultiPolygon([[a, []], [b, []]]) plot_polygon(multi1, ax=ax, add_points=False, color=BLUE) plot_points(multi1, ax=ax, color=GRAY, alpha=0.7) ax.set_title('a) valid') set_limits(ax, -1, 3, -1, 3) #2: invalid self-touching ring ax = fig.add_subplot(122) c = [(0, 0), (0, 1.5), (1, 1.5), (1, 0), (0, 0)] d = [(1, 0.5), (1, 2), (2, 2), (2, 0.5), (1, 0.5)] multi2 = MultiPolygon([[c, []], [d, []]]) plot_polygon(multi2, ax=ax, add_points=False, color=RED) plot_points(multi2, ax=ax, color=GRAY, alpha=0.7) ax.set_title('b) invalid') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/parallel_offset.py000066400000000000000000000032501456366510000205450ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely import LineString, get_point from shapely.plotting import plot_line, plot_points from figures import SIZE, BLUE, GRAY, set_limits line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) line_bounds = line.bounds ax_range = [int(line_bounds[0] - 1.0), int(line_bounds[2] + 1.0)] ay_range = [int(line_bounds[1] - 1.0), int(line_bounds[3] + 1.0)] fig = plt.figure(1, figsize=(SIZE[0], 1.5 * SIZE[1]), dpi=90) # 1 ax = fig.add_subplot(221) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'left', join_style=1) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('a) left, round') set_limits(ax, -2, 4, -1, 3) #2 ax = fig.add_subplot(222) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'left', join_style=2) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('b) left, mitred') set_limits(ax, -2, 4, -1, 3) #3 ax = fig.add_subplot(223) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'left', join_style=3) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('c) left, beveled') set_limits(ax, -2, 4, -1, 3) #4 ax = fig.add_subplot(224) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'right', join_style=1) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('d) right, round') set_limits(ax, -2, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/parallel_offset_mitre.py000066400000000000000000000033771456366510000217570ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely import LineString, get_point from shapely.plotting import plot_line, plot_points from figures import SIZE, BLUE, GRAY, set_limits line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) line_bounds = line.bounds ax_range = [int(line_bounds[0] - 1.0), int(line_bounds[2] + 1.0)] ay_range = [int(line_bounds[1] - 1.0), int(line_bounds[3] + 1.0)] fig = plt.figure(1, figsize=(SIZE[0], 1.5 * SIZE[1]), dpi=90) # 1 ax = fig.add_subplot(221) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'left', join_style=2, mitre_limit=0.1) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('a) left, limit=0.1') set_limits(ax, -2, 4, -1, 3) #2 ax = fig.add_subplot(222) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'left', join_style=2, mitre_limit=10.0) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('b) left, limit=10.0') set_limits(ax, -2, 4, -1, 3) #3 ax = fig.add_subplot(223) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'right', join_style=2, mitre_limit=0.1) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('c) right, limit=0.1') set_limits(ax, -2, 4, -1, 3) #4 ax = fig.add_subplot(224) plot_line(line, ax, add_points=False, color=GRAY) plot_points(get_point(line, 0), ax=ax, color=GRAY) offset = line.parallel_offset(0.5, 'right', join_style=2, mitre_limit=10.0) plot_line(offset, ax=ax, add_points=False, color=BLUE) ax.set_title('d) right, limit=10.0') set_limits(ax, -2, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/polygon.py000066400000000000000000000016661456366510000171030ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Polygon from shapely.plotting import plot_polygon, plot_points from figures import SIZE, BLUE, GRAY, RED, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 1: valid polygon ax = fig.add_subplot(121) ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int = [(1, 0), (0.5, 0.5), (1, 1), (1.5, 0.5), (1, 0)][::-1] polygon = Polygon(ext, [int]) plot_polygon(polygon, ax=ax, add_points=False, color=BLUE) plot_points(polygon, ax=ax, color=GRAY, alpha=0.7) ax.set_title('a) valid') set_limits(ax, -1, 3, -1, 3) #2: invalid self-touching ring ax = fig.add_subplot(122) ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int = [(1, 0), (0, 1), (0.5, 1.5), (1.5, 0.5), (1, 0)][::-1] polygon = Polygon(ext, [int]) plot_polygon(polygon, ax=ax, add_points=False, color=RED) plot_points(polygon, ax=ax, color=GRAY, alpha=0.7) ax.set_title('b) invalid') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/polygon2.py000066400000000000000000000020271456366510000171550ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Polygon from shapely.plotting import plot_polygon, plot_points from figures import GRAY, RED, SIZE, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) # 3: invalid polygon, ring touch along a line ax = fig.add_subplot(121) ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int = [(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1), (0.5, 0)] polygon = Polygon(ext, [int]) plot_polygon(polygon, ax=ax, add_points=False, color=RED) plot_points(polygon, ax=ax, color=GRAY, alpha=0.7) ax.set_title('c) invalid') set_limits(ax, -1, 3, -1, 3) #4: invalid self-touching ring ax = fig.add_subplot(122) ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)] int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)] int_2 = [(0.5, 1.25), (1, 1.25), (1, 1.75), (0.5, 1.75)] polygon = Polygon(ext, [int_1, int_2]) plot_polygon(polygon, ax=ax, add_points=False, color=RED) plot_points(polygon, ax=ax, color=GRAY, alpha=0.7) ax.set_title('d) invalid') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/rotate.py000066400000000000000000000015541456366510000167060ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import LineString from shapely import affinity from shapely.plotting import plot_line from figures import SIZE, BLUE, GRAY, set_limits, add_origin fig = plt.figure(1, figsize=SIZE, dpi=90) line = LineString([(1, 3), (1, 1), (4, 1)]) # 1 ax = fig.add_subplot(121) plot_line(line, ax=ax, add_points=False, color=GRAY) plot_line(affinity.rotate(line, 90, 'center'), ax=ax, add_points=False, color=BLUE) add_origin(ax, line, 'center') ax.set_title("90\N{DEGREE SIGN}, default origin (center)") set_limits(ax, 0, 5, 0, 4) # 2 ax = fig.add_subplot(122) plot_line(line, ax=ax, add_points=False, color=GRAY) plot_line(affinity.rotate(line, 90, 'centroid'), ax=ax, add_points=False, color=BLUE) add_origin(ax, line, 'centroid') ax.set_title("90\N{DEGREE SIGN}, origin='centroid'") set_limits(ax, 0, 5, 0, 4) plt.show() shapely-2.0.3/docs/code/scale.py000066400000000000000000000017311456366510000164740ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Polygon from shapely import affinity from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits, add_origin fig = plt.figure(1, figsize=SIZE, dpi=90) triangle = Polygon([(1, 1), (2, 3), (3, 1)]) # 1 ax = fig.add_subplot(121) plot_polygon(triangle, ax=ax, add_points=False, color=GRAY, alpha=0.5) triangle_a = affinity.scale(triangle, xfact=1.5, yfact=-1) plot_polygon(triangle_a, ax=ax, add_points=False, color=BLUE, alpha=0.5) add_origin(ax, triangle, 'center') ax.set_title("a) xfact=1.5, yfact=-1") set_limits(ax, 0, 5, 0, 4) # 2 ax = fig.add_subplot(122) plot_polygon(triangle, ax=ax, add_points=False, color=GRAY, alpha=0.5) triangle_b = affinity.scale(triangle, xfact=2, origin=(1, 1)) plot_polygon(triangle_b, ax=ax, add_points=False, color=BLUE, alpha=0.5) add_origin(ax, triangle, (1, 1)) ax.set_title("b) xfact=2, origin=(1, 1)") set_limits(ax, 0, 5, 0, 4) plt.show() shapely-2.0.3/docs/code/simplify.py000066400000000000000000000013501456366510000172360ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPoint, Point from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) p = Point(1, 1).buffer(1.5) # 1 ax = fig.add_subplot(121) q = p.simplify(0.2) plot_polygon(p, ax=ax, add_points=False, color=GRAY, alpha=0.5) plot_polygon(q, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a) tolerance 0.2') set_limits(ax, -1, 3, -1, 3) #2 ax = fig.add_subplot(122) r = p.simplify(0.5) plot_polygon(p, ax=ax, add_points=False, color=GRAY, alpha=0.5) plot_polygon(r, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('b) tolerance 0.5') set_limits(ax, -1, 3, -1, 3) plt.show() shapely-2.0.3/docs/code/skew.py000066400000000000000000000033551456366510000163620ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.wkt import loads as load_wkt from shapely import affinity from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits, add_origin fig = plt.figure(1, figsize=SIZE, dpi=90) # Geometry from JTS TestBuilder with fixed precision model of 100.0 # Using CreateShape > FontGlyphSanSerif and A = triangle.wkt from scale.py R = load_wkt('''\ POLYGON((2.218 2.204, 2.273 2.18, 2.328 2.144, 2.435 2.042, 2.541 1.895, 2.647 1.702, 3 1, 2.626 1, 2.298 1.659, 2.235 1.777, 2.173 1.873, 2.112 1.948, 2.051 2.001, 1.986 2.038, 1.91 2.064, 1.823 2.08, 1.726 2.085, 1.347 2.085, 1.347 1, 1 1, 1 3.567, 1.784 3.567, 1.99 3.556, 2.168 3.521, 2.319 3.464, 2.441 3.383, 2.492 3.334, 2.536 3.279, 2.604 3.152, 2.644 3.002, 2.658 2.828, 2.651 2.712, 2.63 2.606, 2.594 2.51, 2.545 2.425, 2.482 2.352, 2.407 2.29, 2.319 2.241, 2.218 2.204), (1.347 3.282, 1.347 2.371, 1.784 2.371, 1.902 2.378, 2.004 2.4, 2.091 2.436, 2.163 2.487, 2.219 2.552, 2.259 2.63, 2.283 2.722, 2.291 2.828, 2.283 2.933, 2.259 3.025, 2.219 3.103, 2.163 3.167, 2.091 3.217, 2.004 3.253, 1.902 3.275, 1.784 3.282, 1.347 3.282))''') # 1 ax = fig.add_subplot(121) plot_polygon(R, ax=ax, add_points=False, color=GRAY, alpha=0.5) skewR = affinity.skew(R, xs=20, origin=(1, 1)) plot_polygon(skewR, ax=ax, add_points=False, color=BLUE, alpha=0.5) add_origin(ax, R, (1, 1)) ax.set_title("a) xs=20, origin(1, 1)") set_limits(ax, 0, 5, 0, 4) # 2 ax = fig.add_subplot(122) plot_polygon(R, ax=ax, add_points=False, color=GRAY, alpha=0.5) skewR = affinity.skew(R, ys=30) plot_polygon(skewR, ax=ax, add_points=False, color=BLUE, alpha=0.5) add_origin(ax, R, 'center') ax.set_title("b) ys=30") set_limits(ax, 0, 5, 0, 4) plt.show() shapely-2.0.3/docs/code/triangulate.py000066400000000000000000000010601456366510000177170ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPoint from shapely.ops import triangulate from shapely.plotting import plot_polygon, plot_points from figures import SIZE, BLUE, GRAY, set_limits points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) triangles = triangulate(points) fig = plt.figure(1, figsize=SIZE, dpi=90) ax = fig.add_subplot(111) for triangle in triangles: plot_polygon(triangle, ax=ax, add_points=False, color=BLUE) plot_points(points, ax=ax, color=GRAY) set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/unary_union.py000066400000000000000000000012011456366510000177430ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Point from shapely.ops import unary_union from shapely.plotting import plot_polygon from figures import SIZE, BLUE, GRAY, set_limits polygons = [Point(i, 0).buffer(0.7) for i in range(5)] fig = plt.figure(1, figsize=SIZE, dpi=90) # 1 ax = fig.add_subplot(121) for ob in polygons: plot_polygon(ob, ax=ax, add_points=False, color=GRAY) ax.set_title('a) polygons') set_limits(ax, -2, 6, -2, 2) #2 ax = fig.add_subplot(122) u = unary_union(polygons) plot_polygon(u, ax=ax, add_points=False, color=BLUE) ax.set_title('b) union') set_limits(ax, -2, 6, -2, 2) plt.show() shapely-2.0.3/docs/code/union.py000066400000000000000000000016401456366510000165340ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import Point from shapely.plotting import plot_polygon, plot_line from figures import SIZE, BLUE, GRAY, set_limits fig = plt.figure(1, figsize=SIZE, dpi=90) a = Point(1, 1).buffer(1.5) b = Point(2, 1).buffer(1.5) # 1 ax = fig.add_subplot(121) plot_polygon(a, ax=ax, add_points=False, color=GRAY, alpha=0.2) plot_polygon(b, ax=ax, add_points=False, color=GRAY, alpha=0.2) c = a.union(b) plot_polygon(c, ax=ax, add_points=False, color=BLUE, alpha=0.5) ax.set_title('a.union(b)') set_limits(ax, -1, 4, -1, 3) #2 ax = fig.add_subplot(122) plot_line(a.exterior, ax=ax, add_points=False, color=GRAY, linewidth=3) plot_line(b.exterior, ax=ax, add_points=False, color=GRAY, linewidth=3) u = a.exterior.union(b.exterior) plot_line(u, ax=ax, add_points=False, color=BLUE, linewidth=3) ax.set_title('a.boundary.union(b.boundary)') set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/code/voronoi_diagram.py000066400000000000000000000010661456366510000205650ustar00rootroot00000000000000import matplotlib.pyplot as plt from shapely.geometry import MultiPoint from shapely.ops import voronoi_diagram from shapely.plotting import plot_polygon, plot_points from figures import SIZE, BLUE, GRAY, set_limits points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) regions = voronoi_diagram(points) fig = plt.figure(1, figsize=SIZE, dpi=90) ax = fig.add_subplot(111) for region in regions.geoms: plot_polygon(region, ax=ax, add_points=False, color=BLUE) plot_points(points, ax=ax, color=GRAY) set_limits(ax, -1, 4, -1, 3) plt.show() shapely-2.0.3/docs/conf.py000066400000000000000000000104241456366510000154170ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # set an environment variable for shapely.decorators.requires_geos to see if we # are in a doc build import os os.environ["SPHINX_DOC_BUILD"] = "1" # -- Project information ----------------------------------------------------- project = 'Shapely' copyright = '2011-2024, Sean Gillies and Shapely contributors' # The full version, including alpha/beta/rc tags. import shapely release = shapely.__version__.split("+")[0] # -- 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 = [ 'matplotlib.sphinxext.plot_directive', 'sphinx.ext.intersphinx', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.mathjax', 'numpydoc', 'sphinx_remove_toctrees' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # 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', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_book_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # html_css_files = [ # 'custom.css', # ] # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # -- Options for LaTeX output -------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Shapely.tex', 'Shapely Documentation', 'Sean Gillies', 'manual'), ] # --Options for sphinx extensions ----------------------------------------------- # connect docs in other projects intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'numpy': ('https://numpy.org/doc/stable/', None), } plot_rcparams = { 'savefig.bbox': "tight" } # -- Automatic generation of API reference pages ----------------------------- numpydoc_show_class_members = False autosummary_generate = True remove_from_toctrees = ["reference/*"] def rstjinja(app, docname, source): """ Render our pages as a jinja template for fancy templating goodness. """ # https://www.ericholscher.com/blog/2016/jul/25/integrating-jinja-rst-sphinx/ # Make sure we're outputting HTML if app.builder.format != 'html': return source[0] = app.builder.templates.render_string(source[0], app.config.html_context) def get_module_functions(module, exclude=None): """Return a list of function names for the given submodule.""" mod = getattr(shapely, module) return mod.__all__ html_context = { 'get_module_functions': get_module_functions } # write dummy _reference.rst with all functions listed to ensure the reference/ # stub pages are crated (the autogeneration of those stub pages by autosummary # happens before the jinja rendering is done, and thus at that point the # autosummary directives do not yet contain the final content template = """ :orphan: .. autogenerated file .. currentmodule:: shapely .. autosummary:: :toctree: reference/ """ modules = [ "_geometry", "creation", "constructive", "coordinates", "io", "linear", "measurement", "predicates", "set_operations"] functions = [func for mod in modules for func in get_module_functions(mod)] template += " " + "\n ".join(functions) with open("_reference.rst", "w") as f: f.write(template) def setup(app): app.connect("source-read", rstjinja) shapely-2.0.3/docs/constructive.rst000066400000000000000000000003211456366510000173750ustar00rootroot00000000000000Constructive operations ======================= .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("constructive") %} {{ function }} {% endfor %} shapely-2.0.3/docs/coordinates.rst000066400000000000000000000003141456366510000171610ustar00rootroot00000000000000Coordinate operations ===================== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("coordinates") %} {{ function }} {% endfor %} shapely-2.0.3/docs/creation.rst000066400000000000000000000003241456366510000164540ustar00rootroot00000000000000.. _ref-creation: Geometry creation ================= .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("creation") %} {{ function }} {% endfor %} shapely-2.0.3/docs/design.rst000066400000000000000000000026041456366510000161240ustar00rootroot00000000000000============ Design Notes ============ Shapely provides classes that implement, more or less, the interfaces in the OGC's simple feature access specification [1]_. The classes are defined in similarly named modules under ``shapely.geometry``: ``Point`` is in ``shapely.geometry.point``, ``MultiPolygon`` is in ``shapely.geometry.multipolygon``. These classes derive from ``shapely.geometry.base.BaseGeometry``. The simple features methods of ``BaseGeometry`` call functions registered in a class variable ``impl``. For example, ``BaseGeometry.area`` calls ``BaseGeometry.impl['area']``. The default registry is in the ``shapely.impl`` module. Its items are classes that operate on single geometric objects or pairs of geometric objects. Pluggability is a goal of this design, but we're not there yet. Some work needs to be done before anybody can use CGAL as a Shapely backend. In sum, Shapely's stack is 4 layers: * Python geometry classes in ``shapely.geometry`` * An implementation registry: an abstraction that permits alternate geometry engines, even a mix of geometry engines. The default is in ``shapely.impl``. * The GEOS implementations of methods for the registry in ``shapely.geos``. * libgeos: algorithms written in C++. .. [1] John R. Herring, Ed., “OpenGIS Implementation Specification for Geographic information - Simple feature access - Part 1: Common architecture,” Oct. 2006. shapely-2.0.3/docs/environment.yml000066400000000000000000000003071456366510000172060ustar00rootroot00000000000000name: shapely_docs channels: - conda-forge dependencies: - python=3.10 - geos=3.11 - numpy - cython - sphinx-book-theme - sphinx-remove-toctrees - numpydoc=1.1 - matplotlib - pip shapely-2.0.3/docs/geometry.rst000066400000000000000000000122061456366510000165050ustar00rootroot00000000000000Geometry ======== Shapely geometry classes, such as ``shapely.Point``, are the central data types in Shapely. Each geometry class extends the ``shapely.Geometry`` base class, which is a container of the underlying GEOS geometry object, to provide geometry type-specific attributes and behavior. The ``Geometry`` object keeps track of the underlying GEOS geometry and lets the python garbage collector free its memory when it is not used anymore. Geometry objects are immutable. This means that after constructed, they cannot be changed in place. Every Shapely operation will result in a new object being returned. Geometry types ~~~~~~~~~~~~~~ .. currentmodule:: shapely .. autosummary:: :toctree: reference/ Point LineString LinearRing Polygon MultiPoint MultiLineString MultiPolygon GeometryCollection Construction ~~~~~~~~~~~~ Geometries can be constructed directly using Shapely geometry classes: .. code:: python >>> from shapely import Point, LineString >>> Point(5.2, 52.1) >>> LineString([(0, 0), (1, 2)]) Geometries can also be constructed from a WKT (Well-Known Text) or WKB (Well-Known Binary) representation: .. code:: python >>> from shapely import from_wkb, from_wkt >>> from_wkt("POINT (5.2 52.1)") >>> from_wkb(b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?") A more efficient way of constructing geometries is by making use of the (vectorized) functions described in :ref:`ref-creation`. Pickling ~~~~~~~~ Geometries can be serialized using pickle: >>> import pickle >>> from shapely import Point >>> pickled = pickle.dumps(Point(1, 1)) >>> pickle.loads(pickled) .. warning:: Pickling will convert linearrings to linestrings. See :func:`shapely.to_wkb` for a complete list of limitations. Hashing ~~~~~~~ Geometries can be used as elements in sets or as keys in dictionaries. Python uses a technique called *hashing* for lookups in these datastructures. Shapely generates this hash from the WKB representation. Therefore, geometries are equal if and only if their WKB representations are equal. .. code:: python >>> from shapely import Point >>> point_1 = Point(5.2, 52.1) >>> point_2 = Point(1, 1) >>> point_3 = Point(5.2, 52.1) >>> {point_1, point_2, point_3} {, } .. warning:: Due to limitations of WKB, linearrings will equal linestrings if they contain the exact same points. See :func:`shapely.to_wkb`. Comparing two geometries directly is also supported. This is the same as using :func:`shapely.equals_exact` with a ``tolerance`` value of zero. >>> point_1 == point_2 False >>> point_1 == point_3 True >>> point_1 != point_2 True Formatting ~~~~~~~~~~ Geometries can be formatted to strings using properties, functions, or a Python format specification. The most convenient is to use ``.wkb_hex`` and ``.wkt`` properties. .. code:: python >>> from shapely import Point, to_wkb, to_wkt, to_geojson >>> pt = Point(-169.910918, -18.997564) >>> pt.wkb_hex 0101000000CF6A813D263D65C0BDAAB35A60FF32C0 >>> pt.wkt POINT (-169.910918 -18.997564) More output options can be found using using :func:`~shapely.to_wkb`, :func:`~shapely.to_wkt`, and :func:`~shapely.to_geojson` functions. .. code:: python >>> to_wkb(pt, hex=True, byte_order=0) 0000000001C0653D263D816ACFC032FF605AB3AABD >>> to_wkt(pt, rounding_precision=3) POINT (-169.911 -18.998) >>> print(to_geojson(pt, indent=2)) { "type": "Point", "coordinates": [ -169.910918, -18.997564 ] } A format specification may also be used to control the format and precision. .. code:: python >>> print(f"Cave near {pt:.3f}") Cave near POINT (-169.911 -18.998) >>> print(f"or hex-encoded as {pt:x}") or hex-encoded as 0101000000cf6a813d263d65c0bdaab35a60ff32c0 Shapely has a format specification inspired from Python's :ref:`python:formatspec`, described next. Semantic for format specification --------------------------------- .. productionlist:: format-spec format_spec: [0][.`precision`][`type`] precision: `digit`+ digit: "0"..."9" type: "f" | "F" | "g" | "G" | "x" | "X" Format types ``'f'`` and ``'F'`` are to use a fixed-point notation, which is activated by setting GEOS' trim option off. The upper case variant converts ``nan`` to ``NAN`` and ``inf`` to ``INF``. Format types ``'g'`` and ``'G'`` are to use a "general format", where unnecessary digits are trimmed. This notation is activated by setting GEOS' trim option on. The upper case variant is similar to ``'F'``, and may also display an upper-case ``"E"`` if scientific notation is required. Note that this representation may be different for GEOS 3.10.0 and later, which does not use scientific notation. For numeric outputs ``'f'`` and ``'g'``, the precision is optional, and if not specified, rounding precision will be disabled showing full precision. Format types ``'x'`` and ``'X'`` show a hex-encoded string representation of WKB or Well-Known Binary, with the case of the output matched the case of the format type character. shapely-2.0.3/docs/index.rst000066400000000000000000000010061456366510000157550ustar00rootroot00000000000000.. include:: ../README.rst .. include:: ../CREDITS.txt .. include:: ../FAQ.rst .. toctree:: :caption: User Guide :hidden: installation User Manual migration migration_pygeos release .. toctree:: :caption: API Reference :hidden: geometry properties creation io measurement predicates set_operations constructive linear coordinates strtree testing Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` shapely-2.0.3/docs/installation.rst000066400000000000000000000137271456366510000173640ustar00rootroot00000000000000Installation ============ Built distributions ------------------- Built distributions don't require compiling Shapely and its dependencies, and can be installed using ``pip`` or ``conda``. In addition, Shapely is also available via some system package management tools like apt. Installation from PyPI ^^^^^^^^^^^^^^^^^^^^^^ Shapely is available as a binary distribution (wheel) for Linux, macOS, and Windows platforms on `PyPI `__. The distribution includes the most recent version of GEOS available at the time of the Shapely release. Install the binary wheel with pip as follows:: $ pip install shapely Installation using conda ^^^^^^^^^^^^^^^^^^^^^^^^ Shapely is available on the conda-forge channel. Install as follows:: $ conda install shapely --channel conda-forge Installation from source with custom GEOS libary ------------------------------------------------ You may want to use a specific GEOS version or a GEOS distribution that is already present on your system (for compatibility with other modules that depend on GEOS, such as cartopy or osgeo.ogr). In such cases you will need to ensure the GEOS library is installed on your system and then compile Shapely from source yourself, by directing pip to ignore the binary wheels. On Linux:: $ sudo apt install libgeos-dev # skip this if you already have GEOS $ pip install shapely --no-binary shapely On macOS:: $ brew install geos # skip this if you already have GEOS $ pip install shapely --no-binary shapely If you've installed GEOS to a standard location on Linux or macOS, the installation will automatically find it using ``geos-config``. See the notes below on GEOS discovery at compile time to configure this. We do not have a recipe for Windows platforms. The following steps should enable you to build Shapely yourself: - Get a C compiler applicable to your Python version (https://wiki.python.org/moin/WindowsCompilers) - Download and install a GEOS binary (https://trac.osgeo.org/osgeo4w/) - Set GEOS_INCLUDE_PATH and GEOS_LIBRARY_PATH environment variables (see below for notes on GEOS discovery) - Run ``pip install shapely --no-binary`` - Make sure the GEOS .dll files are available on the PATH Installation for local development ----------------------------------- This is similar to installing with a custom GEOS binary, but then instead of installing Shapely with pip from PyPI, you clone the package from Github:: $ git clone git@github.com:shapely/shapely.git $ cd shapely/ Install it in development mode using ``pip``:: $ pip install -e .[test] For development, use of a virtual environment is strongly recommended. For example using ``venv``: .. code-block:: console $ python3 -m venv . $ source bin/activate (env) $ pip install -e .[test] Or using ``conda``: .. code-block:: console $ conda create -n env python=3 geos numpy cython pytest $ conda activate env (env) $ pip install -e . Testing Shapely --------------- Shapely can be tested using ``pytest``:: $ pip install pytest # or shapely[test] $ pytest --pyargs shapely.tests GEOS discovery (compile time) ----------------------------- If GEOS is installed on Linux or macOS, the ``geos-config`` command line utility should be available and ``pip`` will find GEOS automatically. If the correct ``geos-config`` is not on the PATH, you can add it as follows (on Linux/macOS):: $ export PATH=/path/to/geos/bin:$PATH Alternatively, you can specify where Shapely should look for GEOS library and header files using environment variables (on Linux/macOS):: $ export GEOS_INCLUDE_PATH=/path/to/geos/include $ export GEOS_LIBRARY_PATH=/path/to/geos/lib On Windows, there is no ``geos-config`` and the include and lib folders need to be specified manually in any case:: $ set GEOS_INCLUDE_PATH=C:\path\to\geos\include $ set GEOS_LIBRARY_PATH=C:\path\to\geos\lib Common locations of GEOS (to be suffixed by ``lib``, ``include`` or ``bin``): * Anaconda (Linux/macOS): ``$CONDA_PREFIX/Library`` * Anaconda (Windows): ``%CONDA_PREFIX%\Library`` * OSGeo4W (Windows): ``C:\OSGeo4W64`` GEOS discovery (runtime) ------------------------ Shapely is dynamically linked to GEOS. This means that the same GEOS library that was used during Shapely compilation is required on your system at runtime. When using Shapely that was distributed as a binary wheel or through conda, this is automatically the case and you can stop reading. In other cases this can be tricky, especially if you have multiple GEOS installations next to each other. We only include some guidelines here to address this issue as this document is not intended as a general guide of shared library discovery. If you encounter exceptions like: .. code-block:: none ImportError: libgeos_c.so.1: cannot open shared object file: No such file or directory You will have to make the shared library file available to the Python interpreter. There are in general four ways of making Python aware of the location of shared library: 1. Copy the shared libraries into the ``shapely`` module directory (this is how Windows binary wheels work: they are distributed with the correct dlls in the ``shapely`` module directory) 2. Copy the shared libraries into the library directory of the Python interpreter (this is how Anaconda environments work) 3. Copy the shared libraries into some system location (``C:\Windows\System32``; ``/usr/local/lib``, this happens if you installed GEOS through ``apt`` or ``brew``) 4. Add the shared library location to a the dynamic linker path variable at runtime. (Advanced usage; Linux and macOS only; on Windows this method was deprecated in Python 3.8) The filenames of the GEOS shared libraries are: * On Linux: ``libgeos-*.so.*, libgeos_c-*.so.*`` * On macOS: ``libgeos.dylib, libgeos_c.dylib`` * On Windows: ``geos-*.dll, geos_c-*.dll`` Note that Shapely does not make use of any RUNPATH (RPATH) header. The location of the GEOS shared library is not stored inside the compiled Shapely library. shapely-2.0.3/docs/io.rst000066400000000000000000000002621456366510000152600ustar00rootroot00000000000000Input/Output ============ .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("io") %} {{ function }} {% endfor %} shapely-2.0.3/docs/linear.rst000066400000000000000000000003071456366510000161230ustar00rootroot00000000000000Linestring operations ===================== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("linear") %} {{ function }} {% endfor %} shapely-2.0.3/docs/manual.rst000066400000000000000000002605131456366510000161350ustar00rootroot00000000000000.. _manual: ======================= The Shapely User Manual ======================= :Author: Sean Gillies, :Version: |release| :Date: |today| :Copyright: This work is licensed under a `Creative Commons Attribution 3.0 United States License`__. .. __: https://creativecommons.org/licenses/by/3.0/us/ :Abstract: This document explains how to use the Shapely Python package for computational geometry. .. _intro: Introduction ============ Deterministic spatial analysis is an important component of computational approaches to problems in agriculture, ecology, epidemiology, sociology, and many other fields. What is the surveyed perimeter/area ratio of these patches of animal habitat? Which properties in this town intersect with the 50-year flood contour from this new flooding model? What are the extents of findspots for ancient ceramic wares with maker's marks "A" and "B", and where do the extents overlap? What's the path from home to office that best skirts identified zones of location based spam? These are just a few of the possible questions addressable using non-statistical spatial analysis, and more specifically, computational geometry. Shapely is a Python package for set-theoretic analysis and manipulation of planar features using functions from the well known and widely deployed GEOS_ library. GEOS, a port of the `Java Topology Suite`_ (JTS), is the geometry engine of the PostGIS_ spatial extension for the PostgreSQL RDBMS. The designs of JTS and GEOS are largely guided by the `Open Geospatial Consortium`_'s Simple Features Access Specification [1]_ and Shapely adheres mainly to the same set of standard classes and operations. Shapely is thereby deeply rooted in the conventions of the geographic information systems (GIS) world, but aspires to be equally useful to programmers working on non-conventional problems. The first premise of Shapely is that Python programmers should be able to perform PostGIS type geometry operations outside of an RDBMS. Not all geographic data originate or reside in a RDBMS or are best processed using SQL. We can load data into a spatial RDBMS to do work, but if there's no mandate to manage (the "M" in "RDBMS") the data over time in the database we're using the wrong tool for the job. The second premise is that the persistence, serialization, and map projection of features are significant, but orthogonal problems. You may not need a hundred GIS format readers and writers or the multitude of State Plane projections, and Shapely doesn't burden you with them. The third premise is that Python idioms trump GIS (or Java, in this case, since the GEOS library is derived from JTS, a Java project) idioms. If you enjoy and profit from idiomatic Python, appreciate packages that do one thing well, and agree that a spatially enabled RDBMS is often enough the wrong tool for your computational geometry job, Shapely might be for you. .. _intro-spatial-data-model: Spatial Data Model ------------------ The fundamental types of geometric objects implemented by Shapely are points, curves, and surfaces. Each is associated with three sets of (possibly infinite) points in the plane. The `interior`, `boundary`, and `exterior` sets of a feature are mutually exclusive and their union coincides with the entire plane [2]_. * A `Point` has an `interior` set of exactly one point, a `boundary` set of exactly no points, and an `exterior` set of all other points. A `Point` has a topological dimension of 0. * A `Curve` has an `interior` set consisting of the infinitely many points along its length (imagine a `Point` dragged in space), a `boundary` set consisting of its two end points, and an `exterior` set of all other points. A `Curve` has a topological dimension of 1. * A `Surface` has an `interior` set consisting of the infinitely many points within (imagine a `Curve` dragged in space to cover an area), a `boundary` set consisting of one or more `Curves`, and an `exterior` set of all other points including those within holes that might exist in the surface. A `Surface` has a topological dimension of 2. That may seem a bit esoteric, but will help clarify the meanings of Shapely's spatial predicates, and it's as deep into theory as this manual will go. Consequences of point-set theory, including some that manifest themselves as "gotchas", for different classes will be discussed later in this manual. The point type is implemented by a `Point` class; curve by the `LineString` and `LinearRing` classes; and surface by a `Polygon` class. Shapely implements no smooth (`i.e.` having continuous tangents) curves. All curves must be approximated by linear splines. All rounded patches must be approximated by regions bounded by linear splines. Collections of points are implemented by a `MultiPoint` class, collections of curves by a `MultiLineString` class, and collections of surfaces by a `MultiPolygon` class. These collections aren't computationally significant, but are useful for modeling certain kinds of features. A Y-shaped line feature, for example, is well modeled as a whole by a `MultiLineString`. The standard data model has additional constraints specific to certain types of geometric objects that will be discussed in following sections of this manual. See also https://web.archive.org/web/20160719195511/http://www.vividsolutions.com/jts/discussion.htm for more illustrations of this data model. .. _intro-relationships: Relationships ------------- The spatial data model is accompanied by a group of natural language relationships between geometric objects – `contains`, `intersects`, `overlaps`, `touches`, etc. – and a theoretical framework for understanding them using the 3x3 matrix of the mutual intersections of their component point sets [3]_: the DE-9IM. A comprehensive review of the relationships in terms of the DE-9IM is found in [4]_ and will not be reiterated in this manual. .. _intro-operations: Operations ---------- Following the JTS technical specs [5]_, this manual will make a distinction between constructive (`buffer`, `convex hull`) and set-theoretic operations (`intersection`, `union`, etc.). The individual operations will be fully described in a following section of the manual. .. _intro-coordinate-systems: Coordinate Systems ------------------ Even though the Earth is not flat – and for that matter not exactly spherical – there are many analytic problems that can be approached by transforming Earth features to a Cartesian plane, applying tried and true algorithms, and then transforming the results back to geographic coordinates. This practice is as old as the tradition of accurate paper maps. Shapely does not support coordinate system transformations. All operations on two or more features presume that the features exist in the same Cartesian plane. .. _objects: Geometric Objects ================= Geometric objects are created in the typical Python fashion, using the classes themselves as instance factories. A few of their intrinsic properties will be discussed in this sections, others in the following sections on operations and serializations. Instances of ``Point``, ``LineString``, and ``LinearRing`` have as their most important attribute a finite sequence of coordinates that determines their interior, boundary, and exterior point sets. A line string can be determined by as few as 2 points, but contains an infinite number of points. Coordinate sequences are immutable. A third `z` coordinate value may be used when constructing instances, but has no effect on geometric analysis. All operations are performed in the `x-y` plane. In all constructors, numeric values are converted to type ``float``. In other words, ``Point(0, 0)`` and ``Point(0.0, 0.0)`` produce geometrically equivalent instances. Shapely does not check the topological simplicity or validity of instances when they are constructed as the cost is unwarranted in most cases. Validating factories are easily implemented using the :attr:``is_valid`` predicate by users that require them. .. note:: Shapely is a planar geometry library and `z`, the height above or below the plane, is ignored in geometric analysis. There is a potential pitfall for users here: coordinate tuples that differ only in `z` are not distinguished from each other and their application can result in surprisingly invalid geometry objects. For example, ``LineString([(0, 0, 0), (0, 0, 1)])`` does not return a vertical line of unit length, but an invalid line in the plane with zero length. Similarly, ``Polygon([(0, 0, 0), (0, 0, 1), (1, 1, 1)])`` is not bounded by a closed ring and is invalid. General Attributes and Methods ------------------------------ .. attribute:: object.area Returns the area (``float``) of the object. .. attribute:: object.bounds Returns a ``(minx, miny, maxx, maxy)`` tuple (``float`` values) that bounds the object. .. attribute:: object.length Returns the length (``float``) of the object. .. attribute:: object.minimum_clearance Returns the smallest distance by which a node could be moved to produce an invalid geometry. This can be thought of as a measure of the robustness of a geometry, where larger values of minimum clearance indicate a more robust geometry. If no minimum clearance exists for a geometry, such as a point, this will return `math.infinity`. `New in Shapely 1.7.1` Requires GEOS 3.6 or higher. .. code-block:: pycon >>> from shapely import Polygon >>> Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]).minimum_clearance 1.0 .. attribute:: object.geom_type Returns a string specifying the `Geometry Type` of the object in accordance with [1]_. .. code-block:: pycon >>> from shapely import Point, LineString >>> Point(0, 0).geom_type 'Point' .. method:: object.distance(other) Returns the minimum distance (``float``) to the `other` geometric object. .. code-block:: pycon >>> Point(0,0).distance(Point(1,1)) 1.4142135623730951 .. method:: object.hausdorff_distance(other) Returns the Hausdorff distance (``float``) to the `other` geometric object. The Hausdorff distance between two geometries is the furthest distance that a point on either geometry can be from the nearest point to it on the other geometry. `New in Shapely 1.6.0` .. code-block:: pycon >>> point = Point(1, 1) >>> line = LineString([(2, 0), (2, 4), (3, 4)]) >>> point.hausdorff_distance(line) 3.605551275463989 >>> point.distance(Point(3, 4)) 3.605551275463989 .. method:: object.representative_point() Returns a cheaply computed point that is guaranteed to be within the geometric object. .. note:: This is not in general the same as the centroid. .. code-block:: pycon >>> donut = Point(0, 0).buffer(2.0).difference(Point(0, 0).buffer(1.0)) >>> donut.centroid >>> donut.representative_point() .. _points: Points ------ .. class:: Point(coordinates) The `Point` constructor takes positional coordinate values or point tuple parameters. .. code-block:: pycon >>> from shapely import Point >>> point = Point(0.0, 0.0) >>> q = Point((0.0, 0.0)) A `Point` has zero area and zero length. .. code-block:: pycon >>> point.area 0.0 >>> point.length 0.0 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> point.bounds (0.0, 0.0, 0.0, 0.0) Coordinate values are accessed via `coords`, `x`, `y`, and `z` properties. .. code-block:: pycon >>> list(point.coords) [(0.0, 0.0)] >>> point.x 0.0 >>> point.y 0.0 Coordinates may also be sliced. `New in version 1.2.14`. .. code-block:: pycon >>> point.coords[:] [(0.0, 0.0)] The `Point` constructor also accepts another `Point` instance, thereby making a copy. .. code-block:: pycon >>> Point(point) .. _linestrings: LineStrings ----------- .. class:: LineString(coordinates) The `LineString` constructor takes an ordered sequence of 2 or more ``(x, y[, z])`` point tuples. The constructed `LineString` object represents one or more connected linear splines between the points. Repeated points in the ordered sequence are allowed, but may incur performance penalties and should be avoided. A `LineString` may cross itself (*i.e.* be `complex` and not `simple`). .. plot:: code/linestring.py Figure 1. A simple `LineString` on the left, a complex `LineString` on the right. The (`MultiPoint`) boundary of each is shown in black, the other points that describe the lines are shown in grey. A `LineString` has zero area and non-zero length. .. code-block:: pycon >>> from shapely import LineString >>> line = LineString([(0, 0), (1, 1)]) >>> line.area 0.0 >>> line.length 1.4142135623730951 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> line.bounds (0.0, 0.0, 1.0, 1.0) The defining coordinate values are accessed via the `coords` property. .. code-block:: pycon >>> len(line.coords) 2 >>> list(line.coords) [(0.0, 0.0), (1.0, 1.0)] Coordinates may also be sliced. `New in version 1.2.14`. .. code-block:: pycon >>> line.coords[:] [(0.0, 0.0), (1.0, 1.0)] >>> line.coords[1:] [(1.0, 1.0)] The constructor also accepts another `LineString` instance, thereby making a copy. .. code-block:: pycon >>> LineString(line) A `LineString` may also be constructed using a sequence of mixed `Point` instances or coordinate tuples. The individual coordinates are copied into the new object. .. code-block:: pycon >>> LineString([Point(0.0, 1.0), (2.0, 3.0), Point(4.0, 5.0)]) .. _linearrings: LinearRings ----------- .. class:: LinearRing(coordinates) The `LinearRing` constructor takes an ordered sequence of ``(x, y[, z])`` point tuples. The sequence may be explicitly closed by passing identical values in the first and last indices. Otherwise, the sequence will be implicitly closed by copying the first tuple to the last index. As with a `LineString`, repeated points in the ordered sequence are allowed, but may incur performance penalties and should be avoided. A `LinearRing` may not cross itself, and may not touch itself at a single point. .. plot:: code/linearring.py Figure 2. A valid `LinearRing` on the left, an invalid self-touching `LinearRing` on the right. The points that describe the rings are shown in grey. A ring's boundary is `empty`. .. note:: Shapely will not prevent the creation of such rings, but exceptions will be raised when they are operated on. A `LinearRing` has zero area and non-zero length. .. code-block:: pycon >>> from shapely import LinearRing >>> ring = LinearRing([(0, 0), (1, 1), (1, 0)]) >>> ring.area 0.0 >>> ring.length 3.414213562373095 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> ring.bounds (0.0, 0.0, 1.0, 1.0) Defining coordinate values are accessed via the `coords` property. .. code-block:: pycon >>> len(ring.coords) 4 >>> list(ring.coords) [(0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] The `LinearRing` constructor also accepts another `LineString` or `LinearRing` instance, thereby making a copy. .. code-block:: pycon >>> LinearRing(ring) As with `LineString`, a sequence of `Point` instances is not a valid constructor parameter. .. _polygons: Polygons -------- .. class:: Polygon(shell [,holes=None]) The `Polygon` constructor takes two positional parameters. The first is an ordered sequence of ``(x, y[, z])`` point tuples and is treated exactly as in the `LinearRing` case. The second is an optional unordered sequence of ring-like sequences specifying the interior boundaries or "holes" of the feature. Rings of a `valid` `Polygon` may not cross each other, but may touch at a single point only. Again, Shapely will not prevent the creation of invalid features, but exceptions will be raised when they are operated on. .. plot:: code/polygon.py Figure 3. On the left, a valid `Polygon` with one interior ring that touches the exterior ring at one point, and on the right a `Polygon` that is `invalid` because its interior ring touches the exterior ring at more than one point. The points that describe the rings are shown in grey. .. plot:: code/polygon2.py Figure 4. On the left, a `Polygon` that is `invalid` because its exterior and interior rings touch along a line, and on the right, a `Polygon` that is `invalid` because its interior rings touch along a line. A `Polygon` has non-zero area and non-zero length. .. code-block:: pycon >>> from shapely import Polygon >>> polygon = Polygon([(0, 0), (1, 1), (1, 0)]) >>> polygon.area 0.5 >>> polygon.length 3.414213562373095 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> polygon.bounds (0.0, 0.0, 1.0, 1.0) Component rings are accessed via `exterior` and `interiors` properties. .. code-block:: pycon >>> list(polygon.exterior.coords) [(0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] >>> list(polygon.interiors) [] The `Polygon` constructor also accepts instances of `LineString` and `LinearRing`. .. code-block:: pycon >>> coords = [(0, 0), (1, 1), (1, 0)] >>> r = LinearRing(coords) >>> s = Polygon(r) >>> s.area 0.5 >>> t = Polygon(s.buffer(1.0).exterior, [r]) >>> t.area 6.5507620529190325 Rectangular polygons occur commonly, and can be conveniently constructed using the :func:`shapely.geometry.box()` function. .. function:: shapely.geometry.box(minx, miny, maxx, maxy, ccw=True) Makes a rectangular polygon from the provided bounding box values, with counter-clockwise order by default. `New in version 1.2.9`. For example: .. code-block:: pycon >>> from shapely import box >>> b = box(0.0, 0.0, 1.0, 1.0) >>> b >>> list(b.exterior.coords) [(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)] This is the first appearance of an explicit polygon handedness in Shapely. To obtain a polygon with a known orientation, use :func:`shapely.geometry.polygon.orient()`: .. function:: shapely.geometry.polygon.orient(polygon, sign=1.0) Returns a properly oriented copy of the given polygon. The signed area of the result will have the given sign. A sign of 1.0 means that the coordinates of the product's exterior ring will be oriented counter-clockwise and the interior rings (holes) will be oriented clockwise. `New in version 1.2.10`. .. _collections: Collections ----------- Heterogeneous collections of geometric objects may result from some Shapely operations. For example, two `LineStrings` may intersect along a line and at a point. To represent these kind of results, Shapely provides frozenset_-like, immutable collections of geometric objects. The collections may be homogeneous (`MultiPoint` etc.) or heterogeneous. .. code-block:: python >>> a = LineString([(0, 0), (1, 1), (1,2), (2,2)]) >>> b = LineString([(0, 0), (1, 1), (2,1), (2,2)]) >>> x = a.intersection(b) >>> x >>> list(x.geoms) [, ] .. plot:: code/geometrycollection.py :class: figure Figure 5. a) a green and a yellow line that intersect along a line and at a single point; b) the intersection (in blue) is a collection containing one `LineString` and one `Point`. Members of a `GeometryCollection` are accessed via the ``geoms`` property. .. code-block:: pycon >>> list(x.geoms) [, ] .. note:: When possible, it is better to use one of the homogeneous collection types described below. .. _multipoints: Collections of Points --------------------- .. class:: MultiPoint(points) The `MultiPoint` constructor takes a sequence of ``(x, y[, z ])`` point tuples. A `MultiPoint` has zero area and zero length. .. code-block:: pycon >>> from shapely import MultiPoint >>> points = MultiPoint([(0.0, 0.0), (1.0, 1.0)]) >>> points.area 0.0 >>> points.length 0.0 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> points.bounds (0.0, 0.0, 1.0, 1.0) Members of a multi-point collection are accessed via the ``geoms`` property. .. code-block:: pycon >>> list(points.geoms) [, ] The constructor also accepts another `MultiPoint` instance or an unordered sequence of `Point` instances, thereby making copies. .. code-block:: pycon >>> MultiPoint([Point(0, 0), Point(1, 1)]) .. _multilinestrings: Collections of Lines -------------------- .. class:: MultiLineString(lines) The `MultiLineString` constructor takes a sequence of line-like sequences or objects. .. plot:: code/multilinestring.py Figure 6. On the left, a `simple`, disconnected `MultiLineString`, and on the right, a non-simple `MultiLineString`. The points defining the objects are shown in gray, the boundaries of the objects in black. A `MultiLineString` has zero area and non-zero length. .. code-block:: pycon >>> from shapely import MultiLineString >>> coords = [((0, 0), (1, 1)), ((-1, 0), (1, 0))] >>> lines = MultiLineString(coords) >>> lines.area 0.0 >>> lines.length 3.414213562373095 Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> lines.bounds (-1.0, 0.0, 1.0, 1.0) Its members are instances of `LineString` and are accessed via the ``geoms`` property. .. code-block:: pycon >>> len(lines.geoms) 2 >>> print(list(lines.geoms)) [, ] The constructor also accepts another instance of `MultiLineString` or an unordered sequence of `LineString` instances, thereby making copies. .. code-block:: pycon >>> MultiLineString(lines) >>> MultiLineString(lines.geoms) .. _multipolygons: Collections of Polygons ----------------------- .. class:: MultiPolygon(polygons) The `MultiPolygon` constructor takes a sequence of exterior ring and hole list tuples: [((a1, ..., aM), [(b1, ..., bN), ...]), ...]. More clearly, the constructor also accepts an unordered sequence of `Polygon` instances, thereby making copies. .. code-block:: pycon >>> from shapely import MultiPolygon >>> polygons = MultiPolygon([polygon, s, t]) >>> len(polygons.geoms) 3 .. plot:: code/multipolygon.py Figure 7. On the left, a `valid` `MultiPolygon` with 2 members, and on the right, a `MultiPolygon` that is invalid because its members touch at an infinite number of points (along a line). Its `x-y` bounding box is a ``(minx, miny, maxx, maxy)`` tuple. .. code-block:: pycon >>> polygons.bounds (-1.0, -1.0, 2.0, 2.0) Its members are instances of `Polygon` and are accessed via the ``geoms`` property. .. code-block:: pycon >>> len(polygons.geoms) 3 .. _empties: Empty features -------------- An "empty" feature is one with a point set that coincides with the empty set; not ``None``, but like ``set([])``. Empty features can be created by calling the various constructors with no arguments. Almost no operations are supported by empty features. .. code-block:: pycon >>> line = LineString() >>> line.is_empty True >>> line.length 0.0 >>> line.bounds (nan, nan, nan, nan) >>> list(line.coords) [] Coordinate sequences -------------------- The list of coordinates that describe a geometry are represented as the ``CoordinateSequence`` object. These sequences should not be initialised directly, but can be accessed from an existing geometry as the ``Geometry.coords`` property. .. code-block:: pycon >>> line = LineString([(0, 1), (2, 3), (4, 5)]) >>> line.coords Coordinate sequences can be indexed, sliced and iterated over as if they were a list of coordinate tuples. .. code-block:: pycon >>> line.coords[0] (0.0, 1.0) >>> line.coords[1:] [(2.0, 3.0), (4.0, 5.0)] >>> for x, y in line.coords: ... print("x={}, y={}".format(x, y)) ... x=0.0, y=1.0 x=2.0, y=3.0 x=4.0, y=5.0 Polygons have a coordinate sequence for their exterior and each of their interior rings. .. code-block:: pycon >>> poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)]) >>> poly.exterior.coords Multipart geometries do not have a coordinate sequence. Instead the coordinate sequences are stored on their component geometries. .. code-block:: pycon >>> p = MultiPoint([(0, 0), (1, 1), (2, 2)]) >>> p.geoms[2].coords Linear Referencing Methods -------------------------- It can be useful to specify position along linear features such as `LineStrings` and `MultiLineStrings` with a 1-dimensional referencing system. Shapely supports linear referencing based on length or distance, evaluating the distance along a geometric object to the projection of a given point, or the point at a given distance along the object. .. method:: object.interpolate(distance[, normalized=False]) Return a point at the specified distance along a linear geometric object. If the `normalized` arg is ``True``, the distance will be interpreted as a fraction of the geometric object's length. .. code-block:: pycon >>> ip = LineString([(0, 0), (0, 1), (1, 1)]).interpolate(1.5) >>> ip >>> LineString([(0, 0), (0, 1), (1, 1)]).interpolate(0.75, normalized=True) .. method:: object.project(other[, normalized=False]) Returns the distance along this geometric object to a point nearest the `other` object. If the `normalized` arg is ``True``, return the distance normalized to the length of the object. The :meth:`~object.project` method is the inverse of :meth:`~object.interpolate`. .. code-block:: pycon >>> LineString([(0, 0), (0, 1), (1, 1)]).project(ip) 1.5 >>> LineString([(0, 0), (0, 1), (1, 1)]).project(ip, normalized=True) 0.75 For example, the linear referencing methods might be used to cut lines at a specified distance. .. code-block:: python def cut(line, distance): # Cuts a line in two at a distance from its starting point if distance <= 0.0 or distance >= line.length: return [LineString(line)] coords = list(line.coords) for i, p in enumerate(coords): pd = line.project(Point(p)) if pd == distance: return [ LineString(coords[:i+1]), LineString(coords[i:])] if pd > distance: cp = line.interpolate(distance) return [ LineString(coords[:i] + [(cp.x, cp.y)]), LineString([(cp.x, cp.y)] + coords[i:])] .. code-block:: pycon >>> line = LineString([(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0)]) >>> print([list(x.coords) for x in cut(line, 1.0)]) # doctest: +SKIP [[(0.0, 0.0), (1.0, 0.0)], [(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0), (5.0, 0.0)]] >>> print([list(x.coords) for x in cut(line, 2.5)]) # doctest: +SKIP [[(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (2.5, 0.0)], [(2.5, 0.0), (3.0, 0.0), (4.0, 0.0), (5.0, 0.0)]] .. _predicates: Predicates and Relationships ============================ Objects of the types explained in :ref:`objects` provide standard [1]_ predicates as attributes (for unary predicates) and methods (for binary predicates). Whether unary or binary, all return ``True`` or ``False``. .. _unary-predicates: Unary Predicates ---------------- Standard unary predicates are implemented as read-only property attributes. An example will be shown for each. .. attribute:: object.has_z Returns ``True`` if the feature has not only `x` and `y`, but also `z` coordinates for 3D (or so-called, 2.5D) geometries. .. code-block:: pycon >>> Point(0, 0).has_z False >>> Point(0, 0, 0).has_z True .. attribute:: object.is_ccw Returns ``True`` if coordinates are in counter-clockwise order (bounding a region with positive signed area). This method applies to `LinearRing` objects only. `New in version 1.2.10`. .. code-block:: pycon >>> LinearRing([(1,0), (1,1), (0,0)]).is_ccw True A ring with an undesired orientation can be reversed like this: .. code-block:: pycon >>> ring = LinearRing([(0,0), (1,1), (1,0)]) >>> ring.is_ccw False >>> ring2 = LinearRing(list(ring.coords)[::-1]) >>> ring2.is_ccw True .. attribute:: object.is_empty Returns ``True`` if the feature's `interior` and `boundary` (in point set terms) coincide with the empty set. .. code-block:: pycon >>> Point().is_empty True >>> Point(0, 0).is_empty False .. note:: With the help of the :external+python:mod:`operator` module's :external+python:func:`~operator.attrgetter` function, unary predicates such as ``is_empty`` can be easily used as predicates for the built in :external+python:func:`filter`. .. code-block:: pycon >>> from operator import attrgetter >>> empties = filter(attrgetter('is_empty'), [Point(), Point(0, 0)]) >>> len(list(empties)) 1 .. attribute:: object.is_ring Returns ``True`` if the feature is a closed and simple ``LineString``. A closed feature's `boundary` coincides with the empty set. .. code-block:: pycon >>> LineString([(0, 0), (1, 1), (1, -1)]).is_ring False >>> LinearRing([(0, 0), (1, 1), (1, -1)]).is_ring True This property is applicable to `LineString` and `LinearRing` instances, but meaningless for others. .. attribute:: object.is_simple Returns ``True`` if the feature does not cross itself. .. note:: The simplicity test is meaningful only for `LineStrings` and `LinearRings`. .. code-block:: pycon >>> LineString([(0, 0), (1, 1), (1, -1), (0, 1)]).is_simple False Operations on non-simple `LineStrings` are fully supported by Shapely. .. attribute:: object.is_valid Returns ``True`` if a feature is "valid" in the sense of [1]_. .. note:: The validity test is meaningful only for `Polygons` and `MultiPolygons`. ``True`` is always returned for other types of geometries. A valid `Polygon` may not possess any overlapping exterior or interior rings. A valid `MultiPolygon` may not collect any overlapping polygons. Operations on invalid features may fail. .. code-block:: pycon >>> MultiPolygon([Point(0, 0).buffer(2.0), Point(1, 1).buffer(2.0)]).is_valid False The two points above are close enough that the polygons resulting from the buffer operations (explained in a following section) overlap. .. note:: The ``is_valid`` predicate can be used to write a validating decorator that could ensure that only valid objects are returned from a constructor function. .. code-block:: python from functools import wraps def validate(func): @wraps(func) def wrapper(*args, **kwargs): ob = func(*args, **kwargs) if not ob.is_valid: raise TopologicalError( "Given arguments do not determine a valid geometric object") return ob return wrapper .. code-block:: pycon >>> @validate # doctest: +SKIP ... def ring(coordinates): ... return LinearRing(coordinates) ... >>> coords = [(0, 0), (1, 1), (1, -1), (0, 1)] >>> ring(coords) # doctest: +SKIP Traceback (most recent call last): File "", line 1, in File "", line 7, in wrapper shapely.geos.TopologicalError: Given arguments do not determine a valid geometric object .. _binary-predicates: Binary Predicates ----------------- Standard binary predicates are implemented as methods. These predicates evaluate topological, set-theoretic relationships. In a few cases the results may not be what one might expect starting from different assumptions. All take another geometric object as argument and return ``True`` or ``False``. .. method:: object.__eq__(other) Returns ``True`` if the two objects are of the same geometric type, and the coordinates of the two objects match precisely. .. method:: object.equals(other) Returns ``True`` if the set-theoretic `boundary`, `interior`, and `exterior` of the object coincide with those of the other. The coordinates passed to the object constructors are of these sets, and determine them, but are not the entirety of the sets. This is a potential "gotcha" for new users. Equivalent lines, for example, can be constructed differently. .. code-block:: pycon >>> a = LineString([(0, 0), (1, 1)]) >>> b = LineString([(0, 0), (0.5, 0.5), (1, 1)]) >>> c = LineString([(0, 0), (0, 0), (1, 1)]) >>> a.equals(b) True >>> a == b False >>> b.equals(c) True >>> b == c False .. method:: object.equals_exact(other, tolerance) Returns ``True`` if the object is within a specified `tolerance`. .. method:: object.contains(other) Returns ``True`` if no points of `other` lie in the exterior of the `object` and at least one point of the interior of `other` lies in the interior of `object`. This predicate applies to all types, and is inverse to :meth:`~object.within`. The expression ``a.contains(b) == b.within(a)`` always evaluates to ``True``. .. code-block:: pycon >>> coords = [(0, 0), (1, 1)] >>> LineString(coords).contains(Point(0.5, 0.5)) True >>> Point(0.5, 0.5).within(LineString(coords)) True A line's endpoints are part of its `boundary` and are therefore not contained. .. code-block:: pycon >>> LineString(coords).contains(Point(1.0, 1.0)) False .. note:: Binary predicates can be used directly as predicates for ``filter()`` or ``itertools.ifilter()``. .. code-block:: pycon >>> line = LineString(coords) >>> contained = list(filter(line.contains, [Point(), Point(0.5, 0.5)])) >>> len(contained) 1 >>> contained [] .. method:: object.covers(other) Returns ``True`` if every point of `other` is a point on the interior or boundary of `object`. This is similar to ``object.contains(other)`` except that this does not require any interior points of `other` to lie in the interior of `object`. .. method:: object.covered_by(other) Returns ``True`` if every point of `object` is a point on the interior or boundary of `other`. This is equivalent to ``other.covers(object)``. `New in version 1.8`. .. method:: object.crosses(other) Returns ``True`` if the `interior` of the object intersects the `interior` of the other but does not contain it, and the dimension of the intersection is less than the dimension of the one or the other. .. code-block:: pycon >>> LineString(coords).crosses(LineString([(0, 1), (1, 0)])) True A line does not cross a point that it contains. .. code-block:: pycon >>> LineString(coords).crosses(Point(0.5, 0.5)) False .. method:: object.disjoint(other) Returns ``True`` if the `boundary` and `interior` of the object do not intersect at all with those of the other. .. code-block:: pycon >>> Point(0, 0).disjoint(Point(1, 1)) True This predicate applies to all types and is the inverse of :meth:`~object.intersects`. .. method:: object.intersects(other) Returns ``True`` if the `boundary` or `interior` of the object intersect in any way with those of the other. In other words, geometric objects intersect if they have any boundary or interior point in common. .. method:: object.overlaps(other) Returns ``True`` if the geometries have more than one but not all points in common, have the same dimension, and the intersection of the interiors of the geometries has the same dimension as the geometries themselves. .. method:: object.touches(other) Returns ``True`` if the objects have at least one point in common and their interiors do not intersect with any part of the other. Overlapping features do not therefore `touch`, another potential "gotcha". For example, the following lines touch at ``(1, 1)``, but do not overlap. .. code-block:: pycon >>> a = LineString([(0, 0), (1, 1)]) >>> b = LineString([(1, 1), (2, 2)]) >>> a.touches(b) True .. method:: object.within(other) Returns ``True`` if the object's `boundary` and `interior` intersect only with the `interior` of the other (not its `boundary` or `exterior`). This applies to all types and is the inverse of :meth:`~object.contains`. Used in a ``sorted()`` `key`, :meth:`~object.within` makes it easy to spatially sort objects. Let's say we have 4 stereotypic features: a point that is contained by a polygon which is itself contained by another polygon, and a free spirited point contained by none .. code-block:: pycon >>> a = Point(2, 2) >>> b = Polygon([[1, 1], [1, 3], [3, 3], [3, 1]]) >>> c = Polygon([[0, 0], [0, 4], [4, 4], [4, 0]]) >>> d = Point(-1, -1) and that copies of these are collected into a list .. code-block:: pycon >>> features = [c, a, d, b, c] that we'd prefer to have ordered as ``[d, c, c, b, a]`` in reverse containment order. As explained in the Python `Sorting HowTo`_, we can define a key function that operates on each list element and returns a value for comparison. Our key function will be a wrapper class that implements ``__lt__()`` using Shapely's binary :meth:`~object.within` predicate. .. code-block:: python >>> class Within: ... def __init__(self, o): ... self.o = o ... def __lt__(self, other): ... return self.o.within(other.o) As the howto says, the `less than` comparison is guaranteed to be used in sorting. That's what we'll rely on to spatially sort. Trying it out on features `d` and `c`, we see that it works. .. code-block:: pycon >>> Within(d) < Within(c) False It also works on the list of features, producing the order we want. .. code-block:: pycon >>> [d, c, c, b, a] == sorted(features, key=Within, reverse=True) True DE-9IM Relationships -------------------- The :meth:`~object.relate` method tests all the DE-9IM [4]_ relationships between objects, of which the named relationship predicates above are a subset. .. method:: object.relate(other) Returns a string representation of the DE-9IM matrix of relationships between an object's `interior`, `boundary`, `exterior` and those of another geometric object. The named relationship predicates (:meth:`~object.contains`, etc.) are typically implemented as wrappers around :meth:`~object.relate`. Two different points have mainly ``F`` (false) values in their matrix; the intersection of their `external` sets (the 9th element) is a ``2`` dimensional object (the rest of the plane). The intersection of the `interior` of one with the `exterior` of the other is a ``0`` dimensional object (3rd and 7th elements of the matrix). .. code-block:: pycon >>> Point(0, 0).relate(Point(1, 1)) 'FF0FFF0F2' The matrix for a line and a point on the line has more "true" (not ``F``) elements. .. code-block:: pycon >>> Point(0, 0).relate(LineString([(0, 0), (1, 1)])) 'F0FFFF102' .. method:: object.relate_pattern(other, pattern) Returns True if the DE-9IM string code for the relationship between the geometries satisfies the pattern, otherwise False. The :meth:`~object.relate_pattern` compares the DE-9IM code string for two geometries against a specified pattern. If the string matches the pattern then ``True`` is returned, otherwise ``False``. The pattern specified can be an exact match (``0``, ``1`` or ``2``), a boolean match (``T`` or ``F``), or a wildcard (``*``). For example, the pattern for the `within` predicate is ``T*****FF*``. .. code-block:: pycon >>> point = Point(0.5, 0.5) >>> square = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]) >>> square.relate_pattern(point, 'T*****FF*') True >>> point.within(square) True Note that the order or the geometries is significant, as demonstrated below. In this example the square contains the point, but the point does not contain the square. .. code-block:: pycon >>> point.relate(square) '0FFFFF212' >>> square.relate(point) '0F2FF1FF2' Further discussion of the DE-9IM matrix is beyond the scope of this manual. See [4]_ and https://pypi.org/project/de9im/. .. _analysis-methods: Spatial Analysis Methods ======================== As well as boolean attributes and methods, Shapely provides analysis methods that return new geometric objects. .. _set-theoretic-methods: Set-theoretic Methods --------------------- Almost every binary predicate method has a counterpart that returns a new geometric object. In addition, the set-theoretic `boundary` of an object is available as a read-only attribute. .. note:: These methods will `always` return a geometric object. An intersection of disjoint geometries for example will return an empty `GeometryCollection`, not `None` or `False`. To test for a non-empty result, use the geometry's :attr:`~object.is_empty` property. .. attribute:: object.boundary Returns a lower dimensional object representing the object's set-theoretic `boundary`. The boundary of a polygon is a line, the boundary of a line is a collection of points. The boundary of a point is an empty collection. .. code-block:: pycon >>> coords = [((0, 0), (1, 1)), ((-1, 0), (1, 0))] >>> lines = MultiLineString(coords) >>> lines.boundary >>> list(lines.boundary.geoms) [, , , ] >>> lines.boundary.boundary See the figures in :ref:`linestrings` and :ref:`multilinestrings` for the illustration of lines and their boundaries. .. attribute:: object.centroid Returns a representation of the object's geometric centroid (point). .. code-block:: pycon >>> LineString([(0, 0), (1, 1)]).centroid .. note:: The centroid of an object might be one of its points, but this is not guaranteed. .. method:: object.difference(other) Returns a representation of the points making up this geometric object that do not make up the *other* object. .. code-block:: pycon >>> a = Point(1, 1).buffer(1.5) >>> b = Point(2, 1).buffer(1.5) >>> a.difference(b) .. note:: The :meth:`~object.buffer` method is used to produce approximately circular polygons in the examples of this section; it will be explained in detail later in this manual. .. plot:: code/difference.py Figure 8. Differences between two approximately circular polygons. .. note:: Shapely can not represent the difference between an object and a lower dimensional object (such as the difference between a polygon and a line or point) as a single object, and in these cases the difference method returns a copy of the object named ``self``. .. method:: object.intersection(other) Returns a representation of the intersection of this object with the `other` geometric object. .. code-block:: pycon >>> a = Point(1, 1).buffer(1.5) >>> b = Point(2, 1).buffer(1.5) >>> a.intersection(b) See the figure under :meth:`~object.symmetric_difference` below. .. method:: object.symmetric_difference(other) Returns a representation of the points in this object not in the `other` geometric object, and the points in the `other` not in this geometric object. .. code-block:: pycon >>> a = Point(1, 1).buffer(1.5) >>> b = Point(2, 1).buffer(1.5) >>> a.symmetric_difference(b) .. plot:: code/intersection-sym-difference.py .. method:: object.union(other) Returns a representation of the union of points from this object and the `other` geometric object. The type of object returned depends on the relationship between the operands. The union of polygons (for example) will be a polygon or a multi-polygon depending on whether they intersect or not. .. code-block:: pycon >>> a = Point(1, 1).buffer(1.5) >>> b = Point(2, 1).buffer(1.5) >>> a.union(b) The semantics of these operations vary with type of geometric object. For example, compare the boundary of the union of polygons to the union of their boundaries. .. code-block:: pycon >>> a.union(b).boundary >>> a.boundary.union(b.boundary) .. plot:: code/union.py .. note:: :meth:`~object.union` is an expensive way to find the cumulative union of many objects. See :func:`shapely.unary_union` for a more effective method. Several of these set-theoretic methods can be invoked using overloaded operators: - `intersection` can be accessed with and, `&` - `union` can be accessed with or, `|` - `difference` can be accessed with minus, `-` - `symmetric_difference` can be accessed with xor, `^` .. code-block:: pycon >>> from shapely import wkt >>> p1 = wkt.loads('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))') >>> p2 = wkt.loads('POLYGON((0.5 0, 1.5 0, 1.5 1, 0.5 1, 0.5 0))') >>> p1 & p2 >>> p1 | p2 >>> p1 - p2 >>> (p1 ^ p2).wkt 'MULTIPOLYGON (((0 0, 0 1, 0.5 1, 0.5 0, 0 0)), ((1 1, 1.5 1, 1.5 0, 1 0, 1 1)))' Constructive Methods -------------------- Shapely geometric object have several methods that yield new objects not derived from set-theoretic analysis. .. method:: object.buffer(distance, quad_segs=16, cap_style=1, join_style=1, mitre_limit=5.0, single_sided=False) Returns an approximate representation of all points within a given `distance` of the this geometric object. The styles of caps are specified by integer values: 1 (round), 2 (flat), 3 (square). These values are also enumerated by the object :class:`shapely.BufferCapStyle` (see below). The styles of joins between offset segments are specified by integer values: 1 (round), 2 (mitre), and 3 (bevel). These values are also enumerated by the object :class:`shapely.BufferJoinStyle` (see below). .. data:: shapely.BufferCapStyle ========= ===== Attribute Value ========= ===== round 1 flat 2 square 3 ========= ===== .. data:: shapely.BufferJoinStyle ========= ===== Attribute Value ========= ===== round 1 mitre 2 bevel 3 ========= ===== .. code-block:: pycon >>> from shapely import BufferCapStyle, BufferJoinStyle >>> BufferCapStyle.flat.value 2 >>> BufferJoinStyle.bevel.value 3 A positive distance has an effect of dilation; a negative distance, erosion. The optional `quad_segs` argument determines the number of segments used to approximate a quarter circle around a point. .. code-block:: pycon >>> line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) >>> dilated = line.buffer(0.5) >>> eroded = dilated.buffer(-0.3) .. plot:: code/buffer.py Figure 9. Dilation of a line (left) and erosion of a polygon (right). New object is shown in blue. The default (`quad_segs` of 16) buffer of a point is a polygonal patch with 99.8% of the area of the circular disk it approximates. .. code-block:: pycon >>> p = Point(0, 0).buffer(10.0) >>> len(p.exterior.coords) 65 >>> p.area 313.6548490545941 With a `quad_segs` of 1, the buffer is a square patch. .. code-block:: pycon >>> q = Point(0, 0).buffer(10.0, 1) >>> len(q.exterior.coords) 5 >>> q.area 200.0 You may want a buffer only on one side. You can achieve this effect with `single_sided` option. The side used is determined by the sign of the buffer distance: - a positive distance indicates the left-hand side - a negative distance indicates the right-hand side .. code-block:: pycon >>> line = LineString([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) >>> left_hand_side = line.buffer(0.5, single_sided=True) >>> right_hand_side = line.buffer(-0.3, single_sided=True) .. plot:: code/buffer_single_side.py Figure 10. Single sided buffer of 0.5 left hand (left) and of 0.3 right hand (right). The single-sided buffer of point geometries is the same as the regular buffer. The End Cap Style for single-sided buffers is always ignored, and forced to the equivalent of `BufferCapStyle.flat`. Passed a `distance` of 0, :meth:`~object.buffer` can sometimes be used to "clean" self-touching or self-crossing polygons such as the classic "bowtie". Users have reported that very small distance values sometimes produce cleaner results than 0. Your mileage may vary when cleaning surfaces. .. code-block:: pycon >>> coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)] >>> bowtie = Polygon(coords) >>> bowtie.is_valid False >>> clean = bowtie.buffer(0) >>> clean.is_valid True >>> clean >>> len(clean.geoms) 2 >>> list(clean.geoms[0].exterior.coords) [(0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (0.0, 0.0)] >>> list(clean.geoms[1].exterior.coords) [(1.0, 1.0), (2.0, 2.0), (2.0, 0.0), (1.0, 1.0)] Buffering splits the polygon in two at the point where they touch. .. attribute:: object.convex_hull Returns a representation of the smallest convex `Polygon` containing all the points in the object unless the number of points in the object is less than three. For two points, the convex hull collapses to a `LineString`; for 1, a `Point`. .. code-block:: pycon >>> Point(0, 0).convex_hull >>> MultiPoint([(0, 0), (1, 1)]).convex_hull >>> MultiPoint([(0, 0), (1, 1), (1, -1)]).convex_hull .. plot:: code/convex_hull.py Figure 11. Convex hull (blue) of 2 points (left) and of 6 points (right). .. attribute:: object.envelope Returns a representation of the point or smallest rectangular polygon (with sides parallel to the coordinate axes) that contains the object. .. code-block:: pycon >>> Point(0, 0).envelope >>> MultiPoint([(0, 0), (1, 1)]).envelope .. attribute:: object.minimum_rotated_rectangle Returns the general minimum bounding rectangle that contains the object. Unlike envelope this rectangle is not constrained to be parallel to the coordinate axes. If the convex hull of the object is a degenerate (line or point) this degenerate is returned. `New in Shapely 1.6.0` .. code-block:: pycon >>> Point(0, 0).minimum_rotated_rectangle >>> MultiPoint([(0,0),(1,1),(2,0.5)]).minimum_rotated_rectangle .. plot:: code/minimum_rotated_rectangle.py Figure 12. Minimum rotated rectangle for a multipoint feature (left) and a linestring feature (right). .. method:: object.parallel_offset(distance, side, resolution=16, join_style=1, mitre_limit=5.0) Returns a LineString or MultiLineString geometry at a distance from the object on its right or its left side. Older alternative method to the :meth:`~object.offset_curve` method, but uses `resolution` instead of `quad_segs` and a `side` keyword ('left' or 'right') instead of sign of the distance. This method is kept for backwards compatibility for now, but is is recommended to use :meth:`~object.offset_curve` instead. .. method:: object.offset_curve(distance, quad_segs=16, join_style=1, mitre_limit=5.0) Returns a LineString or MultiLineString geometry at a distance from the object on its right or its left side. The `distance` parameter must be a float value. The side is determined by the sign of the `distance` parameter (negative for right side offset, positive for left side offset). Left and right are determined by following the direction of the given geometric points of the LineString. Note: the behaviour regarding orientation of the resulting line depends on the GEOS version. With GEOS < 3.11, the line retains the same direction for a left offset (positive distance) or has reverse direction for a right offset (negative distance), and this behaviour was documented as such in previous Shapely versions. Starting with GEOS 3.11, the function tries to preserve the orientation of the original line. The resolution of the offset around each vertex of the object is parameterized as in the :meth:`~object.buffer` method (using `quad_segs`). The `join_style` is for outside corners between line segments. Accepted integer values are 1 (round), 2 (mitre), and 3 (bevel). See also :data:`shapely.BufferJoinStyle`. Severely mitered corners can be controlled by the `mitre_limit` parameter (spelled in British English, en-gb). The corners of a parallel line will be further from the original than most places with the mitre join style. The ratio of this further distance to the specified `distance` is the miter ratio. Corners with a ratio which exceed the limit will be beveled. .. note:: This method may sometimes return a `MultiLineString` where a simple `LineString` was expected; for example, an offset to a slightly curved LineString. .. note:: This method is only available for `LinearRing` and `LineString` objects. .. plot:: code/parallel_offset.py Figure 13. Three styles of parallel offset lines on the left side of a simple line string (its starting point shown as a circle) and one offset on the right side, a multipart. The effect of the `mitre_limit` parameter is shown below. .. plot:: code/parallel_offset_mitre.py Figure 14. Large and small mitre_limit values for left and right offsets. .. method:: object.simplify(tolerance, preserve_topology=True) Returns a simplified representation of the geometric object. All points in the simplified object will be within the `tolerance` distance of the original geometry. By default a slower algorithm is used that preserves topology. If preserve topology is set to ``False`` the much quicker Douglas-Peucker algorithm [6]_ is used. .. code-block:: pycon >>> p = Point(0.0, 0.0) >>> x = p.buffer(1.0) >>> x.area 3.1365484905459398 >>> len(x.exterior.coords) 65 >>> s = x.simplify(0.05, preserve_topology=False) >>> s.area 3.061467458920719 >>> len(s.exterior.coords) 17 .. plot:: code/simplify.py Figure 15. Simplification of a nearly circular polygon using a tolerance of 0.2 (left) and 0.5 (right). .. note:: `Invalid` geometric objects may result from simplification that does not preserve topology and simplification may be sensitive to the order of coordinates: two geometries differing only in order of coordinates may be simplified differently. Affine Transformations ====================== A collection of affine transform functions are in the :mod:`shapely.affinity` module, which return transformed geometries by either directly supplying coefficients to an affine transformation matrix, or by using a specific, named transform (`rotate`, `scale`, etc.). The functions can be used with all geometry types (except `GeometryCollection`), and 3D types are either preserved or supported by 3D affine transformations. `New in version 1.2.17`. .. function:: shapely.affinity.affine_transform(geom, matrix) Returns a transformed geometry using an affine transformation matrix. The coefficient ``matrix`` is provided as a list or tuple with 6 or 12 items for 2D or 3D transformations, respectively. For 2D affine transformations, the 6 parameter ``matrix`` is: ``[a, b, d, e, xoff, yoff]`` which represents the augmented matrix: .. math:: \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & x_\mathrm{off} \\ d & e & y_\mathrm{off} \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} or the equations for the transformed coordinates: .. math:: x' &= a x + b y + x_\mathrm{off} \\ y' &= d x + e y + y_\mathrm{off}. For 3D affine transformations, the 12 parameter ``matrix`` is: ``[a, b, c, d, e, f, g, h, i, xoff, yoff, zoff]`` which represents the augmented matrix: .. math:: \begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & c & x_\mathrm{off} \\ d & e & f & y_\mathrm{off} \\ g & h & i & z_\mathrm{off} \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} or the equations for the transformed coordinates: .. math:: x' &= a x + b y + c z + x_\mathrm{off} \\ y' &= d x + e y + f z + y_\mathrm{off} \\ z' &= g x + h y + i z + z_\mathrm{off}. .. function:: shapely.affinity.rotate(geom, angle, origin='center', use_radians=False) Returns a rotated geometry on a 2D plane. The angle of rotation can be specified in either degrees (default) or radians by setting ``use_radians=True``. Positive angles are counter-clockwise and negative are clockwise rotations. The point of origin can be a keyword ``'center'`` for the bounding box center (default), ``'centroid'`` for the geometry's centroid, a `Point` object or a coordinate tuple ``(x0, y0)``. The affine transformation matrix for 2D rotation with angle :math:`\theta` is: .. math:: \begin{bmatrix} \cos{\theta} & -\sin{\theta} & x_\mathrm{off} \\ \sin{\theta} & \cos{\theta} & y_\mathrm{off} \\ 0 & 0 & 1 \end{bmatrix} where the offsets are calculated from the origin :math:`(x_0, y_0)`: .. math:: x_\mathrm{off} &= x_0 - x_0 \cos{\theta} + y_0 \sin{\theta} \\ y_\mathrm{off} &= y_0 - x_0 \sin{\theta} - y_0 \cos{\theta} .. code-block:: pycon >>> from shapely import affinity >>> line = LineString([(1, 3), (1, 1), (4, 1)]) >>> rotated_a = affinity.rotate(line, 90) >>> rotated_b = affinity.rotate(line, 90, origin='centroid') .. plot:: code/rotate.py Figure 16. Rotation of a `LineString` (gray) by an angle of 90° counter-clockwise (blue) using different origins. .. function:: shapely.affinity.scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin='center') Returns a scaled geometry, scaled by factors along each dimension. The point of origin can be a keyword ``'center'`` for the 2D bounding box center (default), ``'centroid'`` for the geometry's 2D centroid, a `Point` object or a coordinate tuple ``(x0, y0, z0)``. Negative scale factors will mirror or reflect coordinates. The general 3D affine transformation matrix for scaling is: .. math:: \begin{bmatrix} x_\mathrm{fact} & 0 & 0 & x_\mathrm{off} \\ 0 & y_\mathrm{fact} & 0 & y_\mathrm{off} \\ 0 & 0 & z_\mathrm{fact} & z_\mathrm{off} \\ 0 & 0 & 0 & 1 \end{bmatrix} where the offsets are calculated from the origin :math:`(x_0, y_0, z_0)`: .. math:: x_\mathrm{off} &= x_0 - x_0 x_\mathrm{fact} \\ y_\mathrm{off} &= y_0 - y_0 y_\mathrm{fact} \\ z_\mathrm{off} &= z_0 - z_0 z_\mathrm{fact} .. code-block:: pycon >>> triangle = Polygon([(1, 1), (2, 3), (3, 1)]) >>> triangle_a = affinity.scale(triangle, xfact=1.5, yfact=-1) >>> triangle_a.exterior.coords[:] [(0.5, 3.0), (2.0, 1.0), (3.5, 3.0), (0.5, 3.0)] >>> triangle_b = affinity.scale(triangle, xfact=2, origin=(1,1)) >>> triangle_b.exterior.coords[:] [(1.0, 1.0), (3.0, 3.0), (5.0, 1.0), (1.0, 1.0)] .. plot:: code/scale.py Figure 17. Scaling of a gray triangle to blue result: a) by a factor of 1.5 along x-direction, with reflection across y-axis; b) by a factor of 2 along x-direction with custom origin at (1, 1). .. function:: shapely.affinity.skew(geom, xs=0.0, ys=0.0, origin='center', use_radians=False) Returns a skewed geometry, sheared by angles along x and y dimensions. The shear angle can be specified in either degrees (default) or radians by setting ``use_radians=True``. The point of origin can be a keyword ``'center'`` for the bounding box center (default), ``'centroid'`` for the geometry's centroid, a `Point` object or a coordinate tuple ``(x0, y0)``. The general 2D affine transformation matrix for skewing is: .. math:: \begin{bmatrix} 1 & \tan{x_s} & x_\mathrm{off} \\ \tan{y_s} & 1 & y_\mathrm{off} \\ 0 & 0 & 1 \end{bmatrix} where the offsets are calculated from the origin :math:`(x_0, y_0)`: .. math:: x_\mathrm{off} &= -y_0 \tan{x_s} \\ y_\mathrm{off} &= -x_0 \tan{y_s} .. plot:: code/skew.py Figure 18. Skewing of a gray "R" to blue result: a) by a shear angle of 20° along the x-direction and an origin at (1, 1); b) by a shear angle of 30° along the y-direction, using default origin. .. function:: shapely.affinity.translate(geom, xoff=0.0, yoff=0.0, zoff=0.0) Returns a translated geometry shifted by offsets along each dimension. The general 3D affine transformation matrix for translation is: .. math:: \begin{bmatrix} 1 & 0 & 0 & x_\mathrm{off} \\ 0 & 1 & 0 & y_\mathrm{off} \\ 0 & 0 & 1 & z_\mathrm{off} \\ 0 & 0 & 0 & 1 \end{bmatrix} Other Transformations ===================== Shapely supports map projections and other arbitrary transformations of geometric objects. .. function:: shapely.ops.transform(func, geom) Applies `func` to all coordinates of `geom` and returns a new geometry of the same type from the transformed coordinates. `func` maps x, y, and optionally z to output xp, yp, zp. The input parameters may be iterable types like lists or arrays or single values. The output shall be of the same type: scalars in, scalars out; lists in, lists out. `transform` tries to determine which kind of function was passed in by calling `func` first with n iterables of coordinates, where n is the dimensionality of the input geometry. If `func` raises a `TypeError` when called with iterables as arguments, then it will instead call `func` on each individual coordinate in the geometry. `New in version 1.2.18`. For example, here is an identity function applicable to both types of input (scalar or array). .. code-block:: python def id_func(x, y, z=None): return tuple(filter(None, [x, y, z])) g2 = transform(id_func, g1) If using `pyproj>=2.1.0`, the preferred method to project geometries is: .. code-block:: python import pyproj from shapely import Point from shapely.ops import transform wgs84_pt = Point(-72.2495, 43.886) wgs84 = pyproj.CRS('EPSG:4326') utm = pyproj.CRS('EPSG:32618') project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform utm_point = transform(project, wgs84_pt) It is important to note that in the example above, the `always_xy` kwarg is required as Shapely only supports coordinates in X,Y order, and in PROJ 6 the WGS84 CRS uses the EPSG-defined Lat/Lon coordinate order instead of the expected Lon/Lat. If using `pyproj < 2.1`, then the canonical example is: .. code-block:: python from functools import partial import pyproj from shapely.ops import transform wgs84 = pyproj.Proj(init='epsg:4326') utm = pyproj.Proj(init='epsg:32618') project = partial( pyproj.transform, wgs84, utm) utm_point = transform(project, wgs84_pt) Lambda expressions such as the one in .. code-block:: python g2 = transform(lambda x, y, z=None: (x+1.0, y+1.0), g1) also satisfy the requirements for `func`. Other Operations ================ Merging Linear Features ----------------------- Sequences of touching lines can be merged into `MultiLineStrings` or `Polygons` using functions in the :mod:`shapely.ops` module. .. function:: shapely.ops.polygonize(lines) Returns an iterator over polygons constructed from the input `lines`. As with the :class:`MultiLineString` constructor, the input elements may be any line-like object. .. code-block:: pycon >>> from shapely.ops import polygonize >>> lines = [ ... ((0, 0), (1, 1)), ... ((0, 0), (0, 1)), ... ((0, 1), (1, 1)), ... ((1, 1), (1, 0)), ... ((1, 0), (0, 0)) ... ] >>> list(polygonize(lines)) [, ] .. function:: shapely.ops.polygonize_full(lines) Creates polygons from a source of lines, returning the polygons and leftover geometries. The source may be a MultiLineString, a sequence of LineString objects, or a sequence of objects than can be adapted to LineStrings. Returns a tuple of objects: (polygons, cut edges, dangles, invalid ring lines). Each are a geometry collection. Dangles are edges which have one or both ends which are not incident on another edge endpoint. Cut edges are connected at both ends but do not form part of polygon. Invalid ring lines form rings which are invalid (bowties, etc). `New in version 1.2.18.` .. code-block:: pycon >>> from shapely.ops import polygonize_full >>> lines = [ ... ((0, 0), (1, 1)), ... ((0, 0), (0, 1)), ... ((0, 1), (1, 1)), ... ((1, 1), (1, 0)), ... ((1, 0), (0, 0)), ... ((5, 5), (6, 6)), ... ((1, 1), (100, 100)), ... ] >>> result, cuts, dangles, invalids = polygonize_full(lines) >>> len(result.geoms) 2 >>> list(result.geoms) [, ] >>> list(dangles.geoms) [, ] .. function:: shapely.ops.linemerge(lines) Returns a `LineString` or `MultiLineString` representing the merger of all contiguous elements of `lines`. As with :func:`shapely.ops.polygonize`, the input elements may be any line-like object. .. code-block:: python >>> from shapely.ops import linemerge >>> linemerge(lines) >>> list(linemerge(lines).geoms) [, , , , ] Efficient Rectangle Clipping ---------------------------- The :func:`~shapely.ops.clip_by_rect` function in `shapely.ops` returns the portion of a geometry within a rectangle. .. function:: shapely.ops.clip_by_rect(geom, xmin, ymin, xmax, ymax) The geometry is clipped in a fast but possibly dirty way. The output is not guaranteed to be valid. No exceptions will be raised for topological errors. `New in version 1.7.` Requires GEOS 3.5.0 or higher .. code-block:: python >>> from shapely.ops import clip_by_rect >>> polygon = Polygon( ... shell=[(0, 0), (0, 30), (30, 30), (30, 0), (0, 0)], ... holes=[[(10, 10), (20, 10), (20, 20), (10, 20), (10, 10)]], ... ) >>> clipped_polygon = clip_by_rect(polygon, 5, 5, 15, 15) >>> clipped_polygon Efficient Unions ---------------- The :func:`~shapely.ops.unary_union` function in `shapely.ops` is more efficient than accumulating with :meth:`~object.union`. .. plot:: code/unary_union.py .. function:: shapely.ops.unary_union(geoms) Returns a representation of the union of the given geometric objects. Areas of overlapping `Polygons` will get merged. `LineStrings` will get fully dissolved and noded. Duplicate `Points` will get merged. .. code-block:: pycon >>> from shapely.ops import unary_union >>> polygons = [Point(i, 0).buffer(0.7) for i in range(5)] >>> unary_union(polygons) Because the union merges the areas of overlapping `Polygons` it can be used in an attempt to fix invalid `MultiPolygons`. As with the zero distance :meth:`~object.buffer` trick, your mileage may vary when using this. .. code-block:: pycon >>> m = MultiPolygon(polygons) >>> m.area 7.684543801837549 >>> m.is_valid False >>> unary_union(m).area 6.610301355116799 >>> unary_union(m).is_valid True .. function:: shapely.ops.cascaded_union(geoms) Returns a representation of the union of the given geometric objects. .. note:: In 1.8.0 :func:`shapely.ops.cascaded_union` is deprecated, as it was superseded by :func:`shapely.ops.unary_union`. Delaunay triangulation ---------------------- The :func:`~shapely.ops.triangulate` function in `shapely.ops` calculates a Delaunay triangulation from a collection of points. .. plot:: code/triangulate.py .. function:: shapely.ops.triangulate(geom, tolerance=0.0, edges=False) Returns a Delaunay triangulation of the vertices of the input geometry. The source may be any geometry type. All vertices of the geometry will be used as the points of the triangulation. The `tolerance` keyword argument sets the snapping tolerance used to improve the robustness of the triangulation computation. A tolerance of 0.0 specifies that no snapping will take place. If the `edges` keyword argument is `False` a list of `Polygon` triangles will be returned. Otherwise a list of `LineString` edges is returned. `New in version 1.4.0` .. code-block:: pycon >>> from shapely.ops import triangulate >>> points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) >>> triangulate(points) [, , , , ] Voronoi Diagram --------------- The :func:`~shapely.ops.voronoi_diagram` function in `shapely.ops` constructs a Voronoi diagram from a collection points, or the vertices of any geometry. .. plot:: code/voronoi_diagram.py .. function:: shapely.ops.voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False) Constructs a Voronoi diagram from the vertices of the input geometry. The source may be any geometry type. All vertices of the geometry will be used as the input points to the diagram. The `envelope` keyword argument provides an envelope to use to clip the resulting diagram. If `None`, it will be calculated automatically. The diagram will be clipped to the *larger* of the provided envelope or an envelope surrounding the sites. The `tolerance` keyword argument sets the snapping tolerance used to improve the robustness of the computation. A tolerance of 0.0 specifies that no snapping will take place. The tolerance `argument` can be finicky and is known to cause the algorithm to fail in several cases. If you're using `tolerance` and getting a failure, try removing it. The test cases in `tests/test_voronoi_diagram.py` show more details. If the `edges` keyword argument is `False` a list of `Polygon`s will be returned. Otherwise a list of `LineString` edges is returned. .. code-block:: pycon >>> from shapely.ops import voronoi_diagram >>> points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)]) >>> regions = voronoi_diagram(points) >>> list(regions.geoms) [, , , , , ] Nearest points -------------- The :func:`~shapely.ops.nearest_points` function in `shapely.ops` calculates the nearest points in a pair of geometries. .. function:: shapely.ops.nearest_points(geom1, geom2) Returns a tuple of the nearest points in the input geometries. The points are returned in the same order as the input geometries. `New in version 1.4.0`. .. code-block:: pycon >>> from shapely.ops import nearest_points >>> triangle = Polygon([(0, 0), (1, 0), (0.5, 1), (0, 0)]) >>> square = Polygon([(0, 2), (1, 2), (1, 3), (0, 3), (0, 2)]) >>> list(nearest_points(triangle, square)) [, ] Note that the nearest points may not be existing vertices in the geometries. Snapping -------- The :func:`~shapely.ops.snap` function in `shapely.ops` snaps the vertices in one geometry to the vertices in a second geometry with a given tolerance. .. function:: shapely.ops.snap(geom1, geom2, tolerance) Snaps vertices in `geom1` to vertices in the `geom2`. A copy of the snapped geometry is returned. The input geometries are not modified. The `tolerance` argument specifies the minimum distance between vertices for them to be snapped. `New in version 1.5.0` .. code-block:: pycon >>> from shapely.ops import snap >>> square = Polygon([(1,1), (2, 1), (2, 2), (1, 2), (1, 1)]) >>> line = LineString([(0,0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)]) >>> result = snap(line, square, 0.5) >>> result Shared paths ------------ The :func:`~shapely.ops.shared_paths` function in `shapely.ops` finds the shared paths between two linear geometries. .. function:: shapely.ops.shared_paths(geom1, geom2) Finds the shared paths between `geom1` and `geom2`, where both geometries are `LineStrings`. A `GeometryCollection` is returned with two elements. The first element is a `MultiLineString` containing shared paths with the same direction for both inputs. The second element is a MultiLineString containing shared paths with the opposite direction for the two inputs. `New in version 1.6.0` .. code-block:: pycon >>> from shapely.ops import shared_paths >>> g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)]) >>> g2 = LineString([(5, 0), (30, 0), (30, 5), (0, 5)]) >>> forward, backward = shared_paths(g1, g2).geoms >>> forward >>> backward Splitting --------- The :func:`~shapely.ops.split` function in `shapely.ops` splits a geometry by another geometry. .. function:: shapely.ops.split(geom, splitter) Splits a geometry by another geometry and returns a collection of geometries. This function is the theoretical opposite of the union of the split geometry parts. If the splitter does not split the geometry, a collection with a single geometry equal to the input geometry is returned. The function supports: * Splitting a (Multi)LineString by a (Multi)Point or (Multi)LineString or (Multi)Polygon boundary * Splitting a (Multi)Polygon by a LineString It may be convenient to snap the splitter with low tolerance to the geometry. For example in the case of splitting a line by a point, the point must be exactly on the line, for the line to be correctly split. When splitting a line by a polygon, the boundary of the polygon is used for the operation. When splitting a line by another line, a ValueError is raised if the two overlap at some segment. `New in version 1.6.0` .. code-block:: pycon >>> from shapely.ops import split >>> pt = Point((1, 1)) >>> line = LineString([(0,0), (2,2)]) >>> result = split(line, pt) >>> result Substring --------- The :func:`~shapely.ops.substring` function in :mod:`shapely.ops` returns a line segment between specified distances along a `LineString`. .. function:: shapely.ops.substring(geom, start_dist, end_dist[, normalized=False]) Return the `LineString` between `start_dist` and `end_dist` or a `Point` if they are at the same location Negative distance values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values. If the start distance equals the end distance, a point is being returned. If the start distance is actually past the end distance, then the reversed substring is returned such that the start distance is at the first coordinate. If the normalized arg is ``True``, the distance will be interpreted as a fraction of the geometry's length `New in version 1.7.0` Here are some examples that return `LineString` geometries. .. code-block:: pycon >>> from shapely.ops import substring >>> ls = LineString((i, 0) for i in range(6)) >>> ls >>> substring(ls, start_dist=1, end_dist=3) >>> substring(ls, start_dist=3, end_dist=1) >>> substring(ls, start_dist=1, end_dist=-3) >>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True) And here is an example that returns a `Point`. .. code-block:: pycon >>> substring(ls, start_dist=2.5, end_dist=-2.5) Prepared Geometry Operations ---------------------------- Shapely geometries can be processed into a state that supports more efficient batches of operations. .. function:: prepared.prep(ob) Creates and returns a prepared geometric object. To test one polygon containment against a large batch of points, one should first use the :func:`prepared.prep` function. .. code-block:: pycon >>> from shapely.prepared import prep >>> points = [...] # large list of points >>> polygon = Point(0.0, 0.0).buffer(1.0) >>> prepared_polygon = prep(polygon) >>> prepared_polygon >>> hits = filter(prepared_polygon.contains, points) Prepared geometries instances have the following methods: ``contains``, ``contains_properly``, ``covers``, and ``intersects``. All have exactly the same arguments and usage as their counterparts in non-prepared geometric objects. Diagnostics ----------- .. function:: validation.explain_validity(ob): Returns a string explaining the validity or invalidity of the object. `New in version 1.2.1`. The messages may or may not have a representation of a problem point that can be parsed out. .. code-block:: pycon >>> coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)] >>> p = Polygon(coords) >>> from shapely.validation import explain_validity >>> explain_validity(p) 'Ring Self-intersection[1 1]' .. function:: validation.make_valid(ob) Returns a valid representation of the geometry, if it is invalid. If it is valid, the input geometry will be returned. In many cases, in order to create a valid geometry, the input geometry must be split into multiple parts or multiple geometries. If the geometry must be split into multiple parts of the same geometry type, then a multi-part geometry (e.g. a MultiPolygon) will be returned. if the geometry must be split into multiple parts of different types, then a GeometryCollection will be returned. For example, this operation on a geometry with a bow-tie structure: .. code-block:: pycon >>> from shapely.validation import make_valid >>> coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)] >>> p = Polygon(coords) >>> make_valid(p) Yields a MultiPolygon with two parts: .. plot:: code/make_valid_multipolygon.py While this operation: .. code-block:: pycon >>> from shapely.validation import make_valid >>> coords = [(0, 2), (0, 1), (2, 0), (0, 0), (0, 2)] >>> p = Polygon(coords) >>> make_valid(p) Yields a GeometryCollection with a Polygon and a LineString: .. plot:: code/make_valid_geometrycollection.py `New in version 1.8` `Requires GEOS > 3.8` The Shapely version, GEOS library version, and GEOS C API version are accessible via ``shapely.__version__``, ``shapely.geos_version_string``, and ``shapely.geos_capi_version``. .. code-block:: pycon >>> import shapely >>> shapely.__version__ # doctest: +SKIP '2.0.0' >>> shapely.geos_version # doctest: +SKIP (3, 10, 2) >>> shapely.geos_capi_version_string # doctest: +SKIP '3.10.2-CAPI-1.16.0' Polylabel --------- .. function:: shapely.ops.polylabel(polygon, tolerance) Finds the approximate location of the pole of inaccessibility for a given polygon. Based on Vladimir Agafonkin's polylabel_. `New in version 1.6.0` .. note:: Prior to 1.7 `polylabel` must be imported from `shapely.algorithms.polylabel` instead of `shapely.ops`. .. code-block:: pycon >>> from shapely.ops import polylabel >>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50), ... (-100, -20), (-150, -200)]).buffer(100) >>> label = polylabel(polygon, tolerance=10) >>> label STR-packed R-tree ================= Shapely provides an interface to the query-only GEOS R-tree packed using the Sort-Tile-Recursive algorithm. Pass a list of geometry objects to the STRtree constructor to create a spatial index that you can query with another geometric object. Query-only means that once created, the `STRtree` is immutable. You cannot add or remove geometries. .. class:: strtree.STRtree(geometries) :noindex: The `STRtree` constructor takes a sequence of geometric objects. References to these geometric objects are kept and stored in the R-tree. `New in version 1.4.0`. .. method:: strtree.query(geom) :noindex: Returns the integer indices of all geometries in the `strtree` whose extents intersect the extent of `geom`. This means that a subsequent search through the returned subset using the desired binary predicate (eg. intersects, crosses, contains, overlaps) may be necessary to further filter the results according to their specific spatial relationships. .. code-block:: pycon >>> from shapely import STRtree >>> points = [Point(i, i) for i in range(10)] >>> tree = STRtree(points) >>> query_geom = Point(2,2).buffer(0.99) >>> [points[idx].wkt for idx in tree.query(query_geom)] ['POINT (2 2)'] >>> query_geom = Point(2, 2).buffer(1.0) >>> [points[idx].wkt for idx in tree.query(query_geom)] ['POINT (1 1)', 'POINT (2 2)', 'POINT (3 3)'] >>> [points[idx].wkt for idx in tree.query(query_geom, predicate="intersects")] ['POINT (2 2)'] .. method:: strtree.nearest(geom) :noindex: Returns the nearest geometry in `strtree` to `geom`. .. code-block:: pycon >>> points = [Point(i, i) for i in range(10)] >>> tree = STRtree(points) >>> idx = tree.nearest(Point(2.2, 2.2)) >>> points[idx] Interoperation ============== Shapely provides 4 avenues for interoperation with other software. Well-Known Formats ------------------ A `Well Known Text` (WKT) or `Well Known Binary` (WKB) representation [1]_ of any geometric object can be had via its ``wkt`` or ``wkb`` attribute. These representations allow interchange with many GIS programs. PostGIS, for example, trades in hex-encoded WKB. .. code-block:: pycon >>> Point(0, 0).wkt 'POINT (0 0)' >>> Point(0, 0).wkb b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' >>> Point(0, 0).wkb_hex '010100000000000000000000000000000000000000' The `shapely.wkt` and `shapely.wkb` modules provide `dumps()` and `loads()` functions that work almost exactly as their `pickle` and `simplejson` module counterparts. To serialize a geometric object to a binary or text string, use ``dumps()``. To deserialize a string and get a new geometric object of the appropriate type, use ``loads()``. The default settings for the wkt attribute and `shapely.wkt.dumps()` function are different. By default, the attribute's value is trimmed of excess decimals, while this is not the case for `dumps()`, though it can be replicated by setting `trim=True`. .. function:: shapely.wkb.dumps(ob) Returns a WKB representation of `ob`. .. function:: shapely.wkb.loads(wkb) Returns a geometric object from a WKB representation `wkb`. .. code-block:: pycon >>> from shapely import wkb >>> pt = Point(0, 0) >>> wkb.dumps(pt) b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' >>> pt.wkb b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' >>> wkb.loads(pt.wkb).wkt 'POINT (0 0)' All of Shapely's geometry types are supported by these functions. .. function:: shapely.wkt.dumps(ob) Returns a WKT representation of `ob`. Several keyword arguments are available to alter the WKT which is returned; see the docstrings for more details. .. function:: shapely.wkt.loads(wkt) Returns a geometric object from a WKT representation `wkt`. .. code-block:: pycon >>> from shapely import wkt >>> pt = Point(0, 0) >>> thewkt = wkt.dumps(pt) >>> thewkt 'POINT (0.0000000000000000 0.0000000000000000)' >>> pt.wkt 'POINT (0 0)' >>> wkt.dumps(pt, trim=True) 'POINT (0 0)' .. _array-interface: Numpy and Python Arrays ----------------------- All geometric objects with coordinate sequences (`Point`, `LinearRing`, `LineString`) provide the Numpy array interface and can thereby be converted or adapted to Numpy arrays. .. code-block:: pycon >>> import numpy as np >>> np.asarray(Point(0, 0).coords) array([[0., 0.]]) >>> np.asarray(LineString([(0, 0), (1, 1)]).coords) array([[0., 0.], [1., 1.]]) The coordinates of the same types of geometric objects can be had as standard Python arrays of `x` and `y` values via the ``xy`` attribute. .. code-block:: pycon >>> Point(0, 0).xy (array('d', [0.0]), array('d', [0.0])) >>> LineString([(0, 0), (1, 1)]).xy (array('d', [0.0, 1.0]), array('d', [0.0, 1.0])) Python Geo Interface -------------------- Any object that provides the GeoJSON-like `Python geo interface`_ can be converted to a Shapely geometry using the :func:`shapely.geometry.shape` function. .. function:: shapely.geometry.shape(context) Returns a new, independent geometry with coordinates `copied` from the context. For example, a dictionary: .. code-block:: pycon >>> from shapely.geometry import shape >>> data = {"type": "Point", "coordinates": (0.0, 0.0)} >>> geom = shape(data) >>> geom.geom_type 'Point' >>> list(geom.coords) [(0.0, 0.0)] Or a simple placemark-type object: .. code-block:: pycon >>> class GeoThing: ... def __init__(self, d): ... self.__geo_interface__ = d >>> thing = GeoThing({"type": "Point", "coordinates": (0.0, 0.0)}) >>> geom = shape(thing) >>> geom.geom_type 'Point' >>> list(geom.coords) [(0.0, 0.0)] The GeoJSON-like mapping of a geometric object can be obtained using :func:`shapely.geometry.mapping`. .. function:: shapely.geometry.mapping(ob) Returns a GeoJSON-like mapping from a Geometry or any object which implements ``__geo_interface__``. `New in version 1.2.3`. For example, using the same `GeoThing` class: .. code-block:: pycon >>> from shapely.geometry import mapping >>> thing = GeoThing({"type": "Point", "coordinates": (0.0, 0.0)}) >>> m = mapping(thing) >>> m['type'] 'Point' >>> m['coordinates'] (0.0, 0.0) Performance =========== Shapely uses the GEOS_ library for all operations. GEOS is written in C++ and used in many applications and you can expect that all operations are highly optimized. The creation of new geometries with many coordinates, however, involves some overhead that might slow down your code. Conclusion ========== We hope that you will enjoy and profit from using Shapely. This manual will be updated and improved regularly. Its source is available at https://github.com/shapely/shapely/tree/main/docs/. References ========== .. [1] John R. Herring, Ed., “OpenGIS Implementation Specification for Geographic information - Simple feature access - Part 1: Common architecture,” Oct. 2006. .. [2] M.J. Egenhofer and John R. Herring, Categorizing Binary Topological Relations Between Regions, Lines, and Points in Geographic Databases, Orono, ME: University of Maine, 1991. .. [3] E. Clementini, P. Di Felice, and P. van Oosterom, “A Small Set of Formal Topological Relationships Suitable for End-User Interaction,” Third International Symposium on Large Spatial Databases (SSD). Lecture Notes in Computer Science no. 692, David Abel and Beng Chin Ooi, Eds., Singapore: Springer Verlag, 1993, pp. 277-295. .. [4] C. Strobl, “Dimensionally Extended Nine-Intersection Model (DE-9IM),” Encyclopedia of GIS, S. Shekhar and H. Xiong, Eds., Springer, 2008, pp. 240-245. [|Strobl-PDF|_] .. [5] Martin Davis, “JTS Technical Specifications,” Mar. 2003. [|JTS-PDF|_] .. [6] David H. Douglas and Thomas K. Peucker, “Algorithms for the Reduction of the Number of Points Required to Represent a Digitized Line or its Caricature,” Cartographica: The International Journal for Geographic Information and Geovisualization, vol. 10, Dec. 1973, pp. 112-122. .. _GEOS: https://libgeos.org/ .. _Java Topology Suite: https://projects.eclipse.org/projects/locationtech.jts .. _PostGIS: https://postgis.net .. _Open Geospatial Consortium: https://www.opengeospatial.org/ .. _Strobl-PDF: https://giswiki.hsr.ch/images/3/3d/9dem_springer.pdf .. |Strobl-PDF| replace:: PDF .. _JTS-PDF: https://github.com/locationtech/jts/raw/master/doc/JTS%20Technical%20Specs.pdf .. |JTS-PDF| replace:: PDF .. _frozenset: https://docs.python.org/library/stdtypes.html#frozenset .. _Sorting HowTo: https://wiki.python.org/moin/HowTo/Sorting/ .. _Python geo interface: https://gist.github.com/2217756 .. _polylabel: https://github.com/mapbox/polylabel shapely-2.0.3/docs/measurement.rst000066400000000000000000000002701456366510000171750ustar00rootroot00000000000000Measurement =========== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("measurement") %} {{ function }} {% endfor %} shapely-2.0.3/docs/migration.rst000066400000000000000000000306411456366510000166460ustar00rootroot00000000000000.. _migration: ============================== Migrating to Shapely 1.8 / 2.0 ============================== Shapely 1.8.0 is a transitional version introducing several warnings in preparation of the upcoming changes in 2.0.0. Shapely 2.0.0 will be a major release with a refactor of the internals with considerable performance improvements (based on the developments in the `PyGEOS `__ package), along with several breaking changes. This guide gives an overview of the most important changes with details on what will change in 2.0.0, how we warn for this in 1.8.0, and how you can update your code to be future-proof. For more background, see `RFC 1: Roadmap for Shapely 2.0 `__. Geometry objects will become immutable ====================================== Geometry objects will become immutable in version 2.0.0. In Shapely 1.x, some of the geometry classes are mutable, meaning that you can change their coordinates in-place. Illustrative code:: >>> from shapely.geometry import LineString >>> line = LineString([(0,0), (2, 2)]) >>> print(line) LINESTRING (0 0, 2 2) >>> line.coords = [(0, 0), (10, 0), (10, 10)] >>> print(line) LINESTRING (0 0, 10 0, 10 10) In Shapely 1.8, this will start raising a warning:: >>> line.coords = [(0, 0), (10, 0), (10, 10)] ShapelyDeprecationWarning: Setting the 'coords' to mutate a Geometry in place is deprecated, and will not be possible any more in Shapely 2.0 and starting with version 2.0.0, all geometry objects will become immutable. As a consequence, they will also become hashable and therefore usable as, for example, dictionary keys. **How do I update my code?** There is no direct alternative for mutating the coordinates of an existing geometry, except for creating a new geometry object with the new coordinates. Setting custom attributes ------------------------- Another consequence of the geometry objects becoming immutable is that assigning custom attributes, which currently works, will no longer be possible. Currently you can do:: >>> line.name = "my_geometry" >>> line.name 'my_geometry' In Shapely 1.8, this will start raising a warning, and will raise an AttributeError in Shapely 2.0. **How do I update my code?** There is no direct alternative for adding custom attributes to geometry objects. You can use other Python data structures such as (GeoJSON-like) dictionaries or GeoPandas' GeoDataFrames to store attributes alongside geometry features. Multi-part geometries will no longer be "sequences" (length, iterable, indexable) ================================================================================= In Shapely 1.x, multi-part geometries (MultiPoint, MultiLineString, MultiPolygon and GeometryCollection) implement a part of the "sequence" python interface (making them list-like). This means you can iterate through the object to get the parts, index into the object to get a specific part, and ask for the number of parts with the ``len()`` method. Some examples of this with Shapely 1.x: >>> from shapely.geometry import Point, MultiPoint >>> mp = MultiPoint([(1, 1), (2, 2), (3, 3)]) >>> print(mp) MULTIPOINT (1 1, 2 2, 3 3) >>> for part in mp: ... print(part) POINT (1 1) POINT (2 2) POINT (3 3) >>> print(mp[1]) POINT (2 2) >>> len(mp) 3 >>> list(mp) [, , ] Starting with Shapely 1.8, all the examples above will start raising a deprecation warning. For example: >>> for part in mp: ... print(part) ShapelyDeprecationWarning: Iteration over multi-part geometries is deprecated and will be removed in Shapely 2.0. Use the `geoms` property to access the constituent parts of a multi-part geometry. POINT (1 1) POINT (2 2) POINT (3 3) In Shapely 2.0, all those examples will raise an error. **How do I update my code?** To access the geometry parts of a multi-part geometry, you can use the ``.geoms`` attribute, as the warning indicates. The examples above can be updated to:: >>> for part in mp.geoms: ... print(part) POINT (1 1) POINT (2 2) POINT (3 3) >>> print(mp.geoms[1]) POINT (2 2) >>> len(mp.geoms) 3 >>> list(mp.geoms) [, , ] The single-part geometries (Point, LineString, Polygon) already didn't support those features, and for those classes there is no change in behaviour for this aspect. Interoperability with NumPy and the array interface =================================================== Conversion of the coordinates to (NumPy) arrays ----------------------------------------------- Shapely provides an array interface to have easy access to the coordinates as, for example, NumPy arrays (:ref:`manual section `). A small example:: >>> line = LineString([(0, 0), (1, 1), (2, 2)]) >>> import numpy as np >>> np.asarray(line) array([[0., 0.], [1., 1.], [2., 2.]]) In addition, there are also the explicit ``array_interface()`` method and ``ctypes`` attribute to get access to the coordinates as array data: >>> line.ctypes >>> line.array_interface() {'version': 3, 'typestr': ', 'shape': (3, 2)} This functionality is available for Point, LineString, LinearRing and MultiPoint. For more robust interoperability with NumPy, this array interface will be removed from those geometry classes, and limited to the ``coords``. Starting with Shapely 1.8, converting a geometry object to a NumPy array directly will start raising a warning:: >>> np.asarray(line) ShapelyDeprecationWarning: The array interface is deprecated and will no longer work in Shapely 2.0. Convert the '.coords' to a NumPy array instead. array([[0., 0.], [1., 1.], [2., 2.]]) **How do I update my code?** To convert a geometry to a NumPy array, you can convert the ``.coords`` attribute instead:: >>> line.coords >>> np.array(line.coords) array([[0., 0.], [1., 1.], [2., 2.]]) The ``array_interface()`` method and ``ctypes`` attribute will be removed in Shapely 2.0, but since Shapely will start requiring NumPy as a dependency, you can use NumPy or its array interface directly. Check the NumPy docs on the :py:attr:`ctypes ` attribute or the :ref:`array interface ` for more details. Creating NumPy arrays of geometry objects ----------------------------------------- Shapely geometry objects can be stored in NumPy arrays using the ``object`` dtype. In general, one could create such an array from a list of geometries as follows:: >>> from shapely.geometry import Point >>> arr = np.array([Point(0, 0), Point(1, 1), Point(2, 2)]) >>> arr array([, , ], dtype=object) The above works for point geometries, but because in Shapely 1.x, some geometry types are sequence-like (see above), NumPy can try to "unpack" them when creating an array. Therefore, for more robust creation of a NumPy array from a list of geometries, it's generally recommended to this in a two-step way (first creating an empty array and then filling it):: geoms = [Point(0, 0), Point(1, 1), Point(2, 2)] arr = np.empty(len(geoms), dtype="object") arr[:] = geoms This code snippet results in the same array as the example above, and works for all geometry types and Shapely/NumPy versions. However, starting with Shapely 1.8, the above code will show deprecation warnings that cannot be avoided (depending on the geometry type, NumPy tries to access the array interface of the objects or check if an object is iterable or has a length, and those operations are all deprecated now. The end result is still correct, but the warnings appear nonetheless). Specifically in this case, it is fine to ignore those warnings (and the only way to make them go away):: import warnings from shapely.errors import ShapelyDeprecationWarning geoms = [Point(0, 0), Point(1, 1), Point(2, 2)] arr = np.empty(len(geoms), dtype="object") with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=ShapelyDeprecationWarning) arr[:] = geoms In Shapely 2.0, the geometry objects will no longer be sequence like and those deprecation warnings will be removed (and thus the ``filterwarnings`` will no longer be necessary), and creation of NumPy arrays will generally be more robust. If you maintain code that depends on Shapely, and you want to have it work with multiple versions of Shapely, the above code snippet provides a context manager that can be copied into your project:: import contextlib import shapely import warnings from packaging import version # https://packaging.pypa.io/ SHAPELY_GE_20 = version.parse(shapely.__version__) >= version.parse("2.0a1") try: from shapely.errors import ShapelyDeprecationWarning as shapely_warning except ImportError: shapely_warning = None if shapely_warning is not None and not SHAPELY_GE_20: @contextlib.contextmanager def ignore_shapely2_warnings(): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=shapely_warning) yield else: @contextlib.contextmanager def ignore_shapely2_warnings(): yield This can then be used when creating NumPy arrays (be careful to *only* use it for this specific purpose, and not generally suppress those warnings):: geoms = [...] arr = np.empty(len(geoms), dtype="object") with ignore_shapely2_warnings(): arr[:] = geoms Consistent creation of empty geometries ======================================= Shapely 1.x is inconsistent in creating empty geometries between various creation methods. A small example for an empty Polygon geometry: .. code-block:: pycon # Using an empty constructor results in a GeometryCollection >>> from shapely.geometry import Polygon >>> g1 = Polygon() >>> type(g1) >>> g1.wkt GEOMETRYCOLLECTION EMPTY # Converting from WKT gives a correct empty polygon >>> from shapely import wkt >>> g2 = wkt.loads("POLYGON EMPTY") >>> type(g2) >>> g2.wkt POLYGON EMPTY Shapely 1.8 does not yet change this inconsistent behaviour, but starting with Shapely 2.0, the different methods will always consistently give an empty geometry object of the correct type, instead of using an empty GeometryCollection as "generic" empty geometry object. **How do I update my code?** Those cases that will change don't raise a warning, but you will need to update your code if you rely on the fact that empty geometry objects are of the GeometryCollection type. Use the ``.is_empty`` attribute for robustly checking if a geometry object is an empty geometry. In addition, the WKB serialization methods will start supporting empty Points (using ``"POINT (NaN NaN)"`` to represent an empty point). Other deprecated functionality ============================== There are some other various functions and methods deprecated in Shapely 1.8 as well: - The adapters to create geometry-like proxy objects with coordinates stored outside Shapely geometries are deprecated and will be removed in Shapely 2.0 (e.g. created using ``asShape()``). They have little to no benefit compared to the normal geometry classes, as thus you can convert to your data to a normal geometry object instead. Use the ``shape()`` function instead to convert a GeoJSON-like dict to a Shapely geometry. - The ``empty()`` method on a geometry object is deprecated. - The ``shapely.ops.cascaded_union`` function is deprecated. Use ``shapely.ops.unary_union`` instead, which internally already uses a cascaded union operation for better performance. shapely-2.0.3/docs/migration_pygeos.rst000066400000000000000000000070501456366510000202320ustar00rootroot00000000000000.. _migration-pygeos: ===================== Migrating from PyGEOS ===================== The PyGEOS package was merged with Shapely in December 2021 and will be released as part of Shapely 2.0. No further development will take place for the PyGEOS package (except for providing up to date packages until Shapely 2.0 is released). Therefore, everybody using PyGEOS is highly recommended to migrate to Shapely 2.0. Generally speaking, this should be a smooth experience because all functionality of PyGEOS was added to Shapely. All vectorized functions availabe in ``pygeos`` have been added to the top-level ``shapely`` module, with only minor differences (see below). Migrating from PyGEOS to Shapely 2.0 can thus be done by replacing the ``pygeos`` import and module calls:: import pygeos polygon = pygeos.box(0, 0, 2, 2) points = pygeos.points(...) pygeos.contains(polygon, points) Using Shapely 2.0, this can now be written as:: import shapely polygon = shapely.box(0, 0, 2, 2) points = shapely.points(...) shapely.contains(polygon, points) In addition, you now also have the scalar interface of Shapely which wasn't implemented in PyGEOS. Differences between PyGEOS and Shapely 2.0 ========================================== STRtree API changes ------------------- Functionality-wise, everything from ``pygeos.STRtree`` is available in Shapely 2.0. But while merging into Shapely, some methods have been changed or merged: - The ``query()`` and ``query_bulk()`` methods have been merged into a single ``query()`` method. The ``query()`` method now accepts an array of geometries as well in addition to a single geometry, and in that case it will return 2D array of indices. It should thus be a matter of replacing ``query_bulk`` with ``query`` in your code. See :meth:`.STRtree.query` for more details. - The ``nearest()`` method was changed to return an array of the same shape as the input geometries. Thus, for a scalar geometry it now returns a single integer index (instead of a (2, 1) array), and for an array of geometries it now returns a 1D array of indices ((n,) array instead of a (2, n) array). See :meth:`.STRtree.nearest` for more details. - The ``nearest_all()`` method has been replaced with ``query_nearest()``. For an array of geometries, the output is the same, but when passing a scalar geometry as input, the method now returns a 1D array instead of a 2D array (consistent with ``query()``). In addition, this method gained the new ``exclusive`` and ``all_matches`` keywords (with defaults preserving existing behaviour from PyGEOS). See :meth:`.STRtree.query_nearest` for more details. Other differences ----------------- - The ``pygeos.Geometry(..)`` constructor has not been retained in Shapely (the class exists as base class, but the constructor is not callable). Use one of the subclasses, or ``shapely.from_wkt(..)``, instead. - The ``apply()`` function was renamed to ``transform()``. - The ``tolerance`` keyword of the ``segmentize()`` function was renamed to ``max_segment_length``. - The ``quadsegs`` keyword of the ``buffer()`` and ``offset_curve()`` functions was renamed to ``quad_segs``. - The ``preserve_topology`` keyword of ``simplify()`` now defaults to ``True`` instead of ``False``. - The behaviour of ``union_all()`` / ``intersection_all()`` / ``symmetric_difference_all`` was changed to return an empty GeometryCollection for an empty or all-None sequence as input (instead of returning None). - The ``radius`` keyword of the ``buffer()`` funtion was renamed to ``distance``. shapely-2.0.3/docs/predicates.rst000066400000000000000000000002651456366510000167770ustar00rootroot00000000000000Predicates ========== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("predicates") %} {{ function }} {% endfor %} shapely-2.0.3/docs/properties.rst000066400000000000000000000003061456366510000170440ustar00rootroot00000000000000Geometry properties =================== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("_geometry") %} {{ function }} {% endfor %} shapely-2.0.3/docs/release.rst000066400000000000000000000001471456366510000162730ustar00rootroot00000000000000============= Release notes ============= .. toctree:: :maxdepth: 2 release/2.x release/1.x shapely-2.0.3/docs/release/000077500000000000000000000000001456366510000155375ustar00rootroot00000000000000shapely-2.0.3/docs/release/1.x.rst000066400000000000000000000734541456366510000167140ustar00rootroot00000000000000Version 1.x =========== 1.8.4 (2022-08-17) ------------------ Bug fixes: - The new c_geom_p type caused a regression and has been removed (#1487). 1.8.3 (2022-08-16) ------------------ Deprecations: The STRtree class will be changed in 2.0.0 and will not be compatible with the class in versions 1.8.x. This change obsoletes the deprecation announcement in 1.8a3 (below). Packaging: Wheels for 1.8.3 published on PyPI include GEOS 3.10.3. Bug fixes: - The signature for GEOSMinimumClearance has been corrected, fixing an issue affecting aarch64-darwin (#1480) - Return and arg types have been corrected and made more strict for area, length, and distance properties. - A new c_geom_p type has been created to replace c_void_p when calling GEOS functions (#1479). - An incorrect polygon-line intersection (#1427) has been fixed in GEOS 3.10.3, which will be included in wheels published to PyPI. - GEOS buffer parameters are now destroyed, fixing a memory leak (#1440). 1.8.2 (2022-05-03) ------------------ - Make Polygons and MultiPolygons closed by definition, like LinearRings. Resolves #1246. - Perform frozen app check for GEOS before conda env check on macos as we already do on linux (#1301). - Fix leak of GEOS coordinate sequence in nearest_points reported in #1098. 1.8.1.post1 (2022-02-17) ------------------------ This post-release addresses a defect in the 1.8.1 source distribution. No .c files are included in the 1.8.1.post1 sdist and Cython is required to build and install from source. 1.8.1 (2022-02-16) ------------------ Packaging: Wheels for 1.8.1 published on PyPI include GEOS 3.10.2. This version is the best version of GEOS yet. Discrepancies in behavior compared to previous versions are considered to be improvements. For the first time, we will publish wheels for macos_arm64 (see PR #1310). Python version support: Shapely 1.8.1 works with Pythons 3.6-3.10. Bug fixes: - Require Cython >= 0.29.24 to support Python 3.10 (#1224). - Fix array_interface_base (#1235). 1.8.0 (2021-10-25) ------------------ This is the final 1.8.0 release. There have been no changes since 1.8rc2. 1.8rc2 (2021-10-19) ------------------- Build: A pyproject.toml file has been added to specify build dependencies for the _vectorized and _speedups modules (#1128). To install shapely without these build dependencies, use the features of your build tool that disable PEP 517 and 518 support. Bug fixes: - Part of PR #1042, which added a new primary GEOS library name to be searched for, has been reverted by PR #1201. 1.8rc1 (2021-10-04) ------------------- Deprecations: The almost_exact() method of BaseGeometry has been deprecated. It is confusing and will be removed in 2.0.0. The equals_exact() method is to be used instead. Bug fixes: - We ensure that the _speedups module is always imported before _vectorized to avoid an unexplained condition on Windows with Python 3.8 and 3.9 (#1184). 1.8a3 (2021-08-24) ------------------ Deprecations: The STRtree class deprecation warnings have been removed. The class in 2.0.0 will be backwards compatible with the class in 1.8.0. Bug fixes: - The __array_interface__ raises only AttributeError, all other exceptions are deprecated starting with Numpy 1.21 (#1173). - The STRtree class now uses a pair of item, geom sequences internally instead of a dict (#1177). 1.8a2 (2021-07-15) ------------------ Python version support: Shapely 1.8 will support only Python versions >= 3.6. New features: - The STRtree nearest*() methods now take an optional argument that specifies exclusion of the input geometry from results (#1115). - A GeometryTypeError has been added to shapely.errors and is consistently raised instead of TypeError or ValueError as in version 1.7. For backwards compatibility, the new exception will derive from TypeError and Value error until version 2.0 (#1099). - The STRtree class constructor now takes an optional second argument, a sequence of objects to be stored in the tree. If not provided, the sequence indices of the geometries will be stored, as before (#1112). - The STRtree class has new query_geoms(), query_items(), nearest_geom(), and nearest_item() methods (#1112). The query() and nearest() methods remain as aliases for query_geoms() and nearest_geom(). Bug fixes: - We no longer attempt to load libc to get the free function on Linux, but get it from the global symbol table. - GEOS error messages printed when GEOS_getCoordSeq() is passed an empty geometry are avoided by never passing an empty geometry (#1134). - Python's builtin super() is now used only as described in PEP 3135 (#1109). - Only load conda GEOS dll if it exists (on Windows) (#1108). - Add /opt/homebrew/lib to the list of directories to be searched for the GEOS shared library. - Added new library search path to assist app creation with cx_Freeze. 1.8a1 (2021-03-03) ------------------ Shapely 1.8.0 will be a transitional version. There are a few bug fixes and new features, but it is mainly about warning of the upcoming changes in 2.0.0. Several more pre-releases before 1.8.0 are expected. See the migration guide to Shapely 1.8 / 2.0 for more details on how to update your code (https://shapely.readthedocs.io/en/latest/migration.html). Python version support: Shapely 1.8 will support only Python versions >= 3.5 (#884). Deprecations: The following functions and geometry attributes and methods will be removed in version 2.0.0. - ops.cascaded_union - geometry .empty() - geometry .ctypes and .__array_interface__ - multi-part geometry .__len__ - setting custom attributes on geometry objects Geometry objects will become immutable in version 2.0.0. The STRtree class will be entirely changed in 2.0.0. The exact future API is not yet decided, but will be decided before 1.8.0 is released. Deprecation warnings will be emitted in 1.8a1 when any of these features are used. The deprecated .to_wkb() and .to_wkt() methods on the geometry objects have been removed. New features: - Add a normalize() method to geometry classes, exposing the GEOSNormalize algorithm (#1090). - Initialize STRtree with a capacity of 10 items per node (#1070). - Load libraries relocated to shapely/.libs by auditwheel versions < 3.1 or relocated to Shapely.libs by auditwheel versions >= 3.1. - shapely.ops.voronoi_diagram() computes the Voronoi Diagram of a geometry or geometry collection (#833, #851). - shapely.validation.make_valid() fixes invalid geometries (#883) Bug fixes: - For pyinstaller we now handle the case of more than one GEOS library in the environment, such as when fiona and rasterio wheels are co-installed with shapely (#1071). - The ops.split function now splits on touch to eliminate confusing discrepancies between results using multi and single part splitters (#1034). - Several issues with duplication and order of vertices in ops.substring have been fixed (#1008). Packaging: - The wheels uploaded to PyPI will include GEOS 3.9.1. 1.7.1 (2020-08-20) ------------------ - ``STRtree`` now safely implements the pickle protocol (#915). - Documentation has been added for ``minimum_clearance`` (#875, #874). - In ``STRtree.__del__()`` we guard against calling ``GEOSSTRtree_destroy`` when the lgeos module has already been torn down on exit (#897, #830). - Documentation for the ``overlaps()`` method has been corrected (#920). - Correct the test in ``shapely.geometry.base.BaseGeometry.empty()`` to eliminate memory leaks like the one reported in #745. - Get free() not from libc but from the processes global symbols (#891), fixing a bug that manifests on OS X 10.15 and 10.16. - Extracting substrings from complex lines has been made more correct (#848, #849). - Splitting of complex geometries has been sped up by preparing the input geometry (#871). - Fix bug in concatenation of function argtypes (#866). - Improved documentation of STRtree usage (#857). - Improved handling for empty list or list of lists in GeoJSON coordinates (#852). - The polylabel algorithm now accounts for polygon holes (#851, #817). 1.7.0 (2020-01-28) ------------------ This is the final 1.7.0 release. There have been no changes since 1.7b1. 1.7b1 (2020-01-13) ------------------ First beta release. 1.7a3 (2019-12-31) ------------------ New features: - The buffer operation can now be single-sides (#806, #727). Bug fixes: - Add /usr/local/lib to the list of directories to be searched for the GEOS shared library (#795). - ops.substring now returns a line with coords in end-to-front order when given a start position that is greater than the end position (#628). - Implement ``__bool__()`` for geometry base classes so that ``bool(geom)`` returns the logical complement of ``geom.is_empty`` (#754). - Remove assertion on the number of version-like strings found in the GEOS version string. It could be 2 or 3. 1.7a2 (2019-06-21) ------------------ - Nearest neighbor search has been added to STRtree (#668). - Disallow sequences of MultiPolygons as arguments to the MultiPolygon constructor, resolving #588. - Removed vendorized `functools` functions previously used to support Python 2.5. Bug fixes: - Avoid reloading the GEOS shared library when using an installed binary wheel on OS X (#735), resolving issue #553. - The shapely.ops.orient function can now orient multi polygons and geometry collections as well as polygons (#733). - Polygons can now be constructed from sequences of point objects as well as sequences of x, y sequences (#732). - The exterior of an empty polygon is now equal to an empty linear ring (#731). - The bounds property of an empty point object now returns an empty tuple, consistent with other geometry types (#723). - Segmentation faults when non-string values are passed to the WKT loader are avoided by #700. - Failure of ops.substring when the sub linestring coincides with the beginning of the linestring has been fixed (#658). - Segmentation faults from interpolating on an empty linestring are prevented by #655. - A missing special case for rectangular polygons has been added to the polylabel algorithm (#644). - LinearRing can be created from a LineString (#638). - The prepared geometry validation condition has been tightened in #632 to fix the bug reported in #631. - Attempting to interpolate an empty geometry no longer results in a segmentation fault, raising `ValueError` instead (#653). 1.7a1 (2018-07-29) ------------------ New features: - A Python version check is made by the package setup script. Shapely 1.7 supports only Python versions 2.7 and 3.4+ (#610). - Added a new `EmptyGeometry` class to support GeoPandas (#514). - Added new `shapely.ops.substring` function (#459). - Added new `shapely.ops.clip_by_rect` function (#583). - Use DLLs indicated in sys._MEIPASS' to support PyInstaller frozen apps (#523). - `shapely.wkb.dumps` now accepts an `srid` integer keyword argument to write WKB data including a spatial reference ID in the output data (#593). Bug fixes: - `shapely.geometry.shape` can now marshal empty GeoJSON representations (#573). - An exception is raised when an attempt is made to `prepare` a `PreparedGeometry` (#577, #595). - Keyword arguments have been removed from a geometry object's `wkt` property getter (#581, #594). 1.6.4.post1 (2018-01-24) ------------------------ - Fix broken markup in this change log, which restores our nicely formatted readme on PyPI. 1.6.4 (2018-01-24) ------------------ - Handle a ``TypeError`` that can occur when geometries are torn down (#473, #528). 1.6.3 (2017-12-09) ------------------ - AttributeError is no longer raised when accessing __geo_interface__ of an empty polygon (#450). - ``asShape`` now handles empty coordinates in mappings as ``shape`` does (#542). Please note that ``asShape`` is likely to be deprecated in a future version of Shapely. - Check for length of LineString coordinates in speed mode, preventing crashes when using LineStrings with only one coordinate (#546). 1.6.2 (2017-10-30) ------------------ - A 1.6.2.post1 release has been made to fix a problem with macosx wheels uploaded to PyPI. 1.6.2 (2017-10-26) ------------------ - Splitting a linestring by one of its end points will now succeed instead of failing with a ``ValueError`` (#524, #533). - Missing documentation of a geometry's ``overlaps`` predicate has been added (#522). 1.6.1 (2017-09-01) ------------------ - Avoid ``STRTree`` crashes due to dangling references (#505) by maintaining references to added geometries. - Reduce log level to debug when reporting on calls to ctypes ``CDLL()`` that don't succeed and are retried (#515). - Clarification: applications like GeoPandas that need an empty geometry object should use ``BaseGeometry()`` instead of ``Point()`` or ``Polygon()``. An ``EmptyGeometry`` class has been added in the master development branch and will be available in the next non-bugfix release. 1.6.0 (2017-08-21) ------------------ Shapely 1.6.0 adds new attributes to existing geometry classes and new functions (``split()`` and ``polylabel()``) to the shapely.ops module. Exceptions are consolidated in a shapely.errors module and logging practices have been improved. Shapely's optional features depending on Numpy are now gathered into a requirements set named "vectorized" and these may be installed like ``pip install shapely[vectorized]``. Much of the work on 1.6.0 was aimed to improve the project's build and packaging scripts and to minimize run-time dependencies. Shapely now vendorizes packaging to use during builds only and never again invokes the geos-config utility at run-time. In addition to the changes listed under the alpha and beta pre-releases below, the following change has been made to the project: - Project documentation is now hosted at https://shapely.readthedocs.io/en/latest/. Thank you all for using, promoting, and contributing to the Shapely project. 1.6b5 (2017-08-18) ------------------ Bug fixes: - Passing a single coordinate to ``LineString()`` with speedups disabled now raises a ValueError as happens with speedups enabled. This resolves #509. 1.6b4 (2017-02-15) ------------------ Bug fixes: - Isolate vendorized packaging in a _vendor directory, remove obsolete dist-info, and remove packaging from project requirements (resolves #468). 1.6b3 (2016-12-31) ------------------ Bug fixes: - Level for log messages originating from the GEOS notice handler reduced from WARNING to INFO (#447). - Permit speedups to be imported again without Numpy (#444). 1.6b2 (2016-12-12) ------------------ New features: - Add support for GeometryCollection to shape and asShape functions (#422). 1.6b1 (2016-12-12) ------------------ Bug fixes: - Implemented __array_interface__ for empty Points and LineStrings (#403). 1.6a3 (2016-12-01) ------------------ Bug fixes: - Remove accidental hard requirement of Numpy (#431). Packaging: - Put Numpy in an optional requirement set named "vectorized" (#431). 1.6a2 (2016-11-09) ------------------ Bug fixes: - Shapely no longer configures logging in ``geos.py`` (#415). Refactoring: - Consolidation of exceptions in ``shapely.errors``. - ``UnsupportedGEOSVersionError`` is raised when GEOS < 3.3.0 (#407). Packaging: - Added new library search paths to assist Anaconda (#413). - geos-config will now be bypassed when NO_GEOS_CONFIG env var is set. This allows configuration of Shapely builds on Linux systems that for whatever reasons do not include the geos-config program (#322). 1.6a1 (2016-09-14) ------------------ New features: - A new error derived from NotImplementedError, with a more useful message, is raised when the GEOS backend doesn't support a called method (#216). - The ``project()`` method of LineString has been extended to LinearRing geometries (#286). - A new ``minimum_rotated_rectangle`` attribute has been added to the base geometry class (#354). - A new ``shapely.ops.polylabel()`` function has been added. It computes a point suited for labeling concave polygons (#395). - A new ``shapely.ops.split()`` function has been added. It splits a geometry by another geometry of lesser dimension: polygon by line, line by point (#293, #371). - ``Polygon.from_bounds()`` constructs a Polygon from bounding coordinates (#392). - Support for testing with Numpy 1.4.1 has been added (#301). - Support creating all kinds of empty geometries from empty lists of Python objects (#397, #404). Refactoring: - Switch from ``SingleSidedBuffer()`` to ``OffsetCurve()`` for GEOS >= 3.3 (#270). - Cython speedups are now enabled by default (#252). Packaging: - Packaging 16.7, a setup dependency, is vendorized (#314). - Infrastructure for building manylinux1 wheels has been added (#391). - The system's ``geos-config`` program is now only checked when ``setup.py`` is executed, never during normal use of the module (#244). - Added new library search paths to assist PyInstaller (#382) and Windows (#343). 1.5.17 (2016-08-31) ------------------- - Bug fix: eliminate memory leak in geom_factory() (#408). - Bug fix: remove mention of negative distances in parallel_offset and note that vertices of right hand offset lines are reversed (#284). 1.5.16 (2016-05-26) ------------------- - Bug fix: eliminate memory leak when unpickling geometry objects (#384, #385). - Bug fix: prevent crashes when attempting to pickle a prepared geometry, raising ``PicklingError`` instead (#386). - Packaging: extension modules in the OS X wheels uploaded to PyPI link only libgeos_c.dylib now (you can verify and compare to previous releases with ``otool -L shapely/vectorized/_vectorized.so``). 1.5.15 (2016-03-29) ------------------- - Bug fix: use uintptr_t to store pointers instead of long in _geos.pxi, preventing an overflow error (#372, #373). Note that this bug fix was erroneously reported to have been made in 1.5.14, but was not. 1.5.14 (2016-03-27) ------------------- - Bug fix: use ``type()`` instead of ``isinstance()`` when evaluating geometry equality, preventing instances of base and derived classes from being mistaken for equals (#317). - Bug fix: ensure that empty geometries are created when constructors have no args (#332, #333). - Bug fix: support app "freezing" better on Windows by not relying on the ``__file__`` attribute (#342, #377). - Bug fix: ensure that empty polygons evaluate to be ``==`` (#355). - Bug fix: filter out empty geometries that can cause segfaults when creating and loading STRtrees (#345, #348). - Bug fix: no longer attempt to reuse GEOS DLLs already loaded by Rasterio or Fiona on OS X (#374, #375). 1.5.13 (2015-10-09) ------------------- - Restore setup and runtime discovery and loading of GEOS shared library to state at version 1.5.9 (#326). - On OS X we try to reuse any GEOS shared library that may have been loaded via import of Fiona or Rasterio in order to avoid a bug involving the GEOS AbstractSTRtree (#324, #327). 1.5.12 (2015-08-27) ------------------- - Remove configuration of root logger from libgeos.py (#312). - Skip test_fallbacks on Windows (#308). - Call setlocale(locale.LC_ALL, "") instead of resetlocale() on Windows when tearing down the locale test (#308). - Fix for Sphinx warnings (#309). - Addition of .cache, .idea, .pyd, .pdb to .gitignore (#310). 1.5.11 (2015-08-23) ------------------- - Remove packaging module requirement added in 1.5.10 (#305). Distutils can't parse versions using 'rc', but if we stick to 'a' and 'b' we will be fine. 1.5.10 (2015-08-22) ------------------- - Monkey patch affinity module by absolute reference (#299). - Raise TopologicalError in relate() instead of crashing (#294, #295, #303). 1.5.9 (2015-05-27) ------------------ - Fix for 64 bit speedups compatibility (#274). 1.5.8 (2015-04-29) ------------------ - Setup file encoding bug fix (#254). - Support for pyinstaller (#261). - Major prepared geometry operation fix for Windows (#268, #269). - Major fix for OS X binary wheel (#262). 1.5.7 (2015-03-16) ------------------ - Test and fix buggy error and notice handlers (#249). 1.5.6 (2015-02-02) ------------------ - Fix setup regression (#232, #234). - SVG representation improvements (#233, #237). 1.5.5 (2015-01-20) ------------------ - MANIFEST changes to restore _geox.pxi (#231). 1.5.4 (2015-01-19) ------------------ - Fixed OS X binary wheel library load path (#224). 1.5.3 (2015-01-12) ------------------ - Fixed ownership and potential memory leak in polygonize (#223). - Wider release of binary wheels for OS X. 1.5.2 (2015-01-04) ------------------ - Fail installation if GEOS dependency is not met, preventing update breakage (#218, #219). 1.5.1 (2014-12-04) ------------------ - Restore geometry hashing (#209). 1.5.0 (2014-12-02) ------------------ - Affine transformation speedups (#197). - New `==` rich comparison (#195). - Geometry collection constructor (#200). - ops.snap() backed by GEOSSnap (#201). - Clearer exceptions in cases of topological invalidity (#203). 1.4.4 (2014-11-02) ------------------ - Proper conversion of numpy float32 vals to coords (#186). 1.4.3 (2014-10-01) ------------------ - Fix for endianness bug in WKB writer (#174). 1.4.2 (2014-09-29) ------------------ - Fix bungled 1.4.1 release (#176). 1.4.1 (2014-09-23) ------------------ - Return of support for GEOS 3.2 (#176, #178). 1.4.0 (2014-09-08) ------------------ - SVG representations for IPython's inline image protocol. - Efficient and fast vectorized contains(). - Change mitre_limit default to 5.0; raise ValueError with 0.0 (#139). - Allow mix of tuples and Points in sped-up LineString ctor (#152). - New STRtree class (#73). - Add ops.nearest_points() (#147). - Faster creation of geometric objects from others (cloning) (#165). - Removal of tests from package. 1.3.3 (2014-07-23) ------------------ - Allow single-part geometries as argument to ops.cacaded_union() (#135). - Support affine transformations of LinearRings (#112). 1.3.2 (2014-05-13) ------------------ - Let LineString() take a sequence of Points (#130). 1.3.1 (2014-04-22) ------------------ - More reliable proxy cleanup on exit (#106). - More robust DLL loading on all platforms (#114). 1.3.0 (2013-12-31) ------------------ - Include support for Python 3.2 and 3.3 (#56), minimum version is now 2.6. - Switch to GEOS WKT/WKB Reader/Writer API, with defaults changed to enable 3D output dimensions, and to 'trim' WKT output for GEOS >=3.3.0. - Use GEOS version instead of GEOS C API version to determine library capabilities (#65). 1.2.19 (2013-12-30) ------------------- - Add buffering style options (#55). 1.2.18 (2013-07-23) -------------------- - Add shapely.ops.transform. - Permit empty sequences in collection constructors (#49, #50). - Individual polygons in MultiPolygon.__geo_interface__ are changed to tuples to match Polygon.__geo_interface__ (#51). - Add shapely.ops.polygonize_full (#57). 1.2.17 (2013-01-27) ------------------- - Avoid circular import between wkt/wkb and geometry.base by moving calls to GEOS serializers to the latter module. - Set _ndim when unpickling (issue #6). - Don't install DLLs to Python's DLL directory (#37). - Add affinity module of affine transformation (#31). - Fix NameError that blocked installation with PyPy (#40, #41). 1.2.16 (2012-09-18) ------------------- - Add ops.unary_union function. - Alias ops.cascaded_union to ops.unary_union when GEOS CAPI >= (1,7,0). - Add geos_version_string attribute to shapely.geos. - Ensure parent is set when child geometry is accessed. - Generate _speedups.c using Cython when building from repo when missing, stale, or the build target is "sdist". - The is_simple predicate of invalid, self-intersecting linear rings now returns ``False``. - Remove VERSION.txt from repo, it's now written by the distutils setup script with value of shapely.__version__. 1.2.15 (2012-06-27) ------------------- - Eliminate numerical sensitivity in a method chaining test (Debian bug #663210). - Account for cascaded union of random buffered test points being a polygon or multipolygon (Debian bug #666655). - Use Cython to build speedups if it is installed. - Avoid stumbling over SVN revision numbers in GEOS C API version strings. 1.2.14 (2012-01-23) ------------------- - A geometry's coords property is now sliceable, yielding a list of coordinate values. - Homogeneous collections are now sliceable, yielding a new collection of the same type. 1.2.13 (2011-09-16) ------------------- - Fixed errors in speedups on 32bit systems when GEOS references memory above 2GB. - Add shapely.__version__ attribute. - Update the manual. 1.2.12 (2011-08-15) ------------------- - Build Windows distributions with VC7 or VC9 as appropriate. - More verbose report on failure to speed up. - Fix for prepared geometries broken in 1.2.11. - DO NOT INSTALL 1.2.11 1.2.11 (2011-08-04) ------------------- - Ignore AttributeError during exit. - PyPy 1.5 support. - Prevent operation on prepared geometry crasher (#12). - Optional Cython speedups for Windows. - Linux 3 platform support. 1.2.10 (2011-05-09) ------------------- - Add optional Cython speedups. - Add is_cww predicate to LinearRing. - Add function that forces orientation of Polygons. - Disable build of speedups on Windows pending packaging work. 1.2.9 (2011-03-31) ------------------ - Remove extra glob import. - Move examples to shapely.examples. - Add box() constructor for rectangular polygons. - Fix extraneous imports. 1.2.8 (2011-12-03) ------------------ - New parallel_offset method (#6). - Support for Python 2.4. 1.2.7 (2010-11-05) ------------------ - Support for Windows eggs. 1.2.6 (2010-10-21) ------------------ - The geoms property of an empty collection yields [] instead of a ValueError (#3). - The coords and geometry type sproperties have the same behavior as above. - Ensure that z values carry through into products of operations (#4). 1.2.5 (2010-09-19) ------------------ - Stop distributing docs/_build. - Include library fallbacks in test_dlls.py for linux platform. 1.2.4 (2010-09-09) ------------------ - Raise AttributeError when there's no backend support for a method. - Raise OSError if libgeos_c.so (or variants) can't be found and loaded. - Add geos_c DLL loading support for linux platforms where find_library doesn't work. 1.2.3 (2010-08-17) ------------------ - Add mapping function. - Fix problem with GEOSisValidReason symbol for GEOS < 3.1. 1.2.2 (2010-07-23) ------------------ - Add representative_point method. 1.2.1 (2010-06-23) ------------------ - Fixed bounds of singular polygons. - Added shapely.validation.explain_validity function (#226). 1.2 (2010-05-27) ---------------- - Final release. 1.2rc2 (2010-05-26) ------------------- - Add examples and tests to MANIFEST.in. - Release candidate 2. 1.2rc1 (2010-05-25) ------------------- - Release candidate. 1.2b7 (2010-04-22) ------------------ - Memory leak associated with new empty geometry state fixed. 1.2b6 (2010-04-13) ------------------ - Broken GeometryCollection fixed. 1.2b5 (2010-04-09) ------------------ - Objects can be constructed from others of the same type, thereby making copies. Collections can be constructed from sequences of objects, also making copies. - Collections are now iterators over their component objects. - New code for manual figures, using the descartes package. 1.2b4 (2010-03-19) ------------------ - Adds support for the "sunos5" platform. 1.2b3 (2010-02-28) ------------------ - Only provide simplification implementations for GEOS C API >= 1.5. 1.2b2 (2010-02-19) ------------------ - Fix cascaded_union bug introduced in 1.2b1 (#212). 1.2b1 (2010-02-18) ------------------ - Update the README. Remove cruft from setup.py. Add some version 1.2 metadata regarding required Python version (>=2.5,<3) and external dependency (libgeos_c >= 3.1). 1.2a6 (2010-02-09) ------------------ - Add accessor for separate arrays of X and Y values (#210). TODO: fill gap here 1.2a1 (2010-01-20) ------------------ - Proper prototyping of WKB writer, and avoidance of errors on 64-bit systems (#191). - Prototype libgeos_c functions in a way that lets py2exe apps import shapely (#189). 1.2 Branched (2009-09-19) 1.0.12 (2009-04-09) ------------------- - Fix for references held by topology and predicate descriptors. 1.0.11 (2008-11-20) ------------------- - Work around bug in GEOS 2.2.3, GEOSCoordSeq_getOrdinate not exported properly (#178). 1.0.10 (2008-11-17) ------------------- - Fixed compatibility with GEOS 2.2.3 that was broken in 1.0.8 release (#176). 1.0.9 (2008-11-16) ------------------ - Find and load MacPorts libgeos. 1.0.8 (2008-11-01) ------------------ - Fill out GEOS function result and argument types to prevent faults on a 64-bit arch. 1.0.7 (2008-08-22) ------------------ - Polygon rings now have the same dimensions as parent (#168). - Eliminated reference cycles in polygons (#169). 1.0.6 (2008-07-10) ------------------ - Fixed adaptation of multi polygon data. - Raise exceptions earlier from binary predicates. - Beginning distributing new windows DLLs (#166). 1.0.5 (2008-05-20) ------------------ - Added access to GEOS polygonizer function. - Raise exception when insufficient coordinate tuples are passed to LinearRing constructor (#164). 1.0.4 (2008-05-01) ------------------ - Disentangle Python and topological equality (#163). - Add shape(), a factory that copies coordinates from a geo interface provider. To be used instead of asShape() unless you really need to store coordinates outside shapely for efficient use in other code. - Cache GEOS geometries in adapters (#163). 1.0.3 (2008-04-09) ------------------ - Do not release GIL when calling GEOS functions (#158). - Prevent faults when chaining multiple GEOS operators (#159). 1.0.2 (2008-02-26) ------------------ - Fix loss of dimensionality in polygon rings (#155). 1.0.1 (2008-02-08) ------------------ - Allow chaining expressions involving coordinate sequences and geometry parts (#151). - Protect against abnormal use of coordinate accessors (#152). - Coordinate sequences now implement the numpy array protocol (#153). 1.0 (2008-01-18) ---------------- - Final release. 1.0 RC2 (2008-01-16) -------------------- - Added temporary solution for #149. 1.0 RC1 (2008-01-14) -------------------- - First release candidate shapely-2.0.3/docs/release/2.x.rst000066400000000000000000000514211456366510000167030ustar00rootroot00000000000000Version 2.x =========== .. _version-2-0-3: Version 2.0.3 (2024-02-16) -------------------------- Bug fixes: - Fix regression in the ``oriented_envelope`` ufunc to accept array-like input in case of GEOS<3.12 (#1929). Packaging related: - The binary wheels are not yet compatible with a future NumPy 2.0 release, therefore a ``numpy<2`` upper pin was added to the requirements (#1972). - Upgraded the GEOS version in the binary wheel distributions to 3.11.3. .. _version-2-0-2: Version 2.0.2 (2023-10-12) -------------------------- Bug fixes: - Fix regression in the (in)equality comparison (``geom1 == geom2``) using ``__eq__`` to not ignore the z-coordinates (#1732). - Fix ``MultiPolygon()`` constructor to accept polygons without holes (#1850). - Fix :func:`minimum_rotated_rectangle` (:func:`oriented_envelope`) to always return the minimum area solution (instead of minimum width). In practice, it will use the GEOS implementation only for GEOS 3.12+, and for older GEOS versions fall back to the implementation that was included in Shapely < 2. Wheels are available for Python 3.12 (and still include GEOS 3.11.2). Building from source is now compatible with Cython 3. .. _version-2-0-1: Version 2.0.1 (2023-01-30) -------------------------- Bug fixes: - Fix regression in the ``Polygon()`` constructor taking a sequence of Points (#1662). - Fix regression in the geometry constructors when passing ``decimal.Decimal`` coordinate values (#1707). - Fix ``STRtree()`` to not make the passed geometry array immutable as side-effect of the constructor (#1714). - Fix the ``directed`` keyword in ``shapely.ops.linemerge()`` (#1695). Improvements: - Expose the function to get a matplotlib Patch object from a (Multi)Polygon (without already plotting it) publicly as :func:`shapely.plotting.patch_from_polygon` (#1704). Acknowledgments ^^^^^^^^^^^^^^^ Thanks to everyone who contributed to this release! People with a "+" by their names contributed a patch for the first time. * Brendan Ward * Erik Pettersson + * Hood Chatham + * Idan Miara + * Joris Van den Bossche * Martin Fleischmann * Michał Górny + * Sebastian Castro + .. _version-2-0-0: Version 2.0.0 (2022-12-12) -------------------------- Shapely 2.0 version is a major release featuring a complete refactor of the internals and new vectorized (element-wise) array operations, providing considerable performance improvements (based on the developments in the `PyGEOS `__ package), along with several breaking API changes and many feature improvements. For more background, see `RFC 1: Roadmap for Shapely 2.0 `__. Refactor of the internals ^^^^^^^^^^^^^^^^^^^^^^^^^ Shapely wraps the GEOS C++ library for use in Python. Before 2.0, Shapely used ``ctypes`` to link to GEOS at runtime, but doing so resulted in extra overhead and installation challenges. With 2.0, the internals of Shapely have been refactored to expose GEOS functionality through a Python C extension module that is compiled in advance. The pointer to the actual GEOS Geometry object is stored in a lightweight `Python extension type `__. A single `Geometry` Python extension type is defined in C wrapping a `GEOSGeometry` pointer. This extension type is further subclassed in Python to provide the geometry type-specific classes from Shapely (Point, LineString, Polygon, etc). The GEOS pointer is accessible from C as a static attribute of the Python object (an attribute of the C struct that makes up a Python object), which enables using vectorized functions within C and thus avoiding Python overhead while looping over an array of geometries (see next section). Vectorized (element-wise) geometry operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before the 2.0 release, Shapely only provided an interface for scalar (individual) geometry objects. Users had to loop over individual geometries within an array of geometries and call scalar methods or properties, which is both more verbose to use and has a large performance overhead. Shapely 2.0 exposes GEOS operations as vectorized functions that operate on arrays of geometries using a familiar NumPy interface. Those functions are implemented as :ref:`NumPy universal functions ` (or ufunc for short). A universal function is a function that operates on n-dimensional arrays in an element-by-element fashion and supports array broadcasting. All loops over geometries are implemented in C, which results in substantial performance improvements when performing operations using many geometries. This also allows operations to be less verbose. NumPy is now a required dependency. An example of this functionality using a small array of points and a single polygon:: >>> import shapely >>> from shapely import Point, box >>> import numpy as np >>> geoms = np.array([Point(0, 0), Point(1, 1), Point(2, 2)]) >>> polygon = box(0, 0, 2, 2) Before Shapely 2.0, a ``for`` loop was required to operate over an array of geometries:: >>> [polygon.contains(point) for point in geoms] [False, True, False] In Shapely 2.0, we can now compute whether the points are contained in the polygon directly with one function call:: >>> shapely.contains(polygon, geoms) array([False, True, False]) This results in a considerable speedup, especially for larger arrays of geometries, as well as a nicer user interface that avoids the need to write ``for`` loops. Depending on the operation, this can give a performance increase with factors of 4x to 100x. In general, the greatest speedups are for lightweight GEOS operations, such as ``contains``, which would previously have been dominated by the high overhead of ``for`` loops in Python. See https://caspervdw.github.io/Introducing-Pygeos/ for more detailed examples. The new vectorized functions are available in the top-level ``shapely`` namespace. All the familiar geospatial methods and attributes from the geometry classes now have an equivalent as top-level function (with some small name deviations, such as the ``.wkt`` attribute being available as a ``to_wkt()`` function). Some methods from submodules (for example, several functions from the ``shapely.ops`` submodule such as ``polygonize()``) are also made available in a vectorized version as top-level function. A full list of functions can be found in the API docs (see the pages listed under "API REFERENCE" in the left sidebar). * Vectorized constructor functions * Optionally output to a user-specified array (``out`` keyword argument) when constructing geometries from ``indices``. * Enable bulk construction of geometries with different number of coordinates by optionally taking index arrays in all creation functions. Shapely 2.0 API changes (deprecated in 1.8) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Shapely 1.8 release included several deprecation warnings about API changes that would happen in Shapely 2.0 and that can be fixed in your code (making it compatible with both <=1.8 and >=2.0). See :ref:`migration` for more details on how to update your code. It is highly recommended to first upgrade to Shapely 1.8 and resolve all deprecation warnings before upgrading to Shapely 2.0. Summary of changes: * Geometries are now immutable and hashable. * Multi-part geometries such as MultiPolygon no longer behave as "sequences". This means that they no longer have a length, are not iterable, and are not indexable anymore. Use the ``.geoms`` attribute instead to access individual parts of a multi-part geometry. * Geometry objects no longer directly implement the numpy array interface to expose their coordinates. To convert to an array of coordinates, use the ``.coords`` attribute instead (``np.asarray(geom.coords)``). * The following attributes and methods on the Geometry classes were previously deprecated and are now removed from Shapely 2.0: * ``array_interface()`` and ``ctypes`` * ``asShape()``, and the adapters classes to create geometry-like proxy objects (use ``shape()`` instead). * ``empty()`` method Some new deprecations have been introduced in Shapely 2.0: * Directly calling the base class ``BaseGeometry()`` constructor or the ``EmptyGeometry()`` constructor is deprecated and will raise an error in the future. To create an empty geometry, use one of the subclasses instead, for example ``GeometryCollection()`` (#1022). * The ``shapely.speedups`` module (the ``enable`` and ``disable`` functions) is deprecated and will be removed in the future. The module no longer has any affect in Shapely >=2.0. Breaking API changes ^^^^^^^^^^^^^^^^^^^^ Some additional backwards incompatible API changes were included in Shapely 2.0 that were not deprecated in Shapely 1.8: * Consistent creation of empty geometries (for example ``Polygon()`` now actually creates an empty Polygon instead of an empty geometry collection). * The ``.bounds`` attribute of an empty geometry now returns a tuple of NaNs instead of an empty tuple (#1023). * The ``preserve_topology`` keyword of ``simplify()`` now defaults to ``True`` (#1392). * A ``GeometryCollection`` that consists of all empty sub-geometries now returns those empty geometries from its ``.geoms`` attribute instead of returning an empty list (#1420). * The ``Point(..)`` constructor no longer accepts a sequence of coordinates consisting of more than one coordinate pair (previously, subsequent coordinates were ignored) (#1600). * The unused ``shape_factory()`` method and ``HeterogeneousGeometrySequence`` class are removed (#1421). * The undocumented ``__geom__`` attribute has been removed. If necessary (although not recommended for use beyond experimentation), use the ``_geom`` attribute to access the raw GEOS pointer (#1417). * The ``logging`` functionality has been removed. All error messages from GEOS are now raised as Python exceptions (#998). * Several custom exception classes defined in ``shapely.errors`` that are no longer used internally have been removed. Errors from GEOS are now raised as ``GEOSException`` (#1306). The ``STRtree`` interface has been substantially changed. See the section :ref:`below ` for more details. Additionally, starting with GEOS 3.11 (which is included in the binary wheels on PyPI), the behaviour of the ``parallel_offset`` (``offset_curve``) method changed regarding the orientation of the resulting line. With GEOS < 3.11, the line retains the same direction for a left offset (positive distance) or has opposite direction for a right offset (negative distance), and this behaviour was documented as such in previous Shapely versions. Starting with GEOS 3.11, the function tries to preserve the orientation of the original line. New features ^^^^^^^^^^^^ Geometry subclasses are now available in the top-level namespace ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Following the new vectorized functions in the top-level ``shapely`` namespace, the Geometry subclasses (``Point``, ``LineString``, ``Polygon``, etc) are now available in the top-level namespace as well. Thus it is no longer needed to import those from the ``shapely.geometry`` submodule. The following:: from shapely.geometry import Point can be replaced with:: from shapely import Point or:: import shapely shapely.Point(...) Note: for backwards compatibility (and being able to write code that works for both <=1.8 and >2.0), those classes still remain accessible from the ``shapely.geometry`` submodule as well. More informative repr with truncated WKT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The repr (``__repr__``) of Geometry objects has been simplified and improved to include a descriptive Well-Known-Text (WKT) formatting. Instead of showing the class name and id:: >>> Point(0, 0) we now get:: >>> Point(0, 0) For large geometries with many coordinates, the output gets truncated to 80 characters. Support for fixed precision model for geometries and in overlay functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GEOS 3.9.0 overhauled the overlay operations (union, intersection, (symmetric) difference). A complete rewrite, dubbed "OverlayNG", provides a more robust implementation (no more TopologyExceptions even on valid input), the ability to specify the output precision model, and significant performance optimizations. When installing Shapely with GEOS >= 3.9 (which is the case for PyPI wheels and conda-forge packages), you automatically get these improvements (also for previous versions of Shapely) when using the overlay operations. Shapely 2.0 also includes the ability to specify the precision model directly: * The :func:`.set_precision` function can be used to conform a geometry to a certain grid size (may round and reduce coordinates), and this will then also be used by subsequent overlay methods. A :func:`.get_precision` function is also available to inspect the precision model of geometries. * The ``grid_size`` keyword in the overlay methods can also be used to specify the precision model of the output geometry (without first conforming the input geometries). Releasing the GIL for multithreaded applications ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Shapely itself is not multithreaded, but its functions generally allow for multithreading by releasing the Global Interpreter Lock (GIL) during execution. Normally in Python, the GIL prevents multiple threads from computing at the same time. Shapely functions internally release this constraint so that the heavy lifting done by GEOS can be done in parallel, from a single Python process. .. _changelog-2-strtree: STRtree API changes and improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The biggest change in the :class:`.STRtree` interface is that all operations now return indices of the input tree or query geometries, instead of the geometries itself. These indices can be used to index into anything associated with the input geometries, including the input geometries themselves, or custom items stored in another object of the same length and order as the geometries. In addition, Shapely 2.0 includes several improvements to ``STRtree``: * Directly include predicate evaluation in :meth:`.STRtree.query` by specifying the ``predicate`` keyword. If a predicate is provided, tree geometries with bounding boxes that overlap the bounding boxes of the input geometries are further filtered to those that meet the predicate (using prepared geometries under the hood for efficiency). * Query multiple input geometries (spatial join style) with :meth:`.STRtree.query` by passing an array of geometries. In this case, the return value is a 2D array with shape (2, n) where the subarrays correspond to the indices of the input geometries and indices of the tree geometries associated with each. * A new :meth:`.STRtree.query_nearest` method was added, returning the index of the nearest geometries in the tree for each input geometry. Compared to :meth:`.STRtree.nearest`, which only returns the index of a single nearest geometry for each input geometry, this new methods allows for: * returning all equidistant nearest geometries, * excluding nearest geometries that are equal to the input, * specifying an ``max_distance`` to limit the search radius, potentially increasing the performance, * optionally returning the distance. * Fixed ``STRtree`` creation to allow querying the tree in a multi-threaded context. Bindings for new GEOS functionalities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Several (new) functions from GEOS are now exposed in Shapely: * :func:`.hausdorff_distance` and :func:`.frechet_distance` * :func:`.contains_properly` * :func:`.extract_unique_points` * :func:`.reverse` * :func:`.node` * :func:`.contains_xy` and :func:`.intersects_xy` * :func:`.build_area` (GEOS >= 3.8) * :func:`.minimum_bounding_circle` and :func:`.minimum_bounding_radius` (GEOS >= 3.8) * :func:`.coverage_union` and :func:`.coverage_union_all` (GEOS >= 3.8) * :func:`.segmentize` (GEOS >= 3.10) * :func:`.dwithin` (GEOS >= 3.10) * :func:`.remove_repeated_points` (GEOS >= 3.11) * :func:`.line_merge` added `directed` parameter (GEOS > 3.11) * :func:`.concave_hull` (GEOS >= 3.11) In addition some aliases for existing methods have been added to provide a method name consistent with GEOS or PostGIS: * :func:`.line_interpolate_point` (``interpolate``) * :func:`.line_locate_point` (``project``) * :func:`.offset_curve` (``parallel_offset``) * :func:`.point_on_surface` (``representative_point``) * :func:`.oriented_envelope` (``minimum_rotated_rectangle``) * :func:`.delaunay_triangles` (``ops.triangulate``) * :func:`.voronoi_polygons` (``ops.voronoi_diagram``) * :func:`.shortest_line` (``ops.nearest_points``) * :func:`.is_valid_reason` (``validation.explain_validity``) Getting information / parts / coordinates from geometries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A set of GEOS getter functions are now also exposed to inspect geometries: * :func:`.get_dimensions` * :func:`.get_coordinate_dimension` * :func:`.get_srid` * :func:`.get_num_points` * :func:`.get_num_interior_rings` * :func:`.get_num_geometries` * :func:`.get_num_coordinates` * :func:`.get_precision` Several functions are added to extract parts: * :func:`.get_geometry` to get a geometry from a GeometryCollection or Multi-part geometry. * :func:`.get_exterior_ring` and :func:`.get_interior_ring` to get one of the rings of a Polygon. * :func:`.get_point` to get a point (vertex) of a linestring or linearring. * :func:`.get_x`, :func:`.get_y` and :func:`.get_z` to get the x/y/z coordinate of a Point. Methods to extract all parts or coordinates at once have been added: * The :func:`.get_parts` function can be used to get individual parts of an array of multi-part geometries. * The :func:`.get_rings` function, similar as ``get_parts`` but specifically to extract the rings of Polygon geometries. * The :func:`.get_coordinates` function to get all coordinates from a geometry or array of goemetries as an array of floats. Each of those three functions has an optional ``return_index`` keyword, which allows to also return the indexes of the original geometries in the source array. Prepared geometries ~~~~~~~~~~~~~~~~~~~ Prepared geometries are now no longer separate objects, but geometry objects themselves can be prepared (this makes the ``shapely.prepared`` module superfluous). The :func:`.prepare()` function generates a GEOS prepared geometry which is stored on the Geometry object itself. All binary predicates (except ``equals``) will make use of this if the input geometry has already been prepared. Helper functions :func:`.destroy_prepared` and :func:`.is_prepared` are also available. New IO methods (GeoJSON, ragged arrays) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Added GeoJSON input/output capabilities :func:`.from_geojson` and :func:`.to_geojson` for GEOS >= 3.10. * Added conversion to/from ragged array representation using a contiguous array of coordinates and offset arrays: :func:`.to_ragged_array` and :func:`.from_ragged_array`. Other improvements ~~~~~~~~~~~~~~~~~~ * Added :func:`.force_2d` and :func:`.force_3d` to change the dimensionality of the coordinates in a geometry. * Addition of a :func:`.total_bounds` function to return the outer bounds of an array of geometries. * Added :func:`.empty` to create a geometry array pre-filled with None or with empty geometries. * Performance improvement in constructing LineStrings or LinearRings from numpy arrays for GEOS >= 3.10. * Updated the :func:`~shapely.box` ufunc to use internal C function for creating polygon (about 2x faster) and added ``ccw`` parameter to create polygon in counterclockwise (default) or clockwise direction. * Start of a benchmarking suite using ASV. * Added ``shapely.testing.assert_geometries_equal``. Bug fixes ~~~~~~~~~ * Fixed several corner cases in WKT and WKB serialization for varying GEOS versions, including: * Fixed the WKT serialization of single part 3D empty geometries to correctly include "Z" (for GEOS >= 3.9.0). * Handle empty points in WKB serialization by conversion to ``POINT (nan, nan)`` consistently for all GEOS versions (GEOS started doing this for >= 3.9.0). Acknowledgments ^^^^^^^^^^^^^^^ Thanks to everyone who contributed to this release! People with a "+" by their names contributed a patch for the first time. * Adam J. Stewart + * Alan D. Snow + * Ariel Kadouri * Bas Couwenberg * Ben Beasley * Brendan Ward + * Casper van der Wel + * Ewout ter Hoeven + * Geir Arne Hjelle + * James Gaboardi * James Myatt + * Joris Van den Bossche * Keith Jenkins + * Kian Meng Ang + * Krishna Chaitanya + * Kyle Barron * Martin Fleischmann + * Martin Lackner + * Mike Taves * Phil Chiu + * Tanguy Ophoff + * Tom Clancy * Sean Gillies * Giorgos Papadokostakis + * Mattijn van Hoek + * enrico ferreguti + * gpapadok + * mattijn + * odidev + shapely-2.0.3/docs/set_operations.rst000066400000000000000000000003011456366510000177010ustar00rootroot00000000000000Set operations ============== .. currentmodule:: shapely .. autosummary:: :toctree: reference/ {% for function in get_module_functions("set_operations") %} {{ function }} {% endfor %} shapely-2.0.3/docs/strtree.rst000066400000000000000000000000751456366510000163430ustar00rootroot00000000000000STRTree ======= .. autoclass:: shapely.STRtree :members: shapely-2.0.3/docs/testing.rst000066400000000000000000000005401456366510000163250ustar00rootroot00000000000000Testing ======= The functions in this module are not directly importable from the root ``shapely`` module. Instead, import them from the submodule as follows: >>> from shapely.testing import assert_geometries_equal .. automodule:: shapely.testing :members: :exclude-members: :special-members: :inherited-members: :show-inheritance: shapely-2.0.3/pyproject.toml000066400000000000000000000033171456366510000161070ustar00rootroot00000000000000[build-system] requires = [ "Cython", "oldest-supported-numpy", "setuptools>=61.0.0", ] build-backend = "setuptools.build_meta" [project] name = "shapely" dynamic = ["version"] authors = [ {name = "Sean Gillies"}, ] maintainers = [ {name = "Shapely contributors"}, ] description = "Manipulation and analysis of geometric objects" readme = "README.rst" keywords = ["geometry", "topology", "gis"] license = {text = "BSD 3-Clause"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: Unix", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: GIS", ] requires-python = ">=3.7" dependencies = [ "numpy>=1.14,<2", ] [project.optional-dependencies] test = [ "pytest", "pytest-cov", ] docs = [ "numpydoc==1.1.*", "matplotlib", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees", ] [project.urls] Documentation = "https://shapely.readthedocs.io/" Repository = "https://github.com/shapely/shapely" [tool.setuptools] include-package-data = false [tool.setuptools.packages.find] include = ["shapely", "shapely.*"] [tool.setuptools.package-data] "shapely" = ["*.pxd"] [tool.coverage.run] source = ["shapely"] omit = ["shapely/tests/*"] shapely-2.0.3/setup.cfg000066400000000000000000000011051456366510000150050ustar00rootroot00000000000000[versioneer] VCS = git style = pep440 versionfile_source = shapely/_version.py versionfile_build = shapely/_version.py tag_prefix = parentdir_prefix = shapely- [flake8] max-line-length = 88 ignore = # space before : (needed for how black formats slicing) E203, # too many leading '#' for block comment E266, # TODO line length too long E501, # line break before binary operator W503, [tool:isort] profile = black force_alphabetical_sort_within_sections = true [tool:pytest] doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS xfail_strict = true shapely-2.0.3/setup.py000066400000000000000000000143161456366510000147060ustar00rootroot00000000000000import builtins import logging import os import subprocess import sys from pathlib import Path from pkg_resources import parse_version from setuptools import Extension, find_packages, setup from setuptools.command.build_ext import build_ext as _build_ext # ensure the current directory is on sys.path so versioneer can be imported # when pip uses PEP 517/518 build rules. # https://github.com/python-versioneer/python-versioneer/issues/193 sys.path.insert(0, os.path.dirname(__file__)) import versioneer # Skip Cython build if not available try: from Cython.Build import cythonize except ImportError: cythonize = None log = logging.getLogger(__name__) ch = logging.StreamHandler() log.addHandler(ch) MIN_GEOS_VERSION = "3.5" if "all" in sys.warnoptions: # show GEOS messages in console with: python -W all log.setLevel(logging.DEBUG) def get_geos_config(option): """Get configuration option from the `geos-config` development utility The PATH environment variable should include the path where geos-config is located, or the GEOS_CONFIG environment variable should point to the executable. """ cmd = os.environ.get("GEOS_CONFIG", "geos-config") try: stdout, stderr = subprocess.Popen( [cmd, option], stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate() except OSError: return if stderr and not stdout: log.warning("geos-config %s returned '%s'", option, stderr.decode().strip()) return result = stdout.decode().strip() log.debug("geos-config %s returned '%s'", option, result) return result def get_geos_paths(): """Obtain the paths for compiling and linking with the GEOS C-API First the presence of the GEOS_INCLUDE_PATH and GEOS_INCLUDE_PATH environment variables is checked. If they are both present, these are taken. If one of the two paths was not present, geos-config is called (it should be on the PATH variable). geos-config provides all the paths. If geos-config was not found, no additional paths are provided to the extension. It is still possible to compile in this case using custom arguments to setup.py. """ include_dir = os.environ.get("GEOS_INCLUDE_PATH") library_dir = os.environ.get("GEOS_LIBRARY_PATH") if include_dir and library_dir: return { "include_dirs": ["./src", include_dir], "library_dirs": [library_dir], "libraries": ["geos_c"], } geos_version = get_geos_config("--version") if not geos_version: log.warning( "Could not find geos-config executable. Either append the path to geos-config" " to PATH or manually provide the include_dirs, library_dirs, libraries and " "other link args for compiling against a GEOS version >=%s.", MIN_GEOS_VERSION, ) return {} if parse_version(geos_version) < parse_version(MIN_GEOS_VERSION): raise ImportError( "GEOS version should be >={}, found {}".format( MIN_GEOS_VERSION, geos_version ) ) libraries = [] library_dirs = [] include_dirs = ["./src"] extra_link_args = [] for item in get_geos_config("--cflags").split(): if item.startswith("-I"): include_dirs.extend(item[2:].split(":")) for item in get_geos_config("--clibs").split(): if item.startswith("-L"): library_dirs.extend(item[2:].split(":")) elif item.startswith("-l"): libraries.append(item[2:]) else: extra_link_args.append(item) return { "include_dirs": include_dirs, "library_dirs": library_dirs, "libraries": libraries, "extra_link_args": extra_link_args, } class build_ext(_build_ext): def finalize_options(self): _build_ext.finalize_options(self) # Add numpy include dirs without importing numpy on module level. # derived from scikit-hep: # https://github.com/scikit-hep/root_numpy/pull/292 # Prevent numpy from thinking it is still in its setup process: try: del builtins.__NUMPY_SETUP__ except AttributeError: pass import numpy self.include_dirs.append(numpy.get_include()) ext_modules = [] if "clean" in sys.argv: # delete any previously Cythonized or compiled files in pygeos p = Path(".") for pattern in [ "build/lib.*/shapely/*.so", "shapely/*.c", "shapely/*.so", "shapely/*.pyd", ]: for filename in p.glob(pattern): print(f"removing '{filename}'") filename.unlink() elif "sdist" in sys.argv: if Path("LICENSE_GEOS").exists() or Path("LICENSE_win32").exists(): raise FileExistsError( "Source distributions should not pack LICENSE_GEOS or LICENSE_win32. Please remove the files." ) else: ext_options = get_geos_paths() ext_modules = [ Extension( "shapely.lib", sources=[ "src/c_api.c", "src/coords.c", "src/geos.c", "src/lib.c", "src/pygeom.c", "src/strtree.c", "src/ufuncs.c", "src/vector.c", ], **ext_options, ) ] # Cython is required if not cythonize: sys.exit("ERROR: Cython is required to build shapely from source.") cython_modules = [ Extension( "shapely._geometry_helpers", [ "shapely/_geometry_helpers.pyx", ], **ext_options, ), Extension( "shapely._geos", [ "shapely/_geos.pyx", ], **ext_options, ), ] ext_modules += cythonize( cython_modules, compiler_directives={"language_level": "3"}, # enable once Cython >= 0.3 is released # define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")], ) cmdclass = versioneer.get_cmdclass() cmdclass["build_ext"] = build_ext # see pyproject.toml for static project metadata setup( version=versioneer.get_version(), ext_modules=ext_modules, cmdclass=cmdclass, ) shapely-2.0.3/shapely/000077500000000000000000000000001456366510000146345ustar00rootroot00000000000000shapely-2.0.3/shapely/__init__.py000066400000000000000000000020311456366510000167410ustar00rootroot00000000000000from shapely.lib import GEOSException # NOQA from shapely.lib import Geometry # NOQA from shapely.lib import geos_version, geos_version_string # NOQA from shapely.lib import geos_capi_version, geos_capi_version_string # NOQA from shapely.errors import setup_signal_checks # NOQA from shapely._geometry import * # NOQA from shapely.creation import * # NOQA from shapely.constructive import * # NOQA from shapely.predicates import * # NOQA from shapely.measurement import * # NOQA from shapely.set_operations import * # NOQA from shapely.linear import * # NOQA from shapely.coordinates import * # NOQA from shapely.strtree import * # NOQA from shapely.io import * # NOQA # Submodule always needs to be imported to ensure Geometry subclasses are registered from shapely.geometry import ( # NOQA Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, LinearRing, ) from shapely import _version __version__ = _version.get_versions()["version"] setup_signal_checks() shapely-2.0.3/shapely/_enum.py000066400000000000000000000013111456366510000163050ustar00rootroot00000000000000from enum import IntEnum class ParamEnum(IntEnum): """Wraps IntEnum to provide validation of a requested item. Intended for enums used for function parameters. Use enum.get_value(item) for this behavior instead of builtin enum[item]. """ @classmethod def get_value(cls, item): """Validate incoming item and raise a ValueError with valid options if not present.""" try: return cls[item].value except KeyError: valid_options = {e.name for e in cls} raise ValueError( "'{}' is not a valid option, must be one of '{}'".format( item, "', '".join(valid_options) ) ) shapely-2.0.3/shapely/_geometry.py000066400000000000000000000565651456366510000172210ustar00rootroot00000000000000import warnings from enum import IntEnum import numpy as np from shapely import _geometry_helpers, geos_version, lib from shapely._enum import ParamEnum from shapely.decorators import multithreading_enabled, requires_geos __all__ = [ "GeometryType", "get_type_id", "get_dimensions", "get_coordinate_dimension", "get_num_coordinates", "get_srid", "set_srid", "get_x", "get_y", "get_z", "get_exterior_ring", "get_num_points", "get_num_interior_rings", "get_num_geometries", "get_point", "get_interior_ring", "get_geometry", "get_parts", "get_rings", "get_precision", "set_precision", "force_2d", "force_3d", ] class GeometryType(IntEnum): """The enumeration of GEOS geometry types""" MISSING = -1 POINT = 0 LINESTRING = 1 LINEARRING = 2 POLYGON = 3 MULTIPOINT = 4 MULTILINESTRING = 5 MULTIPOLYGON = 6 GEOMETRYCOLLECTION = 7 # generic @multithreading_enabled def get_type_id(geometry, **kwargs): """Returns the type ID of a geometry. - None (missing) is -1 - POINT is 0 - LINESTRING is 1 - LINEARRING is 2 - POLYGON is 3 - MULTIPOINT is 4 - MULTILINESTRING is 5 - MULTIPOLYGON is 6 - GEOMETRYCOLLECTION is 7 Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- GeometryType Examples -------- >>> from shapely import LineString, Point >>> get_type_id(LineString([(0, 0), (1, 1), (2, 2), (3, 3)])) 1 >>> get_type_id([Point(1, 2), Point(2, 3)]).tolist() [0, 0] """ return lib.get_type_id(geometry, **kwargs) @multithreading_enabled def get_dimensions(geometry, **kwargs): """Returns the inherent dimensionality of a geometry. The inherent dimension is 0 for points, 1 for linestrings and linearrings, and 2 for polygons. For geometrycollections it is the max of the containing elements. Empty collections and None values return -1. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, Point, Polygon >>> point = Point(0, 0) >>> get_dimensions(point) 0 >>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]) >>> get_dimensions(polygon) 2 >>> get_dimensions(GeometryCollection([point, polygon])) 2 >>> get_dimensions(GeometryCollection([])) -1 >>> get_dimensions(None) -1 """ return lib.get_dimensions(geometry, **kwargs) @multithreading_enabled def get_coordinate_dimension(geometry, **kwargs): """Returns the dimensionality of the coordinates in a geometry (2 or 3). Returns -1 for missing geometries (``None`` values). Note that if the first Z coordinate equals ``nan``, this function will return ``2``. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Point >>> get_coordinate_dimension(Point(0, 0)) 2 >>> get_coordinate_dimension(Point(0, 0, 1)) 3 >>> get_coordinate_dimension(None) -1 >>> get_coordinate_dimension(Point(0, 0, float("nan"))) 2 """ return lib.get_coordinate_dimension(geometry, **kwargs) @multithreading_enabled def get_num_coordinates(geometry, **kwargs): """Returns the total number of coordinates in a geometry. Returns 0 for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, Point >>> point = Point(0, 0) >>> get_num_coordinates(point) 1 >>> get_num_coordinates(Point(0, 0, 0)) 1 >>> line = LineString([(0, 0), (1, 1)]) >>> get_num_coordinates(line) 2 >>> get_num_coordinates(GeometryCollection([point, line])) 3 >>> get_num_coordinates(None) 0 """ return lib.get_num_coordinates(geometry, **kwargs) @multithreading_enabled def get_srid(geometry, **kwargs): """Returns the SRID of a geometry. Returns -1 for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- set_srid Examples -------- >>> from shapely import Point >>> point = Point(0, 0) >>> get_srid(point) 0 >>> with_srid = set_srid(point, 4326) >>> get_srid(with_srid) 4326 """ return lib.get_srid(geometry, **kwargs) @multithreading_enabled def set_srid(geometry, srid, **kwargs): """Returns a geometry with its SRID set. Parameters ---------- geometry : Geometry or array_like srid : int **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_srid Examples -------- >>> from shapely import Point >>> point = Point(0, 0) >>> get_srid(point) 0 >>> with_srid = set_srid(point, 4326) >>> get_srid(with_srid) 4326 """ return lib.set_srid(geometry, np.intc(srid), **kwargs) # points @multithreading_enabled def get_x(point, **kwargs): """Returns the x-coordinate of a point Parameters ---------- point : Geometry or array_like Non-point geometries will result in NaN being returned. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_y, get_z Examples -------- >>> from shapely import MultiPoint, Point >>> get_x(Point(1, 2)) 1.0 >>> get_x(MultiPoint([(1, 1), (1, 2)])) nan """ return lib.get_x(point, **kwargs) @multithreading_enabled def get_y(point, **kwargs): """Returns the y-coordinate of a point Parameters ---------- point : Geometry or array_like Non-point geometries will result in NaN being returned. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_x, get_z Examples -------- >>> from shapely import MultiPoint, Point >>> get_y(Point(1, 2)) 2.0 >>> get_y(MultiPoint([(1, 1), (1, 2)])) nan """ return lib.get_y(point, **kwargs) @requires_geos("3.7.0") @multithreading_enabled def get_z(point, **kwargs): """Returns the z-coordinate of a point. Parameters ---------- point : Geometry or array_like Non-point geometries or geometries without 3rd dimension will result in NaN being returned. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_x, get_y Examples -------- >>> from shapely import MultiPoint, Point >>> get_z(Point(1, 2, 3)) 3.0 >>> get_z(Point(1, 2)) nan >>> get_z(MultiPoint([(1, 1, 1), (2, 2, 2)])) nan """ return lib.get_z(point, **kwargs) # linestrings @multithreading_enabled def get_point(geometry, index, **kwargs): """Returns the nth point of a linestring or linearring. Parameters ---------- geometry : Geometry or array_like index : int or array_like Negative values count from the end of the linestring backwards. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_num_points Examples -------- >>> from shapely import LinearRing, LineString, MultiPoint, Point >>> line = LineString([(0, 0), (1, 1), (2, 2), (3, 3)]) >>> get_point(line, 1) >>> get_point(line, -2) >>> get_point(line, [0, 3]).tolist() [, ] The functcion works the same for LinearRing input: >>> get_point(LinearRing([(0, 0), (1, 1), (2, 2), (0, 0)]), 1) For non-linear geometries it returns None: >>> get_point(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]), 1) is None True >>> get_point(Point(1, 1), 0) is None True """ return lib.get_point(geometry, np.intc(index), **kwargs) @multithreading_enabled def get_num_points(geometry, **kwargs): """Returns number of points in a linestring or linearring. Returns 0 for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like The number of points in geometries other than linestring or linearring equals zero. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_point get_num_geometries Examples -------- >>> from shapely import LineString, MultiPoint >>> get_num_points(LineString([(0, 0), (1, 1), (2, 2), (3, 3)])) 4 >>> get_num_points(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)])) 0 >>> get_num_points(None) 0 """ return lib.get_num_points(geometry, **kwargs) # polygons @multithreading_enabled def get_exterior_ring(geometry, **kwargs): """Returns the exterior ring of a polygon. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_interior_ring Examples -------- >>> from shapely import Point, Polygon >>> get_exterior_ring(Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])) >>> get_exterior_ring(Point(1, 1)) is None True """ return lib.get_exterior_ring(geometry, **kwargs) @multithreading_enabled def get_interior_ring(geometry, index, **kwargs): """Returns the nth interior ring of a polygon. Parameters ---------- geometry : Geometry or array_like index : int or array_like Negative values count from the end of the interior rings backwards. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_exterior_ring get_num_interior_rings Examples -------- >>> from shapely import Point, Polygon >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> get_interior_ring(polygon_with_hole, 0) >>> get_interior_ring(polygon_with_hole, 1) is None True >>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]) >>> get_interior_ring(polygon, 0) is None True >>> get_interior_ring(Point(0, 0), 0) is None True """ return lib.get_interior_ring(geometry, np.intc(index), **kwargs) @multithreading_enabled def get_num_interior_rings(geometry, **kwargs): """Returns number of internal rings in a polygon Returns 0 for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like The number of interior rings in non-polygons equals zero. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_exterior_ring get_interior_ring Examples -------- >>> from shapely import Point, Polygon >>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]) >>> get_num_interior_rings(polygon) 0 >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> get_num_interior_rings(polygon_with_hole) 1 >>> get_num_interior_rings(Point(0, 0)) 0 >>> get_num_interior_rings(None) 0 """ return lib.get_num_interior_rings(geometry, **kwargs) # collections @multithreading_enabled def get_geometry(geometry, index, **kwargs): """Returns the nth geometry from a collection of geometries. Parameters ---------- geometry : Geometry or array_like index : int or array_like Negative values count from the end of the collection backwards. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Notes ----- - simple geometries act as length-1 collections - out-of-range values return None See also -------- get_num_geometries, get_parts Examples -------- >>> from shapely import Point, MultiPoint >>> multipoint = MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)]) >>> get_geometry(multipoint, 1) >>> get_geometry(multipoint, -1) >>> get_geometry(multipoint, 5) is None True >>> get_geometry(Point(1, 1), 0) >>> get_geometry(Point(1, 1), 1) is None True """ return lib.get_geometry(geometry, np.intc(index), **kwargs) def get_parts(geometry, return_index=False): """Gets parts of each GeometryCollection or Multi* geometry object; returns a copy of each geometry in the GeometryCollection or Multi* geometry object. Note: This does not return the individual parts of Multi* geometry objects in a GeometryCollection. You may need to call this function multiple times to return individual parts of Multi* geometry objects in a GeometryCollection. Parameters ---------- geometry : Geometry or array_like return_index : bool, default False If True, will return a tuple of ndarrays of (parts, indexes), where indexes are the indexes of the original geometries in the source array. Returns ------- ndarray of parts or tuple of (parts, indexes) See also -------- get_geometry, get_rings Examples -------- >>> from shapely import MultiPoint >>> get_parts(MultiPoint([(0, 1), (2, 3)])).tolist() [, ] >>> parts, index = get_parts([MultiPoint([(0, 1)]), MultiPoint([(4, 5), (6, 7)])], \ return_index=True) >>> parts.tolist() [, , ] >>> index.tolist() [0, 1, 1] """ geometry = np.asarray(geometry, dtype=np.object_) geometry = np.atleast_1d(geometry) if geometry.ndim != 1: raise ValueError("Array should be one dimensional") if return_index: return _geometry_helpers.get_parts(geometry) return _geometry_helpers.get_parts(geometry)[0] def get_rings(geometry, return_index=False): """Gets rings of Polygon geometry object. For each Polygon, the first returned ring is always the exterior ring and potential subsequent rings are interior rings. If the geometry is not a Polygon, nothing is returned (empty array for scalar geometry input or no element in output array for array input). Parameters ---------- geometry : Geometry or array_like return_index : bool, default False If True, will return a tuple of ndarrays of (rings, indexes), where indexes are the indexes of the original geometries in the source array. Returns ------- ndarray of rings or tuple of (rings, indexes) See also -------- get_exterior_ring, get_interior_ring, get_parts Examples -------- >>> from shapely import Polygon >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> get_rings(polygon_with_hole).tolist() [, ] With ``return_index=True``: >>> polygon = Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)]) >>> rings, index = get_rings([polygon, polygon_with_hole], return_index=True) >>> rings.tolist() [, , ] >>> index.tolist() [0, 1, 1] """ geometry = np.asarray(geometry, dtype=np.object_) geometry = np.atleast_1d(geometry) if geometry.ndim != 1: raise ValueError("Array should be one dimensional") if return_index: return _geometry_helpers.get_parts(geometry, extract_rings=True) return _geometry_helpers.get_parts(geometry, extract_rings=True)[0] @multithreading_enabled def get_num_geometries(geometry, **kwargs): """Returns number of geometries in a collection. Returns 0 for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like The number of geometries in points, linestrings, linearrings and polygons equals one. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_num_points get_geometry Examples -------- >>> from shapely import MultiPoint, Point >>> get_num_geometries(MultiPoint([(0, 0), (1, 1), (2, 2), (3, 3)])) 4 >>> get_num_geometries(Point(1, 1)) 1 >>> get_num_geometries(None) 0 """ return lib.get_num_geometries(geometry, **kwargs) @requires_geos("3.6.0") @multithreading_enabled def get_precision(geometry, **kwargs): """Get the precision of a geometry. If a precision has not been previously set, it will be 0 (double precision). Otherwise, it will return the precision grid size that was set on a geometry. Returns NaN for not-a-geometry values. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- set_precision Examples -------- >>> from shapely import Point >>> point = Point(1, 1) >>> get_precision(point) 0.0 >>> geometry = set_precision(point, 1.0) >>> get_precision(geometry) 1.0 >>> get_precision(None) nan """ return lib.get_precision(geometry, **kwargs) class SetPrecisionMode(ParamEnum): valid_output = 0 pointwise = 1 keep_collapsed = 2 @requires_geos("3.6.0") @multithreading_enabled def set_precision(geometry, grid_size, mode="valid_output", **kwargs): """Returns geometry with the precision set to a precision grid size. By default, geometries use double precision coordinates (grid_size = 0). Coordinates will be rounded if a precision grid is less precise than the input geometry. Duplicated vertices will be dropped from lines and polygons for grid sizes greater than 0. Line and polygon geometries may collapse to empty geometries if all vertices are closer together than grid_size. Z values, if present, will not be modified. Note: subsequent operations will always be performed in the precision of the geometry with higher precision (smaller "grid_size"). That same precision will be attached to the operation outputs. Also note: input geometries should be geometrically valid; unexpected results may occur if input geometries are not. Returns None if geometry is None. Parameters ---------- geometry : Geometry or array_like grid_size : float Precision grid size. If 0, will use double precision (will not modify geometry if precision grid size was not previously set). If this value is more precise than input geometry, the input geometry will not be modified. mode : {'valid_output', 'pointwise', 'keep_collapsed'}, default 'valid_output' This parameter determines how to handle invalid output geometries. There are three modes: 1. `'valid_output'` (default): The output is always valid. Collapsed geometry elements (including both polygons and lines) are removed. Duplicate vertices are removed. 2. `'pointwise'`: Precision reduction is performed pointwise. Output geometry may be invalid due to collapse or self-intersection. Duplicate vertices are not removed. In GEOS this option is called NO_TOPO. .. note:: 'pointwise' mode requires at least GEOS 3.10. It is accepted in earlier versions, but the results may be unexpected. 3. `'keep_collapsed'`: Like the default mode, except that collapsed linear geometry elements are preserved. Collapsed polygonal input elements are removed. Duplicate vertices are removed. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_precision Examples -------- >>> from shapely import LineString, Point >>> set_precision(Point(0.9, 0.9), 1.0) >>> set_precision(Point(0.9, 0.9, 0.9), 1.0) >>> set_precision(LineString([(0, 0), (0, 0.1), (0, 1), (1, 1)]), 1.0) >>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="valid_output") >>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="pointwise") >>> set_precision(LineString([(0, 0), (0, 0.1), (0.1, 0.1)]), 1.0, mode="keep_collapsed") >>> set_precision(None, 1.0) is None True """ if isinstance(mode, str): mode = SetPrecisionMode.get_value(mode) elif not np.isscalar(mode): raise TypeError("mode only accepts scalar values") if mode == SetPrecisionMode.pointwise and geos_version < (3, 10, 0): warnings.warn( "'pointwise' is only supported for GEOS 3.10", UserWarning, stacklevel=2, ) return lib.set_precision(geometry, grid_size, np.intc(mode), **kwargs) @multithreading_enabled def force_2d(geometry, **kwargs): """Forces the dimensionality of a geometry to 2D. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point, Polygon, from_wkt >>> force_2d(Point(0, 0, 1)) >>> force_2d(Point(0, 0)) >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) >>> force_2d(from_wkt("POLYGON Z EMPTY")) >>> force_2d(None) is None True """ return lib.force_2d(geometry, **kwargs) @multithreading_enabled def force_3d(geometry, z=0.0, **kwargs): """Forces the dimensionality of a geometry to 3D. 2D geometries will get the provided Z coordinate; Z coordinates of 3D geometries are unchanged (unless they are nan). Note that for empty geometries, 3D is only supported since GEOS 3.9 and then still only for simple geometries (non-collections). Parameters ---------- geometry : Geometry or array_like z : float or array_like, default 0.0 **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point >>> force_3d(Point(0, 0), z=3) >>> force_3d(Point(0, 0, 0), z=3) >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) >>> force_3d(None) is None True """ if np.isnan(z).any(): raise ValueError("It is not allowed to set the Z coordinate to NaN.") return lib.force_3d(geometry, z, **kwargs) shapely-2.0.3/shapely/_geometry_helpers.pyx000066400000000000000000000342111456366510000211130ustar00rootroot00000000000000# distutils: define_macros=GEOS_USE_ONLY_R_API cimport cython from cpython cimport PyObject from cython cimport view from libc.stdint cimport uintptr_t import numpy as np cimport numpy as np import shapely from shapely._geos cimport ( GEOSContextHandle_t, GEOSCoordSequence, GEOSGeom_clone_r, GEOSGeom_createCollection_r, GEOSGeom_createEmptyPolygon_r, GEOSGeom_createLinearRing_r, GEOSGeom_createLineString_r, GEOSGeom_createPoint_r, GEOSGeom_createPolygon_r, GEOSGeom_destroy_r, GEOSGeometry, GEOSGeomTypeId_r, GEOSGetExteriorRing_r, GEOSGetGeometryN_r, GEOSGetInteriorRingN_r, get_geos_handle, ) from shapely._pygeos_api cimport ( import_shapely_c_api, PyGEOS_CoordSeq_FromBuffer, PyGEOS_CreateGeometry, PyGEOS_GetGEOSGeometry, ) # initialize Shapely C API import_shapely_c_api() def _check_out_array(object out, Py_ssize_t size): if out is None: return np.empty(shape=(size, ), dtype=object) if not isinstance(out, np.ndarray): raise TypeError("out array must be of numpy.ndarray type") if not out.flags.writeable: raise TypeError("out array must be writeable") if out.dtype != object: raise TypeError("out array dtype must be object") if out.ndim != 1: raise TypeError("out must be a one-dimensional array.") if out.shape[0] < size: raise ValueError(f"out array is too small ({out.shape[0]} < {size})") return out @cython.boundscheck(False) @cython.wraparound(False) def simple_geometries_1d(object coordinates, object indices, int geometry_type, object out = None): cdef Py_ssize_t idx = 0 cdef unsigned int coord_idx = 0 cdef Py_ssize_t geom_idx = 0 cdef unsigned int geom_size = 0 cdef unsigned int ring_closure = 0 cdef GEOSGeometry *geom = NULL cdef GEOSCoordSequence *seq = NULL # Cast input arrays and define memoryviews for later usage coordinates = np.asarray(coordinates, dtype=np.float64, order="C") if coordinates.ndim != 2: raise TypeError("coordinates must be a two-dimensional array.") indices = np.asarray(indices, dtype=np.intp) # intp is what bincount takes if indices.ndim != 1: raise TypeError("indices must be a one-dimensional array.") if coordinates.shape[0] != indices.shape[0]: raise ValueError("geometries and indices do not have equal size.") cdef unsigned int dims = coordinates.shape[1] if dims not in {2, 3}: raise ValueError("coordinates should N by 2 or N by 3.") if geometry_type not in {0, 1, 2}: raise ValueError(f"Invalid geometry_type: {geometry_type}.") if coordinates.shape[0] == 0: # return immediately if there are no geometries to return return np.empty(shape=(0, ), dtype=np.object_) if np.any(indices[1:] < indices[:indices.shape[0] - 1]): raise ValueError("The indices must be sorted.") cdef const double[:, :] coord_view = coordinates # get the geometry count per collection (this raises on negative indices) cdef unsigned int[:] coord_counts = np.bincount(indices).astype(np.uint32) # The final target array cdef Py_ssize_t n_geoms = coord_counts.shape[0] # Allow missing indices only if 'out' was given explicitly (if 'out' is not # supplied by the user, we would have to come up with an output value ourselves). cdef char allow_missing = out is not None out = _check_out_array(out, n_geoms) cdef object[:] out_view = out with get_geos_handle() as geos_handle: for geom_idx in range(n_geoms): geom_size = coord_counts[geom_idx] if geom_size == 0: if allow_missing: continue else: raise ValueError( f"Index {geom_idx} is missing from the input indices." ) # check if we need to close a linearring if geometry_type == 2: ring_closure = 0 if geom_size == 3: ring_closure = 1 else: for coord_idx in range(dims): if coord_view[idx, coord_idx] != coord_view[idx + geom_size - 1, coord_idx]: ring_closure = 1 break # check the resulting size to prevent invalid rings if geom_size + ring_closure < 4: # the error equals PGERR_LINEARRING_NCOORDS (in shapely/src/geos.h) raise ValueError("A linearring requires at least 4 coordinates.") seq = PyGEOS_CoordSeq_FromBuffer(geos_handle, &coord_view[idx, 0], geom_size, dims, ring_closure) if seq == NULL: return # GEOSException is raised by get_geos_handle idx += geom_size if geometry_type == 0: geom = GEOSGeom_createPoint_r(geos_handle, seq) elif geometry_type == 1: geom = GEOSGeom_createLineString_r(geos_handle, seq) elif geometry_type == 2: geom = GEOSGeom_createLinearRing_r(geos_handle, seq) if geom == NULL: return # GEOSException is raised by get_geos_handle out_view[geom_idx] = PyGEOS_CreateGeometry(geom, geos_handle) return out cdef const GEOSGeometry* GetRingN(GEOSContextHandle_t handle, GEOSGeometry* polygon, int n): if n == 0: return GEOSGetExteriorRing_r(handle, polygon) else: return GEOSGetInteriorRingN_r(handle, polygon, n - 1) @cython.boundscheck(False) @cython.wraparound(False) def get_parts(object[:] array, bint extract_rings=0): cdef Py_ssize_t geom_idx = 0 cdef Py_ssize_t part_idx = 0 cdef Py_ssize_t idx = 0 cdef Py_ssize_t count cdef GEOSGeometry *geom = NULL cdef const GEOSGeometry *part = NULL if extract_rings: counts = shapely.get_num_interior_rings(array) is_polygon = (shapely.get_type_id(array) == 3) & (~shapely.is_empty(array)) counts += is_polygon count = counts.sum() else: counts = shapely.get_num_geometries(array) count = counts.sum() if count == 0: # return immediately if there are no geometries to return return ( np.empty(shape=(0, ), dtype=object), np.empty(shape=(0, ), dtype=np.intp) ) parts = np.empty(shape=(count, ), dtype=object) index = np.empty(shape=(count, ), dtype=np.intp) cdef int[:] counts_view = counts cdef object[:] parts_view = parts cdef np.intp_t[:] index_view = index with get_geos_handle() as geos_handle: for geom_idx in range(array.size): if counts_view[geom_idx] <= 0: # No parts to return, skip this item continue if PyGEOS_GetGEOSGeometry(array[geom_idx], &geom) == 0: raise TypeError("One of the arguments is of incorrect type. " "Please provide only Geometry objects.") if geom == NULL: continue for part_idx in range(counts_view[geom_idx]): index_view[idx] = geom_idx if extract_rings: part = GetRingN(geos_handle, geom, part_idx) else: part = GEOSGetGeometryN_r(geos_handle, geom, part_idx) if part == NULL: return # GEOSException is raised by get_geos_handle # clone the geometry to keep it separate from the inputs part = GEOSGeom_clone_r(geos_handle, part) if part == NULL: return # GEOSException is raised by get_geos_handle # cast part back to to discard const qualifier # pending issue #227 parts_view[idx] = PyGEOS_CreateGeometry(part, geos_handle) idx += 1 return parts, index cdef _deallocate_arr(void* handle, np.intp_t[:] arr, Py_ssize_t last_geom_i): """Deallocate a temporary geometry array to prevent memory leaks""" cdef Py_ssize_t i = 0 cdef GEOSGeometry *g for i in range(last_geom_i): g = arr[i] if g != NULL: GEOSGeom_destroy_r(handle, arr[i]) @cython.boundscheck(False) @cython.wraparound(False) def collections_1d(object geometries, object indices, int geometry_type = 7, object out = None): """Converts geometries + indices to collections Allowed geometry type conversions are: - linearrings to polygons - points to multipoints - linestrings/linearrings to multilinestrings - polygons to multipolygons - any to geometrycollections """ cdef Py_ssize_t geom_idx_1 = 0 cdef Py_ssize_t coll_idx = 0 cdef unsigned int coll_size = 0 cdef Py_ssize_t coll_geom_idx = 0 cdef GEOSGeometry *geom = NULL cdef GEOSGeometry *coll = NULL cdef int expected_type = -1 cdef int expected_type_alt = -1 cdef int curr_type = -1 if geometry_type == 3: # POLYGON expected_type = 2 elif geometry_type == 4: # MULTIPOINT expected_type = 0 elif geometry_type == 5: # MULTILINESTRING expected_type = 1 expected_type_alt = 2 elif geometry_type == 6: # MULTIPOLYGON expected_type = 3 elif geometry_type == 7: pass else: raise ValueError(f"Invalid geometry_type: {geometry_type}.") # Cast input arrays and define memoryviews for later usage geometries = np.asarray(geometries, dtype=object) if geometries.ndim != 1: raise TypeError("geometries must be a one-dimensional array.") indices = np.asarray(indices, dtype=np.intp) # intp is what bincount takes if indices.ndim != 1: raise TypeError("indices must be a one-dimensional array.") if geometries.shape[0] != indices.shape[0]: raise ValueError("geometries and indices do not have equal size.") if geometries.shape[0] == 0: # return immediately if there are no geometries to return return np.empty(shape=(0, ), dtype=object) if np.any(indices[1:] < indices[:indices.shape[0] - 1]): raise ValueError("The indices should be sorted.") # get the geometry count per collection (this raises on negative indices) cdef int[:] collection_size = np.bincount(indices).astype(np.int32) # A temporary array for the geometries that will be given to CreateCollection. # Its size equals max(collection_size) to accomodate the largest collection. temp_geoms = np.empty(shape=(np.max(collection_size), ), dtype=np.intp) cdef np.intp_t[:] temp_geoms_view = temp_geoms # The final target array cdef Py_ssize_t n_colls = collection_size.shape[0] # Allow missing indices only if 'out' was given explicitly (if 'out' is not # supplied by the user, we would have to come up with an output value ourselves). cdef char allow_missing = out is not None out = _check_out_array(out, n_colls) cdef object[:] out_view = out with get_geos_handle() as geos_handle: for coll_idx in range(n_colls): if collection_size[coll_idx] == 0: if allow_missing: continue else: raise ValueError( f"Index {coll_idx} is missing from the input indices." ) coll_size = 0 # fill the temporary array with geometries belonging to this collection for coll_geom_idx in range(collection_size[coll_idx]): if PyGEOS_GetGEOSGeometry(geometries[geom_idx_1 + coll_geom_idx], &geom) == 0: _deallocate_arr(geos_handle, temp_geoms_view, coll_size) raise TypeError( "One of the arguments is of incorrect type. Please provide only Geometry objects." ) # ignore missing values if geom == NULL: continue # Check geometry subtype for non-geometrycollections if geometry_type != 7: curr_type = GEOSGeomTypeId_r(geos_handle, geom) if curr_type == -1: _deallocate_arr(geos_handle, temp_geoms_view, coll_size) return # GEOSException is raised by get_geos_handle if curr_type != expected_type and curr_type != expected_type_alt: _deallocate_arr(geos_handle, temp_geoms_view, coll_size) raise TypeError( f"One of the arguments has unexpected geometry type {curr_type}." ) # assign to the temporary geometry array geom = GEOSGeom_clone_r(geos_handle, geom) if geom == NULL: _deallocate_arr(geos_handle, temp_geoms_view, coll_size) return # GEOSException is raised by get_geos_handle temp_geoms_view[coll_size] = geom coll_size += 1 # create the collection if geometry_type != 3: # Collection coll = GEOSGeom_createCollection_r( geos_handle, geometry_type, &temp_geoms_view[0], coll_size ) elif coll_size != 0: # Polygon, non-empty coll = GEOSGeom_createPolygon_r( geos_handle, temp_geoms_view[0], NULL if coll_size <= 1 else &temp_geoms_view[1], coll_size - 1 ) else: # Polygon, empty coll = GEOSGeom_createEmptyPolygon_r( geos_handle ) if coll == NULL: return # GEOSException is raised by get_geos_handle out_view[coll_idx] = PyGEOS_CreateGeometry(coll, geos_handle) geom_idx_1 += collection_size[coll_idx] return out def _geom_factory(uintptr_t g): with get_geos_handle() as geos_handle: geom = PyGEOS_CreateGeometry(g, geos_handle) return geom shapely-2.0.3/shapely/_geos.pxd000066400000000000000000000055111456366510000164470ustar00rootroot00000000000000""" Provides a wrapper for GEOS types and functions. Note: GEOS functions in Cython must be called using the get_geos_handle context manager. Example: with get_geos_handle() as geos_handle: SomeGEOSFunc(geos_handle, ...) """ cdef extern from "geos_c.h": # Types ctypedef void *GEOSContextHandle_t ctypedef struct GEOSGeometry ctypedef struct GEOSCoordSequence ctypedef void (*GEOSMessageHandler_r)(const char *message, void *userdata) # GEOS Context & Messaging GEOSContextHandle_t GEOS_init_r() nogil void GEOS_finish_r(GEOSContextHandle_t handle) nogil void GEOSContext_setErrorMessageHandler_r(GEOSContextHandle_t handle, GEOSMessageHandler_r ef, void* userData) nogil void GEOSContext_setNoticeMessageHandler_r(GEOSContextHandle_t handle, GEOSMessageHandler_r nf, void* userData) nogil # Geometry functions const GEOSGeometry* GEOSGetGeometryN_r(GEOSContextHandle_t handle, const GEOSGeometry* g, int n) nogil const GEOSGeometry* GEOSGetExteriorRing_r(GEOSContextHandle_t handle, const GEOSGeometry* g) nogil const GEOSGeometry* GEOSGetInteriorRingN_r(GEOSContextHandle_t handle, const GEOSGeometry* g, int n) nogil int GEOSGeomTypeId_r(GEOSContextHandle_t handle, GEOSGeometry* g) nogil # Geometry creation / destruction GEOSGeometry* GEOSGeom_clone_r(GEOSContextHandle_t handle, const GEOSGeometry* g) nogil GEOSGeometry* GEOSGeom_createPoint_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil GEOSGeometry* GEOSGeom_createLineString_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil GEOSGeometry* GEOSGeom_createLinearRing_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil GEOSGeometry* GEOSGeom_createEmptyPolygon_r(GEOSContextHandle_t handle) nogil GEOSGeometry* GEOSGeom_createPolygon_r(GEOSContextHandle_t handle, GEOSGeometry* shell, GEOSGeometry** holes, unsigned int nholes) nogil GEOSGeometry* GEOSGeom_createCollection_r(GEOSContextHandle_t handle, int type, GEOSGeometry** geoms, unsigned int ngeoms) nogil void GEOSGeom_destroy_r(GEOSContextHandle_t handle, GEOSGeometry* g) nogil # Coordinate sequences GEOSCoordSequence* GEOSCoordSeq_create_r(GEOSContextHandle_t handle, unsigned int size, unsigned int dims) nogil void GEOSCoordSeq_destroy_r(GEOSContextHandle_t handle, GEOSCoordSequence* s) nogil int GEOSCoordSeq_setX_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil int GEOSCoordSeq_setY_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil int GEOSCoordSeq_setZ_r(GEOSContextHandle_t handle, GEOSCoordSequence* s, unsigned int idx, double val) nogil cdef class get_geos_handle: cdef GEOSContextHandle_t handle cdef char* last_error cdef char* last_warning cdef GEOSContextHandle_t __enter__(self) shapely-2.0.3/shapely/_geos.pyx000066400000000000000000000027311456366510000164750ustar00rootroot00000000000000# distutils: define_macros=GEOS_USE_ONLY_R_API #from shapely import GEOSException from libc.stdio cimport snprintf from libc.stdlib cimport free, malloc import warnings from shapely import GEOSException cdef void geos_message_handler(const char* message, void* userdata) noexcept: snprintf(userdata, 1024, "%s", message) cdef class get_geos_handle: '''This class provides a context manager that wraps the GEOS context handle. Example ------- with get_geos_handle() as geos_handle: SomeGEOSFunc(geos_handle, ...) ''' cdef GEOSContextHandle_t __enter__(self): self.handle = GEOS_init_r() self.last_error = malloc((1025) * sizeof(char)) self.last_error[0] = 0 self.last_warning = malloc((1025) * sizeof(char)) self.last_warning[0] = 0 GEOSContext_setErrorMessageHandler_r( self.handle, &geos_message_handler, self.last_error ) GEOSContext_setNoticeMessageHandler_r( self.handle, &geos_message_handler, self.last_warning ) return self.handle def __exit__(self, type, value, traceback): try: if self.last_error[0] != 0: raise GEOSException(self.last_error) if self.last_warning[0] != 0: warnings.warn(self.last_warning) finally: GEOS_finish_r(self.handle) free(self.last_error) free(self.last_warning) shapely-2.0.3/shapely/_pygeos_api.pxd000066400000000000000000000027561456366510000176610ustar00rootroot00000000000000""" Provides a wrapper for the shapely.lib C API for use in Cython. Internally, the shapely C extension uses a PyCapsule to provide run-time access to function pointers within the C API. To use these functions, you must first call the following function in each Cython module: `import_shapely_c_api()` This uses a macro to dynamically load the functions from pointers in the PyCapsule. Each C function in shapely.lib exposed in the C API must be specially-wrapped to enable this capability. Segfaults will occur if the C API is not imported properly. """ cimport numpy as np from cpython.ref cimport PyObject from shapely._geos cimport GEOSContextHandle_t, GEOSCoordSequence, GEOSGeometry cdef extern from "c_api.h": # shapely.lib C API loader; returns -1 on error # MUST be called before calling other C API functions int import_shapely_c_api() except -1 # C functions provided by the shapely.lib C API # Note: GeometryObjects are always managed as Python objects # in Cython to avoid memory leaks, not PyObject* (even though # they are declared that way in the header file). object PyGEOS_CreateGeometry(GEOSGeometry *ptr, GEOSContextHandle_t ctx) char PyGEOS_GetGEOSGeometry(PyObject *obj, GEOSGeometry **out) nogil GEOSCoordSequence* PyGEOS_CoordSeq_FromBuffer(GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure) nogil shapely-2.0.3/shapely/_ragged_array.py000066400000000000000000000401321456366510000177740ustar00rootroot00000000000000""" This modules provides a conversion to / from a ragged (or "jagged") array representation of the geometries. A ragged array is an irregular array of arrays of which each element can have a different length. As a result, such an array cannot be represented as a standard, rectangular nD array. The coordinates of geometries can be represented as arrays of arrays of coordinate pairs (possibly multiple levels of nesting, depending on the geometry type). Geometries, as a ragged array of coordinates, can be efficiently represented as contiguous arrays of coordinates provided that there is another data structure that keeps track of which range of coordinate values corresponds to a given geometry. This can be done using offsets, counts, or indices. This module currently implements offsets into the coordinates array. This is the ragged array representation defined by the the Apache Arrow project as "variable size list array" (https://arrow.apache.org/docs/format/Columnar.html#variable-size-list-layout). See for example https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#representations-features for different options. The exact usage of the Arrow list array with varying degrees of nesting for the different geometry types is defined by the GeoArrow project: https://github.com/geoarrow/geoarrow """ import numpy as np from shapely import creation from shapely._geometry import ( GeometryType, get_coordinate_dimension, get_parts, get_rings, get_type_id, ) from shapely.coordinates import get_coordinates from shapely.predicates import is_empty __all__ = ["to_ragged_array", "from_ragged_array"] # # GEOS -> coords/offset arrays (to_ragged_array) def _get_arrays_point(arr, include_z): # only one array of coordinates coords = get_coordinates(arr, include_z=include_z) # empty points are represented by NaNs empties = is_empty(arr) if empties.any(): indices = np.nonzero(empties)[0] indices = indices - np.arange(len(indices)) coords = np.insert(coords, indices, np.nan, axis=0) return coords, () def _indices_to_offsets(indices, n): offsets = np.insert(np.bincount(indices).cumsum(), 0, 0) if len(offsets) != n + 1: # last geometries might be empty or missing offsets = np.pad( offsets, (0, n + 1 - len(offsets)), "constant", constant_values=offsets[-1], ) return offsets def _get_arrays_multipoint(arr, include_z): # explode/flatten the MultiPoints _, part_indices = get_parts(arr, return_index=True) # the offsets into the multipoint parts offsets = _indices_to_offsets(part_indices, len(arr)) # only one array of coordinates coords = get_coordinates(arr, include_z=include_z) return coords, (offsets,) def _get_arrays_linestring(arr, include_z): # the coords and offsets into the coordinates of the linestrings coords, indices = get_coordinates(arr, return_index=True, include_z=include_z) offsets = _indices_to_offsets(indices, len(arr)) return coords, (offsets,) def _get_arrays_multilinestring(arr, include_z): # explode/flatten the MultiLineStrings arr_flat, part_indices = get_parts(arr, return_index=True) # the offsets into the multilinestring parts offsets2 = _indices_to_offsets(part_indices, len(arr)) # the coords and offsets into the coordinates of the linestrings coords, indices = get_coordinates(arr_flat, return_index=True, include_z=include_z) offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0) return coords, (offsets1, offsets2) def _get_arrays_polygon(arr, include_z): # explode/flatten the Polygons into Rings arr_flat, ring_indices = get_rings(arr, return_index=True) # the offsets into the exterior/interior rings of the multipolygon parts offsets2 = _indices_to_offsets(ring_indices, len(arr)) # the coords and offsets into the coordinates of the rings coords, indices = get_coordinates(arr_flat, return_index=True, include_z=include_z) offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0) return coords, (offsets1, offsets2) def _get_arrays_multipolygon(arr, include_z): # explode/flatten the MultiPolygons arr_flat, part_indices = get_parts(arr, return_index=True) # the offsets into the multipolygon parts offsets3 = _indices_to_offsets(part_indices, len(arr)) # explode/flatten the Polygons into Rings arr_flat2, ring_indices = get_rings(arr_flat, return_index=True) # the offsets into the exterior/interior rings of the multipolygon parts offsets2 = np.insert(np.bincount(ring_indices).cumsum(), 0, 0) # the coords and offsets into the coordinates of the rings coords, indices = get_coordinates(arr_flat2, return_index=True, include_z=include_z) offsets1 = np.insert(np.bincount(indices).cumsum(), 0, 0) return coords, (offsets1, offsets2, offsets3) def to_ragged_array(geometries, include_z=None): """ Converts geometries to a ragged array representation using a contiguous array of coordinates and offset arrays. This function converts an array of geometries to a ragged array (i.e. irregular array of arrays) of coordinates, represented in memory using a single contiguous array of the coordinates, and up to 3 offset arrays that keep track where each sub-array starts and ends. This follows the in-memory layout of the variable size list arrays defined by Apache Arrow, as specified for geometries by the GeoArrow project: https://github.com/geoarrow/geoarrow. Parameters ---------- geometries : array_like Array of geometries (1-dimensional). include_z : bool, default None If False, return 2D geometries. If True, include the third dimension in the output (if a geometry has no third dimension, the z-coordinates will be NaN). By default, will infer the dimensionality from the input geometries. Note that this inference can be unreliable with empty geometries (for a guaranteed result, it is recommended to specify the keyword). Returns ------- tuple of (geometry_type, coords, offsets) geometry_type : GeometryType The type of the input geometries (required information for roundtrip). coords : np.ndarray Contiguous array of shape (n, 2) or (n, 3) of all coordinates of all input geometries. offsets: tuple of np.ndarray Offset arrays that make it possible to reconstruct the geometries from the flat coordinates array. The number of offset arrays depends on the geometry type. See https://github.com/geoarrow/geoarrow/blob/main/format.md for details. Notes ----- Mixed singular and multi geometry types of the same basic type are allowed (e.g., Point and MultiPoint) and all singular types will be treated as multi types. GeometryCollections and other mixed geometry types are not supported. See also -------- from_ragged_array Examples -------- Consider a Polygon with one hole (interior ring): >>> import shapely >>> polygon = shapely.Polygon( ... [(0, 0), (10, 0), (10, 10), (0, 10)], ... holes=[[(2, 2), (3, 2), (2, 3)]] ... ) >>> polygon This polygon can be thought of as a list of rings (first ring is the exterior ring, subsequent rings are the interior rings), and each ring as a list of coordinate pairs. This is very similar to how GeoJSON represents the coordinates: >>> import json >>> json.loads(shapely.to_geojson(polygon))["coordinates"] [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]], [[2.0, 2.0], [3.0, 2.0], [2.0, 3.0], [2.0, 2.0]]] This function will return a similar list of lists of lists, but using a single contiguous array of coordinates, and multiple arrays of offsets: >>> geometry_type, coords, offsets = shapely.to_ragged_array([polygon]) >>> geometry_type >>> coords array([[ 0., 0.], [10., 0.], [10., 10.], [ 0., 10.], [ 0., 0.], [ 2., 2.], [ 3., 2.], [ 2., 3.], [ 2., 2.]]) >>> offsets (array([0, 5, 9]), array([0, 2])) As an example how to interpret the offsets: the i-th ring in the coordinates is represented by ``offsets[0][i]`` to ``offsets[0][i+1]``: >>> exterior_ring_start, exterior_ring_end = offsets[0][0], offsets[0][1] >>> coords[exterior_ring_start:exterior_ring_end] array([[ 0., 0.], [10., 0.], [10., 10.], [ 0., 10.], [ 0., 0.]]) """ geometries = np.asarray(geometries) if include_z is None: include_z = np.any( get_coordinate_dimension(geometries[~is_empty(geometries)]) == 3 ) geom_types = np.unique(get_type_id(geometries)) # ignore missing values (type of -1) geom_types = geom_types[geom_types >= 0] if len(geom_types) == 1: typ = GeometryType(geom_types[0]) if typ == GeometryType.POINT: coords, offsets = _get_arrays_point(geometries, include_z) elif typ == GeometryType.LINESTRING: coords, offsets = _get_arrays_linestring(geometries, include_z) elif typ == GeometryType.POLYGON: coords, offsets = _get_arrays_polygon(geometries, include_z) elif typ == GeometryType.MULTIPOINT: coords, offsets = _get_arrays_multipoint(geometries, include_z) elif typ == GeometryType.MULTILINESTRING: coords, offsets = _get_arrays_multilinestring(geometries, include_z) elif typ == GeometryType.MULTIPOLYGON: coords, offsets = _get_arrays_multipolygon(geometries, include_z) else: raise ValueError(f"Geometry type {typ.name} is not supported") elif len(geom_types) == 2: if set(geom_types) == {GeometryType.POINT, GeometryType.MULTIPOINT}: typ = GeometryType.MULTIPOINT coords, offsets = _get_arrays_multipoint(geometries, include_z) elif set(geom_types) == {GeometryType.LINESTRING, GeometryType.MULTILINESTRING}: typ = GeometryType.MULTILINESTRING coords, offsets = _get_arrays_multilinestring(geometries, include_z) elif set(geom_types) == {GeometryType.POLYGON, GeometryType.MULTIPOLYGON}: typ = GeometryType.MULTIPOLYGON coords, offsets = _get_arrays_multipolygon(geometries, include_z) else: raise ValueError( "Geometry type combination is not supported " f"({[GeometryType(t).name for t in geom_types]})" ) else: raise ValueError( "Geometry type combination is not supported " f"({[GeometryType(t).name for t in geom_types]})" ) return typ, coords, offsets # # coords/offset arrays -> GEOS (from_ragged_array) def _point_from_flatcoords(coords): result = creation.points(coords) # Older versions of GEOS (<= 3.9) don't automatically convert NaNs # to empty points -> do manually empties = np.isnan(coords).all(axis=1) if empties.any(): result[empties] = creation.empty(1, geom_type=GeometryType.POINT).item() return result def _multipoint_from_flatcoords(coords, offsets): # recreate points points = creation.points(coords) # recreate multipoints multipoint_parts = np.diff(offsets) multipoint_indices = np.repeat(np.arange(len(multipoint_parts)), multipoint_parts) result = np.empty(len(offsets) - 1, dtype=object) result = creation.multipoints(points, indices=multipoint_indices, out=result) result[multipoint_parts == 0] = creation.empty( 1, geom_type=GeometryType.MULTIPOINT ).item() return result def _linestring_from_flatcoords(coords, offsets): # recreate linestrings linestring_n = np.diff(offsets) linestring_indices = np.repeat(np.arange(len(linestring_n)), linestring_n) result = np.empty(len(offsets) - 1, dtype=object) result = creation.linestrings(coords, indices=linestring_indices, out=result) result[linestring_n == 0] = creation.empty( 1, geom_type=GeometryType.LINESTRING ).item() return result def _multilinestrings_from_flatcoords(coords, offsets1, offsets2): # recreate linestrings linestrings = _linestring_from_flatcoords(coords, offsets1) # recreate multilinestrings multilinestring_parts = np.diff(offsets2) multilinestring_indices = np.repeat( np.arange(len(multilinestring_parts)), multilinestring_parts ) result = np.empty(len(offsets2) - 1, dtype=object) result = creation.multilinestrings( linestrings, indices=multilinestring_indices, out=result ) result[multilinestring_parts == 0] = creation.empty( 1, geom_type=GeometryType.MULTILINESTRING ).item() return result def _polygon_from_flatcoords(coords, offsets1, offsets2): # recreate rings ring_lengths = np.diff(offsets1) ring_indices = np.repeat(np.arange(len(ring_lengths)), ring_lengths) rings = creation.linearrings(coords, indices=ring_indices) # recreate polygons polygon_rings_n = np.diff(offsets2) polygon_indices = np.repeat(np.arange(len(polygon_rings_n)), polygon_rings_n) result = np.empty(len(offsets2) - 1, dtype=object) result = creation.polygons(rings, indices=polygon_indices, out=result) result[polygon_rings_n == 0] = creation.empty( 1, geom_type=GeometryType.POLYGON ).item() return result def _multipolygons_from_flatcoords(coords, offsets1, offsets2, offsets3): # recreate polygons polygons = _polygon_from_flatcoords(coords, offsets1, offsets2) # recreate multipolygons multipolygon_parts = np.diff(offsets3) multipolygon_indices = np.repeat( np.arange(len(multipolygon_parts)), multipolygon_parts ) result = np.empty(len(offsets3) - 1, dtype=object) result = creation.multipolygons(polygons, indices=multipolygon_indices, out=result) result[multipolygon_parts == 0] = creation.empty( 1, geom_type=GeometryType.MULTIPOLYGON ).item() return result def from_ragged_array(geometry_type, coords, offsets=None): """ Creates geometries from a contiguous array of coordinates and offset arrays. This function creates geometries from the ragged array representation as returned by ``to_ragged_array``. This follows the in-memory layout of the variable size list arrays defined by Apache Arrow, as specified for geometries by the GeoArrow project: https://github.com/geoarrow/geoarrow. See :func:`to_ragged_array` for more details. Parameters ---------- geometry_type : GeometryType The type of geometry to create. coords : np.ndarray Contiguous array of shape (n, 2) or (n, 3) of all coordinates for the geometries. offsets: tuple of np.ndarray Offset arrays that allow to reconstruct the geometries based on the flat coordinates array. The number of offset arrays depends on the geometry type. See https://github.com/geoarrow/geoarrow/blob/main/format.md for details. Returns ------- np.ndarray Array of geometries (1-dimensional). See Also -------- to_ragged_array """ if geometry_type == GeometryType.POINT: assert offsets is None or len(offsets) == 0 return _point_from_flatcoords(coords) if geometry_type == GeometryType.LINESTRING: return _linestring_from_flatcoords(coords, *offsets) if geometry_type == GeometryType.POLYGON: return _polygon_from_flatcoords(coords, *offsets) elif geometry_type == GeometryType.MULTIPOINT: return _multipoint_from_flatcoords(coords, *offsets) elif geometry_type == GeometryType.MULTILINESTRING: return _multilinestrings_from_flatcoords(coords, *offsets) elif geometry_type == GeometryType.MULTIPOLYGON: return _multipolygons_from_flatcoords(coords, *offsets) else: raise ValueError(f"Geometry type {geometry_type.name} is not supported") shapely-2.0.3/shapely/_version.py000066400000000000000000000563371456366510000170500ustar00rootroot00000000000000# This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. # Generated by versioneer-0.28 # https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" import errno import functools import os import re import subprocess import sys from typing import Callable, Dict def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = " (tag: 2.0.3, maint-2.0)" git_full = "76e77f00491490f031d6eb3e45059b907cc6b79c" git_date = "2024-02-16 14:59:28 +0100" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "" cfg.parentdir_prefix = "shapely-" cfg.versionfile_source = "shapely/_version.py" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY: Dict[str, str] = {} HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) process = None popen_kwargs = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW popen_kwargs["startupinfo"] = startupinfo for command in commands: try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git process = subprocess.Popen( [command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs, ) break except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, process.returncode return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { "version": dirname[len(parentdir_prefix) :], "full-revisionid": None, "dirty": False, "error": None, "date": None, } rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print( "Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix) ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) except OSError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) return { "version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date, } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return { "version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None, } @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] # GIT_DIR can interfere with correct operation of Versioneer. # It may be intended to be passed to the Versioneer-versioned project, # but that should not change where we get our version from. env = os.environ.copy() env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = runner( GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*", ], cwd=root, ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") branch_name = branch_name.strip() if branch_name == "HEAD": # If we aren't exactly on a branch, pick a branch which represents # the current commit. If all else fails, we are on a branchless # commit. branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) # --contains was added in git-1.5.4 if rc != 0 or branches is None: raise NotThisMethod("'git branch --contains' returned error") branches = branches.split("\n") # Remove the first line if we're running detached if "(" in branches[0]: branches.pop(0) # Strip off the leading "* " from the list of branches. branches = [branch[2:] for branch in branches] if "master" in branches: branch_name = "master" elif not branches: branch_name = None else: # Pick the first branch that is returned. Good or bad. branch_name = branches[0] pieces["branch"] = branch_name # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( full_tag, tag_prefix, ) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_branch(pieces): """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards (a feature branch will appear "older" than the master branch). Exceptions: 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def pep440_split_post(ver): """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the post-release version number (or -1 if no post-release segment is present). """ vc = str.split(ver, ".post") return vc[0], int(vc[1] or 0) if len(vc) == 2 else None def render_pep440_pre(pieces): """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post0.devDISTANCE """ if pieces["closest-tag"]: if pieces["distance"]: # update the post release segment tag_version, post_version = pep440_split_post(pieces["closest-tag"]) rendered = tag_version if post_version is not None: rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) else: rendered += ".post0.dev%d" % (pieces["distance"]) else: # no commits, use the tag as the version rendered = pieces["closest-tag"] else: # exception #1 rendered = "0.post0.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_post_branch(pieces): """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. Exceptions: 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += "+g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return { "version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None, } if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-branch": rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-post-branch": rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return { "version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date"), } def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: return { "version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None, } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return { "version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None, } shapely-2.0.3/shapely/affinity.py000066400000000000000000000167341456366510000170320ustar00rootroot00000000000000"""Affine transforms, both in general and specific, named transforms.""" from math import cos, pi, sin, tan import numpy as np import shapely __all__ = ["affine_transform", "rotate", "scale", "skew", "translate"] def affine_transform(geom, matrix): r"""Return a transformed geometry using an affine transformation matrix. The coefficient matrix is provided as a list or tuple with 6 or 12 items for 2D or 3D transformations, respectively. For 2D affine transformations, the 6 parameter matrix is:: [a, b, d, e, xoff, yoff] which represents the augmented matrix:: [x'] / a b xoff \ [x] [y'] = | d e yoff | [y] [1 ] \ 0 0 1 / [1] or the equations for the transformed coordinates:: x' = a * x + b * y + xoff y' = d * x + e * y + yoff For 3D affine transformations, the 12 parameter matrix is:: [a, b, c, d, e, f, g, h, i, xoff, yoff, zoff] which represents the augmented matrix:: [x'] / a b c xoff \ [x] [y'] = | d e f yoff | [y] [z'] | g h i zoff | [z] [1 ] \ 0 0 0 1 / [1] or the equations for the transformed coordinates:: x' = a * x + b * y + c * z + xoff y' = d * x + e * y + f * z + yoff z' = g * x + h * y + i * z + zoff """ if len(matrix) == 6: ndim = 2 a, b, d, e, xoff, yoff = matrix if geom.has_z: ndim = 3 i = 1.0 c = f = g = h = zoff = 0.0 elif len(matrix) == 12: ndim = 3 a, b, c, d, e, f, g, h, i, xoff, yoff, zoff = matrix if not geom.has_z: ndim = 2 else: raise ValueError("'matrix' expects either 6 or 12 coefficients") if ndim == 2: A = np.array([[a, b], [d, e]], dtype=float) off = np.array([xoff, yoff], dtype=float) else: A = np.array([[a, b, c], [d, e, f], [g, h, i]], dtype=float) off = np.array([xoff, yoff, zoff], dtype=float) def _affine_coords(coords): return np.matmul(A, coords.T).T + off return shapely.transform(geom, _affine_coords, include_z=ndim == 3) def interpret_origin(geom, origin, ndim): """Returns interpreted coordinate tuple for origin parameter. This is a helper function for other transform functions. The point of origin can be a keyword 'center' for the 2D bounding box center, 'centroid' for the geometry's 2D centroid, a Point object or a coordinate tuple (x0, y0, z0). """ # get coordinate tuple from 'origin' from keyword or Point type if origin == "center": # bounding box center minx, miny, maxx, maxy = geom.bounds origin = ((maxx + minx) / 2.0, (maxy + miny) / 2.0) elif origin == "centroid": origin = geom.centroid.coords[0] elif isinstance(origin, str): raise ValueError(f"'origin' keyword {origin!r} is not recognized") elif getattr(origin, "geom_type", None) == "Point": origin = origin.coords[0] # origin should now be tuple-like if len(origin) not in (2, 3): raise ValueError("Expected number of items in 'origin' to be " "either 2 or 3") if ndim == 2: return origin[0:2] else: # 3D coordinate if len(origin) == 2: return origin + (0.0,) else: return origin def rotate(geom, angle, origin="center", use_radians=False): r"""Returns a rotated geometry on a 2D plane. The angle of rotation can be specified in either degrees (default) or radians by setting ``use_radians=True``. Positive angles are counter-clockwise and negative are clockwise rotations. The point of origin can be a keyword 'center' for the bounding box center (default), 'centroid' for the geometry's centroid, a Point object or a coordinate tuple (x0, y0). The affine transformation matrix for 2D rotation is: / cos(r) -sin(r) xoff \ | sin(r) cos(r) yoff | \ 0 0 1 / where the offsets are calculated from the origin Point(x0, y0): xoff = x0 - x0 * cos(r) + y0 * sin(r) yoff = y0 - x0 * sin(r) - y0 * cos(r) """ if geom.is_empty: return geom if not use_radians: # convert from degrees angle = angle * pi / 180.0 cosp = cos(angle) sinp = sin(angle) if abs(cosp) < 2.5e-16: cosp = 0.0 if abs(sinp) < 2.5e-16: sinp = 0.0 x0, y0 = interpret_origin(geom, origin, 2) # fmt: off matrix = (cosp, -sinp, 0.0, sinp, cosp, 0.0, 0.0, 0.0, 1.0, x0 - x0 * cosp + y0 * sinp, y0 - x0 * sinp - y0 * cosp, 0.0) # fmt: on return affine_transform(geom, matrix) def scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin="center"): r"""Returns a scaled geometry, scaled by factors along each dimension. The point of origin can be a keyword 'center' for the 2D bounding box center (default), 'centroid' for the geometry's 2D centroid, a Point object or a coordinate tuple (x0, y0, z0). Negative scale factors will mirror or reflect coordinates. The general 3D affine transformation matrix for scaling is: / xfact 0 0 xoff \ | 0 yfact 0 yoff | | 0 0 zfact zoff | \ 0 0 0 1 / where the offsets are calculated from the origin Point(x0, y0, z0): xoff = x0 - x0 * xfact yoff = y0 - y0 * yfact zoff = z0 - z0 * zfact """ if geom.is_empty: return geom x0, y0, z0 = interpret_origin(geom, origin, 3) # fmt: off matrix = (xfact, 0.0, 0.0, 0.0, yfact, 0.0, 0.0, 0.0, zfact, x0 - x0 * xfact, y0 - y0 * yfact, z0 - z0 * zfact) # fmt: on return affine_transform(geom, matrix) def skew(geom, xs=0.0, ys=0.0, origin="center", use_radians=False): r"""Returns a skewed geometry, sheared by angles along x and y dimensions. The shear angle can be specified in either degrees (default) or radians by setting ``use_radians=True``. The point of origin can be a keyword 'center' for the bounding box center (default), 'centroid' for the geometry's centroid, a Point object or a coordinate tuple (x0, y0). The general 2D affine transformation matrix for skewing is: / 1 tan(xs) xoff \ | tan(ys) 1 yoff | \ 0 0 1 / where the offsets are calculated from the origin Point(x0, y0): xoff = -y0 * tan(xs) yoff = -x0 * tan(ys) """ if geom.is_empty: return geom if not use_radians: # convert from degrees xs = xs * pi / 180.0 ys = ys * pi / 180.0 tanx = tan(xs) tany = tan(ys) if abs(tanx) < 2.5e-16: tanx = 0.0 if abs(tany) < 2.5e-16: tany = 0.0 x0, y0 = interpret_origin(geom, origin, 2) # fmt: off matrix = (1.0, tanx, 0.0, tany, 1.0, 0.0, 0.0, 0.0, 1.0, -y0 * tanx, -x0 * tany, 0.0) # fmt: on return affine_transform(geom, matrix) def translate(geom, xoff=0.0, yoff=0.0, zoff=0.0): r"""Returns a translated geometry shifted by offsets along each dimension. The general 3D affine transformation matrix for translation is: / 1 0 0 xoff \ | 0 1 0 yoff | | 0 0 1 zoff | \ 0 0 0 1 / """ if geom.is_empty: return geom # fmt: off matrix = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, xoff, yoff, zoff) # fmt: on return affine_transform(geom, matrix) shapely-2.0.3/shapely/algorithms/000077500000000000000000000000001456366510000170055ustar00rootroot00000000000000shapely-2.0.3/shapely/algorithms/__init__.py000066400000000000000000000000001456366510000211040ustar00rootroot00000000000000shapely-2.0.3/shapely/algorithms/_oriented_envelope.py000066400000000000000000000036671456366510000232400ustar00rootroot00000000000000import math from itertools import islice import numpy as np import shapely from shapely.affinity import affine_transform def _oriented_envelope_min_area(geometry, **kwargs): """ Computes the oriented envelope (minimum rotated rectangle) that encloses an input geometry. This is a fallback implementation for GEOS < 3.12 to have the correct minimum area behaviour. """ if geometry is None: return None if geometry.is_empty: return shapely.from_wkt("POLYGON EMPTY") # first compute the convex hull hull = geometry.convex_hull try: coords = hull.exterior.coords except AttributeError: # may be a Point or a LineString return hull # generate the edge vectors between the convex hull's coords edges = ( (pt2[0] - pt1[0], pt2[1] - pt1[1]) for pt1, pt2 in zip(coords, islice(coords, 1, None)) ) def _transformed_rects(): for dx, dy in edges: # compute the normalized direction vector of the edge # vector. length = math.sqrt(dx**2 + dy**2) ux, uy = dx / length, dy / length # compute the normalized perpendicular vector vx, vy = -uy, ux # transform hull from the original coordinate system to # the coordinate system defined by the edge and compute # the axes-parallel bounding rectangle. transf_rect = affine_transform(hull, (ux, uy, vx, vy, 0, 0)).envelope # yield the transformed rectangle and a matrix to # transform it back to the original coordinate system. yield (transf_rect, (ux, vx, uy, vy, 0, 0)) # check for the minimum area rectangle and return it transf_rect, inv_matrix = min(_transformed_rects(), key=lambda r: r[0].area) return affine_transform(transf_rect, inv_matrix) _oriented_envelope_min_area_vectorized = np.frompyfunc( _oriented_envelope_min_area, 1, 1 ) shapely-2.0.3/shapely/algorithms/cga.py000066400000000000000000000011611456366510000201100ustar00rootroot00000000000000import numpy as np import shapely def signed_area(ring): """Return the signed area enclosed by a ring in linear time using the algorithm at: https://web.archive.org/web/20080209143651/http://cgafaq.info:80/wiki/Polygon_Area """ coords = np.array(ring.coords)[:, :2] xs, ys = np.vstack([coords, coords[1]]).T return np.sum(xs[1:-1] * (ys[2:] - ys[:-2])) / 2.0 def is_ccw_impl(name=None): """Predicate implementation""" def is_ccw_op(ring): return signed_area(ring) >= 0.0 if shapely.geos_version >= (3, 7, 0): return shapely.is_ccw else: return is_ccw_op shapely-2.0.3/shapely/algorithms/polylabel.py000066400000000000000000000111241456366510000213410ustar00rootroot00000000000000from heapq import heappop, heappush from shapely.errors import TopologicalError from shapely.geometry import Point class Cell: """A `Cell`'s centroid property is a potential solution to finding the pole of inaccessibility for a given polygon. Rich comparison operators are used for sorting `Cell` objects in a priority queue based on the potential maximum distance of any theoretical point within a cell to a given polygon's exterior boundary. """ def __init__(self, x, y, h, polygon): self.x = x self.y = y self.h = h # half of cell size self.centroid = Point(x, y) # cell centroid, potential solution # distance from cell centroid to polygon exterior self.distance = self._dist(polygon) # max distance to polygon exterior within a cell self.max_distance = self.distance + h * 1.4142135623730951 # sqrt(2) # rich comparison operators for sorting in minimum priority queue def __lt__(self, other): return self.max_distance > other.max_distance def __le__(self, other): return self.max_distance >= other.max_distance def __eq__(self, other): return self.max_distance == other.max_distance def __ne__(self, other): return self.max_distance != other.max_distance def __gt__(self, other): return self.max_distance < other.max_distance def __ge__(self, other): return self.max_distance <= other.max_distance def _dist(self, polygon): """Signed distance from Cell centroid to polygon outline. The returned value is negative if the point is outside of the polygon exterior boundary. """ inside = polygon.contains(self.centroid) distance = self.centroid.distance(polygon.exterior) for interior in polygon.interiors: distance = min(distance, self.centroid.distance(interior)) if inside: return distance return -distance def polylabel(polygon, tolerance=1.0): """Finds pole of inaccessibility for a given polygon. Based on Vladimir Agafonkin's https://github.com/mapbox/polylabel Parameters ---------- polygon : shapely.geometry.Polygon tolerance : int or float, optional `tolerance` represents the highest resolution in units of the input geometry that will be considered for a solution. (default value is 1.0). Returns ------- shapely.geometry.Point A point representing the pole of inaccessibility for the given input polygon. Raises ------ shapely.errors.TopologicalError If the input polygon is not a valid geometry. Example ------- >>> from shapely import LineString >>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50), ... (-100, -20), (-150, -200)]).buffer(100) >>> polylabel(polygon, tolerance=10).wkt 'POINT (59.35615556364569 121.83919629746435)' """ if not polygon.is_valid: raise TopologicalError("Invalid polygon") minx, miny, maxx, maxy = polygon.bounds width = maxx - minx height = maxy - miny cell_size = min(width, height) h = cell_size / 2.0 cell_queue = [] # First best cell approximation is one constructed from the centroid # of the polygon x, y = polygon.centroid.coords[0] best_cell = Cell(x, y, 0, polygon) # Special case for rectangular polygons avoiding floating point error bbox_cell = Cell(minx + width / 2.0, miny + height / 2, 0, polygon) if bbox_cell.distance > best_cell.distance: best_cell = bbox_cell # build a regular square grid covering the polygon x = minx while x < maxx: y = miny while y < maxy: heappush(cell_queue, Cell(x + h, y + h, h, polygon)) y += cell_size x += cell_size # minimum priority queue while cell_queue: cell = heappop(cell_queue) # update the best cell if we find a better one if cell.distance > best_cell.distance: best_cell = cell # continue to the next iteration if we can't find a better solution # based on tolerance if cell.max_distance - best_cell.distance <= tolerance: continue # split the cell into quadrants h = cell.h / 2.0 heappush(cell_queue, Cell(cell.x - h, cell.y - h, h, polygon)) heappush(cell_queue, Cell(cell.x + h, cell.y - h, h, polygon)) heappush(cell_queue, Cell(cell.x - h, cell.y + h, h, polygon)) heappush(cell_queue, Cell(cell.x + h, cell.y + h, h, polygon)) return best_cell.centroid shapely-2.0.3/shapely/constructive.py000066400000000000000000001122501456366510000177370ustar00rootroot00000000000000import numpy as np from shapely import lib from shapely._enum import ParamEnum from shapely.algorithms._oriented_envelope import _oriented_envelope_min_area_vectorized from shapely.decorators import multithreading_enabled, requires_geos __all__ = [ "BufferCapStyle", "BufferJoinStyle", "boundary", "buffer", "offset_curve", "centroid", "clip_by_rect", "concave_hull", "convex_hull", "delaunay_triangles", "segmentize", "envelope", "extract_unique_points", "build_area", "make_valid", "normalize", "node", "point_on_surface", "polygonize", "polygonize_full", "remove_repeated_points", "reverse", "simplify", "snap", "voronoi_polygons", "oriented_envelope", "minimum_rotated_rectangle", "minimum_bounding_circle", ] class BufferCapStyle(ParamEnum): round = 1 flat = 2 square = 3 class BufferJoinStyle(ParamEnum): round = 1 mitre = 2 bevel = 3 @multithreading_enabled def boundary(geometry, **kwargs): """Returns the topological boundary of a geometry. Parameters ---------- geometry : Geometry or array_like This function will return None for geometrycollections. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LinearRing, LineString, \ MultiLineString, MultiPoint, Point, Polygon >>> boundary(Point(0, 0)) >>> boundary(LineString([(0, 0), (1, 1), (1, 2)])) >>> boundary(LinearRing([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])) >>> boundary(Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])) >>> boundary(MultiPoint([(0, 0), (1, 2)])) >>> boundary(MultiLineString([[(0, 0), (1, 1)], [(0, 1), (1, 0)]])) >>> boundary(GeometryCollection([Point(0, 0)])) is None True """ return lib.boundary(geometry, **kwargs) @multithreading_enabled def buffer( geometry, distance, quad_segs=8, cap_style="round", join_style="round", mitre_limit=5.0, single_sided=False, **kwargs ): """ Computes the buffer of a geometry for positive and negative buffer distance. The buffer of a geometry is defined as the Minkowski sum (or difference, for negative distance) of the geometry with a circle with radius equal to the absolute value of the buffer distance. The buffer operation always returns a polygonal result. The negative or zero-distance buffer of lines and points is always empty. Parameters ---------- geometry : Geometry or array_like distance : float or array_like Specifies the circle radius in the Minkowski sum (or difference). quad_segs : int, default 8 Specifies the number of linear segments in a quarter circle in the approximation of circular arcs. cap_style : shapely.BufferCapStyle or {'round', 'square', 'flat'}, default 'round' Specifies the shape of buffered line endings. BufferCapStyle.round ('round') results in circular line endings (see ``quad_segs``). Both BufferCapStyle.square ('square') and BufferCapStyle.flat ('flat') result in rectangular line endings, only BufferCapStyle.flat ('flat') will end at the original vertex, while BufferCapStyle.square ('square') involves adding the buffer width. join_style : shapely.BufferJoinStyle or {'round', 'mitre', 'bevel'}, default 'round' Specifies the shape of buffered line midpoints. BufferJoinStyle.round ('round') results in rounded shapes. BufferJoinStyle.bevel ('bevel') results in a beveled edge that touches the original vertex. BufferJoinStyle.mitre ('mitre') results in a single vertex that is beveled depending on the ``mitre_limit`` parameter. mitre_limit : float, default 5.0 Crops of 'mitre'-style joins if the point is displaced from the buffered vertex by more than this limit. single_sided : bool, default False Only buffer at one side of the geometry. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point, Polygon, BufferCapStyle, BufferJoinStyle >>> buffer(Point(10, 10), 2, quad_segs=1) >>> buffer(Point(10, 10), 2, quad_segs=2) >>> buffer(Point(10, 10), -2, quad_segs=1) >>> line = LineString([(10, 10), (20, 10)]) >>> buffer(line, 2, cap_style="square") >>> buffer(line, 2, cap_style="flat") >>> buffer(line, 2, single_sided=True, cap_style="flat") >>> line2 = LineString([(10, 10), (20, 10), (20, 20)]) >>> buffer(line2, 2, cap_style="flat", join_style="bevel") >>> buffer(line2, 2, cap_style="flat", join_style="mitre") >>> buffer(line2, 2, cap_style="flat", join_style="mitre", mitre_limit=1) >>> square = Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]) >>> buffer(square, 2, join_style="mitre") >>> buffer(square, -2, join_style="mitre") >>> buffer(square, -5, join_style="mitre") >>> buffer(line, float("nan")) is None True """ if isinstance(cap_style, str): cap_style = BufferCapStyle.get_value(cap_style) if isinstance(join_style, str): join_style = BufferJoinStyle.get_value(join_style) if not np.isscalar(quad_segs): raise TypeError("quad_segs only accepts scalar values") if not np.isscalar(cap_style): raise TypeError("cap_style only accepts scalar values") if not np.isscalar(join_style): raise TypeError("join_style only accepts scalar values") if not np.isscalar(mitre_limit): raise TypeError("mitre_limit only accepts scalar values") if not np.isscalar(single_sided): raise TypeError("single_sided only accepts scalar values") return lib.buffer( geometry, distance, np.intc(quad_segs), np.intc(cap_style), np.intc(join_style), mitre_limit, np.bool_(single_sided), **kwargs ) @multithreading_enabled def offset_curve( geometry, distance, quad_segs=8, join_style="round", mitre_limit=5.0, **kwargs ): """ Returns a (Multi)LineString at a distance from the object on its right or its left side. For positive distance the offset will be at the left side of the input line. For a negative distance it will be at the right side. In general, this function tries to preserve the direction of the input. Note: the behaviour regarding orientation of the resulting line depends on the GEOS version. With GEOS < 3.11, the line retains the same direction for a left offset (positive distance) or has opposite direction for a right offset (negative distance), and this behaviour was documented as such in previous Shapely versions. Starting with GEOS 3.11, the function tries to preserve the orientation of the original line. Parameters ---------- geometry : Geometry or array_like distance : float or array_like Specifies the offset distance from the input geometry. Negative for right side offset, positive for left side offset. quad_segs : int, default 8 Specifies the number of linear segments in a quarter circle in the approximation of circular arcs. join_style : {'round', 'bevel', 'mitre'}, default 'round' Specifies the shape of outside corners. 'round' results in rounded shapes. 'bevel' results in a beveled edge that touches the original vertex. 'mitre' results in a single vertex that is beveled depending on the ``mitre_limit`` parameter. mitre_limit : float, default 5.0 Crops of 'mitre'-style joins if the point is displaced from the buffered vertex by more than this limit. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString >>> line = LineString([(0, 0), (0, 2)]) >>> offset_curve(line, 2) >>> offset_curve(line, -2) """ if isinstance(join_style, str): join_style = BufferJoinStyle.get_value(join_style) if not np.isscalar(quad_segs): raise TypeError("quad_segs only accepts scalar values") if not np.isscalar(join_style): raise TypeError("join_style only accepts scalar values") if not np.isscalar(mitre_limit): raise TypeError("mitre_limit only accepts scalar values") return lib.offset_curve( geometry, distance, np.intc(quad_segs), np.intc(join_style), np.double(mitre_limit), **kwargs ) @multithreading_enabled def centroid(geometry, **kwargs): """Computes the geometric center (center-of-mass) of a geometry. For multipoints this is computed as the mean of the input coordinates. For multilinestrings the centroid is weighted by the length of each line segment. For multipolygons the centroid is weighted by the area of each polygon. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, MultiPoint, Polygon >>> centroid(Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)])) >>> centroid(LineString([(0, 0), (2, 2), (10, 10)])) >>> centroid(MultiPoint([(0, 0), (10, 10)])) >>> centroid(Polygon()) """ return lib.centroid(geometry, **kwargs) @multithreading_enabled def clip_by_rect(geometry, xmin, ymin, xmax, ymax, **kwargs): """ Returns the portion of a geometry within a rectangle. The geometry is clipped in a fast but possibly dirty way. The output is not guaranteed to be valid. No exceptions will be raised for topological errors. Note: empty geometries or geometries that do not overlap with the specified bounds will result in GEOMETRYCOLLECTION EMPTY. Parameters ---------- geometry : Geometry or array_like The geometry to be clipped xmin : float Minimum x value of the rectangle ymin : float Minimum y value of the rectangle xmax : float Maximum x value of the rectangle ymax : float Maximum y value of the rectangle **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Polygon >>> line = LineString([(0, 0), (10, 10)]) >>> clip_by_rect(line, 0., 0., 1., 1.) >>> polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]) >>> clip_by_rect(polygon, 0., 0., 1., 1.) """ if not all(np.isscalar(val) for val in [xmin, ymin, xmax, ymax]): raise TypeError("xmin/ymin/xmax/ymax only accepts scalar values") return lib.clip_by_rect( geometry, np.double(xmin), np.double(ymin), np.double(xmax), np.double(ymax), **kwargs ) @requires_geos("3.11.0") @multithreading_enabled def concave_hull(geometry, ratio=0.0, allow_holes=False, **kwargs): """Computes a concave geometry that encloses an input geometry. Parameters ---------- geometry : Geometry or array_like ratio : float, default 0.0 Number in the range [0, 1]. Higher numbers will include fewer vertices in the hull. allow_holes : bool, default False If set to True, the concave hull may have holes. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import MultiPoint, Polygon >>> concave_hull(MultiPoint([(0, 0), (0, 3), (1, 1), (3, 0), (3, 3)]), ratio=0.1) >>> concave_hull(MultiPoint([(0, 0), (0, 3), (1, 1), (3, 0), (3, 3)]), ratio=1.0) >>> concave_hull(Polygon()) """ if not np.isscalar(ratio): raise TypeError("ratio must be scalar") if not np.isscalar(allow_holes): raise TypeError("allow_holes must be scalar") return lib.concave_hull(geometry, np.double(ratio), np.bool_(allow_holes), **kwargs) @multithreading_enabled def convex_hull(geometry, **kwargs): """Computes the minimum convex geometry that encloses an input geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import MultiPoint, Polygon >>> convex_hull(MultiPoint([(0, 0), (10, 0), (10, 10)])) >>> convex_hull(Polygon()) """ return lib.convex_hull(geometry, **kwargs) @multithreading_enabled def delaunay_triangles(geometry, tolerance=0.0, only_edges=False, **kwargs): """Computes a Delaunay triangulation around the vertices of an input geometry. The output is a geometrycollection containing polygons (default) or linestrings (see only_edges). Returns an None if an input geometry contains less than 3 vertices. Parameters ---------- geometry : Geometry or array_like tolerance : float or array_like, default 0.0 Snap input vertices together if their distance is less than this value. only_edges : bool or array_like, default False If set to True, the triangulation will return a collection of linestrings instead of polygons. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, MultiPoint, Polygon >>> points = MultiPoint([(50, 30), (60, 30), (100, 100)]) >>> delaunay_triangles(points) >>> delaunay_triangles(points, only_edges=True) >>> delaunay_triangles(MultiPoint([(50, 30), (51, 30), (60, 30), (100, 100)]), \ tolerance=2) >>> delaunay_triangles(Polygon([(50, 30), (60, 30), (100, 100), (50, 30)])) >>> delaunay_triangles(LineString([(50, 30), (60, 30), (100, 100)])) >>> delaunay_triangles(GeometryCollection([])) """ return lib.delaunay_triangles(geometry, tolerance, only_edges, **kwargs) @multithreading_enabled def envelope(geometry, **kwargs): """Computes the minimum bounding box that encloses an input geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, MultiPoint, Point >>> envelope(LineString([(0, 0), (10, 10)])) >>> envelope(MultiPoint([(0, 0), (10, 10)])) >>> envelope(Point(0, 0)) >>> envelope(GeometryCollection([])) """ return lib.envelope(geometry, **kwargs) @multithreading_enabled def extract_unique_points(geometry, **kwargs): """Returns all distinct vertices of an input geometry as a multipoint. Note that only 2 dimensions of the vertices are considered when testing for equality. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, MultiPoint, Point, Polygon >>> extract_unique_points(Point(0, 0)) >>> extract_unique_points(LineString([(0, 0), (1, 1), (1, 1)])) >>> extract_unique_points(Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])) >>> extract_unique_points(MultiPoint([(0, 0), (1, 1), (0, 0)])) >>> extract_unique_points(LineString()) """ return lib.extract_unique_points(geometry, **kwargs) @requires_geos("3.8.0") @multithreading_enabled def build_area(geometry, **kwargs): """Creates an areal geometry formed by the constituent linework of given geometry. Equivalent of the PostGIS ST_BuildArea() function. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, Polygon >>> polygon1 = Polygon([(0, 0), (3, 0), (3, 3), (0, 3), (0, 0)]) >>> polygon2 = Polygon([(1, 1), (1, 2), (2, 2), (1, 1)]) >>> build_area(GeometryCollection([polygon1, polygon2])) """ return lib.build_area(geometry, **kwargs) @requires_geos("3.8.0") @multithreading_enabled def make_valid(geometry, **kwargs): """Repairs invalid geometries. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import is_valid, Polygon >>> polygon = Polygon([(0, 0), (1, 1), (1, 2), (1, 1), (0, 0)]) >>> is_valid(polygon) False >>> make_valid(polygon) """ return lib.make_valid(geometry, **kwargs) @multithreading_enabled def normalize(geometry, **kwargs): """Converts Geometry to normal form (or canonical form). This method orders the coordinates, rings of a polygon and parts of multi geometries consistently. Typically useful for testing purposes (for example in combination with ``equals_exact``). Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import MultiLineString >>> line = MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]) >>> normalize(line) """ return lib.normalize(geometry, **kwargs) @multithreading_enabled def point_on_surface(geometry, **kwargs): """Returns a point that intersects an input geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, MultiPoint, Polygon >>> point_on_surface(Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)])) >>> point_on_surface(LineString([(0, 0), (2, 2), (10, 10)])) >>> point_on_surface(MultiPoint([(0, 0), (10, 10)])) >>> point_on_surface(Polygon()) """ return lib.point_on_surface(geometry, **kwargs) @multithreading_enabled def node(geometry, **kwargs): """ Returns the fully noded version of the linear input as MultiLineString. Given a linear input geometry, this function returns a new MultiLineString in which no lines cross each other but only touch at and points. To obtain this, all intersections between segments are computed and added to the segments, and duplicate segments are removed. Non-linear input (points) will result in an empty MultiLineString. This function can for example be used to create a fully-noded linework suitable to passed as input to ``polygonize``. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point >>> line = LineString([(0, 0), (1,1), (0, 1), (1, 0)]) >>> node(line) >>> node(Point(1, 1)) """ return lib.node(geometry, **kwargs) def polygonize(geometries, **kwargs): """Creates polygons formed from the linework of a set of Geometries. Polygonizes an array of Geometries that contain linework which represents the edges of a planar graph. Any type of Geometry may be provided as input; only the constituent lines and rings will be used to create the output polygons. Lines or rings that when combined do not completely close a polygon will result in an empty GeometryCollection. Duplicate segments are ignored. This function returns the polygons within a GeometryCollection. Individual Polygons can be obtained using ``get_geometry`` to get a single polygon or ``get_parts`` to get an array of polygons. MultiPolygons can be constructed from the output using ``shapely.multipolygons(shapely.get_parts(shapely.polygonize(geometries)))``. Parameters ---------- geometries : array_like An array of geometries. axis : int Axis along which the geometries are polygonized. The default is to perform a reduction over the last dimension of the input array. A 1D array results in a scalar geometry. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Returns ------- GeometryCollection or array of GeometryCollections See Also -------- get_parts, get_geometry polygonize_full node Examples -------- >>> from shapely import LineString >>> lines = [ ... LineString([(0, 0), (1, 1)]), ... LineString([(0, 0), (0, 1)]), ... LineString([(0, 1), (1, 1)]) ... ] >>> polygonize(lines) """ return lib.polygonize(geometries, **kwargs) def polygonize_full(geometries, **kwargs): """Creates polygons formed from the linework of a set of Geometries and return all extra outputs as well. Polygonizes an array of Geometries that contain linework which represents the edges of a planar graph. Any type of Geometry may be provided as input; only the constituent lines and rings will be used to create the output polygons. This function performs the same polygonization as ``polygonize`` but does not only return the polygonal result but all extra outputs as well. The return value consists of 4 elements: * The polygonal valid output * **Cut edges**: edges connected on both ends but not part of polygonal output * **dangles**: edges connected on one end but not part of polygonal output * **invalid rings**: polygons formed but which are not valid This function returns the geometries within GeometryCollections. Individual geometries can be obtained using ``get_geometry`` to get a single geometry or ``get_parts`` to get an array of geometries. Parameters ---------- geometries : array_like An array of geometries. axis : int Axis along which the geometries are polygonized. The default is to perform a reduction over the last dimension of the input array. A 1D array results in a scalar geometry. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Returns ------- (polygons, cuts, dangles, invalid) tuple of 4 GeometryCollections or arrays of GeometryCollections See Also -------- polygonize Examples -------- >>> from shapely import LineString >>> lines = [ ... LineString([(0, 0), (1, 1)]), ... LineString([(0, 0), (0, 1), (1, 1)]), ... LineString([(0, 1), (1, 1)]) ... ] >>> polygonize_full(lines) # doctest: +NORMALIZE_WHITESPACE (, , , ) """ return lib.polygonize_full(geometries, **kwargs) @requires_geos("3.11.0") @multithreading_enabled def remove_repeated_points(geometry, tolerance=0.0, **kwargs): """Returns a copy of a Geometry with repeated points removed. From the start of the coordinate sequence, each next point within the tolerance is removed. Removing repeated points with a non-zero tolerance may result in an invalid geometry being returned. Parameters ---------- geometry : Geometry or array_like tolerance : float or array_like, default=0.0 Use 0.0 to remove only exactly repeated points. Examples -------- >>> from shapely import LineString, Polygon >>> remove_repeated_points(LineString([(0,0), (0,0), (1,0)]), tolerance=0) >>> remove_repeated_points(Polygon([(0, 0), (0, .5), (0, 1), (.5, 1), (0,0)]), tolerance=.5) """ return lib.remove_repeated_points(geometry, tolerance, **kwargs) @requires_geos("3.7.0") @multithreading_enabled def reverse(geometry, **kwargs): """Returns a copy of a Geometry with the order of coordinates reversed. If a Geometry is a polygon with interior rings, the interior rings are also reversed. Points are unchanged. None is returned where Geometry is None. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_ccw : Checks if a Geometry is clockwise. Examples -------- >>> from shapely import LineString, Polygon >>> reverse(LineString([(0, 0), (1, 2)])) >>> reverse(Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])) >>> reverse(None) is None True """ return lib.reverse(geometry, **kwargs) @requires_geos("3.10.0") @multithreading_enabled def segmentize(geometry, max_segment_length, **kwargs): """Adds vertices to line segments based on maximum segment length. Additional vertices will be added to every line segment in an input geometry so that segments are no longer than the provided maximum segment length. New vertices will evenly subdivide each segment. Only linear components of input geometries are densified; other geometries are returned unmodified. Parameters ---------- geometry : Geometry or array_like max_segment_length : float or array_like Additional vertices will be added so that all line segments are no longer than this value. Must be greater than 0. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Polygon >>> line = LineString([(0, 0), (0, 10)]) >>> segmentize(line, max_segment_length=5) >>> polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]) >>> segmentize(polygon, max_segment_length=5) >>> segmentize(None, max_segment_length=5) is None True """ return lib.segmentize(geometry, max_segment_length, **kwargs) @multithreading_enabled def simplify(geometry, tolerance, preserve_topology=True, **kwargs): """Returns a simplified version of an input geometry using the Douglas-Peucker algorithm. Parameters ---------- geometry : Geometry or array_like tolerance : float or array_like The maximum allowed geometry displacement. The higher this value, the smaller the number of vertices in the resulting geometry. preserve_topology : bool, default True By default (True), the operation will avoid creating invalid geometries (checking for collapses, ring-intersections, etc), but this is computationally more expensive. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Polygon >>> line = LineString([(0, 0), (1, 10), (0, 20)]) >>> simplify(line, tolerance=0.9) >>> simplify(line, tolerance=1) >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> simplify(polygon_with_hole, tolerance=4, preserve_topology=True) >>> simplify(polygon_with_hole, tolerance=4, preserve_topology=False) """ if preserve_topology: return lib.simplify_preserve_topology(geometry, tolerance, **kwargs) else: return lib.simplify(geometry, tolerance, **kwargs) @multithreading_enabled def snap(geometry, reference, tolerance, **kwargs): """Snaps an input geometry to reference geometry's vertices. Vertices of the first geometry are snapped to vertices of the second. geometry, returning a new geometry; the input geometries are not modified. The result geometry is the input geometry with the vertices snapped. If no snapping occurs then the input geometry is returned unchanged. The tolerance is used to control where snapping is performed. Where possible, this operation tries to avoid creating invalid geometries; however, it does not guarantee that output geometries will be valid. It is the responsibility of the caller to check for and handle invalid geometries. Because too much snapping can result in invalid geometries being created, heuristics are used to determine the number and location of snapped vertices that are likely safe to snap. These heuristics may omit some potential snaps that are otherwise within the tolerance. Parameters ---------- geometry : Geometry or array_like reference : Geometry or array_like tolerance : float or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import snap, distance, LineString, Point, Polygon, MultiPoint, box >>> point = Point(0.5, 2.5) >>> target_point = Point(0, 2) >>> snap(point, target_point, tolerance=1) >>> snap(point, target_point, tolerance=0.49) >>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]) >>> snap(polygon, Point(8, 10), tolerance=5) >>> snap(polygon, LineString([(8, 10), (8, 0)]), tolerance=5) You can snap one line to another, for example to clean imprecise coordinates: >>> line1 = LineString([(0.1, 0.1), (0.49, 0.51), (1.01, 0.89)]) >>> line2 = LineString([(0, 0), (0.5, 0.5), (1.0, 1.0)]) >>> snap(line1, line2, 0.25) Snapping also supports Z coordinates: >>> point1 = Point(0.1, 0.1, 0.5) >>> multipoint = MultiPoint([(0, 0, 1), (0, 0, 0)]) >>> snap(point1, multipoint, 1) Snapping to an empty geometry has no effect: >>> snap(line1, LineString([]), 0.25) Snapping to a non-geometry (None) will always return None: >>> snap(line1, None, 0.25) is None True Only one vertex of a polygon is snapped to a target point, even if all vertices are equidistant to it, in order to prevent collapse of the polygon: >>> poly = box(0, 0, 1, 1) >>> poly >>> snap(poly, Point(0.5, 0.5), 1) """ return lib.snap(geometry, reference, tolerance, **kwargs) @multithreading_enabled def voronoi_polygons( geometry, tolerance=0.0, extend_to=None, only_edges=False, **kwargs ): """Computes a Voronoi diagram from the vertices of an input geometry. The output is a geometrycollection containing polygons (default) or linestrings (see only_edges). Returns empty if an input geometry contains less than 2 vertices or if the provided extent has zero area. Parameters ---------- geometry : Geometry or array_like tolerance : float or array_like, default 0.0 Snap input vertices together if their distance is less than this value. extend_to : Geometry or array_like, optional If provided, the diagram will be extended to cover the envelope of this geometry (unless this envelope is smaller than the input geometry). only_edges : bool or array_like, default False If set to True, the triangulation will return a collection of linestrings instead of polygons. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, MultiPoint, normalize, Point >>> points = MultiPoint([(2, 2), (4, 2)]) >>> normalize(voronoi_polygons(points)) >>> voronoi_polygons(points, only_edges=True) >>> voronoi_polygons(MultiPoint([(2, 2), (4, 2), (4.2, 2)]), 0.5, only_edges=True) >>> voronoi_polygons(points, extend_to=LineString([(0, 0), (10, 10)]), only_edges=True) >>> voronoi_polygons(LineString([(2, 2), (4, 2)]), only_edges=True) >>> voronoi_polygons(Point(2, 2)) """ return lib.voronoi_polygons(geometry, tolerance, extend_to, only_edges, **kwargs) @requires_geos("3.6.0") @multithreading_enabled def _oriented_envelope_geos(geometry, **kwargs): return lib.oriented_envelope(geometry, **kwargs) def oriented_envelope(geometry, **kwargs): """ Computes the oriented envelope (minimum rotated rectangle) that encloses an input geometry, such that the resulting rectangle has minimum area. Unlike envelope this rectangle is not constrained to be parallel to the coordinate axes. If the convex hull of the object is a degenerate (line or point) this degenerate is returned. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon >>> oriented_envelope(MultiPoint([(0, 0), (10, 0), (10, 10)])).normalize() >>> oriented_envelope(LineString([(1, 1), (5, 1), (10, 10)])).normalize() >>> oriented_envelope(Polygon([(1, 1), (15, 1), (5, 10), (1, 1)])).normalize() >>> oriented_envelope(LineString([(1, 1), (10, 1)])).normalize() >>> oriented_envelope(Point(2, 2)) >>> oriented_envelope(GeometryCollection([])) """ if lib.geos_version < (3, 12, 0): f = _oriented_envelope_min_area_vectorized else: f = _oriented_envelope_geos return f(geometry, **kwargs) minimum_rotated_rectangle = oriented_envelope @requires_geos("3.8.0") @multithreading_enabled def minimum_bounding_circle(geometry, **kwargs): """Computes the minimum bounding circle that encloses an input geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon >>> minimum_bounding_circle(Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])) >>> minimum_bounding_circle(LineString([(1, 1), (10, 10)])) >>> minimum_bounding_circle(MultiPoint([(2, 2), (4, 2)])) >>> minimum_bounding_circle(Point(0, 1)) >>> minimum_bounding_circle(GeometryCollection([])) See also -------- minimum_bounding_radius """ return lib.minimum_bounding_circle(geometry, **kwargs) shapely-2.0.3/shapely/coordinates.py000066400000000000000000000160641456366510000175270ustar00rootroot00000000000000import numpy as np from shapely import lib __all__ = ["transform", "count_coordinates", "get_coordinates", "set_coordinates"] def transform(geometry, transformation, include_z=False): """Returns a copy of a geometry array with a function applied to its coordinates. With the default of ``include_z=False``, all returned geometries will be two-dimensional; the third dimension will be discarded, if present. When specifying ``include_z=True``, the returned geometries preserve the dimensionality of the respective input geometries. Parameters ---------- geometry : Geometry or array_like transformation : function A function that transforms a (N, 2) or (N, 3) ndarray of float64 to another (N, 2) or (N, 3) ndarray of float64. include_z : bool, default False If True, include the third dimension in the coordinates array that is passed to the ``transformation`` function. If a geometry has no third dimension, the z-coordinates passed to the function will be NaN. Examples -------- >>> from shapely import LineString, Point >>> transform(Point(0, 0), lambda x: x + 1) >>> transform(LineString([(2, 2), (4, 4)]), lambda x: x * [2, 3]) >>> transform(None, lambda x: x) is None True >>> transform([Point(0, 0), None], lambda x: x).tolist() [, None] By default, the third dimension is ignored: >>> transform(Point(0, 0, 0), lambda x: x + 1) >>> transform(Point(0, 0, 0), lambda x: x + 1, include_z=True) """ geometry_arr = np.array(geometry, dtype=np.object_) # makes a copy coordinates = lib.get_coordinates(geometry_arr, include_z, False) new_coordinates = transformation(coordinates) # check the array to yield understandable error messages if not isinstance(new_coordinates, np.ndarray): raise ValueError("The provided transformation did not return a numpy array") if new_coordinates.dtype != np.float64: raise ValueError( "The provided transformation returned an array with an unexpected " f"dtype ({new_coordinates.dtype})" ) if new_coordinates.shape != coordinates.shape: # if the shape is too small we will get a segfault raise ValueError( "The provided transformation returned an array with an unexpected " f"shape ({new_coordinates.shape})" ) geometry_arr = lib.set_coordinates(geometry_arr, new_coordinates) if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray): return geometry_arr.item() return geometry_arr def count_coordinates(geometry): """Counts the number of coordinate pairs in a geometry array. Parameters ---------- geometry : Geometry or array_like Examples -------- >>> from shapely import LineString, Point >>> count_coordinates(Point(0, 0)) 1 >>> count_coordinates(LineString([(2, 2), (4, 2)])) 2 >>> count_coordinates(None) 0 >>> count_coordinates([Point(0, 0), None]) 1 """ return lib.count_coordinates(np.asarray(geometry, dtype=np.object_)) def get_coordinates(geometry, include_z=False, return_index=False): """Gets coordinates from a geometry array as an array of floats. The shape of the returned array is (N, 2), with N being the number of coordinate pairs. With the default of ``include_z=False``, three-dimensional data is ignored. When specifying ``include_z=True``, the shape of the returned array is (N, 3). Parameters ---------- geometry : Geometry or array_like include_z : bool, default False If, True include the third dimension in the output. If a geometry has no third dimension, the z-coordinates will be NaN. return_index : bool, default False If True, also return the index of each returned geometry as a separate ndarray of integers. For multidimensional arrays, this indexes into the flattened array (in C contiguous order). Examples -------- >>> from shapely import LineString, Point >>> get_coordinates(Point(0, 0)).tolist() [[0.0, 0.0]] >>> get_coordinates(LineString([(2, 2), (4, 4)])).tolist() [[2.0, 2.0], [4.0, 4.0]] >>> get_coordinates(None) array([], shape=(0, 2), dtype=float64) By default the third dimension is ignored: >>> get_coordinates(Point(0, 0, 0)).tolist() [[0.0, 0.0]] >>> get_coordinates(Point(0, 0, 0), include_z=True).tolist() [[0.0, 0.0, 0.0]] When return_index=True, indexes are returned also: >>> geometries = [LineString([(2, 2), (4, 4)]), Point(0, 0)] >>> coordinates, index = get_coordinates(geometries, return_index=True) >>> coordinates.tolist(), index.tolist() ([[2.0, 2.0], [4.0, 4.0], [0.0, 0.0]], [0, 0, 1]) """ return lib.get_coordinates( np.asarray(geometry, dtype=np.object_), include_z, return_index ) def set_coordinates(geometry, coordinates): """Adapts the coordinates of a geometry array in-place. If the coordinates array has shape (N, 2), all returned geometries will be two-dimensional, and the third dimension will be discarded, if present. If the coordinates array has shape (N, 3), the returned geometries preserve the dimensionality of the input geometries. .. warning:: The geometry array is modified in-place! If you do not want to modify the original array, you can do ``set_coordinates(arr.copy(), newcoords)``. Parameters ---------- geometry : Geometry or array_like coordinates: array_like See Also -------- transform : Returns a copy of a geometry array with a function applied to its coordinates. Examples -------- >>> from shapely import LineString, Point >>> set_coordinates(Point(0, 0), [[1, 1]]) >>> set_coordinates([Point(0, 0), LineString([(0, 0), (0, 0)])], [[1, 2], [3, 4], [5, 6]]).tolist() [, ] >>> set_coordinates([None, Point(0, 0)], [[1, 2]]).tolist() [None, ] Third dimension of input geometry is discarded if coordinates array does not include one: >>> set_coordinates(Point(0, 0, 0), [[1, 1]]) >>> set_coordinates(Point(0, 0, 0), [[1, 1, 1]]) """ geometry_arr = np.asarray(geometry, dtype=np.object_) coordinates = np.atleast_2d(np.asarray(coordinates)).astype(np.float64) if coordinates.ndim != 2: raise ValueError( "The coordinate array should have dimension of 2 " f"(has {coordinates.ndim})" ) n_coords = lib.count_coordinates(geometry_arr) if (coordinates.shape[0] != n_coords) or (coordinates.shape[1] not in {2, 3}): raise ValueError( f"The coordinate array has an invalid shape {coordinates.shape}" ) lib.set_coordinates(geometry_arr, coordinates) if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray): return geometry_arr.item() return geometry_arr shapely-2.0.3/shapely/coords.py000066400000000000000000000030421456366510000164760ustar00rootroot00000000000000"""Coordinate sequence utilities """ from array import array class CoordinateSequence: """ Iterative access to coordinate tuples from the parent geometry's coordinate sequence. Example: >>> from shapely.wkt import loads >>> g = loads('POINT (0.0 0.0)') >>> list(g.coords) [(0.0, 0.0)] """ def __init__(self, coords): self._coords = coords def __len__(self): return self._coords.shape[0] def __iter__(self): for i in range(self.__len__()): yield tuple(self._coords[i].tolist()) def __getitem__(self, key): m = self.__len__() if isinstance(key, int): if key + m < 0 or key >= m: raise IndexError("index out of range") if key < 0: i = m + key else: i = key return tuple(self._coords[i].tolist()) elif isinstance(key, slice): res = [] start, stop, stride = key.indices(m) for i in range(start, stop, stride): res.append(tuple(self._coords[i].tolist())) return res else: raise TypeError("key must be an index or slice") def __array__(self, dtype=None): return self._coords @property def xy(self): """X and Y arrays""" m = self.__len__() x = array("d") y = array("d") for i in range(m): xy = self._coords[i].tolist() x.append(xy[0]) y.append(xy[1]) return x, y shapely-2.0.3/shapely/creation.py000066400000000000000000000474251456366510000170260ustar00rootroot00000000000000import numpy as np from shapely import Geometry, GeometryType, lib from shapely._geometry_helpers import collections_1d, simple_geometries_1d from shapely.decorators import multithreading_enabled from shapely.io import from_wkt __all__ = [ "points", "linestrings", "linearrings", "polygons", "multipoints", "multilinestrings", "multipolygons", "geometrycollections", "box", "prepare", "destroy_prepared", "empty", ] def _xyz_to_coords(x, y, z): if y is None: return x if z is None: coords = np.broadcast_arrays(x, y) else: coords = np.broadcast_arrays(x, y, z) return np.stack(coords, axis=-1) @multithreading_enabled def points(coords, y=None, z=None, indices=None, out=None, **kwargs): """Create an array of points. Parameters ---------- coords : array_like An array of coordinate tuples (2- or 3-dimensional) or, if ``y`` is provided, an array of x coordinates. y : array_like, optional z : array_like, optional indices : array_like, optional Indices into the target array where input coordinates belong. If provided, the coords should be 2D with shape (N, 2) or (N, 3) and indices should be an array of shape (N,) with integers in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. Examples -------- >>> points([[0, 1], [4, 5]]).tolist() [, ] >>> points([0, 1, 2]) Notes ----- - GEOS 3.10, 3.11 and 3.12 automatically converts POINT (nan nan) to POINT EMPTY. - GEOS 3.10 and 3.11 will transform a 3D point to 2D if its Z coordinate is NaN. - Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``. Instead provide the coordinates as an array with shape ``(..., 2)`` or ``(..., 3)`` using only the ``coords`` argument. """ coords = _xyz_to_coords(coords, y, z) if indices is None: return lib.points(coords, out=out, **kwargs) else: return simple_geometries_1d(coords, indices, GeometryType.POINT, out=out) @multithreading_enabled def linestrings(coords, y=None, z=None, indices=None, out=None, **kwargs): """Create an array of linestrings. This function will raise an exception if a linestring contains less than two points. Parameters ---------- coords : array_like An array of lists of coordinate tuples (2- or 3-dimensional) or, if ``y`` is provided, an array of lists of x coordinates y : array_like, optional z : array_like, optional indices : array_like, optional Indices into the target array where input coordinates belong. If provided, the coords should be 2D with shape (N, 2) or (N, 3) and indices should be an array of shape (N,) with integers in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. Examples -------- >>> linestrings([[[0, 1], [4, 5]], [[2, 3], [5, 6]]]).tolist() [, ] >>> linestrings([[0, 1], [4, 5], [2, 3], [5, 6], [7, 8]], indices=[0, 0, 1, 1, 1]).tolist() [, ] Notes ----- - Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``. Instead provide the coordinates as a ``(..., 2)`` or ``(..., 3)`` array using only ``coords``. """ coords = _xyz_to_coords(coords, y, z) if indices is None: return lib.linestrings(coords, out=out, **kwargs) else: return simple_geometries_1d(coords, indices, GeometryType.LINESTRING, out=out) @multithreading_enabled def linearrings(coords, y=None, z=None, indices=None, out=None, **kwargs): """Create an array of linearrings. If the provided coords do not constitute a closed linestring, or if there are only 3 provided coords, the first coordinate is duplicated at the end to close the ring. This function will raise an exception if a linearring contains less than three points or if the terminal coordinates contain NaN (not-a-number). Parameters ---------- coords : array_like An array of lists of coordinate tuples (2- or 3-dimensional) or, if ``y`` is provided, an array of lists of x coordinates y : array_like, optional z : array_like, optional indices : array_like, optional Indices into the target array where input coordinates belong. If provided, the coords should be 2D with shape (N, 2) or (N, 3) and indices should be an array of shape (N,) with integers in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. See also -------- linestrings Examples -------- >>> linearrings([[0, 0], [0, 1], [1, 1], [0, 0]]) >>> linearrings([[0, 0], [0, 1], [1, 1]]) Notes ----- - Usage of the ``y`` and ``z`` arguments will prevents lazy evaluation in ``dask``. Instead provide the coordinates as a ``(..., 2)`` or ``(..., 3)`` array using only ``coords``. """ coords = _xyz_to_coords(coords, y, z) if indices is None: return lib.linearrings(coords, out=out, **kwargs) else: return simple_geometries_1d(coords, indices, GeometryType.LINEARRING, out=out) @multithreading_enabled def polygons(geometries, holes=None, indices=None, out=None, **kwargs): """Create an array of polygons. Parameters ---------- geometries : array_like An array of linearrings or coordinates (see linearrings). Unless ``indices`` are given (see description below), this include the outer shells only. The ``holes`` argument should be used to create polygons with holes. holes : array_like, optional An array of lists of linearrings that constitute holes for each shell. Not to be used in combination with ``indices``. indices : array_like, optional Indices into the target array where input geometries belong. If provided, the holes are expected to be present inside ``geometries``; the first geometry for each index is the outer shell and all subsequent geometries in that index are the holes. Both geometries and indices should be 1D and have matching sizes. Indices should be in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. Examples -------- Polygons are constructed from rings: >>> ring_1 = linearrings([[0, 0], [0, 10], [10, 10], [10, 0]]) >>> ring_2 = linearrings([[2, 6], [2, 7], [3, 7], [3, 6]]) >>> polygons([ring_1, ring_2])[0] >>> polygons([ring_1, ring_2])[1] Or from coordinates directly: >>> polygons([[0, 0], [0, 10], [10, 10], [10, 0]]) Adding holes can be done using the ``holes`` keyword argument: >>> polygons(ring_1, holes=[ring_2]) Or using the ``indices`` argument: >>> polygons([ring_1, ring_2], indices=[0, 1])[0] >>> polygons([ring_1, ring_2], indices=[0, 1])[1] >>> polygons([ring_1, ring_2], indices=[0, 0])[0] Missing input values (``None``) are ignored and may result in an empty polygon: >>> polygons(None) >>> polygons(ring_1, holes=[None]) >>> polygons([ring_1, None], indices=[0, 0])[0] """ geometries = np.asarray(geometries) if not isinstance(geometries, Geometry) and np.issubdtype( geometries.dtype, np.number ): geometries = linearrings(geometries) if indices is not None: if holes is not None: raise TypeError("Cannot specify separate holes array when using indices.") return collections_1d(geometries, indices, GeometryType.POLYGON, out=out) if holes is None: # no holes provided: initialize an empty holes array matching shells shape = geometries.shape + (0,) if isinstance(geometries, np.ndarray) else (0,) holes = np.empty(shape, dtype=object) else: holes = np.asarray(holes) # convert holes coordinates into linearrings if np.issubdtype(holes.dtype, np.number): holes = linearrings(holes) return lib.polygons(geometries, holes, out=out, **kwargs) @multithreading_enabled def box(xmin, ymin, xmax, ymax, ccw=True, **kwargs): """Create box polygons. Parameters ---------- xmin : array_like ymin : array_like xmax : array_like ymax : array_like ccw : bool, default True If True, box will be created in counterclockwise direction starting from bottom right coordinate (xmax, ymin). If False, box will be created in clockwise direction starting from bottom left coordinate (xmin, ymin). **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> box(0, 0, 1, 1) >>> box(0, 0, 1, 1, ccw=False) """ return lib.box(xmin, ymin, xmax, ymax, ccw, **kwargs) @multithreading_enabled def multipoints(geometries, indices=None, out=None, **kwargs): """Create multipoints from arrays of points Parameters ---------- geometries : array_like An array of points or coordinates (see points). indices : array_like, optional Indices into the target array where input geometries belong. If provided, both geometries and indices should be 1D and have matching sizes. Indices should be in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. Examples -------- Multipoints are constructed from points: >>> point_1 = points([1, 1]) >>> point_2 = points([2, 2]) >>> multipoints([point_1, point_2]) >>> multipoints([[point_1, point_2], [point_2, None]]).tolist() [, ] Or from coordinates directly: >>> multipoints([[0, 0], [2, 2], [3, 3]]) Multiple multipoints of different sizes can be constructed efficiently using the ``indices`` keyword argument: >>> multipoints([point_1, point_2, point_2], indices=[0, 0, 1]).tolist() [, ] Missing input values (``None``) are ignored and may result in an empty multipoint: >>> multipoints([None]) >>> multipoints([point_1, None], indices=[0, 0]).tolist() [] >>> multipoints([point_1, None], indices=[0, 1]).tolist() [, ] """ typ = GeometryType.MULTIPOINT geometries = np.asarray(geometries) if not isinstance(geometries, Geometry) and np.issubdtype( geometries.dtype, np.number ): geometries = points(geometries) if indices is None: return lib.create_collection(geometries, typ, out=out, **kwargs) else: return collections_1d(geometries, indices, typ, out=out) @multithreading_enabled def multilinestrings(geometries, indices=None, out=None, **kwargs): """Create multilinestrings from arrays of linestrings Parameters ---------- geometries : array_like An array of linestrings or coordinates (see linestrings). indices : array_like, optional Indices into the target array where input geometries belong. If provided, both geometries and indices should be 1D and have matching sizes. Indices should be in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. See also -------- multipoints """ typ = GeometryType.MULTILINESTRING geometries = np.asarray(geometries) if not isinstance(geometries, Geometry) and np.issubdtype( geometries.dtype, np.number ): geometries = linestrings(geometries) if indices is None: return lib.create_collection(geometries, typ, out=out, **kwargs) else: return collections_1d(geometries, indices, typ, out=out) @multithreading_enabled def multipolygons(geometries, indices=None, out=None, **kwargs): """Create multipolygons from arrays of polygons Parameters ---------- geometries : array_like An array of polygons or coordinates (see polygons). indices : array_like, optional Indices into the target array where input geometries belong. If provided, both geometries and indices should be 1D and have matching sizes. Indices should be in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. See also -------- multipoints """ typ = GeometryType.MULTIPOLYGON geometries = np.asarray(geometries) if not isinstance(geometries, Geometry) and np.issubdtype( geometries.dtype, np.number ): geometries = polygons(geometries) if indices is None: return lib.create_collection(geometries, typ, out=out, **kwargs) else: return collections_1d(geometries, indices, typ, out=out) @multithreading_enabled def geometrycollections(geometries, indices=None, out=None, **kwargs): """Create geometrycollections from arrays of geometries Parameters ---------- geometries : array_like An array of geometries indices : array_like, optional Indices into the target array where input geometries belong. If provided, both geometries and indices should be 1D and have matching sizes. Indices should be in increasing order. Missing indices result in a ValueError unless ``out`` is provided, in which case the original value in ``out`` is kept. out : ndarray, optional An array (with dtype object) to output the geometries into. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Ignored if ``indices`` is provided. See also -------- multipoints """ typ = GeometryType.GEOMETRYCOLLECTION if indices is None: return lib.create_collection(geometries, typ, out=out, **kwargs) else: return collections_1d(geometries, indices, typ, out=out) def prepare(geometry, **kwargs): """Prepare a geometry, improving performance of other operations. A prepared geometry is a normal geometry with added information such as an index on the line segments. This improves the performance of the following operations: contains, contains_properly, covered_by, covers, crosses, disjoint, intersects, overlaps, touches, and within. Note that if a prepared geometry is modified, the newly created Geometry object is not prepared. In that case, ``prepare`` should be called again. This function does not recompute previously prepared geometries; it is efficient to call this function on an array that partially contains prepared geometries. This function does not return any values; geometries are modified in place. Parameters ---------- geometry : Geometry or array_like Geometries are changed in place **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_prepared : Identify whether a geometry is prepared already. destroy_prepared : Destroy the prepared part of a geometry. Examples -------- >>> from shapely import Point, buffer, prepare, contains_properly >>> poly = buffer(Point(1.0, 1.0), 1) >>> prepare(poly) >>> contains_properly(poly, [Point(0.0, 0.0), Point(0.5, 0.5)]).tolist() [False, True] """ lib.prepare(geometry, **kwargs) def destroy_prepared(geometry, **kwargs): """Destroy the prepared part of a geometry, freeing up memory. Note that the prepared geometry will always be cleaned up if the geometry itself is dereferenced. This function needs only be called in very specific circumstances, such as freeing up memory without losing the geometries, or benchmarking. Parameters ---------- geometry : Geometry or array_like Geometries are changed inplace **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- prepare """ lib.destroy_prepared(geometry, **kwargs) def empty(shape, geom_type=None, order="C"): """Create a geometry array prefilled with None or with empty geometries. Parameters ---------- shape : int or tuple of int Shape of the empty array, e.g., ``(2, 3)`` or ``2``. geom_type : shapely.GeometryType, optional The desired geometry type in case the array should be prefilled with empty geometries. Default ``None``. order : {'C', 'F'}, optional, default: 'C' Whether to store multi-dimensional data in row-major (C-style) or column-major (Fortran-style) order in memory. Examples -------- >>> empty((2, 3)).tolist() [[None, None, None], [None, None, None]] >>> empty(2, geom_type=GeometryType.POINT).tolist() [, ] """ if geom_type is None: return np.empty(shape, dtype=object, order=order) geom_type = GeometryType(geom_type) # cast int to GeometryType if geom_type is GeometryType.MISSING: return np.empty(shape, dtype=object, order=order) fill_value = from_wkt(geom_type.name + " EMPTY") return np.full(shape, fill_value, dtype=object, order=order) shapely-2.0.3/shapely/decorators.py000066400000000000000000000047731456366510000173660ustar00rootroot00000000000000import os from functools import wraps import numpy as np from shapely import lib from shapely.errors import UnsupportedGEOSVersionError class requires_geos: def __init__(self, version): if version.count(".") != 2: raise ValueError("Version must be .. format") self.version = tuple(int(x) for x in version.split(".")) def __call__(self, func): is_compatible = lib.geos_version >= self.version is_doc_build = os.environ.get("SPHINX_DOC_BUILD") == "1" # set in docs/conf.py if is_compatible and not is_doc_build: return func # return directly, do not change the docstring msg = "'{}' requires at least GEOS {}.{}.{}.".format( func.__name__, *self.version ) if is_compatible: @wraps(func) def wrapped(*args, **kwargs): return func(*args, **kwargs) else: @wraps(func) def wrapped(*args, **kwargs): raise UnsupportedGEOSVersionError(msg) doc = wrapped.__doc__ if doc: # Insert the message at the first double newline position = doc.find("\n\n") + 2 # Figure out the indentation level indent = 0 while True: if doc[position + indent] == " ": indent += 1 else: break wrapped.__doc__ = doc.replace( "\n\n", "\n\n{}.. note:: {}\n\n".format(" " * indent, msg), 1 ) return wrapped def multithreading_enabled(func): """Prepare multithreading by setting the writable flags of object type ndarrays to False. NB: multithreading also requires the GIL to be released, which is done in the C extension (ufuncs.c).""" @wraps(func) def wrapped(*args, **kwargs): array_args = [ arg for arg in args if isinstance(arg, np.ndarray) and arg.dtype == object ] + [ arg for name, arg in kwargs.items() if name not in {"where", "out"} and isinstance(arg, np.ndarray) and arg.dtype == object ] old_flags = [arr.flags.writeable for arr in array_args] try: for arr in array_args: arr.flags.writeable = False return func(*args, **kwargs) finally: for arr, old_flag in zip(array_args, old_flags): arr.flags.writeable = old_flag return wrapped shapely-2.0.3/shapely/errors.py000066400000000000000000000046271456366510000165330ustar00rootroot00000000000000"""Shapely errors.""" import threading from shapely.lib import _setup_signal_checks, GEOSException, ShapelyError # NOQA def setup_signal_checks(interval=10000): """This enables Python signal checks in the ufunc inner loops. Doing so allows termination (using CTRL+C) of operations on large arrays of vectors. Parameters ---------- interval : int, default 10000 Check for interrupts every x iterations. The higher the number, the slower shapely will respond to a signal. However, at low values there will be a negative effect on performance. The default of 10000 does not have any measureable effects on performance. Notes ----- For more information on signals consult the Python docs: https://docs.python.org/3/library/signal.html """ if interval <= 0: raise ValueError("Signal checks interval must be greater than zero.") _setup_signal_checks(interval, threading.main_thread().ident) class UnsupportedGEOSVersionError(ShapelyError): """Raised when the GEOS library version does not support a certain operation.""" class DimensionError(ShapelyError): """An error in the number of coordinate dimensions.""" class TopologicalError(ShapelyError): """A geometry is invalid or topologically incorrect.""" class ShapelyDeprecationWarning(FutureWarning): """ Warning for features that will be removed or behaviour that will be changed in a future release. """ class EmptyPartError(ShapelyError): """An error signifying an empty part was encountered when creating a multi-part.""" class GeometryTypeError(ShapelyError): """ An error raised when the type of the geometry in question is unrecognized or inappropriate. """ def __getattr__(name): import warnings # Alias Shapely 1.8 error classes to ShapelyError with deprecation warning if name in [ "ReadingError", "WKBReadingError", "WKTReadingError", "PredicateError", "InvalidGeometryError", ]: warnings.warn( f"{name} is deprecated and will be removed in a future version. " "Use ShapelyError instead (functions previously raising {name} " "will now raise a ShapelyError instead).", DeprecationWarning, stacklevel=2, ) return ShapelyError raise AttributeError(f"module 'shapely.errors' has no attribute '{name}'") shapely-2.0.3/shapely/geometry/000077500000000000000000000000001456366510000164675ustar00rootroot00000000000000shapely-2.0.3/shapely/geometry/__init__.py000066400000000000000000000013731456366510000206040ustar00rootroot00000000000000"""Geometry classes and factories """ from shapely.geometry.base import CAP_STYLE, JOIN_STYLE from shapely.geometry.collection import GeometryCollection from shapely.geometry.geo import box, mapping, shape from shapely.geometry.linestring import LineString from shapely.geometry.multilinestring import MultiLineString from shapely.geometry.multipoint import MultiPoint from shapely.geometry.multipolygon import MultiPolygon from shapely.geometry.point import Point from shapely.geometry.polygon import LinearRing, Polygon __all__ = [ "box", "shape", "mapping", "Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon", "GeometryCollection", "LinearRing", "CAP_STYLE", "JOIN_STYLE", ] shapely-2.0.3/shapely/geometry/base.py000066400000000000000000001025231456366510000177560ustar00rootroot00000000000000"""Base geometry class and utilities Note: a third, z, coordinate value may be used when constructing geometry objects, but has no effect on geometric analysis. All operations are performed in the x-y plane. Thus, geometries with different z values may intersect or be equal. """ import re from warnings import warn import numpy as np import shapely from shapely._geometry_helpers import _geom_factory from shapely.constructive import BufferCapStyle, BufferJoinStyle from shapely.coords import CoordinateSequence from shapely.errors import GeometryTypeError, GEOSException, ShapelyDeprecationWarning GEOMETRY_TYPES = [ "Point", "LineString", "LinearRing", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon", "GeometryCollection", ] def geom_factory(g, parent=None): """ Creates a Shapely geometry instance from a pointer to a GEOS geometry. .. warning:: The GEOS library used to create the the GEOS geometry pointer and the GEOS library used by Shapely must be exactly the same, or unexpected results or segfaults may occur. .. deprecated:: 2.0 Deprecated in Shapely 2.0, and will be removed in a future version. """ warn( "The 'geom_factory' function is deprecated in Shapely 2.0, and will be " "removed in a future version", DeprecationWarning, stacklevel=2, ) return _geom_factory(g) def dump_coords(geom): """Dump coordinates of a geometry in the same order as data packing""" if not isinstance(geom, BaseGeometry): raise ValueError( "Must be instance of a geometry class; found " + geom.__class__.__name__ ) elif geom.geom_type in ("Point", "LineString", "LinearRing"): return geom.coords[:] elif geom.geom_type == "Polygon": return geom.exterior.coords[:] + [i.coords[:] for i in geom.interiors] elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection": # Recursive call return [dump_coords(part) for part in geom.geoms] else: raise GeometryTypeError("Unhandled geometry type: " + repr(geom.geom_type)) def _maybe_unpack(result): if result.ndim == 0: # convert numpy 0-d array / scalar to python scalar return result.item() else: # >=1 dim array return result class CAP_STYLE: round = BufferCapStyle.round flat = BufferCapStyle.flat square = BufferCapStyle.square class JOIN_STYLE: round = BufferJoinStyle.round mitre = BufferJoinStyle.mitre bevel = BufferJoinStyle.bevel class BaseGeometry(shapely.Geometry): """ Provides GEOS spatial predicates and topological operations. """ __slots__ = [] def __new__(self): warn( "Directly calling the base class 'BaseGeometry()' is deprecated, and " "will raise an error in the future. To create an empty geometry, " "use one of the subclasses instead, for example 'GeometryCollection()'.", ShapelyDeprecationWarning, stacklevel=2, ) return shapely.from_wkt("GEOMETRYCOLLECTION EMPTY") @property def _ndim(self): return shapely.get_coordinate_dimension(self) def __bool__(self): return self.is_empty is False def __nonzero__(self): return self.__bool__() def __format__(self, format_spec): """Format a geometry using a format specification.""" # bypass regexp for simple cases if format_spec == "": return shapely.to_wkt(self, rounding_precision=-1) elif format_spec == "x": return shapely.to_wkb(self, hex=True).lower() elif format_spec == "X": return shapely.to_wkb(self, hex=True) # fmt: off format_spec_regexp = ( "(?:0?\\.(?P[0-9]+))?" "(?P[fFgGxX]?)" ) # fmt: on match = re.fullmatch(format_spec_regexp, format_spec) if match is None: raise ValueError(f"invalid format specifier: {format_spec}") prec, fmt_code = match.groups() if prec: prec = int(prec) else: # GEOS has a default rounding_precision -1 prec = -1 if not fmt_code: fmt_code = "g" if fmt_code in ("g", "G"): res = shapely.to_wkt(self, rounding_precision=prec, trim=True) elif fmt_code in ("f", "F"): res = shapely.to_wkt(self, rounding_precision=prec, trim=False) elif fmt_code in ("x", "X"): raise ValueError("hex representation does not specify precision") else: raise NotImplementedError(f"unhandled fmt_code: {fmt_code}") if fmt_code.isupper(): return res.upper() else: return res def __repr__(self): try: wkt = super().__str__() except (GEOSException, ValueError): # we never want a repr() to fail; that can be very confusing return "".format( self.__class__.__name__ ) # the total length is limited to 80 characters including brackets max_length = 78 if len(wkt) > max_length: return f"<{wkt[: max_length - 3]}...>" return f"<{wkt}>" def __str__(self): return self.wkt def __reduce__(self): return (shapely.from_wkb, (shapely.to_wkb(self, include_srid=True),)) # Operators # --------- def __and__(self, other): return self.intersection(other) def __or__(self, other): return self.union(other) def __sub__(self, other): return self.difference(other) def __xor__(self, other): return self.symmetric_difference(other) def __eq__(self, other): if not isinstance(other, BaseGeometry): return NotImplemented # equal_nan=False is the default, but not yet available for older numpy # TODO updated once we require numpy >= 1.19 return type(other) == type(self) and np.array_equal( self.coords, other.coords # , equal_nan=False ) def __ne__(self, other): if not isinstance(other, BaseGeometry): return NotImplemented return not self.__eq__(other) def __hash__(self): return super().__hash__() # Coordinate access # ----------------- @property def coords(self): """Access to geometry's coordinates (CoordinateSequence)""" coords_array = shapely.get_coordinates(self, include_z=self.has_z) return CoordinateSequence(coords_array) @property def xy(self): """Separate arrays of X and Y coordinate values""" raise NotImplementedError # Python feature protocol @property def __geo_interface__(self): """Dictionary representation of the geometry""" raise NotImplementedError # Type of geometry and its representations # ---------------------------------------- def geometryType(self): warn( "The 'GeometryType()' method is deprecated, and will be removed in " "the future. You can use the 'geom_type' attribute instead.", ShapelyDeprecationWarning, stacklevel=2, ) return self.geom_type @property def type(self): warn( "The 'type' attribute is deprecated, and will be removed in " "the future. You can use the 'geom_type' attribute instead.", ShapelyDeprecationWarning, stacklevel=2, ) return self.geom_type @property def wkt(self): """WKT representation of the geometry""" # TODO(shapely-2.0) keep default of not trimming? return shapely.to_wkt(self, rounding_precision=-1) @property def wkb(self): """WKB representation of the geometry""" return shapely.to_wkb(self) @property def wkb_hex(self): """WKB hex representation of the geometry""" return shapely.to_wkb(self, hex=True) def svg(self, scale_factor=1.0, **kwargs): """Raises NotImplementedError""" raise NotImplementedError def _repr_svg_(self): """SVG representation for iPython notebook""" svg_top = ( '" else: # Establish SVG canvas that will fit all the data + small space xmin, ymin, xmax, ymax = self.bounds if xmin == xmax and ymin == ymax: # This is a point; buffer using an arbitrary size xmin, ymin, xmax, ymax = self.buffer(1).bounds else: # Expand bounds by a fraction of the data ranges expand = 0.04 # or 4%, same as R plots widest_part = max([xmax - xmin, ymax - ymin]) expand_amount = widest_part * expand xmin -= expand_amount ymin -= expand_amount xmax += expand_amount ymax += expand_amount dx = xmax - xmin dy = ymax - ymin width = min([max([100.0, dx]), 300]) height = min([max([100.0, dy]), 300]) try: scale_factor = max([dx, dy]) / max([width, height]) except ZeroDivisionError: scale_factor = 1.0 view_box = f"{xmin} {ymin} {dx} {dy}" transform = f"matrix(1,0,0,-1,0,{ymax + ymin})" return svg_top + ( 'width="{1}" height="{2}" viewBox="{0}" ' 'preserveAspectRatio="xMinYMin meet">' '{4}' ).format(view_box, width, height, transform, self.svg(scale_factor)) @property def geom_type(self): """Name of the geometry's type, such as 'Point'""" return GEOMETRY_TYPES[shapely.get_type_id(self)] # Real-valued properties and methods # ---------------------------------- @property def area(self): """Unitless area of the geometry (float)""" return float(shapely.area(self)) def distance(self, other): """Unitless distance to other geometry (float)""" return _maybe_unpack(shapely.distance(self, other)) def hausdorff_distance(self, other): """Unitless hausdorff distance to other geometry (float)""" return _maybe_unpack(shapely.hausdorff_distance(self, other)) @property def length(self): """Unitless length of the geometry (float)""" return float(shapely.length(self)) @property def minimum_clearance(self): """Unitless distance by which a node could be moved to produce an invalid geometry (float)""" return float(shapely.minimum_clearance(self)) # Topological properties # ---------------------- @property def boundary(self): """Returns a lower dimension geometry that bounds the object The boundary of a polygon is a line, the boundary of a line is a collection of points. The boundary of a point is an empty (null) collection. """ return shapely.boundary(self) @property def bounds(self): """Returns minimum bounding region (minx, miny, maxx, maxy)""" return tuple(shapely.bounds(self).tolist()) @property def centroid(self): """Returns the geometric center of the object""" return shapely.centroid(self) def point_on_surface(self): """Returns a point guaranteed to be within the object, cheaply. Alias of `representative_point`. """ return shapely.point_on_surface(self) def representative_point(self): """Returns a point guaranteed to be within the object, cheaply. Alias of `point_on_surface`. """ return shapely.point_on_surface(self) @property def convex_hull(self): """Imagine an elastic band stretched around the geometry: that's a convex hull, more or less The convex hull of a three member multipoint, for example, is a triangular polygon. """ return shapely.convex_hull(self) @property def envelope(self): """A figure that envelopes the geometry""" return shapely.envelope(self) @property def oriented_envelope(self): """ Returns the oriented envelope (minimum rotated rectangle) that encloses the geometry. Unlike envelope this rectangle is not constrained to be parallel to the coordinate axes. If the convex hull of the object is a degenerate (line or point) this degenerate is returned. Alias of `minimum_rotated_rectangle`. """ return shapely.oriented_envelope(self) @property def minimum_rotated_rectangle(self): """ Returns the oriented envelope (minimum rotated rectangle) that encloses the geometry. Unlike `envelope` this rectangle is not constrained to be parallel to the coordinate axes. If the convex hull of the object is a degenerate (line or point) this degenerate is returned. Alias of `oriented_envelope`. """ return shapely.oriented_envelope(self) def buffer( self, distance, quad_segs=16, cap_style="round", join_style="round", mitre_limit=5.0, single_sided=False, **kwargs, ): """Get a geometry that represents all points within a distance of this geometry. A positive distance produces a dilation, a negative distance an erosion. A very small or zero distance may sometimes be used to "tidy" a polygon. Parameters ---------- distance : float The distance to buffer around the object. resolution : int, optional The resolution of the buffer around each vertex of the object. quad_segs : int, optional Sets the number of line segments used to approximate an angle fillet. cap_style : shapely.BufferCapStyle or {'round', 'square', 'flat'}, default 'round' Specifies the shape of buffered line endings. BufferCapStyle.round ('round') results in circular line endings (see ``quad_segs``). Both BufferCapStyle.square ('square') and BufferCapStyle.flat ('flat') result in rectangular line endings, only BufferCapStyle.flat ('flat') will end at the original vertex, while BufferCapStyle.square ('square') involves adding the buffer width. join_style : shapely.BufferJoinStyle or {'round', 'mitre', 'bevel'}, default 'round' Specifies the shape of buffered line midpoints. BufferJoinStyle.ROUND ('round') results in rounded shapes. BufferJoinStyle.bevel ('bevel') results in a beveled edge that touches the original vertex. BufferJoinStyle.mitre ('mitre') results in a single vertex that is beveled depending on the ``mitre_limit`` parameter. mitre_limit : float, optional The mitre limit ratio is used for very sharp corners. The mitre ratio is the ratio of the distance from the corner to the end of the mitred offset corner. When two line segments meet at a sharp angle, a miter join will extend the original geometry. To prevent unreasonable geometry, the mitre limit allows controlling the maximum length of the join corner. Corners with a ratio which exceed the limit will be beveled. single_side : bool, optional The side used is determined by the sign of the buffer distance: a positive distance indicates the left-hand side a negative distance indicates the right-hand side The single-sided buffer of point geometries is the same as the regular buffer. The End Cap Style for single-sided buffers is always ignored, and forced to the equivalent of CAP_FLAT. quadsegs : int, optional Deprecated alias for `quad_segs`. Returns ------- Geometry Notes ----- The return value is a strictly two-dimensional geometry. All Z coordinates of the original geometry will be ignored. Examples -------- >>> from shapely.wkt import loads >>> g = loads('POINT (0.0 0.0)') 16-gon approx of a unit radius circle: >>> g.buffer(1.0).area # doctest: +ELLIPSIS 3.1365484905459... 128-gon approximation: >>> g.buffer(1.0, 128).area # doctest: +ELLIPSIS 3.141513801144... triangle approximation: >>> g.buffer(1.0, 3).area 3.0 >>> list(g.buffer(1.0, cap_style=BufferCapStyle.square).exterior.coords) [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] >>> g.buffer(1.0, cap_style=BufferCapStyle.square).area 4.0 """ quadsegs = kwargs.pop("quadsegs", None) if quadsegs is not None: warn( "The `quadsegs` argument is deprecated. Use `quad_segs` instead.", FutureWarning, ) quad_segs = quadsegs # TODO deprecate `resolution` keyword for shapely 2.1 resolution = kwargs.pop("resolution", None) if resolution is not None: quad_segs = resolution if kwargs: kwarg = list(kwargs.keys())[0] # noqa raise TypeError(f"buffer() got an unexpected keyword argument '{kwarg}'") if mitre_limit == 0.0: raise ValueError("Cannot compute offset from zero-length line segment") elif not np.isfinite(distance).all(): raise ValueError("buffer distance must be finite") return shapely.buffer( self, distance, quad_segs=quad_segs, cap_style=cap_style, join_style=join_style, mitre_limit=mitre_limit, single_sided=single_sided, ) def simplify(self, tolerance, preserve_topology=True): """Returns a simplified geometry produced by the Douglas-Peucker algorithm Coordinates of the simplified geometry will be no more than the tolerance distance from the original. Unless the topology preserving option is used, the algorithm may produce self-intersecting or otherwise invalid geometries. """ return shapely.simplify(self, tolerance, preserve_topology=preserve_topology) def normalize(self): """Converts geometry to normal form (or canonical form). This method orders the coordinates, rings of a polygon and parts of multi geometries consistently. Typically useful for testing purposes (for example in combination with `equals_exact`). Examples -------- >>> from shapely import MultiLineString >>> line = MultiLineString([[(0, 0), (1, 1)], [(3, 3), (2, 2)]]) >>> line.normalize() """ return shapely.normalize(self) # Overlay operations # --------------------------- def difference(self, other, grid_size=None): """ Returns the difference of the geometries. Refer to `shapely.difference` for full documentation. """ return shapely.difference(self, other, grid_size=grid_size) def intersection(self, other, grid_size=None): """ Returns the intersection of the geometries. Refer to `shapely.intersection` for full documentation. """ return shapely.intersection(self, other, grid_size=grid_size) def symmetric_difference(self, other, grid_size=None): """ Returns the symmetric difference of the geometries. Refer to `shapely.symmetric_difference` for full documentation. """ return shapely.symmetric_difference(self, other, grid_size=grid_size) def union(self, other, grid_size=None): """ Returns the union of the geometries. Refer to `shapely.union` for full documentation. """ return shapely.union(self, other, grid_size=grid_size) # Unary predicates # ---------------- @property def has_z(self): """True if the geometry's coordinate sequence(s) have z values (are 3-dimensional)""" return bool(shapely.has_z(self)) @property def is_empty(self): """True if the set of points in this geometry is empty, else False""" return bool(shapely.is_empty(self)) @property def is_ring(self): """True if the geometry is a closed ring, else False""" return bool(shapely.is_ring(self)) @property def is_closed(self): """True if the geometry is closed, else False Applicable only to 1-D geometries.""" if self.geom_type == "LinearRing": return True return bool(shapely.is_closed(self)) @property def is_simple(self): """True if the geometry is simple, meaning that any self-intersections are only at boundary points, else False""" return bool(shapely.is_simple(self)) @property def is_valid(self): """True if the geometry is valid (definition depends on sub-class), else False""" return bool(shapely.is_valid(self)) # Binary predicates # ----------------- def relate(self, other): """Returns the DE-9IM intersection matrix for the two geometries (string)""" return shapely.relate(self, other) def covers(self, other): """Returns True if the geometry covers the other, else False""" return _maybe_unpack(shapely.covers(self, other)) def covered_by(self, other): """Returns True if the geometry is covered by the other, else False""" return _maybe_unpack(shapely.covered_by(self, other)) def contains(self, other): """Returns True if the geometry contains the other, else False""" return _maybe_unpack(shapely.contains(self, other)) def contains_properly(self, other): """ Returns True if the geometry completely contains the other, with no common boundary points, else False Refer to `shapely.contains_properly` for full documentation. """ return _maybe_unpack(shapely.contains_properly(self, other)) def crosses(self, other): """Returns True if the geometries cross, else False""" return _maybe_unpack(shapely.crosses(self, other)) def disjoint(self, other): """Returns True if geometries are disjoint, else False""" return _maybe_unpack(shapely.disjoint(self, other)) def equals(self, other): """Returns True if geometries are equal, else False. This method considers point-set equality (or topological equality), and is equivalent to (self.within(other) & self.contains(other)). Examples -------- >>> LineString( ... [(0, 0), (2, 2)] ... ).equals( ... LineString([(0, 0), (1, 1), (2, 2)]) ... ) True Returns ------- bool """ return _maybe_unpack(shapely.equals(self, other)) def intersects(self, other): """Returns True if geometries intersect, else False""" return _maybe_unpack(shapely.intersects(self, other)) def overlaps(self, other): """Returns True if geometries overlap, else False""" return _maybe_unpack(shapely.overlaps(self, other)) def touches(self, other): """Returns True if geometries touch, else False""" return _maybe_unpack(shapely.touches(self, other)) def within(self, other): """Returns True if geometry is within the other, else False""" return _maybe_unpack(shapely.within(self, other)) def dwithin(self, other, distance): """ Returns True if geometry is within a given distance from the other, else False. Refer to `shapely.dwithin` for full documentation. """ return _maybe_unpack(shapely.dwithin(self, other, distance)) def equals_exact(self, other, tolerance): """True if geometries are equal to within a specified tolerance. Parameters ---------- other : BaseGeometry The other geometry object in this comparison. tolerance : float Absolute tolerance in the same units as coordinates. This method considers coordinate equality, which requires coordinates to be equal and in the same order for all components of a geometry. Because of this it is possible for "equals()" to be True for two geometries and "equals_exact()" to be False. Examples -------- >>> LineString( ... [(0, 0), (2, 2)] ... ).equals_exact( ... LineString([(0, 0), (1, 1), (2, 2)]), ... 1e-6 ... ) False Returns ------- bool """ return _maybe_unpack(shapely.equals_exact(self, other, tolerance)) def almost_equals(self, other, decimal=6): """True if geometries are equal at all coordinates to a specified decimal place. .. deprecated:: 1.8.0 The 'almost_equals()' method is deprecated and will be removed in Shapely 2.1 because the name is confusing. The 'equals_exact()' method should be used instead. Refers to approximate coordinate equality, which requires coordinates to be approximately equal and in the same order for all components of a geometry. Because of this it is possible for "equals()" to be True for two geometries and "almost_equals()" to be False. Examples -------- >>> LineString( ... [(0, 0), (2, 2)] ... ).equals_exact( ... LineString([(0, 0), (1, 1), (2, 2)]), ... 1e-6 ... ) False Returns ------- bool """ warn( "The 'almost_equals()' method is deprecated and will be " "removed in Shapely 2.1; use 'equals_exact()' instead", ShapelyDeprecationWarning, stacklevel=2, ) return self.equals_exact(other, 0.5 * 10 ** (-decimal)) def relate_pattern(self, other, pattern): """Returns True if the DE-9IM string code for the relationship between the geometries satisfies the pattern, else False""" return _maybe_unpack(shapely.relate_pattern(self, other, pattern)) # Linear referencing # ------------------ def line_locate_point(self, other, normalized=False): """Returns the distance along this geometry to a point nearest the specified point If the normalized arg is True, return the distance normalized to the length of the linear geometry. Alias of `project`. """ return shapely.line_locate_point(self, other, normalized=normalized) def project(self, other, normalized=False): """Returns the distance along this geometry to a point nearest the specified point If the normalized arg is True, return the distance normalized to the length of the linear geometry. Alias of `line_locate_point`. """ return shapely.line_locate_point(self, other, normalized=normalized) def line_interpolate_point(self, distance, normalized=False): """Return a point at the specified distance along a linear geometry Negative length values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values. If the normalized arg is True, the distance will be interpreted as a fraction of the geometry's length. Alias of `interpolate`. """ return shapely.line_interpolate_point(self, distance, normalized=normalized) def interpolate(self, distance, normalized=False): """Return a point at the specified distance along a linear geometry Negative length values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values. If the normalized arg is True, the distance will be interpreted as a fraction of the geometry's length. Alias of `line_interpolate_point`. """ return shapely.line_interpolate_point(self, distance, normalized=normalized) def segmentize(self, max_segment_length): """Adds vertices to line segments based on maximum segment length. Additional vertices will be added to every line segment in an input geometry so that segments are no longer than the provided maximum segment length. New vertices will evenly subdivide each segment. Only linear components of input geometries are densified; other geometries are returned unmodified. Parameters ---------- max_segment_length : float or array_like Additional vertices will be added so that all line segments are no longer this value. Must be greater than 0. Examples -------- >>> from shapely import LineString, Polygon >>> LineString([(0, 0), (0, 10)]).segmentize(max_segment_length=5) >>> Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]).segmentize(max_segment_length=5) """ return shapely.segmentize(self, max_segment_length) def reverse(self): """Returns a copy of this geometry with the order of coordinates reversed. If the geometry is a polygon with interior rings, the interior rings are also reversed. Points are unchanged. See also -------- is_ccw : Checks if a geometry is clockwise. Examples -------- >>> from shapely import LineString, Polygon >>> LineString([(0, 0), (1, 2)]).reverse() >>> Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]).reverse() """ return shapely.reverse(self) class BaseMultipartGeometry(BaseGeometry): __slots__ = [] @property def coords(self): raise NotImplementedError( "Sub-geometries may have coordinate sequences, " "but multi-part geometries do not" ) @property def geoms(self): return GeometrySequence(self) def __bool__(self): return self.is_empty is False def __eq__(self, other): if not isinstance(other, BaseGeometry): return NotImplemented return ( type(other) == type(self) and len(self.geoms) == len(other.geoms) and all(a == b for a, b in zip(self.geoms, other.geoms)) ) def __hash__(self): return super().__hash__() def svg(self, scale_factor=1.0, color=None): """Returns a group of SVG elements for the multipart geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG stroke-width. Default is 1. color : str, optional Hex string for stroke or fill color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. """ if self.is_empty: return "" if color is None: color = "#66cc99" if self.is_valid else "#ff3333" return "" + "".join(p.svg(scale_factor, color) for p in self.geoms) + "" class GeometrySequence: """ Iterative access to members of a homogeneous multipart geometry. """ # Attributes # ---------- # _parent : object # Parent (Shapely) geometry _parent = None def __init__(self, parent): self._parent = parent def _get_geom_item(self, i): return shapely.get_geometry(self._parent, i) def __iter__(self): for i in range(self.__len__()): yield self._get_geom_item(i) def __len__(self): return shapely.get_num_geometries(self._parent) def __getitem__(self, key): m = self.__len__() if isinstance(key, (int, np.integer)): if key + m < 0 or key >= m: raise IndexError("index out of range") if key < 0: i = m + key else: i = key return self._get_geom_item(i) elif isinstance(key, slice): res = [] start, stop, stride = key.indices(m) for i in range(start, stop, stride): res.append(self._get_geom_item(i)) return type(self._parent)(res or None) else: raise TypeError("key must be an index or slice") class EmptyGeometry(BaseGeometry): def __new__(self): """Create an empty geometry.""" warn( "The 'EmptyGeometry()' constructor to create an empty geometry is " "deprecated, and will raise an error in the future. Use one of the " "geometry subclasses instead, for example 'GeometryCollection()'.", ShapelyDeprecationWarning, stacklevel=2, ) return shapely.from_wkt("GEOMETRYCOLLECTION EMPTY") shapely-2.0.3/shapely/geometry/collection.py000066400000000000000000000030771456366510000212030ustar00rootroot00000000000000"""Multi-part collections of geometries """ import shapely from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry class GeometryCollection(BaseMultipartGeometry): """ A collection of one or more geometries that may contain more than one type of geometry. Parameters ---------- geoms : list A list of shapely geometry instances, which may be of varying geometry types. Attributes ---------- geoms : sequence A sequence of Shapely geometry instances Examples -------- Create a GeometryCollection with a Point and a LineString >>> from shapely import LineString, Point >>> p = Point(51, -1) >>> l = LineString([(52, -1), (49, 2)]) >>> gc = GeometryCollection([p, l]) """ __slots__ = [] def __new__(self, geoms=None): if not geoms: # TODO better empty constructor return shapely.from_wkt("GEOMETRYCOLLECTION EMPTY") if isinstance(geoms, BaseGeometry): # TODO(shapely-2.0) do we actually want to split Multi-part geometries? # this is needed for the split() tests if hasattr(geoms, "geoms"): geoms = geoms.geoms else: geoms = [geoms] return shapely.geometrycollections(geoms) @property def __geo_interface__(self): geometries = [] for geom in self.geoms: geometries.append(geom.__geo_interface__) return dict(type="GeometryCollection", geometries=geometries) shapely.lib.registry[7] = GeometryCollection shapely-2.0.3/shapely/geometry/conftest.py000066400000000000000000000003401456366510000206630ustar00rootroot00000000000000"""Autouse fixtures for doctests.""" import pytest from shapely.geometry.linestring import LineString @pytest.fixture(autouse=True) def add_linestring(doctest_namespace): doctest_namespace["LineString"] = LineString shapely-2.0.3/shapely/geometry/geo.py000066400000000000000000000100021456366510000176040ustar00rootroot00000000000000""" Geometry factories based on the geo interface """ import numpy as np from shapely.errors import GeometryTypeError from shapely.geometry.collection import GeometryCollection from shapely.geometry.linestring import LineString from shapely.geometry.multilinestring import MultiLineString from shapely.geometry.multipoint import MultiPoint from shapely.geometry.multipolygon import MultiPolygon from shapely.geometry.point import Point from shapely.geometry.polygon import LinearRing, Polygon def _is_coordinates_empty(coordinates): """Helper to identify if coordinates or subset of coordinates are empty""" if coordinates is None: return True if isinstance(coordinates, (list, tuple, np.ndarray)): if len(coordinates) == 0: return True return all(map(_is_coordinates_empty, coordinates)) else: return False def _empty_shape_for_no_coordinates(geom_type): """Return empty counterpart for geom_type""" if geom_type == "point": return Point() elif geom_type == "multipoint": return MultiPoint() elif geom_type == "linestring": return LineString() elif geom_type == "multilinestring": return MultiLineString() elif geom_type == "polygon": return Polygon() elif geom_type == "multipolygon": return MultiPolygon() else: raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}") def box(minx, miny, maxx, maxy, ccw=True): """Returns a rectangular polygon with configurable normal vector""" coords = [(maxx, miny), (maxx, maxy), (minx, maxy), (minx, miny)] if not ccw: coords = coords[::-1] return Polygon(coords) def shape(context): """ Returns a new, independent geometry with coordinates *copied* from the context. Changes to the original context will not be reflected in the geometry object. Parameters ---------- context : a GeoJSON-like dict, which provides a "type" member describing the type of the geometry and "coordinates" member providing a list of coordinates, or an object which implements __geo_interface__. Returns ------- Geometry object Examples -------- Create a Point from GeoJSON, and then create a copy using __geo_interface__. >>> context = {'type': 'Point', 'coordinates': [0, 1]} >>> geom = shape(context) >>> geom.geom_type == 'Point' True >>> geom.wkt 'POINT (0 1)' >>> geom2 = shape(geom) >>> geom == geom2 True """ if hasattr(context, "__geo_interface__"): ob = context.__geo_interface__ else: ob = context geom_type = ob.get("type").lower() if "coordinates" in ob and _is_coordinates_empty(ob["coordinates"]): return _empty_shape_for_no_coordinates(geom_type) elif geom_type == "point": return Point(ob["coordinates"]) elif geom_type == "linestring": return LineString(ob["coordinates"]) elif geom_type == "linearring": return LinearRing(ob["coordinates"]) elif geom_type == "polygon": return Polygon(ob["coordinates"][0], ob["coordinates"][1:]) elif geom_type == "multipoint": return MultiPoint(ob["coordinates"]) elif geom_type == "multilinestring": return MultiLineString(ob["coordinates"]) elif geom_type == "multipolygon": return MultiPolygon([[c[0], c[1:]] for c in ob["coordinates"]]) elif geom_type == "geometrycollection": geoms = [shape(g) for g in ob.get("geometries", [])] return GeometryCollection(geoms) else: raise GeometryTypeError(f"Unknown geometry type: {geom_type!r}") def mapping(ob): """ Returns a GeoJSON-like mapping from a Geometry or any object which implements __geo_interface__ Parameters ---------- ob : An object which implements __geo_interface__. Returns ------- dict Examples -------- >>> pt = Point(0, 0) >>> mapping(pt) {'type': 'Point', 'coordinates': (0.0, 0.0)} """ return ob.__geo_interface__ shapely-2.0.3/shapely/geometry/linestring.py000066400000000000000000000152021456366510000212170ustar00rootroot00000000000000"""Line strings and related utilities """ import numpy as np import shapely from shapely.geometry.base import BaseGeometry, JOIN_STYLE from shapely.geometry.point import Point __all__ = ["LineString"] class LineString(BaseGeometry): """ A geometry type composed of one or more line segments. A LineString is a one-dimensional feature and has a non-zero length but zero area. It may approximate a curve and need not be straight. Unlike a LinearRing, a LineString is not closed. Parameters ---------- coordinates : sequence A sequence of (x, y, [,z]) numeric coordinate pairs or triples, or an array-like with shape (N, 2) or (N, 3). Also can be a sequence of Point objects. Examples -------- Create a LineString with two segments >>> a = LineString([[0, 0], [1, 0], [1, 1]]) >>> a.length 2.0 """ __slots__ = [] def __new__(self, coordinates=None): if coordinates is None: # empty geometry # TODO better constructor return shapely.from_wkt("LINESTRING EMPTY") elif isinstance(coordinates, LineString): if type(coordinates) == LineString: # return original objects since geometries are immutable return coordinates else: # LinearRing # TODO convert LinearRing to LineString more directly coordinates = coordinates.coords else: if hasattr(coordinates, "__array__"): coordinates = np.asarray(coordinates) if isinstance(coordinates, np.ndarray) and np.issubdtype( coordinates.dtype, np.number ): pass else: # check coordinates on points def _coords(o): if isinstance(o, Point): return o.coords[0] else: return [float(c) for c in o] coordinates = [_coords(o) for o in coordinates] if len(coordinates) == 0: # empty geometry # TODO better constructor + should shapely.linestrings handle this? return shapely.from_wkt("LINESTRING EMPTY") geom = shapely.linestrings(coordinates) if not isinstance(geom, LineString): raise ValueError("Invalid values passed to LineString constructor") return geom @property def __geo_interface__(self): return {"type": "LineString", "coordinates": tuple(self.coords)} def svg(self, scale_factor=1.0, stroke_color=None, opacity=None): """Returns SVG polyline element for the LineString geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG stroke-width. Default is 1. stroke_color : str, optional Hex string for stroke color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.8 """ if self.is_empty: return "" if stroke_color is None: stroke_color = "#66cc99" if self.is_valid else "#ff3333" if opacity is None: opacity = 0.8 pnt_format = " ".join(["{},{}".format(*c) for c in self.coords]) return ( '' ).format(pnt_format, 2.0 * scale_factor, stroke_color, opacity) @property def xy(self): """Separate arrays of X and Y coordinate values Example: >>> x, y = LineString([(0, 0), (1, 1)]).xy >>> list(x) [0.0, 1.0] >>> list(y) [0.0, 1.0] """ return self.coords.xy def offset_curve( self, distance, quad_segs=16, join_style=JOIN_STYLE.round, mitre_limit=5.0, ): """Returns a LineString or MultiLineString geometry at a distance from the object on its right or its left side. The side is determined by the sign of the `distance` parameter (negative for right side offset, positive for left side offset). The resolution of the buffer around each vertex of the object increases by increasing the `quad_segs` keyword parameter. The join style is for outside corners between line segments. Accepted values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and JOIN_STYLE.bevel (3). The mitre ratio limit is used for very sharp corners. It is the ratio of the distance from the corner to the end of the mitred offset corner. When two line segments meet at a sharp angle, a miter join will extend far beyond the original geometry. To prevent unreasonable geometry, the mitre limit allows controlling the maximum length of the join corner. Corners with a ratio which exceed the limit will be beveled. Note: the behaviour regarding orientation of the resulting line depends on the GEOS version. With GEOS < 3.11, the line retains the same direction for a left offset (positive distance) or has reverse direction for a right offset (negative distance), and this behaviour was documented as such in previous Shapely versions. Starting with GEOS 3.11, the function tries to preserve the orientation of the original line. """ if mitre_limit == 0.0: raise ValueError("Cannot compute offset from zero-length line segment") elif not np.isfinite(distance): raise ValueError("offset_curve distance must be finite") return shapely.offset_curve(self, distance, quad_segs, join_style, mitre_limit) def parallel_offset( self, distance, side="right", resolution=16, join_style=JOIN_STYLE.round, mitre_limit=5.0, ): """ Alternative method to :meth:`offset_curve` method. Older alternative method to the :meth:`offset_curve` method, but uses ``resolution`` instead of ``quad_segs`` and a ``side`` keyword ('left' or 'right') instead of sign of the distance. This method is kept for backwards compatibility for now, but is is recommended to use :meth:`offset_curve` instead. """ if side == "right": distance *= -1 return self.offset_curve( distance, quad_segs=resolution, join_style=join_style, mitre_limit=mitre_limit, ) shapely.lib.registry[1] = LineString shapely-2.0.3/shapely/geometry/multilinestring.py000066400000000000000000000053541456366510000223010ustar00rootroot00000000000000"""Collections of linestrings and related utilities """ import shapely from shapely.errors import EmptyPartError from shapely.geometry import linestring from shapely.geometry.base import BaseMultipartGeometry __all__ = ["MultiLineString"] class MultiLineString(BaseMultipartGeometry): """ A collection of one or more LineStrings. A MultiLineString has non-zero length and zero area. Parameters ---------- lines : sequence A sequence LineStrings, or a sequence of line-like coordinate sequences or array-likes (see accepted input for LineString). Attributes ---------- geoms : sequence A sequence of LineStrings Examples -------- Construct a MultiLineString containing two LineStrings. >>> lines = MultiLineString([[[0, 0], [1, 2]], [[4, 4], [5, 6]]]) """ __slots__ = [] def __new__(self, lines=None): if not lines: # allow creation of empty multilinestrings, to support unpickling # TODO better empty constructor return shapely.from_wkt("MULTILINESTRING EMPTY") elif isinstance(lines, MultiLineString): return lines lines = getattr(lines, "geoms", lines) m = len(lines) subs = [] for i in range(m): line = linestring.LineString(lines[i]) if line.is_empty: raise EmptyPartError( "Can't create MultiLineString with empty component" ) subs.append(line) if len(lines) == 0: return shapely.from_wkt("MULTILINESTRING EMPTY") return shapely.multilinestrings(subs) @property def __geo_interface__(self): return { "type": "MultiLineString", "coordinates": tuple(tuple(c for c in g.coords) for g in self.geoms), } def svg(self, scale_factor=1.0, stroke_color=None, opacity=None): """Returns a group of SVG polyline elements for the LineString geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG stroke-width. Default is 1. stroke_color : str, optional Hex string for stroke color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.8 """ if self.is_empty: return "" if stroke_color is None: stroke_color = "#66cc99" if self.is_valid else "#ff3333" return ( "" + "".join(p.svg(scale_factor, stroke_color, opacity) for p in self.geoms) + "" ) shapely.lib.registry[5] = MultiLineString shapely-2.0.3/shapely/geometry/multipoint.py000066400000000000000000000051731456366510000212530ustar00rootroot00000000000000"""Collections of points and related utilities """ import shapely from shapely.errors import EmptyPartError from shapely.geometry import point from shapely.geometry.base import BaseMultipartGeometry __all__ = ["MultiPoint"] class MultiPoint(BaseMultipartGeometry): """ A collection of one or more Points. A MultiPoint has zero area and zero length. Parameters ---------- points : sequence A sequence of Points, or a sequence of (x, y [,z]) numeric coordinate pairs or triples, or an array-like of shape (N, 2) or (N, 3). Attributes ---------- geoms : sequence A sequence of Points Examples -------- Construct a MultiPoint containing two Points >>> from shapely import Point >>> ob = MultiPoint([[0.0, 0.0], [1.0, 2.0]]) >>> len(ob.geoms) 2 >>> type(ob.geoms[0]) == Point True """ __slots__ = [] def __new__(self, points=None): if points is None: # allow creation of empty multipoints, to support unpickling # TODO better empty constructor return shapely.from_wkt("MULTIPOINT EMPTY") elif isinstance(points, MultiPoint): return points m = len(points) subs = [] for i in range(m): p = point.Point(points[i]) if p.is_empty: raise EmptyPartError("Can't create MultiPoint with empty component") subs.append(p) if len(points) == 0: return shapely.from_wkt("MULTIPOINT EMPTY") return shapely.multipoints(subs) @property def __geo_interface__(self): return { "type": "MultiPoint", "coordinates": tuple(g.coords[0] for g in self.geoms), } def svg(self, scale_factor=1.0, fill_color=None, opacity=None): """Returns a group of SVG circle elements for the MultiPoint geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG circle diameters. Default is 1. fill_color : str, optional Hex string for fill color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.6 """ if self.is_empty: return "" if fill_color is None: fill_color = "#66cc99" if self.is_valid else "#ff3333" return ( "" + "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms) + "" ) shapely.lib.registry[4] = MultiPoint shapely-2.0.3/shapely/geometry/multipolygon.py000066400000000000000000000074031456366510000216070ustar00rootroot00000000000000"""Collections of polygons and related utilities """ import shapely from shapely.geometry import polygon from shapely.geometry.base import BaseMultipartGeometry __all__ = ["MultiPolygon"] class MultiPolygon(BaseMultipartGeometry): """ A collection of one or more Polygons. If component polygons overlap the collection is invalid and some operations on it may fail. Parameters ---------- polygons : sequence A sequence of Polygons, or a sequence of (shell, holes) tuples where shell is the sequence representation of a linear ring (see LinearRing) and holes is a sequence of such linear rings. Attributes ---------- geoms : sequence A sequence of `Polygon` instances Examples -------- Construct a MultiPolygon from a sequence of coordinate tuples >>> from shapely import Polygon >>> ob = MultiPolygon([ ... ( ... ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), ... [((0.1,0.1), (0.1,0.2), (0.2,0.2), (0.2,0.1))] ... ) ... ]) >>> len(ob.geoms) 1 >>> type(ob.geoms[0]) == Polygon True """ __slots__ = [] def __new__(self, polygons=None): if not polygons: # allow creation of empty multipolygons, to support unpickling # TODO better empty constructor return shapely.from_wkt("MULTIPOLYGON EMPTY") elif isinstance(polygons, MultiPolygon): return polygons polygons = getattr(polygons, "geoms", polygons) polygons = [ p for p in polygons if p and not (isinstance(p, polygon.Polygon) and p.is_empty) ] L = len(polygons) # Bail immediately if we have no input points. if L == 0: return shapely.from_wkt("MULTIPOLYGON EMPTY") # This function does not accept sequences of MultiPolygons: there is # no implicit flattening. if isinstance(polygons[0], MultiPolygon): raise ValueError("Sequences of multi-polygons are not valid arguments") subs = [] for i in range(L): ob = polygons[i] if not isinstance(ob, polygon.Polygon): shell = ob[0] if len(ob) > 1: holes = ob[1] else: holes = None p = polygon.Polygon(shell, holes) else: p = polygon.Polygon(ob) subs.append(p) return shapely.multipolygons(subs) @property def __geo_interface__(self): allcoords = [] for geom in self.geoms: coords = [] coords.append(tuple(geom.exterior.coords)) for hole in geom.interiors: coords.append(tuple(hole.coords)) allcoords.append(tuple(coords)) return {"type": "MultiPolygon", "coordinates": allcoords} def svg(self, scale_factor=1.0, fill_color=None, opacity=None): """Returns group of SVG path elements for the MultiPolygon geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG stroke-width. Default is 1. fill_color : str, optional Hex string for fill color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.6 """ if self.is_empty: return "" if fill_color is None: fill_color = "#66cc99" if self.is_valid else "#ff3333" return ( "" + "".join(p.svg(scale_factor, fill_color, opacity) for p in self.geoms) + "" ) shapely.lib.registry[6] = MultiPolygon shapely-2.0.3/shapely/geometry/point.py000066400000000000000000000101271456366510000201730ustar00rootroot00000000000000"""Points and related utilities """ import numpy as np import shapely from shapely.errors import DimensionError from shapely.geometry.base import BaseGeometry __all__ = ["Point"] class Point(BaseGeometry): """ A geometry type that represents a single coordinate with x,y and possibly z values. A point is a zero-dimensional feature and has zero length and zero area. Parameters ---------- args : float, or sequence of floats The coordinates can either be passed as a single parameter, or as individual float values using multiple parameters: 1) 1 parameter: a sequence or array-like of with 2 or 3 values. 2) 2 or 3 parameters (float): x, y, and possibly z. Attributes ---------- x, y, z : float Coordinate values Examples -------- Constructing the Point using separate parameters for x and y: >>> p = Point(1.0, -1.0) Constructing the Point using a list of x, y coordinates: >>> p = Point([1.0, -1.0]) >>> print(p) POINT (1 -1) >>> p.y -1.0 >>> p.x 1.0 """ __slots__ = [] def __new__(self, *args): if len(args) == 0: # empty geometry # TODO better constructor return shapely.from_wkt("POINT EMPTY") elif len(args) > 3: raise TypeError(f"Point() takes at most 3 arguments ({len(args)} given)") elif len(args) == 1: coords = args[0] if isinstance(coords, Point): return coords # Accept either (x, y) or [(x, y)] if not hasattr(coords, "__getitem__"): # generators coords = list(coords) coords = np.asarray(coords).squeeze() else: # 2 or 3 args coords = np.array(args).squeeze() if coords.ndim > 1: raise ValueError( f"Point() takes only scalar or 1-size vector arguments, got {args}" ) if not np.issubdtype(coords.dtype, np.number): coords = [float(c) for c in coords] geom = shapely.points(coords) if not isinstance(geom, Point): raise ValueError("Invalid values passed to Point constructor") return geom # Coordinate getters and setters @property def x(self): """Return x coordinate.""" return shapely.get_x(self) @property def y(self): """Return y coordinate.""" return shapely.get_y(self) @property def z(self): """Return z coordinate.""" if not shapely.has_z(self): raise DimensionError("This point has no z coordinate.") # return shapely.get_z(self) -> get_z only supported for GEOS 3.7+ return self.coords[0][2] @property def __geo_interface__(self): return {"type": "Point", "coordinates": self.coords[0]} def svg(self, scale_factor=1.0, fill_color=None, opacity=None): """Returns SVG circle element for the Point geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG circle diameter. Default is 1. fill_color : str, optional Hex string for fill color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.6 """ if self.is_empty: return "" if fill_color is None: fill_color = "#66cc99" if self.is_valid else "#ff3333" if opacity is None: opacity = 0.6 return ( '' ).format(self, 3.0 * scale_factor, 1.0 * scale_factor, fill_color, opacity) @property def xy(self): """Separate arrays of X and Y coordinate values Example: >>> x, y = Point(0, 0).xy >>> list(x) [0.0] >>> list(y) [0.0] """ return self.coords.xy shapely.lib.registry[0] = Point shapely-2.0.3/shapely/geometry/polygon.py000066400000000000000000000261461456366510000205410ustar00rootroot00000000000000"""Polygons and their linear ring components """ import numpy as np import shapely from shapely.algorithms.cga import is_ccw_impl, signed_area from shapely.errors import TopologicalError from shapely.geometry.base import BaseGeometry from shapely.geometry.linestring import LineString from shapely.geometry.point import Point __all__ = ["Polygon", "LinearRing"] def _unpickle_linearring(wkb): linestring = shapely.from_wkb(wkb) srid = shapely.get_srid(linestring) linearring = shapely.linearrings(shapely.get_coordinates(linestring)) if srid: linearring = shapely.set_srid(linearring, srid) return linearring class LinearRing(LineString): """ A geometry type composed of one or more line segments that forms a closed loop. A LinearRing is a closed, one-dimensional feature. A LinearRing that crosses itself or touches itself at a single point is invalid and operations on it may fail. Parameters ---------- coordinates : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples, or an array-like with shape (N, 2) or (N, 3). Also can be a sequence of Point objects. Notes ----- Rings are automatically closed. There is no need to specify a final coordinate pair identical to the first. Examples -------- Construct a square ring. >>> ring = LinearRing( ((0, 0), (0, 1), (1 ,1 ), (1 , 0)) ) >>> ring.is_closed True >>> list(ring.coords) [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] >>> ring.length 4.0 """ __slots__ = [] def __new__(self, coordinates=None): if coordinates is None: # empty geometry # TODO better way? return shapely.from_wkt("LINEARRING EMPTY") elif isinstance(coordinates, LineString): if type(coordinates) == LinearRing: # return original objects since geometries are immutable return coordinates elif not coordinates.is_valid: raise TopologicalError("An input LineString must be valid.") else: # LineString # TODO convert LineString to LinearRing more directly? coordinates = coordinates.coords else: if hasattr(coordinates, "__array__"): coordinates = np.asarray(coordinates) if isinstance(coordinates, np.ndarray) and np.issubdtype( coordinates.dtype, np.number ): pass else: # check coordinates on points def _coords(o): if isinstance(o, Point): return o.coords[0] else: return [float(c) for c in o] coordinates = np.array([_coords(o) for o in coordinates]) if not np.issubdtype(coordinates.dtype, np.number): # conversion of coords to 2D array failed, this might be due # to inconsistent coordinate dimensionality raise ValueError("Inconsistent coordinate dimensionality") if len(coordinates) == 0: # empty geometry # TODO better constructor + should shapely.linearrings handle this? return shapely.from_wkt("LINEARRING EMPTY") geom = shapely.linearrings(coordinates) if not isinstance(geom, LinearRing): raise ValueError("Invalid values passed to LinearRing constructor") return geom @property def __geo_interface__(self): return {"type": "LinearRing", "coordinates": tuple(self.coords)} def __reduce__(self): """WKB doesn't differentiate between LineString and LinearRing so we need to move the coordinate sequence into the correct geometry type""" return (_unpickle_linearring, (shapely.to_wkb(self, include_srid=True),)) @property def is_ccw(self): """True is the ring is oriented counter clock-wise""" return bool(is_ccw_impl()(self)) @property def is_simple(self): """True if the geometry is simple, meaning that any self-intersections are only at boundary points, else False""" return bool(shapely.is_simple(self)) shapely.lib.registry[2] = LinearRing class InteriorRingSequence: _parent = None _ndim = None _index = 0 _length = 0 def __init__(self, parent): self._parent = parent self._ndim = parent._ndim def __iter__(self): self._index = 0 self._length = self.__len__() return self def __next__(self): if self._index < self._length: ring = self._get_ring(self._index) self._index += 1 return ring else: raise StopIteration def __len__(self): return shapely.get_num_interior_rings(self._parent) def __getitem__(self, key): m = self.__len__() if isinstance(key, int): if key + m < 0 or key >= m: raise IndexError("index out of range") if key < 0: i = m + key else: i = key return self._get_ring(i) elif isinstance(key, slice): res = [] start, stop, stride = key.indices(m) for i in range(start, stop, stride): res.append(self._get_ring(i)) return res else: raise TypeError("key must be an index or slice") def _get_ring(self, i): return shapely.get_interior_ring(self._parent, i) class Polygon(BaseGeometry): """ A geometry type representing an area that is enclosed by a linear ring. A polygon is a two-dimensional feature and has a non-zero area. It may have one or more negative-space "holes" which are also bounded by linear rings. If any rings cross each other, the feature is invalid and operations on it may fail. Parameters ---------- shell : sequence A sequence of (x, y [,z]) numeric coordinate pairs or triples, or an array-like with shape (N, 2) or (N, 3). Also can be a sequence of Point objects. holes : sequence A sequence of objects which satisfy the same requirements as the shell parameters above Attributes ---------- exterior : LinearRing The ring which bounds the positive space of the polygon. interiors : sequence A sequence of rings which bound all existing holes. Examples -------- Create a square polygon with no holes >>> coords = ((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)) >>> polygon = Polygon(coords) >>> polygon.area 1.0 """ __slots__ = [] def __new__(self, shell=None, holes=None): if shell is None: # empty geometry # TODO better way? return shapely.from_wkt("POLYGON EMPTY") elif isinstance(shell, Polygon): # return original objects since geometries are immutable return shell else: shell = LinearRing(shell) if holes is not None: if len(holes) == 0: # shapely constructor cannot handle holes=[] holes = None else: holes = [LinearRing(ring) for ring in holes] geom = shapely.polygons(shell, holes=holes) if not isinstance(geom, Polygon): raise ValueError("Invalid values passed to Polygon constructor") return geom @property def exterior(self): return shapely.get_exterior_ring(self) @property def interiors(self): if self.is_empty: return [] return InteriorRingSequence(self) @property def coords(self): raise NotImplementedError( "Component rings have coordinate sequences, but the polygon does not" ) def __eq__(self, other): if not isinstance(other, BaseGeometry): return NotImplemented if not isinstance(other, Polygon): return False check_empty = (self.is_empty, other.is_empty) if all(check_empty): return True elif any(check_empty): return False my_coords = [self.exterior.coords] + [ interior.coords for interior in self.interiors ] other_coords = [other.exterior.coords] + [ interior.coords for interior in other.interiors ] if not len(my_coords) == len(other_coords): return False # equal_nan=False is the default, but not yet available for older numpy return np.all( [ np.array_equal(left, right) # , equal_nan=False) for left, right in zip(my_coords, other_coords) ] ) def __hash__(self): return super().__hash__() @property def __geo_interface__(self): if self.exterior == LinearRing(): coords = [] else: coords = [tuple(self.exterior.coords)] for hole in self.interiors: coords.append(tuple(hole.coords)) return {"type": "Polygon", "coordinates": tuple(coords)} def svg(self, scale_factor=1.0, fill_color=None, opacity=None): """Returns SVG path element for the Polygon geometry. Parameters ========== scale_factor : float Multiplication factor for the SVG stroke-width. Default is 1. fill_color : str, optional Hex string for fill color. Default is to use "#66cc99" if geometry is valid, and "#ff3333" if invalid. opacity : float Float number between 0 and 1 for color opacity. Default value is 0.6 """ if self.is_empty: return "" if fill_color is None: fill_color = "#66cc99" if self.is_valid else "#ff3333" if opacity is None: opacity = 0.6 exterior_coords = [["{},{}".format(*c) for c in self.exterior.coords]] interior_coords = [ ["{},{}".format(*c) for c in interior.coords] for interior in self.interiors ] path = " ".join( [ "M {} L {} z".format(coords[0], " L ".join(coords[1:])) for coords in exterior_coords + interior_coords ] ) return ( '' ).format(2.0 * scale_factor, path, fill_color, opacity) @classmethod def from_bounds(cls, xmin, ymin, xmax, ymax): """Construct a `Polygon()` from spatial bounds.""" return cls([(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]) shapely.lib.registry[3] = Polygon def orient(polygon, sign=1.0): s = float(sign) rings = [] ring = polygon.exterior if signed_area(ring) / s >= 0.0: rings.append(ring) else: rings.append(list(ring.coords)[::-1]) for ring in polygon.interiors: if signed_area(ring) / s <= 0.0: rings.append(ring) else: rings.append(list(ring.coords)[::-1]) return Polygon(rings[0], rings[1:]) shapely-2.0.3/shapely/geos.py000066400000000000000000000003361456366510000161450ustar00rootroot00000000000000""" Proxies for libgeos, GEOS-specific exceptions, and utilities """ import shapely geos_version_string = shapely.geos_capi_version_string geos_version = shapely.geos_version geos_capi_version = shapely.geos_capi_version shapely-2.0.3/shapely/io.py000066400000000000000000000320401456366510000156140ustar00rootroot00000000000000import numpy as np from shapely import lib from shapely._enum import ParamEnum # include ragged array functions here for reference documentation purpose from shapely._ragged_array import from_ragged_array, to_ragged_array from shapely.decorators import requires_geos from shapely.errors import UnsupportedGEOSVersionError __all__ = [ "from_geojson", "from_ragged_array", "from_wkb", "from_wkt", "to_geojson", "to_ragged_array", "to_wkb", "to_wkt", ] # Allowed options for handling WKB/WKT decoding errors # Note: cannot use standard constructor since "raise" is a keyword DecodingErrorOptions = ParamEnum( "DecodingErrorOptions", {"ignore": 0, "warn": 1, "raise": 2} ) WKBFlavorOptions = ParamEnum("WKBFlavorOptions", {"extended": 1, "iso": 2}) def to_wkt( geometry, rounding_precision=6, trim=True, output_dimension=3, old_3d=False, **kwargs, ): """ Converts to the Well-Known Text (WKT) representation of a Geometry. The Well-known Text format is defined in the `OGC Simple Features Specification for SQL `__. The following limitations apply to WKT serialization: - for GEOS <= 3.8 a multipoint with an empty sub-geometry will raise an exception - for GEOS <= 3.8 empty geometries are always serialized to 2D - for GEOS >= 3.9 only simple empty geometries can be 3D, collections are still always 2D Parameters ---------- geometry : Geometry or array_like rounding_precision : int, default 6 The rounding precision when writing the WKT string. Set to a value of -1 to indicate the full precision. trim : bool, default True If True, trim unnecessary decimals (trailing zeros). output_dimension : int, default 3 The output dimension for the WKT string. Supported values are 2 and 3. Specifying 3 means that up to 3 dimensions will be written but 2D geometries will still be represented as 2D in the WKT string. old_3d : bool, default False Enable old style 3D/4D WKT generation. By default, new style 3D/4D WKT (ie. "POINT Z (10 20 30)") is returned, but with ``old_3d=True`` the WKT will be formatted in the style "POINT (10 20 30)". **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Point >>> to_wkt(Point(0, 0)) 'POINT (0 0)' >>> to_wkt(Point(0, 0), rounding_precision=3, trim=False) 'POINT (0.000 0.000)' >>> to_wkt(Point(0, 0), rounding_precision=-1, trim=False) 'POINT (0.0000000000000000 0.0000000000000000)' >>> to_wkt(Point(1, 2, 3), trim=True) 'POINT Z (1 2 3)' >>> to_wkt(Point(1, 2, 3), trim=True, output_dimension=2) 'POINT (1 2)' >>> to_wkt(Point(1, 2, 3), trim=True, old_3d=True) 'POINT (1 2 3)' Notes ----- The defaults differ from the default of the GEOS library. To mimic this, use:: to_wkt(geometry, rounding_precision=-1, trim=False, output_dimension=2) """ if not np.isscalar(rounding_precision): raise TypeError("rounding_precision only accepts scalar values") if not np.isscalar(trim): raise TypeError("trim only accepts scalar values") if not np.isscalar(output_dimension): raise TypeError("output_dimension only accepts scalar values") if not np.isscalar(old_3d): raise TypeError("old_3d only accepts scalar values") return lib.to_wkt( geometry, np.intc(rounding_precision), np.bool_(trim), np.intc(output_dimension), np.bool_(old_3d), **kwargs, ) def to_wkb( geometry, hex=False, output_dimension=3, byte_order=-1, include_srid=False, flavor="extended", **kwargs, ): r""" Converts to the Well-Known Binary (WKB) representation of a Geometry. The Well-Known Binary format is defined in the `OGC Simple Features Specification for SQL `__. The following limitations apply to WKB serialization: - linearrings will be converted to linestrings - a point with only NaN coordinates is converted to an empty point - for GEOS <= 3.7, empty points are always serialized to 3D if output_dimension=3, and to 2D if output_dimension=2 - for GEOS == 3.8, empty points are always serialized to 2D Parameters ---------- geometry : Geometry or array_like hex : bool, default False If true, export the WKB as a hexidecimal string. The default is to return a binary bytes object. output_dimension : int, default 3 The output dimension for the WKB. Supported values are 2 and 3. Specifying 3 means that up to 3 dimensions will be written but 2D geometries will still be represented as 2D in the WKB represenation. byte_order : int, default -1 Defaults to native machine byte order (-1). Use 0 to force big endian and 1 for little endian. include_srid : bool, default False If True, the SRID is be included in WKB (this is an extension to the OGC WKB specification). Not allowed when flavor is "iso". flavor : {"iso", "extended"}, default "extended" Which flavor of WKB will be returned. The flavor determines how extra dimensionality is encoded with the type number, and whether SRID can be included in the WKB. ISO flavor is "more standard" for 3D output, and does not support SRID embedding. Both flavors are equivalent when ``output_dimension=2`` (or with 2D geometries) and ``include_srid=False``. The `from_wkb` function can read both flavors. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Point >>> point = Point(1, 1) >>> to_wkb(point, byte_order=1) b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?' >>> to_wkb(point, hex=True, byte_order=1) '0101000000000000000000F03F000000000000F03F' """ if not np.isscalar(hex): raise TypeError("hex only accepts scalar values") if not np.isscalar(output_dimension): raise TypeError("output_dimension only accepts scalar values") if not np.isscalar(byte_order): raise TypeError("byte_order only accepts scalar values") if not np.isscalar(include_srid): raise TypeError("include_srid only accepts scalar values") if not np.isscalar(flavor): raise TypeError("flavor only accepts scalar values") if lib.geos_version < (3, 10, 0) and flavor == "iso": raise UnsupportedGEOSVersionError( 'The "iso" option requires at least GEOS 3.10.0' ) if flavor == "iso" and include_srid: raise ValueError('flavor="iso" and include_srid=True cannot be used together') flavor = WKBFlavorOptions.get_value(flavor) return lib.to_wkb( geometry, np.bool_(hex), np.intc(output_dimension), np.intc(byte_order), np.bool_(include_srid), np.intc(flavor), **kwargs, ) @requires_geos("3.10.0") def to_geojson(geometry, indent=None, **kwargs): """Converts to the GeoJSON representation of a Geometry. The GeoJSON format is defined in the `RFC 7946 `__. NaN (not-a-number) coordinates will be written as 'null'. The following are currently unsupported: - Geometries of type LINEARRING: these are output as 'null'. - Three-dimensional geometries: the third dimension is ignored. Parameters ---------- geometry : str, bytes or array_like indent : int, optional If indent is a non-negative integer, then GeoJSON will be formatted. An indent level of 0 will only insert newlines. None (the default) selects the most compact representation. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Point >>> point = Point(1, 1) >>> to_geojson(point) '{"type":"Point","coordinates":[1.0,1.0]}' >>> print(to_geojson(point, indent=2)) { "type": "Point", "coordinates": [ 1.0, 1.0 ] } """ # GEOS Tickets: # - handle linearrings: https://trac.osgeo.org/geos/ticket/1140 # - support 3D: https://trac.osgeo.org/geos/ticket/1141 if indent is None: indent = -1 elif not np.isscalar(indent): raise TypeError("indent only accepts scalar values") elif indent < 0: raise ValueError("indent cannot be negative") return lib.to_geojson(geometry, np.intc(indent), **kwargs) def from_wkt(geometry, on_invalid="raise", **kwargs): """ Creates geometries from the Well-Known Text (WKT) representation. The Well-known Text format is defined in the `OGC Simple Features Specification for SQL `__. Parameters ---------- geometry : str or array_like The WKT string(s) to convert. on_invalid : {"raise", "warn", "ignore"}, default "raise" - raise: an exception will be raised if WKT input geometries are invalid. - warn: a warning will be raised and invalid WKT geometries will be returned as ``None``. - ignore: invalid WKT geometries will be returned as ``None`` without a warning. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from_wkt('POINT (0 0)') """ if not np.isscalar(on_invalid): raise TypeError("on_invalid only accepts scalar values") invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid)) return lib.from_wkt(geometry, invalid_handler, **kwargs) def from_wkb(geometry, on_invalid="raise", **kwargs): r""" Creates geometries from the Well-Known Binary (WKB) representation. The Well-Known Binary format is defined in the `OGC Simple Features Specification for SQL `__. Parameters ---------- geometry : str or array_like The WKB byte object(s) to convert. on_invalid : {"raise", "warn", "ignore"}, default "raise" - raise: an exception will be raised if a WKB input geometry is invalid. - warn: a warning will be raised and invalid WKB geometries will be returned as ``None``. - ignore: invalid WKB geometries will be returned as ``None`` without a warning. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from_wkb(b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?') """ if not np.isscalar(on_invalid): raise TypeError("on_invalid only accepts scalar values") invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid)) # ensure the input has object dtype, to avoid numpy inferring it as a # fixed-length string dtype (which removes trailing null bytes upon access # of array elements) geometry = np.asarray(geometry, dtype=object) return lib.from_wkb(geometry, invalid_handler, **kwargs) @requires_geos("3.10.1") def from_geojson(geometry, on_invalid="raise", **kwargs): """Creates geometries from GeoJSON representations (strings). If a GeoJSON is a FeatureCollection, it is read as a single geometry (with type GEOMETRYCOLLECTION). This may be unpacked using the ``pygeos.get_parts``. Properties are not read. The GeoJSON format is defined in `RFC 7946 `__. The following are currently unsupported: - Three-dimensional geometries: the third dimension is ignored. - Geometries having 'null' in the coordinates. Parameters ---------- geometry : str, bytes or array_like The GeoJSON string or byte object(s) to convert. on_invalid : {"raise", "warn", "ignore"}, default "raise" - raise: an exception will be raised if an input GeoJSON is invalid. - warn: a warning will be raised and invalid input geometries will be returned as ``None``. - ignore: invalid input geometries will be returned as ``None`` without a warning. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_parts Examples -------- >>> from_geojson('{"type": "Point","coordinates": [1, 2]}') """ # GEOS Tickets: # - support 3D: https://trac.osgeo.org/geos/ticket/1141 # - handle null coordinates: https://trac.osgeo.org/geos/ticket/1142 if not np.isscalar(on_invalid): raise TypeError("on_invalid only accepts scalar values") invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid)) # ensure the input has object dtype, to avoid numpy inferring it as a # fixed-length string dtype (which removes trailing null bytes upon access # of array elements) geometry = np.asarray(geometry, dtype=object) return lib.from_geojson(geometry, invalid_handler, **kwargs) shapely-2.0.3/shapely/linear.py000066400000000000000000000162011456366510000164600ustar00rootroot00000000000000from shapely import lib from shapely.decorators import multithreading_enabled from shapely.errors import UnsupportedGEOSVersionError __all__ = [ "line_interpolate_point", "line_locate_point", "line_merge", "shared_paths", "shortest_line", ] @multithreading_enabled def line_interpolate_point(line, distance, normalized=False, **kwargs): """Returns a point interpolated at given distance on a line. Parameters ---------- line : Geometry or array_like For multilinestrings or geometrycollections, the first geometry is taken and the rest is ignored. This function raises a TypeError for non-linear geometries. For empty linear geometries, empty points are returned. distance : float or array_like Negative values measure distance from the end of the line. Out-of-range values will be clipped to the line endings. normalized : bool, default False If True, the distance is a fraction of the total line length instead of the absolute distance. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point >>> line = LineString([(0, 2), (0, 10)]) >>> line_interpolate_point(line, 2) >>> line_interpolate_point(line, 100) >>> line_interpolate_point(line, -2) >>> line_interpolate_point(line, [0.25, -0.25], normalized=True).tolist() [, ] >>> line_interpolate_point(LineString(), 1) """ if normalized: return lib.line_interpolate_point_normalized(line, distance) else: return lib.line_interpolate_point(line, distance) @multithreading_enabled def line_locate_point(line, other, normalized=False, **kwargs): """Returns the distance to the line origin of given point. If given point does not intersect with the line, the point will first be projected onto the line after which the distance is taken. Parameters ---------- line : Geometry or array_like point : Geometry or array_like normalized : bool, default False If True, the distance is a fraction of the total line length instead of the absolute distance. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point >>> line = LineString([(0, 2), (0, 10)]) >>> point = Point(4, 4) >>> line_locate_point(line, point) 2.0 >>> line_locate_point(line, point, normalized=True) 0.25 >>> line_locate_point(line, Point(0, 18)) 8.0 >>> line_locate_point(LineString(), point) nan """ if normalized: return lib.line_locate_point_normalized(line, other) else: return lib.line_locate_point(line, other) @multithreading_enabled def line_merge(line, directed=False, **kwargs): """Returns (Multi)LineStrings formed by combining the lines in a MultiLineString. Lines are joined together at their endpoints in case two lines are intersecting. Lines are not joined when 3 or more lines are intersecting at the endpoints. Line elements that cannot be joined are kept as is in the resulting MultiLineString. The direction of each merged LineString will be that of the majority of the LineStrings from which it was derived. Except if ``directed=True`` is specified, then the operation will not change the order of points within lines and so only lines which can be joined with no change in direction are merged. Parameters ---------- line : Geometry or array_like directed : bool, default False Only combine lines if possible without changing point order. Requires GEOS >= 3.11.0 **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import MultiLineString >>> line_merge(MultiLineString([[(0, 2), (0, 10)], [(0, 10), (5, 10)]])) >>> line_merge(MultiLineString([[(0, 2), (0, 10)], [(0, 11), (5, 10)]])) >>> line_merge(MultiLineString()) >>> line_merge(MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]])) >>> line_merge(MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]]), directed=True) """ if directed: if lib.geos_version < (3, 11, 0): raise UnsupportedGEOSVersionError( "'{}' requires at least GEOS {}.{}.{}.".format( "line_merge", *(3, 11, 0) ) ) return lib.line_merge_directed(line, **kwargs) return lib.line_merge(line, **kwargs) @multithreading_enabled def shared_paths(a, b, **kwargs): """Returns the shared paths between geom1 and geom2. Both geometries should be linestrings or arrays of linestrings. A geometrycollection or array of geometrycollections is returned with two elements in each geometrycollection. The first element is a multilinestring containing shared paths with the same direction for both inputs. The second element is a multilinestring containing shared paths with the opposite direction for the two inputs. Parameters ---------- a : Geometry or array_like b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString >>> line1 = LineString([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> line2 = LineString([(1, 0), (2, 0), (2, 1), (1, 1), (1, 0)]) >>> shared_paths(line1, line2).wkt 'GEOMETRYCOLLECTION (MULTILINESTRING EMPTY, MULTILINESTRING ((1 0, 1 1)))' >>> line3 = LineString([(1, 1), (0, 1)]) >>> shared_paths(line1, line3).wkt 'GEOMETRYCOLLECTION (MULTILINESTRING ((1 1, 0 1)), MULTILINESTRING EMPTY)' """ return lib.shared_paths(a, b, **kwargs) @multithreading_enabled def shortest_line(a, b, **kwargs): """ Returns the shortest line between two geometries. The resulting line consists of two points, representing the nearest points between the geometry pair. The line always starts in the first geometry `a` and ends in he second geometry `b`. The endpoints of the line will not necessarily be existing vertices of the input geometries `a` and `b`, but can also be a point along a line segment. Parameters ---------- a : Geometry or array_like b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- prepare : improve performance by preparing ``a`` (the first argument) (for GEOS>=3.9) Examples -------- >>> from shapely import LineString >>> line1 = LineString([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> line2 = LineString([(0, 3), (3, 0), (5, 3)]) >>> shortest_line(line1, line2) """ return lib.shortest_line(a, b, **kwargs) shapely-2.0.3/shapely/measurement.py000066400000000000000000000227771456366510000175520ustar00rootroot00000000000000import warnings import numpy as np from shapely import lib from shapely.decorators import multithreading_enabled, requires_geos __all__ = [ "area", "distance", "bounds", "total_bounds", "length", "hausdorff_distance", "frechet_distance", "minimum_clearance", "minimum_bounding_radius", ] @multithreading_enabled def area(geometry, **kwargs): """Computes the area of a (multi)polygon. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import MultiPolygon, Polygon >>> polygon = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)]) >>> area(polygon) 100.0 >>> area(MultiPolygon([polygon, Polygon([(10, 10), (10, 20), (20, 20), (20, 10), (10, 10)])])) 200.0 >>> area(Polygon()) 0.0 >>> area(None) nan """ return lib.area(geometry, **kwargs) @multithreading_enabled def distance(a, b, **kwargs): """Computes the Cartesian distance between two geometries. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point, Polygon >>> point = Point(0, 0) >>> distance(Point(10, 0), point) 10.0 >>> distance(LineString([(1, 1), (1, -1)]), point) 1.0 >>> distance(Polygon([(3, 0), (5, 0), (5, 5), (3, 5), (3, 0)]), point) 3.0 >>> distance(Point(), point) nan >>> distance(None, point) nan """ return lib.distance(a, b, **kwargs) @multithreading_enabled def bounds(geometry, **kwargs): """Computes the bounds (extent) of a geometry. For each geometry these 4 numbers are returned: min x, min y, max x, max y. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point, Polygon >>> bounds(Point(2, 3)).tolist() [2.0, 3.0, 2.0, 3.0] >>> bounds(LineString([(0, 0), (0, 2), (3, 2)])).tolist() [0.0, 0.0, 3.0, 2.0] >>> bounds(Polygon()).tolist() [nan, nan, nan, nan] >>> bounds(None).tolist() [nan, nan, nan, nan] """ # We need to provide the `out` argument here for compatibility with # numpy < 1.16. See https://github.com/numpy/numpy/issues/14949 geometry_arr = np.asarray(geometry, dtype=np.object_) out = np.empty(geometry_arr.shape + (4,), dtype="float64") return lib.bounds(geometry_arr, out=out, **kwargs) def total_bounds(geometry, **kwargs): """Computes the total bounds (extent) of the geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Returns ------- numpy ndarray of [xmin, ymin, xmax, ymax] Examples -------- >>> from shapely import LineString, Point, Polygon >>> total_bounds(Point(2, 3)).tolist() [2.0, 3.0, 2.0, 3.0] >>> total_bounds([Point(2, 3), Point(4, 5)]).tolist() [2.0, 3.0, 4.0, 5.0] >>> total_bounds([ ... LineString([(0, 1), (0, 2), (3, 2)]), ... LineString([(4, 4), (4, 6), (6, 7)]) ... ]).tolist() [0.0, 1.0, 6.0, 7.0] >>> total_bounds(Polygon()).tolist() [nan, nan, nan, nan] >>> total_bounds([Polygon(), Point(2, 3)]).tolist() [2.0, 3.0, 2.0, 3.0] >>> total_bounds(None).tolist() [nan, nan, nan, nan] """ b = bounds(geometry, **kwargs) if b.ndim == 1: return b with warnings.catch_warnings(): # ignore 'All-NaN slice encountered' warnings warnings.simplefilter("ignore", RuntimeWarning) return np.array( [ np.nanmin(b[..., 0]), np.nanmin(b[..., 1]), np.nanmax(b[..., 2]), np.nanmax(b[..., 3]), ] ) @multithreading_enabled def length(geometry, **kwargs): """Computes the length of a (multi)linestring or polygon perimeter. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, MultiLineString, Polygon >>> length(LineString([(0, 0), (0, 2), (3, 2)])) 5.0 >>> length(MultiLineString([ ... LineString([(0, 0), (1, 0)]), ... LineString([(1, 0), (2, 0)]) ... ])) 2.0 >>> length(Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])) 40.0 >>> length(LineString()) 0.0 >>> length(None) nan """ return lib.length(geometry, **kwargs) @multithreading_enabled def hausdorff_distance(a, b, densify=None, **kwargs): """Compute the discrete Hausdorff distance between two geometries. The Hausdorff distance is a measure of similarity: it is the greatest distance between any point in A and the closest point in B. The discrete distance is an approximation of this metric: only vertices are considered. The parameter 'densify' makes this approximation less coarse by splitting the line segments between vertices before computing the distance. Parameters ---------- a, b : Geometry or array_like densify : float or array_like, optional The value of densify is required to be between 0 and 1. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString >>> line1 = LineString([(130, 0), (0, 0), (0, 150)]) >>> line2 = LineString([(10, 10), (10, 150), (130, 10)]) >>> hausdorff_distance(line1, line2) # doctest: +ELLIPSIS 14.14... >>> hausdorff_distance(line1, line2, densify=0.5) 70.0 >>> hausdorff_distance(line1, LineString()) nan >>> hausdorff_distance(line1, None) nan """ if densify is None: return lib.hausdorff_distance(a, b, **kwargs) else: return lib.hausdorff_distance_densify(a, b, densify, **kwargs) @requires_geos("3.7.0") @multithreading_enabled def frechet_distance(a, b, densify=None, **kwargs): """Compute the discrete Fréchet distance between two geometries. The Fréchet distance is a measure of similarity: it is the greatest distance between any point in A and the closest point in B. The discrete distance is an approximation of this metric: only vertices are considered. The parameter 'densify' makes this approximation less coarse by splitting the line segments between vertices before computing the distance. Fréchet distance sweep continuously along their respective curves and the direction of curves is significant. This makes it a better measure of similarity than Hausdorff distance for curve or surface matching. Parameters ---------- a, b : Geometry or array_like densify : float or array_like, optional The value of densify is required to be between 0 and 1. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString >>> line1 = LineString([(0, 0), (100, 0)]) >>> line2 = LineString([(0, 0), (50, 50), (100, 0)]) >>> frechet_distance(line1, line2) # doctest: +ELLIPSIS 70.71... >>> frechet_distance(line1, line2, densify=0.5) 50.0 >>> frechet_distance(line1, LineString()) nan >>> frechet_distance(line1, None) nan """ if densify is None: return lib.frechet_distance(a, b, **kwargs) return lib.frechet_distance_densify(a, b, densify, **kwargs) @requires_geos("3.6.0") @multithreading_enabled def minimum_clearance(geometry, **kwargs): """Computes the Minimum Clearance distance. A geometry's "minimum clearance" is the smallest distance by which a vertex of the geometry could be moved to produce an invalid geometry. If no minimum clearance exists for a geometry (for example, a single point, or an empty geometry), infinity is returned. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Polygon >>> polygon = Polygon([(0, 0), (0, 10), (5, 6), (10, 10), (10, 0), (5, 4), (0, 0)]) >>> minimum_clearance(polygon) 2.0 >>> minimum_clearance(Polygon()) inf >>> minimum_clearance(None) nan """ return lib.minimum_clearance(geometry, **kwargs) @requires_geos("3.8.0") @multithreading_enabled def minimum_bounding_radius(geometry, **kwargs): """Computes the radius of the minimum bounding circle that encloses an input geometry. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon >>> minimum_bounding_radius(Polygon([(0, 5), (5, 10), (10, 5), (5, 0), (0, 5)])) 5.0 >>> minimum_bounding_radius(LineString([(1, 1), (1, 10)])) 4.5 >>> minimum_bounding_radius(MultiPoint([(2, 2), (4, 2)])) 1.0 >>> minimum_bounding_radius(Point(0, 1)) 0.0 >>> minimum_bounding_radius(GeometryCollection()) 0.0 See also -------- minimum_bounding_circle """ return lib.minimum_bounding_radius(geometry, **kwargs) shapely-2.0.3/shapely/ops.py000066400000000000000000000631061456366510000160150ustar00rootroot00000000000000"""Support for various GEOS geometry operations """ from warnings import warn import shapely from shapely.algorithms.polylabel import polylabel # noqa from shapely.errors import GeometryTypeError, ShapelyDeprecationWarning from shapely.geometry import ( GeometryCollection, LineString, MultiLineString, MultiPoint, Point, Polygon, shape, ) from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry from shapely.geometry.polygon import orient as orient_ from shapely.prepared import prep __all__ = [ "cascaded_union", "linemerge", "operator", "polygonize", "polygonize_full", "transform", "unary_union", "triangulate", "voronoi_diagram", "split", "nearest_points", "validate", "snap", "shared_paths", "clip_by_rect", "orient", "substring", ] class CollectionOperator: def shapeup(self, ob): if isinstance(ob, BaseGeometry): return ob else: try: return shape(ob) except (ValueError, AttributeError): return LineString(ob) def polygonize(self, lines): """Creates polygons from a source of lines The source may be a MultiLineString, a sequence of LineString objects, or a sequence of objects than can be adapted to LineStrings. """ source = getattr(lines, "geoms", None) or lines try: source = iter(source) except TypeError: source = [source] finally: obs = [self.shapeup(line) for line in source] collection = shapely.polygonize(obs) return collection.geoms def polygonize_full(self, lines): """Creates polygons from a source of lines, returning the polygons and leftover geometries. The source may be a MultiLineString, a sequence of LineString objects, or a sequence of objects than can be adapted to LineStrings. Returns a tuple of objects: (polygons, cut edges, dangles, invalid ring lines). Each are a geometry collection. Dangles are edges which have one or both ends which are not incident on another edge endpoint. Cut edges are connected at both ends but do not form part of polygon. Invalid ring lines form rings which are invalid (bowties, etc). """ source = getattr(lines, "geoms", None) or lines try: source = iter(source) except TypeError: source = [source] finally: obs = [self.shapeup(line) for line in source] return shapely.polygonize_full(obs) def linemerge(self, lines, directed=False): """Merges all connected lines from a source The source may be a MultiLineString, a sequence of LineString objects, or a sequence of objects than can be adapted to LineStrings. Returns a LineString or MultiLineString when lines are not contiguous. """ source = None if getattr(lines, "geom_type", None) == "MultiLineString": source = lines elif hasattr(lines, "geoms"): # other Multi geometries source = MultiLineString([ls.coords for ls in lines.geoms]) elif hasattr(lines, "__iter__"): try: source = MultiLineString([ls.coords for ls in lines]) except AttributeError: source = MultiLineString(lines) if source is None: raise ValueError(f"Cannot linemerge {lines}") return shapely.line_merge(source, directed=directed) def cascaded_union(self, geoms): """Returns the union of a sequence of geometries .. deprecated:: 1.8 This function was superseded by :meth:`unary_union`. """ warn( "The 'cascaded_union()' function is deprecated. " "Use 'unary_union()' instead.", ShapelyDeprecationWarning, stacklevel=2, ) return shapely.union_all(geoms, axis=None) def unary_union(self, geoms): """Returns the union of a sequence of geometries Usually used to convert a collection into the smallest set of polygons that cover the same area. """ return shapely.union_all(geoms, axis=None) operator = CollectionOperator() polygonize = operator.polygonize polygonize_full = operator.polygonize_full linemerge = operator.linemerge cascaded_union = operator.cascaded_union unary_union = operator.unary_union def triangulate(geom, tolerance=0.0, edges=False): """Creates the Delaunay triangulation and returns a list of geometries The source may be any geometry type. All vertices of the geometry will be used as the points of the triangulation. From the GEOS documentation: tolerance is the snapping tolerance used to improve the robustness of the triangulation computation. A tolerance of 0.0 specifies that no snapping will take place. If edges is False, a list of Polygons (triangles) will be returned. Otherwise the list of LineString edges is returned. """ collection = shapely.delaunay_triangles(geom, tolerance=tolerance, only_edges=edges) return [g for g in collection.geoms] def voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False): """ Constructs a Voronoi Diagram [1] from the given geometry. Returns a list of geometries. Parameters ---------- geom: geometry the input geometry whose vertices will be used to calculate the final diagram. envelope: geometry, None clipping envelope for the returned diagram, automatically determined if None. The diagram will be clipped to the larger of this envelope or an envelope surrounding the sites. tolerance: float, 0.0 sets the snapping tolerance used to improve the robustness of the computation. A tolerance of 0.0 specifies that no snapping will take place. edges: bool, False If False, return regions as polygons. Else, return only edges e.g. LineStrings. GEOS documentation can be found at [2] Returns ------- GeometryCollection geometries representing the Voronoi regions. Notes ----- The tolerance `argument` can be finicky and is known to cause the algorithm to fail in several cases. If you're using `tolerance` and getting a failure, try removing it. The test cases in tests/test_voronoi_diagram.py show more details. References ---------- [1] https://en.wikipedia.org/wiki/Voronoi_diagram [2] https://geos.osgeo.org/doxygen/geos__c_8h_source.html (line 730) """ try: result = shapely.voronoi_polygons( geom, tolerance=tolerance, extend_to=envelope, only_edges=edges ) except shapely.GEOSException as err: errstr = "Could not create Voronoi Diagram with the specified inputs " errstr += f"({err!s})." if tolerance: errstr += " Try running again with default tolerance value." raise ValueError(errstr) from err if result.geom_type != "GeometryCollection": return GeometryCollection([result]) return result def validate(geom): return shapely.is_valid_reason(geom) def transform(func, geom): """Applies `func` to all coordinates of `geom` and returns a new geometry of the same type from the transformed coordinates. `func` maps x, y, and optionally z to output xp, yp, zp. The input parameters may iterable types like lists or arrays or single values. The output shall be of the same type. Scalars in, scalars out. Lists in, lists out. For example, here is an identity function applicable to both types of input. def id_func(x, y, z=None): return tuple(filter(None, [x, y, z])) g2 = transform(id_func, g1) Using pyproj >= 2.1, this example will accurately project Shapely geometries: import pyproj wgs84 = pyproj.CRS('EPSG:4326') utm = pyproj.CRS('EPSG:32618') project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform g2 = transform(project, g1) Note that the always_xy kwarg is required here as Shapely geometries only support X,Y coordinate ordering. Lambda expressions such as the one in g2 = transform(lambda x, y, z=None: (x+1.0, y+1.0), g1) also satisfy the requirements for `func`. """ if geom.is_empty: return geom if geom.geom_type in ("Point", "LineString", "LinearRing", "Polygon"): # First we try to apply func to x, y, z sequences. When func is # optimized for sequences, this is the fastest, though zipping # the results up to go back into the geometry constructors adds # extra cost. try: if geom.geom_type in ("Point", "LineString", "LinearRing"): return type(geom)(zip(*func(*zip(*geom.coords)))) elif geom.geom_type == "Polygon": shell = type(geom.exterior)(zip(*func(*zip(*geom.exterior.coords)))) holes = list( type(ring)(zip(*func(*zip(*ring.coords)))) for ring in geom.interiors ) return type(geom)(shell, holes) # A func that assumes x, y, z are single values will likely raise a # TypeError, in which case we'll try again. except TypeError: if geom.geom_type in ("Point", "LineString", "LinearRing"): return type(geom)([func(*c) for c in geom.coords]) elif geom.geom_type == "Polygon": shell = type(geom.exterior)([func(*c) for c in geom.exterior.coords]) holes = list( type(ring)([func(*c) for c in ring.coords]) for ring in geom.interiors ) return type(geom)(shell, holes) elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection": return type(geom)([transform(func, part) for part in geom.geoms]) else: raise GeometryTypeError(f"Type {geom.geom_type!r} not recognized") def nearest_points(g1, g2): """Returns the calculated nearest points in the input geometries The points are returned in the same order as the input geometries. """ seq = shapely.shortest_line(g1, g2) if seq is None: if g1.is_empty: raise ValueError("The first input geometry is empty") else: raise ValueError("The second input geometry is empty") p1 = shapely.get_point(seq, 0) p2 = shapely.get_point(seq, 1) return (p1, p2) def snap(g1, g2, tolerance): """ Snaps an input geometry (g1) to reference (g2) geometry's vertices. Parameters ---------- g1 : geometry The first geometry g2 : geometry The second geometry tolerance : float The snapping tolerance Refer to :func:`shapely.snap` for full documentation. """ return shapely.snap(g1, g2, tolerance) def shared_paths(g1, g2): """Find paths shared between the two given lineal geometries Returns a GeometryCollection with two elements: - First element is a MultiLineString containing shared paths with the same direction for both inputs. - Second element is a MultiLineString containing shared paths with the opposite direction for the two inputs. Parameters ---------- g1 : geometry The first geometry g2 : geometry The second geometry """ if not isinstance(g1, LineString): raise GeometryTypeError("First geometry must be a LineString") if not isinstance(g2, LineString): raise GeometryTypeError("Second geometry must be a LineString") return shapely.shared_paths(g1, g2) class SplitOp: @staticmethod def _split_polygon_with_line(poly, splitter): """Split a Polygon with a LineString""" if not isinstance(poly, Polygon): raise GeometryTypeError("First argument must be a Polygon") if not isinstance(splitter, LineString): raise GeometryTypeError("Second argument must be a LineString") union = poly.boundary.union(splitter) # greatly improves split performance for big geometries with many # holes (the following contains checks) with minimal overhead # for common cases poly = prep(poly) # some polygonized geometries may be holes, we do not want them # that's why we test if the original polygon (poly) contains # an inner point of polygonized geometry (pg) return [ pg for pg in polygonize(union) if poly.contains(pg.representative_point()) ] @staticmethod def _split_line_with_line(line, splitter): """Split a LineString with another (Multi)LineString or (Multi)Polygon""" # if splitter is a polygon, pick it's boundary if splitter.geom_type in ("Polygon", "MultiPolygon"): splitter = splitter.boundary if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, LineString) and not isinstance( splitter, MultiLineString ): raise GeometryTypeError( "Second argument must be either a LineString or a MultiLineString" ) # | s\l | Interior | Boundary | Exterior | # |----------|----------|----------|----------| # | Interior | 0 or F | * | * | At least one of these two must be 0 # | Boundary | 0 or F | * | * | So either '0********' or '[0F]**0*****' # | Exterior | * | * | * | No overlapping interiors ('1********') relation = splitter.relate(line) if relation[0] == "1": # The lines overlap at some segment (linear intersection of interiors) raise ValueError("Input geometry segment overlaps with the splitter.") elif relation[0] == "0" or relation[3] == "0": # The splitter crosses or touches the line's interior --> return multilinestring from the split return line.difference(splitter) else: # The splitter does not cross or touch the line's interior --> return collection with identity line return [line] @staticmethod def _split_line_with_point(line, splitter): """Split a LineString with a Point""" if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, Point): raise GeometryTypeError("Second argument must be a Point") # check if point is in the interior of the line if not line.relate_pattern(splitter, "0********"): # point not on line interior --> return collection with single identity line # (REASONING: Returning a list with the input line reference and creating a # GeometryCollection at the general split function prevents unnecessary copying # of linestrings in multipoint splitting function) return [line] elif line.coords[0] == splitter.coords[0]: # if line is a closed ring the previous test doesn't behave as desired return [line] # point is on line, get the distance from the first point on line distance_on_line = line.project(splitter) coords = list(line.coords) # split the line at the point and create two new lines current_position = 0.0 for i in range(len(coords) - 1): point1 = coords[i] point2 = coords[i + 1] dx = point1[0] - point2[0] dy = point1[1] - point2[1] segment_length = (dx**2 + dy**2) ** 0.5 current_position += segment_length if distance_on_line == current_position: # splitter is exactly on a vertex return [LineString(coords[: i + 2]), LineString(coords[i + 1 :])] elif distance_on_line < current_position: # splitter is between two vertices return [ LineString(coords[: i + 1] + [splitter.coords[0]]), LineString([splitter.coords[0]] + coords[i + 1 :]), ] return [line] @staticmethod def _split_line_with_multipoint(line, splitter): """Split a LineString with a MultiPoint""" if not isinstance(line, LineString): raise GeometryTypeError("First argument must be a LineString") if not isinstance(splitter, MultiPoint): raise GeometryTypeError("Second argument must be a MultiPoint") chunks = [line] for pt in splitter.geoms: new_chunks = [] for chunk in filter(lambda x: not x.is_empty, chunks): # add the newly split 2 lines or the same line if not split new_chunks.extend(SplitOp._split_line_with_point(chunk, pt)) chunks = new_chunks return chunks @staticmethod def split(geom, splitter): """ Splits a geometry by another geometry and returns a collection of geometries. This function is the theoretical opposite of the union of the split geometry parts. If the splitter does not split the geometry, a collection with a single geometry equal to the input geometry is returned. The function supports: - Splitting a (Multi)LineString by a (Multi)Point or (Multi)LineString or (Multi)Polygon - Splitting a (Multi)Polygon by a LineString It may be convenient to snap the splitter with low tolerance to the geometry. For example in the case of splitting a line by a point, the point must be exactly on the line, for the line to be correctly split. When splitting a line by a polygon, the boundary of the polygon is used for the operation. When splitting a line by another line, a ValueError is raised if the two overlap at some segment. Parameters ---------- geom : geometry The geometry to be split splitter : geometry The geometry that will split the input geom Example ------- >>> pt = Point((1, 1)) >>> line = LineString([(0,0), (2,2)]) >>> result = split(line, pt) >>> result.wkt 'GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), LINESTRING (1 1, 2 2))' """ if geom.geom_type in ("MultiLineString", "MultiPolygon"): return GeometryCollection( [i for part in geom.geoms for i in SplitOp.split(part, splitter).geoms] ) elif geom.geom_type == "LineString": if splitter.geom_type in ( "LineString", "MultiLineString", "Polygon", "MultiPolygon", ): split_func = SplitOp._split_line_with_line elif splitter.geom_type == "Point": split_func = SplitOp._split_line_with_point elif splitter.geom_type == "MultiPoint": split_func = SplitOp._split_line_with_multipoint else: raise GeometryTypeError( f"Splitting a LineString with a {splitter.geom_type} is not supported" ) elif geom.geom_type == "Polygon": if splitter.geom_type == "LineString": split_func = SplitOp._split_polygon_with_line else: raise GeometryTypeError( f"Splitting a Polygon with a {splitter.geom_type} is not supported" ) else: raise GeometryTypeError( f"Splitting {geom.geom_type} geometry is not supported" ) return GeometryCollection(split_func(geom, splitter)) split = SplitOp.split def substring(geom, start_dist, end_dist, normalized=False): """Return a line segment between specified distances along a LineString Negative distance values are taken as measured in the reverse direction from the end of the geometry. Out-of-range index values are handled by clamping them to the valid range of values. If the start distance equals the end distance, a Point is returned. If the start distance is actually beyond the end distance, then the reversed substring is returned such that the start distance is at the first coordinate. Parameters ---------- geom : LineString The geometry to get a substring of. start_dist : float The distance along `geom` of the start of the substring. end_dist : float The distance along `geom` of the end of the substring. normalized : bool, False Whether the distance parameters are interpreted as a fraction of the geometry's length. Returns ------- Union[Point, LineString] The substring between `start_dist` and `end_dist` or a Point if they are at the same location. Raises ------ TypeError If `geom` is not a LineString. Examples -------- >>> from shapely.geometry import LineString >>> from shapely.ops import substring >>> ls = LineString((i, 0) for i in range(6)) >>> ls.wkt 'LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)' >>> substring(ls, start_dist=1, end_dist=3).wkt 'LINESTRING (1 0, 2 0, 3 0)' >>> substring(ls, start_dist=3, end_dist=1).wkt 'LINESTRING (3 0, 2 0, 1 0)' >>> substring(ls, start_dist=1, end_dist=-3).wkt 'LINESTRING (1 0, 2 0)' >>> substring(ls, start_dist=0.2, end_dist=-0.6, normalized=True).wkt 'LINESTRING (1 0, 2 0)' Returning a `Point` when `start_dist` and `end_dist` are at the same location. >>> substring(ls, 2.5, -2.5).wkt 'POINT (2.5 0)' """ if not isinstance(geom, LineString): raise GeometryTypeError( "Can only calculate a substring of LineString geometries. " f"A {geom.geom_type} was provided." ) # Filter out cases in which to return a point if start_dist == end_dist: return geom.interpolate(start_dist, normalized) elif not normalized and start_dist >= geom.length and end_dist >= geom.length: return geom.interpolate(geom.length, normalized) elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length: return geom.interpolate(0, normalized) elif normalized and start_dist >= 1 and end_dist >= 1: return geom.interpolate(1, normalized) elif normalized and -start_dist >= 1 and -end_dist >= 1: return geom.interpolate(0, normalized) if normalized: start_dist *= geom.length end_dist *= geom.length # Filter out cases where distances meet at a middle point from opposite ends. if start_dist < 0 < end_dist and abs(start_dist) + end_dist == geom.length: return geom.interpolate(end_dist) elif end_dist < 0 < start_dist and abs(end_dist) + start_dist == geom.length: return geom.interpolate(start_dist) start_point = geom.interpolate(start_dist) end_point = geom.interpolate(end_dist) if start_dist < 0: start_dist = geom.length + start_dist # Values may still be negative, if end_dist < 0: # but only in the out-of-range end_dist = geom.length + end_dist # sense, not the wrap-around sense. reverse = start_dist > end_dist if reverse: start_dist, end_dist = end_dist, start_dist if start_dist < 0: start_dist = 0 # to avoid duplicating the first vertex if reverse: vertex_list = [tuple(*end_point.coords)] else: vertex_list = [tuple(*start_point.coords)] coords = list(geom.coords) current_distance = 0 for p1, p2 in zip(coords, coords[1:]): if start_dist < current_distance < end_dist: vertex_list.append(p1) elif current_distance >= end_dist: break current_distance += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5 if reverse: vertex_list.append(tuple(*start_point.coords)) # reverse direction result vertex_list = reversed(vertex_list) else: vertex_list.append(tuple(*end_point.coords)) return LineString(vertex_list) def clip_by_rect(geom, xmin, ymin, xmax, ymax): """Returns the portion of a geometry within a rectangle The geometry is clipped in a fast but possibly dirty way. The output is not guaranteed to be valid. No exceptions will be raised for topological errors. Parameters ---------- geom : geometry The geometry to be clipped xmin : float Minimum x value of the rectangle ymin : float Minimum y value of the rectangle xmax : float Maximum x value of the rectangle ymax : float Maximum y value of the rectangle Notes ----- Requires GEOS >= 3.5.0 New in 1.7. """ if geom.is_empty: return geom return shapely.clip_by_rect(geom, xmin, ymin, xmax, ymax) def orient(geom, sign=1.0): """A properly oriented copy of the given geometry. The signed area of the result will have the given sign. A sign of 1.0 means that the coordinates of the product's exterior rings will be oriented counter-clockwise. Parameters ---------- geom : Geometry The original geometry. May be a Polygon, MultiPolygon, or GeometryCollection. sign : float, optional. The sign of the result's signed area. Returns ------- Geometry """ if isinstance(geom, BaseMultipartGeometry): return geom.__class__( list( map( lambda geom: orient(geom, sign), geom.geoms, ) ) ) if isinstance(geom, (Polygon,)): return orient_(geom, sign) return geom shapely-2.0.3/shapely/plotting.py000066400000000000000000000140251456366510000170500ustar00rootroot00000000000000""" Plot single geometries using Matplotlib. Note: this module is experimental, and mainly targetting (interactive) exploration, debugging and illustration purposes. """ import numpy as np import shapely def _default_ax(): import matplotlib.pyplot as plt ax = plt.gca() ax.grid(True) ax.set_aspect("equal") return ax def _path_from_polygon(polygon): from matplotlib.path import Path if isinstance(polygon, shapely.MultiPolygon): return Path.make_compound_path( *[_path_from_polygon(poly) for poly in polygon.geoms] ) else: return Path.make_compound_path( Path(np.asarray(polygon.exterior.coords)[:, :2]), *[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors], ) def patch_from_polygon(polygon, **kwargs): """ Gets a Matplotlib patch from a (Multi)Polygon. Note: this function is experimental, and mainly targetting (interactive) exploration, debugging and illustration purposes. Parameters ---------- polygon : shapely.Polygon or shapely.MultiPolygon **kwargs Additional keyword arguments passed to the matplotlib Patch. Returns ------- Matplotlib artist (PathPatch) """ from matplotlib.patches import PathPatch return PathPatch(_path_from_polygon(polygon), **kwargs) def plot_polygon( polygon, ax=None, add_points=True, color=None, facecolor=None, edgecolor=None, linewidth=None, **kwargs ): """ Plot a (Multi)Polygon. Note: this function is experimental, and mainly targetting (interactive) exploration, debugging and illustration purposes. Parameters ---------- polygon : shapely.Polygon or shapely.MultiPolygon ax : matplotlib Axes, default None The axes on which to draw the plot. If not specified, will get the current active axes or create a new figure. add_points : bool, default True If True, also plot the coordinates (vertices) as points. color : matplotlib color specification Color for both the polygon fill (face) and boundary (edge). By default, the fill is using an alpha of 0.3. You can specify `facecolor` and `edgecolor` separately for greater control. facecolor : matplotlib color specification Color for the polygon fill. edgecolor : matplotlib color specification Color for the polygon boundary. linewidth : float The line width for the polygon boundary. **kwargs Additional keyword arguments passed to the matplotlib Patch. Returns ------- Matplotlib artist (PathPatch), if `add_points` is false. A tuple of Matplotlib artists (PathPatch, Line2D), if `add_points` is true. """ from matplotlib import colors if ax is None: ax = _default_ax() if color is None: color = "C0" color = colors.to_rgba(color) if facecolor is None: facecolor = list(color) facecolor[-1] = 0.3 facecolor = tuple(facecolor) if edgecolor is None: edgecolor = color patch = patch_from_polygon( polygon, facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, **kwargs ) ax.add_patch(patch) ax.autoscale_view() if add_points: line = plot_points(polygon, ax=ax, color=color) return patch, line return patch def plot_line(line, ax=None, add_points=True, color=None, linewidth=2, **kwargs): """ Plot a (Multi)LineString/LinearRing. Note: this function is experimental, and mainly targetting (interactive) exploration, debugging and illustration purposes. Parameters ---------- line : shapely.LineString or shapely.LinearRing ax : matplotlib Axes, default None The axes on which to draw the plot. If not specified, will get the current active axes or create a new figure. add_points : bool, default True If True, also plot the coordinates (vertices) as points. color : matplotlib color specification Color for the line (edgecolor under the hood) and pointes. linewidth : float, default 2 The line width for the polygon boundary. **kwargs Additional keyword arguments passed to the matplotlib Patch. Returns ------- Matplotlib artist (PathPatch) """ from matplotlib.patches import PathPatch from matplotlib.path import Path if ax is None: ax = _default_ax() if color is None: color = "C0" if isinstance(line, shapely.MultiLineString): path = Path.make_compound_path( *[Path(np.asarray(mline.coords)[:, :2]) for mline in line.geoms] ) else: path = Path(np.asarray(line.coords)[:, :2]) patch = PathPatch( path, facecolor="none", edgecolor=color, linewidth=linewidth, **kwargs ) ax.add_patch(patch) ax.autoscale_view() if add_points: line = plot_points(line, ax=ax, color=color) return patch, line return patch def plot_points(geom, ax=None, color=None, marker="o", **kwargs): """ Plot a Point/MultiPoint or the vertices of any other geometry type. Parameters ---------- geom : shapely.Geometry Any shapely Geometry object, from which all vertices are extracted and plotted. ax : matplotlib Axes, default None The axes on which to draw the plot. If not specified, will get the current active axes or create a new figure. color : matplotlib color specification Color for the filled points. You can use `markeredgecolor` and `markeredgecolor` to have different edge and fill colors. marker : str, default "o" The matplotlib marker for the points. **kwargs Additional keyword arguments passed to matplotlib `plot` (Line2D). Returns ------- Matplotlib artist (Line2D) """ if ax is None: ax = _default_ax() coords = shapely.get_coordinates(geom) (line,) = ax.plot( coords[:, 0], coords[:, 1], linestyle="", marker=marker, color=color, **kwargs ) return line shapely-2.0.3/shapely/predicates.py000066400000000000000000001002601456366510000173300ustar00rootroot00000000000000import warnings import numpy as np from shapely import lib from shapely.decorators import multithreading_enabled, requires_geos __all__ = [ "has_z", "is_ccw", "is_closed", "is_empty", "is_geometry", "is_missing", "is_prepared", "is_ring", "is_simple", "is_valid", "is_valid_input", "is_valid_reason", "crosses", "contains", "contains_xy", "contains_properly", "covered_by", "covers", "disjoint", "dwithin", "equals", "intersects", "intersects_xy", "overlaps", "touches", "within", "equals_exact", "relate", "relate_pattern", ] @multithreading_enabled def has_z(geometry, **kwargs): """Returns True if a geometry has a Z coordinate. Note that this function returns False if the (first) Z coordinate equals NaN or if the geometry is empty. Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- get_coordinate_dimension Examples -------- >>> from shapely import Point >>> has_z(Point(0, 0)) False >>> has_z(Point(0, 0, 0)) True >>> has_z(Point(0, 0, float("nan"))) False """ return lib.has_z(geometry, **kwargs) @requires_geos("3.7.0") @multithreading_enabled def is_ccw(geometry, **kwargs): """Returns True if a linestring or linearring is counterclockwise. Note that there are no checks on whether lines are actually closed and not self-intersecting, while this is a requirement for is_ccw. The recommended usage of this function for linestrings is ``is_ccw(g) & is_simple(g)`` and for linearrings ``is_ccw(g) & is_valid(g)``. Parameters ---------- geometry : Geometry or array_like This function will return False for non-linear goemetries and for lines with fewer than 4 points (including the closing point). **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_simple : Checks if a linestring is closed and simple. is_valid : Checks additionally if the geometry is simple. Examples -------- >>> from shapely import LinearRing, LineString, Point >>> is_ccw(LinearRing([(0, 0), (0, 1), (1, 1), (0, 0)])) False >>> is_ccw(LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)])) True >>> is_ccw(LineString([(0, 0), (1, 1), (0, 1)])) False >>> is_ccw(Point(0, 0)) False """ return lib.is_ccw(geometry, **kwargs) @multithreading_enabled def is_closed(geometry, **kwargs): """Returns True if a linestring's first and last points are equal. Parameters ---------- geometry : Geometry or array_like This function will return False for non-linestrings. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_ring : Checks additionally if the geometry is simple. Examples -------- >>> from shapely import LineString, Point >>> is_closed(LineString([(0, 0), (1, 1)])) False >>> is_closed(LineString([(0, 0), (0, 1), (1, 1), (0, 0)])) True >>> is_closed(Point(0, 0)) False """ return lib.is_closed(geometry, **kwargs) @multithreading_enabled def is_empty(geometry, **kwargs): """Returns True if a geometry is an empty point, polygon, etc. Parameters ---------- geometry : Geometry or array_like Any geometry type is accepted. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_missing : checks if the object is a geometry Examples -------- >>> from shapely import Point >>> is_empty(Point()) True >>> is_empty(Point(0, 0)) False >>> is_empty(None) False """ return lib.is_empty(geometry, **kwargs) @multithreading_enabled def is_geometry(geometry, **kwargs): """Returns True if the object is a geometry Parameters ---------- geometry : any object or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_missing : check if an object is missing (None) is_valid_input : check if an object is a geometry or None Examples -------- >>> from shapely import GeometryCollection, Point >>> is_geometry(Point(0, 0)) True >>> is_geometry(GeometryCollection()) True >>> is_geometry(None) False >>> is_geometry("text") False """ return lib.is_geometry(geometry, **kwargs) @multithreading_enabled def is_missing(geometry, **kwargs): """Returns True if the object is not a geometry (None) Parameters ---------- geometry : any object or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_geometry : check if an object is a geometry is_valid_input : check if an object is a geometry or None is_empty : checks if the object is an empty geometry Examples -------- >>> from shapely import GeometryCollection, Point >>> is_missing(Point(0, 0)) False >>> is_missing(GeometryCollection()) False >>> is_missing(None) True >>> is_missing("text") False """ return lib.is_missing(geometry, **kwargs) @multithreading_enabled def is_prepared(geometry, **kwargs): """Returns True if a Geometry is prepared. Note that it is not necessary to check if a geometry is already prepared before preparing it. It is more efficient to call ``prepare`` directly because it will skip geometries that are already prepared. This function will return False for missing geometries (None). Parameters ---------- geometry : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_valid_input : check if an object is a geometry or None prepare : prepare a geometry Examples -------- >>> from shapely import Point, prepare >>> geometry = Point(0, 0) >>> is_prepared(Point(0, 0)) False >>> prepare(geometry) >>> is_prepared(geometry) True >>> is_prepared(None) False """ return lib.is_prepared(geometry, **kwargs) @multithreading_enabled def is_valid_input(geometry, **kwargs): """Returns True if the object is a geometry or None Parameters ---------- geometry : any object or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_geometry : checks if an object is a geometry is_missing : checks if an object is None Examples -------- >>> from shapely import GeometryCollection, Point >>> is_valid_input(Point(0, 0)) True >>> is_valid_input(GeometryCollection()) True >>> is_valid_input(None) True >>> is_valid_input(1.0) False >>> is_valid_input("text") False """ return lib.is_valid_input(geometry, **kwargs) @multithreading_enabled def is_ring(geometry, **kwargs): """Returns True if a linestring is closed and simple. Parameters ---------- geometry : Geometry or array_like This function will return False for non-linestrings. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_closed : Checks only if the geometry is closed. is_simple : Checks only if the geometry is simple. Examples -------- >>> from shapely import LineString, Point >>> is_ring(Point(0, 0)) False >>> geom = LineString([(0, 0), (1, 1)]) >>> is_closed(geom), is_simple(geom), is_ring(geom) (False, True, False) >>> geom = LineString([(0, 0), (0, 1), (1, 1), (0, 0)]) >>> is_closed(geom), is_simple(geom), is_ring(geom) (True, True, True) >>> geom = LineString([(0, 0), (1, 1), (0, 1), (1, 0), (0, 0)]) >>> is_closed(geom), is_simple(geom), is_ring(geom) (True, False, False) """ return lib.is_ring(geometry, **kwargs) @multithreading_enabled def is_simple(geometry, **kwargs): """Returns True if a Geometry has no anomalous geometric points, such as self-intersections or self tangency. Note that polygons and linearrings are assumed to be simple. Use is_valid to check these kind of geometries for self-intersections. Parameters ---------- geometry : Geometry or array_like This function will return False for geometrycollections. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_ring : Checks additionally if the geometry is closed. is_valid : Checks whether a geometry is well formed. Examples -------- >>> from shapely import LineString, Polygon >>> is_simple(Polygon([(1, 1), (2, 1), (2, 2), (1, 1)])) True >>> is_simple(LineString([(0, 0), (1, 1), (0, 1), (1, 0), (0, 0)])) False >>> is_simple(None) False """ return lib.is_simple(geometry, **kwargs) @multithreading_enabled def is_valid(geometry, **kwargs): """Returns True if a geometry is well formed. Parameters ---------- geometry : Geometry or array_like Any geometry type is accepted. Returns False for missing values. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_valid_reason : Returns the reason in case of invalid. Examples -------- >>> from shapely import GeometryCollection, LineString, Polygon >>> is_valid(LineString([(0, 0), (1, 1)])) True >>> is_valid(Polygon([(0, 0), (1, 1), (1, 2), (1, 1), (0, 0)])) False >>> is_valid(GeometryCollection()) True >>> is_valid(None) False """ # GEOS is valid will emit warnings for invalid geometries. Suppress them. with warnings.catch_warnings(): warnings.simplefilter("ignore") result = lib.is_valid(geometry, **kwargs) return result def is_valid_reason(geometry, **kwargs): """Returns a string stating if a geometry is valid and if not, why. Parameters ---------- geometry : Geometry or array_like Any geometry type is accepted. Returns None for missing values. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- is_valid : returns True or False Examples -------- >>> from shapely import LineString, Polygon >>> is_valid_reason(LineString([(0, 0), (1, 1)])) 'Valid Geometry' >>> is_valid_reason(Polygon([(0, 0), (1, 1), (1, 2), (1, 1), (0, 0)])) 'Ring Self-intersection[1 1]' >>> is_valid_reason(None) is None True """ return lib.is_valid_reason(geometry, **kwargs) @multithreading_enabled def crosses(a, b, **kwargs): """Returns True if A and B spatially cross. A crosses B if they have some but not all interior points in common, the intersection is one dimension less than the maximum dimension of A or B, and the intersection is not equal to either A or B. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, MultiPoint, Point, Polygon >>> line = LineString([(0, 0), (1, 1)]) >>> # A contains B: >>> crosses(line, Point(0.5, 0.5)) False >>> # A and B intersect at a point but do not share all points: >>> crosses(line, MultiPoint([(0, 1), (0.5, 0.5)])) True >>> crosses(line, LineString([(0, 1), (1, 0)])) True >>> # A is contained by B; their intersection is a line (same dimension): >>> crosses(line, LineString([(0, 0), (2, 2)])) False >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> # A contains B: >>> crosses(area, line) False >>> # A and B intersect with a line (lower dimension) but do not share all points: >>> crosses(area, LineString([(0, 0), (2, 2)])) True >>> # A contains B: >>> crosses(area, Point(0.5, 0.5)) False >>> # A contains some but not all points of B; they intersect at a point: >>> crosses(area, MultiPoint([(2, 2), (0.5, 0.5)])) True """ return lib.crosses(a, b, **kwargs) @multithreading_enabled def contains(a, b, **kwargs): """Returns True if geometry B is completely inside geometry A. A contains B if no points of B lie in the exterior of A and at least one point of the interior of B lies in the interior of A. Note: following this definition, a geometry does not contain its boundary, but it does contain itself. See ``contains_properly`` for a version where a geometry does not contain itself. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- within : ``contains(A, B) == within(B, A)`` contains_properly : contains with no common boundary points prepare : improve performance by preparing ``a`` (the first argument) contains_xy : variant for checking against a Point with x, y coordinates Examples -------- >>> from shapely import LineString, Point, Polygon >>> line = LineString([(0, 0), (1, 1)]) >>> contains(line, Point(0, 0)) False >>> contains(line, Point(0.5, 0.5)) True >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> contains(area, Point(0, 0)) False >>> contains(area, line) True >>> contains(area, LineString([(0, 0), (2, 2)])) False >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> contains(polygon_with_hole, Point(1, 1)) True >>> contains(polygon_with_hole, Point(2, 2)) False >>> contains(polygon_with_hole, LineString([(1, 1), (5, 5)])) False >>> contains(area, area) True >>> contains(area, None) False """ return lib.contains(a, b, **kwargs) @multithreading_enabled def contains_properly(a, b, **kwargs): """Returns True if geometry B is completely inside geometry A, with no common boundary points. A contains B properly if B intersects the interior of A but not the boundary (or exterior). This means that a geometry A does not "contain properly" itself, which contrasts with the ``contains`` function, where common points on the boundary are allowed. Note: this function will prepare the geometries under the hood if needed. You can prepare the geometries in advance to avoid repeated preparation when calling this function multiple times. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- contains : contains which allows common boundary points prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import Polygon >>> area1 = Polygon([(0, 0), (3, 0), (3, 3), (0, 3), (0, 0)]) >>> area2 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> area3 = Polygon([(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)]) ``area1`` and ``area2`` have a common border: >>> contains(area1, area2) True >>> contains_properly(area1, area2) False ``area3`` is completely inside ``area1`` with no common border: >>> contains(area1, area3) True >>> contains_properly(area1, area3) True """ return lib.contains_properly(a, b, **kwargs) @multithreading_enabled def covered_by(a, b, **kwargs): """Returns True if no point in geometry A is outside geometry B. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- covers : ``covered_by(A, B) == covers(B, A)`` prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, Point, Polygon >>> line = LineString([(0, 0), (1, 1)]) >>> covered_by(Point(0, 0), line) True >>> covered_by(Point(0.5, 0.5), line) True >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> covered_by(Point(0, 0), area) True >>> covered_by(line, area) True >>> covered_by(LineString([(0, 0), (2, 2)]), area) False >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> covered_by(Point(1, 1), polygon_with_hole) True >>> covered_by(Point(2, 2), polygon_with_hole) True >>> covered_by(LineString([(1, 1), (5, 5)]), polygon_with_hole) False >>> covered_by(area, area) True >>> covered_by(None, area) False """ return lib.covered_by(a, b, **kwargs) @multithreading_enabled def covers(a, b, **kwargs): """Returns True if no point in geometry B is outside geometry A. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- covered_by : ``covers(A, B) == covered_by(B, A)`` prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, Point, Polygon >>> line = LineString([(0, 0), (1, 1)]) >>> covers(line, Point(0, 0)) True >>> covers(line, Point(0.5, 0.5)) True >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> covers(area, Point(0, 0)) True >>> covers(area, line) True >>> covers(area, LineString([(0, 0), (2, 2)])) False >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> covers(polygon_with_hole, Point(1, 1)) True >>> covers(polygon_with_hole, Point(2, 2)) True >>> covers(polygon_with_hole, LineString([(1, 1), (5, 5)])) False >>> covers(area, area) True >>> covers(area, None) False """ return lib.covers(a, b, **kwargs) @multithreading_enabled def disjoint(a, b, **kwargs): """Returns True if A and B do not share any point in space. Disjoint implies that overlaps, touches, within, and intersects are False. Note missing (None) values are never disjoint. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- intersects : ``disjoint(A, B) == ~intersects(A, B)`` prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import GeometryCollection, LineString, Point >>> line = LineString([(0, 0), (1, 1)]) >>> disjoint(line, Point(0, 0)) False >>> disjoint(line, Point(0, 1)) True >>> disjoint(line, LineString([(0, 2), (2, 0)])) False >>> empty = GeometryCollection() >>> disjoint(line, empty) True >>> disjoint(empty, empty) True >>> disjoint(empty, None) False >>> disjoint(None, None) False """ return lib.disjoint(a, b, **kwargs) @multithreading_enabled def equals(a, b, **kwargs): """Returns True if A and B are spatially equal. If A is within B and B is within A, A and B are considered equal. The ordering of points can be different. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See Also -------- equals_exact : Check if A and B are structurally equal given a specified tolerance. Examples -------- >>> from shapely import GeometryCollection, LineString, Polygon >>> line = LineString([(0, 0), (5, 5), (10, 10)]) >>> equals(line, LineString([(0, 0), (10, 10)])) True >>> equals(Polygon(), GeometryCollection()) True >>> equals(None, None) False """ return lib.equals(a, b, **kwargs) @multithreading_enabled def intersects(a, b, **kwargs): """Returns True if A and B share any portion of space. Intersects implies that overlaps, touches and within are True. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- disjoint : ``intersects(A, B) == ~disjoint(A, B)`` prepare : improve performance by preparing ``a`` (the first argument) intersects_xy : variant for checking against a Point with x, y coordinates Examples -------- >>> from shapely import LineString, Point >>> line = LineString([(0, 0), (1, 1)]) >>> intersects(line, Point(0, 0)) True >>> intersects(line, Point(0, 1)) False >>> intersects(line, LineString([(0, 2), (2, 0)])) True >>> intersects(None, None) False """ return lib.intersects(a, b, **kwargs) @multithreading_enabled def overlaps(a, b, **kwargs): """Returns True if A and B spatially overlap. A and B overlap if they have some but not all points in common, have the same dimension, and the intersection of the interiors of the two geometries has the same dimension as the geometries themselves. That is, only polyons can overlap other polygons and only lines can overlap other lines. If either A or B are None, the output is always False. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, Point, Polygon >>> poly = Polygon([(0, 0), (0, 4), (4, 4), (4, 0), (0, 0)]) >>> # A and B share all points (are spatially equal): >>> overlaps(poly, poly) False >>> # A contains B; all points of B are within A: >>> overlaps(poly, Polygon([(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)])) False >>> # A partially overlaps with B: >>> overlaps(poly, Polygon([(2, 2), (2, 6), (6, 6), (6, 2), (2, 2)])) True >>> line = LineString([(2, 2), (6, 6)]) >>> # A and B are different dimensions; they cannot overlap: >>> overlaps(poly, line) False >>> overlaps(poly, Point(2, 2)) False >>> # A and B share some but not all points: >>> overlaps(line, LineString([(0, 0), (4, 4)])) True >>> # A and B intersect only at a point (lower dimension); they do not overlap >>> overlaps(line, LineString([(6, 0), (0, 6)])) False >>> overlaps(poly, None) False >>> overlaps(None, None) False """ return lib.overlaps(a, b, **kwargs) @multithreading_enabled def touches(a, b, **kwargs): """Returns True if the only points shared between A and B are on the boundary of A and B. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, Point, Polygon >>> line = LineString([(0, 2), (2, 0)]) >>> touches(line, Point(0, 2)) True >>> touches(line, Point(1, 1)) False >>> touches(line, LineString([(0, 0), (1, 1)])) True >>> touches(line, LineString([(0, 0), (2, 2)])) False >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> touches(area, Point(0.5, 0)) True >>> touches(area, Point(0.5, 0.5)) False >>> touches(area, line) True >>> touches(area, Polygon([(0, 1), (1, 1), (1, 2), (0, 2), (0, 1)])) True """ return lib.touches(a, b, **kwargs) @multithreading_enabled def within(a, b, **kwargs): """Returns True if geometry A is completely inside geometry B. A is within B if no points of A lie in the exterior of B and at least one point of the interior of A lies in the interior of B. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- contains : ``within(A, B) == contains(B, A)`` prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import LineString, Point, Polygon >>> line = LineString([(0, 0), (1, 1)]) >>> within(Point(0, 0), line) False >>> within(Point(0.5, 0.5), line) True >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> within(Point(0, 0), area) False >>> within(line, area) True >>> within(LineString([(0, 0), (2, 2)]), area) False >>> polygon_with_hole = Polygon( ... [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], ... holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]] ... ) >>> within(Point(1, 1), polygon_with_hole) True >>> within(Point(2, 2), polygon_with_hole) False >>> within(LineString([(1, 1), (5, 5)]), polygon_with_hole) False >>> within(area, area) True >>> within(None, area) False """ return lib.within(a, b, **kwargs) @multithreading_enabled def equals_exact(a, b, tolerance=0.0, **kwargs): """Returns True if A and B are structurally equal. This method uses exact coordinate equality, which requires coordinates to be equal (within specified tolerance) and and in the same order for all components of a geometry. This is in contrast with the ``equals`` function which uses spatial (topological) equality. Parameters ---------- a, b : Geometry or array_like tolerance : float or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See Also -------- equals : Check if A and B are spatially equal. Examples -------- >>> from shapely import Point, Polygon >>> point1 = Point(50, 50) >>> point2 = Point(50.1, 50.1) >>> equals_exact(point1, point2) False >>> equals_exact(point1, point2, tolerance=0.2) True >>> equals_exact(point1, None, tolerance=0.2) False Difference between structucal and spatial equality: >>> polygon1 = Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]) >>> polygon2 = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)]) >>> equals_exact(polygon1, polygon2) False >>> equals(polygon1, polygon2) True """ return lib.equals_exact(a, b, tolerance, **kwargs) def relate(a, b, **kwargs): """ Returns a string representation of the DE-9IM intersection matrix. Parameters ---------- a, b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import LineString, Point >>> point = Point(0, 0) >>> line = LineString([(0, 0), (1, 1)]) >>> relate(point, line) 'F0FFFF102' """ return lib.relate(a, b, **kwargs) @multithreading_enabled def relate_pattern(a, b, pattern, **kwargs): """ Returns True if the DE-9IM string code for the relationship between the geometries satisfies the pattern, else False. This function compares the DE-9IM code string for two geometries against a specified pattern. If the string matches the pattern then ``True`` is returned, otherwise ``False``. The pattern specified can be an exact match (``0``, ``1`` or ``2``), a boolean match (uppercase ``T`` or ``F``), or a wildcard (``*``). For example, the pattern for the ``within`` predicate is ``'T*F**F***'``. Parameters ---------- a, b : Geometry or array_like pattern : string **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. Examples -------- >>> from shapely import Point, Polygon >>> point = Point(0.5, 0.5) >>> square = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) >>> relate(point, square) '0FFFFF212' >>> relate_pattern(point, square, "T*F**F***") True """ return lib.relate_pattern(a, b, pattern, **kwargs) @multithreading_enabled @requires_geos("3.10.0") def dwithin(a, b, distance, **kwargs): """ Returns True if the geometries are within a given distance. Using this function is more efficient than computing the distance and comparing the result. Parameters ---------- a, b : Geometry or array_like distance : float Negative distances always return False. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- distance : compute the actual distance between A and B prepare : improve performance by preparing ``a`` (the first argument) Examples -------- >>> from shapely import Point >>> point = Point(0.5, 0.5) >>> dwithin(point, Point(2, 0.5), 2) True >>> dwithin(point, Point(2, 0.5), [2, 1.5, 1]).tolist() [True, True, False] >>> dwithin(point, Point(0.5, 0.5), 0) True >>> dwithin(point, None, 100) False """ return lib.dwithin(a, b, distance, **kwargs) @multithreading_enabled def contains_xy(geom, x, y=None, **kwargs): """ Returns True if the Point (x, y) is completely inside geometry A. This is a special-case (and faster) variant of the `contains` function which avoids having to create a Point object if you start from x/y coordinates. Note that in the case of points, the `contains_properly` predicate is equivalent to `contains`. See the docstring of `contains` for more details about the predicate. Parameters ---------- geom : Geometry or array_like x, y : float or array_like Coordinates as separate x and y arrays, or a single array of coordinate x, y tuples. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- contains : variant taking two geometries as input Notes ----- If you compare a small number of polygons or lines with many points, it can be beneficial to prepare the geometries in advance using :func:`shapely.prepare`. Examples -------- >>> from shapely import Point, Polygon >>> area = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) >>> contains(area, Point(0.5, 0.5)) True >>> contains_xy(area, 0.5, 0.5) True """ if y is None: coords = np.asarray(x) x, y = coords[:, 0], coords[:, 1] return lib.contains_xy(geom, x, y, **kwargs) @multithreading_enabled def intersects_xy(geom, x, y=None, **kwargs): """ Returns True if A and the Point (x, y) share any portion of space. This is a special-case (and faster) variant of the `intersects` function which avoids having to create a Point object if you start from x/y coordinates. See the docstring of `intersects` for more details about the predicate. Parameters ---------- geom : Geometry or array_like x, y : float or array_like Coordinates as separate x and y arrays, or a single array of coordinate x, y tuples. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- intersects : variant taking two geometries as input Notes ----- If you compare a single or few geometries with many points, it can be beneficial to prepare the geometries in advance using :func:`shapely.prepare`. The `touches` predicate can be determined with this function by getting the boundary of the geometries: ``intersects_xy(boundary(geom), x, y)``. Examples -------- >>> from shapely import LineString, Point >>> line = LineString([(0, 0), (1, 1)]) >>> intersects(line, Point(0, 0)) True >>> intersects_xy(line, 0, 0) True """ if y is None: coords = np.asarray(x) x, y = coords[:, 0], coords[:, 1] return lib.intersects_xy(geom, x, y, **kwargs) shapely-2.0.3/shapely/prepared.py000066400000000000000000000045421456366510000170150ustar00rootroot00000000000000""" Support for GEOS prepared geometry operations. """ from pickle import PicklingError import shapely class PreparedGeometry: """ A geometry prepared for efficient comparison to a set of other geometries. Example: >>> from shapely.geometry import Point, Polygon >>> triangle = Polygon([(0.0, 0.0), (1.0, 1.0), (1.0, -1.0)]) >>> p = prep(triangle) >>> p.intersects(Point(0.5, 0.5)) True """ def __init__(self, context): if isinstance(context, PreparedGeometry): self.context = context.context else: shapely.prepare(context) self.context = context self.prepared = True def contains(self, other): """Returns True if the geometry contains the other, else False""" return self.context.contains(other) def contains_properly(self, other): """Returns True if the geometry properly contains the other, else False""" # TODO temporary hack until shapely exposes contains properly as predicate function from shapely import STRtree tree = STRtree([other]) idx = tree.query(self.context, predicate="contains_properly") return bool(len(idx)) def covers(self, other): """Returns True if the geometry covers the other, else False""" return self.context.covers(other) def crosses(self, other): """Returns True if the geometries cross, else False""" return self.context.crosses(other) def disjoint(self, other): """Returns True if geometries are disjoint, else False""" return self.context.disjoint(other) def intersects(self, other): """Returns True if geometries intersect, else False""" return self.context.intersects(other) def overlaps(self, other): """Returns True if geometries overlap, else False""" return self.context.overlaps(other) def touches(self, other): """Returns True if geometries touch, else False""" return self.context.touches(other) def within(self, other): """Returns True if geometry is within the other, else False""" return self.context.within(other) def __reduce__(self): raise PicklingError("Prepared geometries cannot be pickled.") def prep(ob): """Creates and returns a prepared geometric object.""" return PreparedGeometry(ob) shapely-2.0.3/shapely/set_operations.py000066400000000000000000000421431456366510000202500ustar00rootroot00000000000000import numpy as np from shapely import GeometryType, lib from shapely.decorators import multithreading_enabled, requires_geos from shapely.errors import UnsupportedGEOSVersionError __all__ = [ "difference", "intersection", "intersection_all", "symmetric_difference", "symmetric_difference_all", "unary_union", "union", "union_all", "coverage_union", "coverage_union_all", ] @multithreading_enabled def difference(a, b, grid_size=None, **kwargs): """Returns the part of geometry A that does not intersect with geometry B. If grid_size is nonzero, input coordinates will be snapped to a precision grid of that size and resulting coordinates will be snapped to that same grid. If 0, this operation will use double precision coordinates. If None, the highest precision of the inputs will be used, which may be previously set using set_precision. Note: returned geometry does not have precision set unless specified previously by set_precision. Parameters ---------- a : Geometry or array_like b : Geometry or array_like grid_size : float, optional Precision grid size; requires GEOS >= 3.9.0. Will use the highest precision of the inputs by default. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- set_precision Examples -------- >>> from shapely import box, LineString, normalize, Polygon >>> line = LineString([(0, 0), (2, 2)]) >>> difference(line, LineString([(1, 1), (3, 3)])) >>> difference(line, LineString()) >>> difference(line, None) is None True >>> box1 = box(0, 0, 2, 2) >>> box2 = box(1, 1, 3, 3) >>> normalize(difference(box1, box2)) >>> box1 = box(0.1, 0.2, 2.1, 2.1) >>> difference(box1, box2, grid_size=1) """ if grid_size is not None: if lib.geos_version < (3, 9, 0): raise UnsupportedGEOSVersionError( "grid_size parameter requires GEOS >= 3.9.0" ) if not np.isscalar(grid_size): raise ValueError("grid_size parameter only accepts scalar values") return lib.difference_prec(a, b, grid_size, **kwargs) return lib.difference(a, b, **kwargs) @multithreading_enabled def intersection(a, b, grid_size=None, **kwargs): """Returns the geometry that is shared between input geometries. If grid_size is nonzero, input coordinates will be snapped to a precision grid of that size and resulting coordinates will be snapped to that same grid. If 0, this operation will use double precision coordinates. If None, the highest precision of the inputs will be used, which may be previously set using set_precision. Note: returned geometry does not have precision set unless specified previously by set_precision. Parameters ---------- a : Geometry or array_like b : Geometry or array_like grid_size : float, optional Precision grid size; requires GEOS >= 3.9.0. Will use the highest precision of the inputs by default. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- intersection_all set_precision Examples -------- >>> from shapely import box, LineString, normalize, Polygon >>> line = LineString([(0, 0), (2, 2)]) >>> intersection(line, LineString([(1, 1), (3, 3)])) >>> box1 = box(0, 0, 2, 2) >>> box2 = box(1, 1, 3, 3) >>> normalize(intersection(box1, box2)) >>> box1 = box(0.1, 0.2, 2.1, 2.1) >>> intersection(box1, box2, grid_size=1) """ if grid_size is not None: if lib.geos_version < (3, 9, 0): raise UnsupportedGEOSVersionError( "grid_size parameter requires GEOS >= 3.9.0" ) if not np.isscalar(grid_size): raise ValueError("grid_size parameter only accepts scalar values") return lib.intersection_prec(a, b, grid_size, **kwargs) return lib.intersection(a, b, **kwargs) @multithreading_enabled def intersection_all(geometries, axis=None, **kwargs): """Returns the intersection of multiple geometries. This function ignores None values when other Geometry elements are present. If all elements of the given axis are None, an empty GeometryCollection is returned. Parameters ---------- geometries : array_like axis : int, optional Axis along which the operation is performed. The default (None) performs the operation over all axes, returning a scalar value. Axis may be negative, in which case it counts from the last to the first axis. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- intersection Examples -------- >>> from shapely import LineString >>> line1 = LineString([(0, 0), (2, 2)]) >>> line2 = LineString([(1, 1), (3, 3)]) >>> intersection_all([line1, line2]) >>> intersection_all([[line1, line2, None]], axis=1).tolist() [] >>> intersection_all([line1, None]) """ geometries = np.asarray(geometries) if axis is None: geometries = geometries.ravel() else: geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim) return lib.intersection_all(geometries, **kwargs) @multithreading_enabled def symmetric_difference(a, b, grid_size=None, **kwargs): """Returns the geometry that represents the portions of input geometries that do not intersect. If grid_size is nonzero, input coordinates will be snapped to a precision grid of that size and resulting coordinates will be snapped to that same grid. If 0, this operation will use double precision coordinates. If None, the highest precision of the inputs will be used, which may be previously set using set_precision. Note: returned geometry does not have precision set unless specified previously by set_precision. Parameters ---------- a : Geometry or array_like b : Geometry or array_like grid_size : float, optional Precision grid size; requires GEOS >= 3.9.0. Will use the highest precision of the inputs by default. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- symmetric_difference_all set_precision Examples -------- >>> from shapely import box, LineString, normalize >>> line = LineString([(0, 0), (2, 2)]) >>> symmetric_difference(line, LineString([(1, 1), (3, 3)])) >>> box1 = box(0, 0, 2, 2) >>> box2 = box(1, 1, 3, 3) >>> normalize(symmetric_difference(box1, box2)) >>> box1 = box(0.1, 0.2, 2.1, 2.1) >>> symmetric_difference(box1, box2, grid_size=1) """ if grid_size is not None: if lib.geos_version < (3, 9, 0): raise UnsupportedGEOSVersionError( "grid_size parameter requires GEOS >= 3.9.0" ) if not np.isscalar(grid_size): raise ValueError("grid_size parameter only accepts scalar values") return lib.symmetric_difference_prec(a, b, grid_size, **kwargs) return lib.symmetric_difference(a, b, **kwargs) @multithreading_enabled def symmetric_difference_all(geometries, axis=None, **kwargs): """Returns the symmetric difference of multiple geometries. This function ignores None values when other Geometry elements are present. If all elements of the given axis are None an empty GeometryCollection is returned. Parameters ---------- geometries : array_like axis : int, optional Axis along which the operation is performed. The default (None) performs the operation over all axes, returning a scalar value. Axis may be negative, in which case it counts from the last to the first axis. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- symmetric_difference Examples -------- >>> from shapely import LineString >>> line1 = LineString([(0, 0), (2, 2)]) >>> line2 = LineString([(1, 1), (3, 3)]) >>> symmetric_difference_all([line1, line2]) >>> symmetric_difference_all([[line1, line2, None]], axis=1).tolist() [] >>> symmetric_difference_all([line1, None]) >>> symmetric_difference_all([None, None]) """ geometries = np.asarray(geometries) if axis is None: geometries = geometries.ravel() else: geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim) return lib.symmetric_difference_all(geometries, **kwargs) @multithreading_enabled def union(a, b, grid_size=None, **kwargs): """Merges geometries into one. If grid_size is nonzero, input coordinates will be snapped to a precision grid of that size and resulting coordinates will be snapped to that same grid. If 0, this operation will use double precision coordinates. If None, the highest precision of the inputs will be used, which may be previously set using set_precision. Note: returned geometry does not have precision set unless specified previously by set_precision. Parameters ---------- a : Geometry or array_like b : Geometry or array_like grid_size : float, optional Precision grid size; requires GEOS >= 3.9.0. Will use the highest precision of the inputs by default. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- union_all set_precision Examples -------- >>> from shapely import box, LineString, normalize >>> line = LineString([(0, 0), (2, 2)]) >>> union(line, LineString([(2, 2), (3, 3)])) >>> union(line, None) is None True >>> box1 = box(0, 0, 2, 2) >>> box2 = box(1, 1, 3, 3) >>> normalize(union(box1, box2)) >>> box1 = box(0.1, 0.2, 2.1, 2.1) >>> union(box1, box2, grid_size=1) """ if grid_size is not None: if lib.geos_version < (3, 9, 0): raise UnsupportedGEOSVersionError( "grid_size parameter requires GEOS >= 3.9.0" ) if not np.isscalar(grid_size): raise ValueError("grid_size parameter only accepts scalar values") return lib.union_prec(a, b, grid_size, **kwargs) return lib.union(a, b, **kwargs) @multithreading_enabled def union_all(geometries, grid_size=None, axis=None, **kwargs): """Returns the union of multiple geometries. This function ignores None values when other Geometry elements are present. If all elements of the given axis are None an empty GeometryCollection is returned. If grid_size is nonzero, input coordinates will be snapped to a precision grid of that size and resulting coordinates will be snapped to that same grid. If 0, this operation will use double precision coordinates. If None, the highest precision of the inputs will be used, which may be previously set using set_precision. Note: returned geometry does not have precision set unless specified previously by set_precision. `unary_union` is an alias of `union_all`. Parameters ---------- geometries : array_like grid_size : float, optional Precision grid size; requires GEOS >= 3.9.0. Will use the highest precision of the inputs by default. axis : int, optional Axis along which the operation is performed. The default (None) performs the operation over all axes, returning a scalar value. Axis may be negative, in which case it counts from the last to the first axis. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- union set_precision Examples -------- >>> from shapely import box, LineString, normalize, Point >>> line1 = LineString([(0, 0), (2, 2)]) >>> line2 = LineString([(2, 2), (3, 3)]) >>> union_all([line1, line2]) >>> union_all([[line1, line2, None]], axis=1).tolist() [] >>> box1 = box(0, 0, 2, 2) >>> box2 = box(1, 1, 3, 3) >>> normalize(union_all([box1, box2])) >>> box1 = box(0.1, 0.2, 2.1, 2.1) >>> union_all([box1, box2], grid_size=1) >>> union_all([None, Point(0, 1)]) >>> union_all([None, None]) >>> union_all([]) """ # for union_all, GEOS provides an efficient route through first creating # GeometryCollections # first roll the aggregation axis backwards geometries = np.asarray(geometries) if axis is None: geometries = geometries.ravel() else: geometries = np.rollaxis(geometries, axis=axis, start=geometries.ndim) # create_collection acts on the inner axis collections = lib.create_collection(geometries, GeometryType.GEOMETRYCOLLECTION) if grid_size is not None: if lib.geos_version < (3, 9, 0): raise UnsupportedGEOSVersionError( "grid_size parameter requires GEOS >= 3.9.0" ) if not np.isscalar(grid_size): raise ValueError("grid_size parameter only accepts scalar values") return lib.unary_union_prec(collections, grid_size, **kwargs) return lib.unary_union(collections, **kwargs) unary_union = union_all @requires_geos("3.8.0") @multithreading_enabled def coverage_union(a, b, **kwargs): """Merges multiple polygons into one. This is an optimized version of union which assumes the polygons to be non-overlapping. Parameters ---------- a : Geometry or array_like b : Geometry or array_like **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- coverage_union_all Examples -------- >>> from shapely import normalize, Polygon >>> polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) >>> normalize(coverage_union(polygon, Polygon([(1, 0), (1, 1), (2, 1), (2, 0), (1, 0)]))) Union with None returns same polygon >>> normalize(coverage_union(polygon, None)) """ return coverage_union_all([a, b], **kwargs) @requires_geos("3.8.0") @multithreading_enabled def coverage_union_all(geometries, axis=None, **kwargs): """Returns the union of multiple polygons of a geometry collection. This is an optimized version of union which assumes the polygons to be non-overlapping. This function ignores None values when other Geometry elements are present. If all elements of the given axis are None, an empty MultiPolygon is returned. Parameters ---------- geometries : array_like axis : int, optional Axis along which the operation is performed. The default (None) performs the operation over all axes, returning a scalar value. Axis may be negative, in which case it counts from the last to the first axis. **kwargs See :ref:`NumPy ufunc docs ` for other keyword arguments. See also -------- coverage_union Examples -------- >>> from shapely import normalize, Polygon >>> polygon_1 = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) >>> polygon_2 = Polygon([(1, 0), (1, 1), (2, 1), (2, 0), (1, 0)]) >>> normalize(coverage_union_all([polygon_1, polygon_2])) >>> normalize(coverage_union_all([polygon_1, None])) >>> normalize(coverage_union_all([None, None])) """ # coverage union in GEOS works over GeometryCollections # first roll the aggregation axis backwards geometries = np.asarray(geometries) if axis is None: geometries = geometries.ravel() else: geometries = np.rollaxis( np.asarray(geometries), axis=axis, start=geometries.ndim ) # create_collection acts on the inner axis collections = lib.create_collection(geometries, GeometryType.GEOMETRYCOLLECTION) return lib.coverage_union(collections, **kwargs) shapely-2.0.3/shapely/speedups.py000066400000000000000000000016641456366510000170450ustar00rootroot00000000000000import warnings __all__ = ["available", "enable", "disable", "enabled"] available = True enabled = True _MSG = ( "This function has no longer any effect, and will be removed in a " "future release. Starting with Shapely 2.0, equivalent speedups are " "always available" ) def enable(): """ This function has no longer any effect, and will be removed in a future release. Previously, this function enabled cython-based speedups. Starting with Shapely 2.0, equivalent speedups are available in every installation. """ warnings.warn(_MSG, DeprecationWarning, stacklevel=2) def disable(): """ This function has no longer any effect, and will be removed in a future release. Previously, this function enabled cython-based speedups. Starting with Shapely 2.0, equivalent speedups are available in every installation. """ warnings.warn(_MSG, DeprecationWarning, stacklevel=2) shapely-2.0.3/shapely/strtree.py000066400000000000000000000476461456366510000167170ustar00rootroot00000000000000from typing import Any, Iterable, Union import numpy as np from shapely import lib from shapely._enum import ParamEnum from shapely.decorators import requires_geos, UnsupportedGEOSVersionError from shapely.geometry.base import BaseGeometry from shapely.predicates import is_empty, is_missing __all__ = ["STRtree"] class BinaryPredicate(ParamEnum): """The enumeration of GEOS binary predicates types""" intersects = 1 within = 2 contains = 3 overlaps = 4 crosses = 5 touches = 6 covers = 7 covered_by = 8 contains_properly = 9 class STRtree: """ A query-only R-tree spatial index created using the Sort-Tile-Recursive (STR) [1]_ algorithm. The tree indexes the bounding boxes of each geometry. The tree is constructed directly at initialization and nodes cannot be added or removed after it has been created. All operations return indices of the input geometries. These indices can be used to index into anything associated with the input geometries, including the input geometries themselves, or custom items stored in another object of the same length as the geometries. Bounding boxes limited to two dimensions and are axis-aligned (equivalent to the ``bounds`` property of a geometry); any Z values present in geometries are ignored for purposes of indexing within the tree. Any mixture of geometry types may be stored in the tree. Note: the tree is more efficient for querying when there are fewer geometries that have overlapping bounding boxes and where there is greater similarity between the outer boundary of a geometry and its bounding box. For example, a MultiPolygon composed of widely-spaced individual Polygons will have a large overall bounding box compared to the boundaries of its individual Polygons, and the bounding box may also potentially overlap many other geometries within the tree. This means that the resulting tree may be less efficient to query than a tree constructed from individual Polygons. Parameters ---------- geoms : sequence A sequence of geometry objects. node_capacity : int, default 10 The maximum number of child nodes per parent node in the tree. References ---------- .. [1] Leutenegger, Scott T.; Edgington, Jeffrey M.; Lopez, Mario A. (February 1997). "STR: A Simple and Efficient Algorithm for R-Tree Packing". https://ia600900.us.archive.org/27/items/nasa_techdoc_19970016975/19970016975.pdf """ def __init__( self, geoms: Iterable[BaseGeometry], node_capacity: int = 10, ): # Keep references to geoms in a copied array so that this array is not # modified while the tree depends on it remaining the same self._geometries = np.array(geoms, dtype=np.object_, copy=True) # initialize GEOS STRtree self._tree = lib.STRtree(self.geometries, node_capacity) def __len__(self): return self._tree.count def __reduce__(self): return (STRtree, (self.geometries,)) @property def geometries(self): """ Geometries stored in the tree in the order used to construct the tree. The order of this array corresponds to the tree indices returned by other STRtree methods. Do not attempt to modify items in the returned array. Returns ------- ndarray of Geometry objects """ return self._geometries def query(self, geometry, predicate=None, distance=None): """ Return the integer indices of all combinations of each input geometry and tree geometries where the bounding box of each input geometry intersects the bounding box of a tree geometry. If the input geometry is a scalar, this returns an array of shape (n, ) with the indices of the matching tree geometries. If the input geometry is an array_like, this returns an array with shape (2,n) where the subarrays correspond to the indices of the input geometries and indices of the tree geometries associated with each. To generate an array of pairs of input geometry index and tree geometry index, simply transpose the result. If a predicate is provided, the tree geometries are first queried based on the bounding box of the input geometry and then are further filtered to those that meet the predicate when comparing the input geometry to the tree geometry: predicate(geometry, tree_geometry) The 'dwithin' predicate requires GEOS >= 3.10. Bounding boxes are limited to two dimensions and are axis-aligned (equivalent to the ``bounds`` property of a geometry); any Z values present in input geometries are ignored when querying the tree. Any input geometry that is None or empty will never match geometries in the tree. Parameters ---------- geometry : Geometry or array_like Input geometries to query the tree and filter results using the optional predicate. predicate : {None, 'intersects', 'within', 'contains', 'overlaps', 'crosses',\ 'touches', 'covers', 'covered_by', 'contains_properly', 'dwithin'}, optional The predicate to use for testing geometries from the tree that are within the input geometry's bounding box. distance : number or array_like, optional Distances around each input geometry within which to query the tree for the 'dwithin' predicate. If array_like, shape must be broadcastable to shape of geometry. Required if predicate='dwithin'. Returns ------- ndarray with shape (n,) if geometry is a scalar Contains tree geometry indices. OR ndarray with shape (2, n) if geometry is an array_like The first subarray contains input geometry indices. The second subarray contains tree geometry indices. Examples -------- >>> from shapely import box, Point >>> import numpy as np >>> points = [Point(0, 0), Point(1, 1), Point(2,2), Point(3, 3)] >>> tree = STRtree(points) Query the tree using a scalar geometry: >>> indices = tree.query(box(0, 0, 1, 1)) >>> indices.tolist() [0, 1] Query using an array of geometries: >>> boxes = np.array([box(0, 0, 1, 1), box(2, 2, 3, 3)]) >>> arr_indices = tree.query(boxes) >>> arr_indices.tolist() [[0, 0, 1, 1], [0, 1, 2, 3]] Or transpose to get all pairs of input and tree indices: >>> arr_indices.T.tolist() [[0, 0], [0, 1], [1, 2], [1, 3]] Retrieve the tree geometries by results of query: >>> tree.geometries.take(indices).tolist() [, ] Retrieve all pairs of input and tree geometries: >>> np.array([boxes.take(arr_indices[0]),\ tree.geometries.take(arr_indices[1])]).T.tolist() [[, ], [, ], [, ], [, ]] Query using a predicate: >>> tree = STRtree([box(0, 0, 0.5, 0.5), box(0.5, 0.5, 1, 1), box(1, 1, 2, 2)]) >>> tree.query(box(0, 0, 1, 1), predicate="contains").tolist() [0, 1] >>> tree.query(Point(0.75, 0.75), predicate="dwithin", distance=0.5).tolist() [0, 1, 2] >>> tree.query(boxes, predicate="contains").tolist() [[0, 0], [0, 1]] >>> tree.query(boxes, predicate="dwithin", distance=0.5).tolist() [[0, 0, 0, 1], [0, 1, 2, 2]] Retrieve custom items associated with tree geometries (records can be in whatever data structure so long as geometries and custom data can be extracted into arrays of the same length and order): >>> records = [ ... {"geometry": Point(0, 0), "value": "A"}, ... {"geometry": Point(2, 2), "value": "B"} ... ] >>> tree = STRtree([record["geometry"] for record in records]) >>> items = np.array([record["value"] for record in records]) >>> items.take(tree.query(box(0, 0, 1, 1))).tolist() ['A'] Notes ----- In the context of a spatial join, input geometries are the "left" geometries that determine the order of the results, and tree geometries are "right" geometries that are joined against the left geometries. This effectively performs an inner join, where only those combinations of geometries that can be joined based on overlapping bounding boxes or optional predicate are returned. """ geometry = np.asarray(geometry) is_scalar = False if geometry.ndim == 0: geometry = np.expand_dims(geometry, 0) is_scalar = True if predicate is None: indices = self._tree.query(geometry, 0) return indices[1] if is_scalar else indices # Requires GEOS >= 3.10 elif predicate == "dwithin": if lib.geos_version < (3, 10, 0): raise UnsupportedGEOSVersionError( "dwithin predicate requires GEOS >= 3.10" ) if distance is None: raise ValueError( "distance parameter must be provided for dwithin predicate" ) distance = np.asarray(distance, dtype="float64") if distance.ndim > 1: raise ValueError("Distance array should be one dimensional") try: distance = np.broadcast_to(distance, geometry.shape) except ValueError: raise ValueError("Could not broadcast distance to match geometry") indices = self._tree.dwithin(geometry, distance) return indices[1] if is_scalar else indices predicate = BinaryPredicate.get_value(predicate) indices = self._tree.query(geometry, predicate) return indices[1] if is_scalar else indices @requires_geos("3.6.0") def nearest(self, geometry) -> Union[Any, None]: """ Return the index of the nearest geometry in the tree for each input geometry based on distance within two-dimensional Cartesian space. This distance will be 0 when input geometries intersect tree geometries. If there are multiple equidistant or intersected geometries in the tree, only a single result is returned for each input geometry, based on the order that tree geometries are visited; this order may be nondeterministic. If any input geometry is None or empty, an error is raised. Any Z values present in input geometries are ignored when finding nearest tree geometries. Parameters ---------- geometry : Geometry or array_like Input geometries to query the tree. Returns ------- scalar or ndarray Indices of geometries in tree. Return value will have the same shape as the input. None is returned if this index is empty. This may change in version 2.0. See also -------- query_nearest: returns all equidistant geometries, exclusive geometries, \ and optional distances Examples -------- >>> from shapely.geometry import Point >>> tree = STRtree([Point(i, i) for i in range(10)]) Query the tree for nearest using a scalar geometry: >>> index = tree.nearest(Point(2.2, 2.2)) >>> index 2 >>> tree.geometries.take(index) Query the tree for nearest using an array of geometries: >>> indices = tree.nearest([Point(2.2, 2.2), Point(4.4, 4.4)]) >>> indices.tolist() [2, 4] >>> tree.geometries.take(indices).tolist() [, ] Nearest only return one object if there are multiple equidistant results: >>> tree = STRtree ([Point(0, 0), Point(0, 0)]) >>> tree.nearest(Point(0, 0)) 0 """ if self._tree.count == 0: return None geometry_arr = np.asarray(geometry, dtype=object) if is_missing(geometry_arr).any() or is_empty(geometry_arr).any(): raise ValueError( "Cannot determine nearest geometry for empty geometry or " "missing value (None)." ) # _tree.nearest returns ndarray with shape (2, 1) -> index in input # geometries and index into tree geometries indices = self._tree.nearest(np.atleast_1d(geometry_arr))[1] if geometry_arr.ndim == 0: return indices[0] else: return indices @requires_geos("3.6.0") def query_nearest( self, geometry, max_distance=None, return_distance=False, exclusive=False, all_matches=True, ): """Return the index of the nearest geometries in the tree for each input geometry based on distance within two-dimensional Cartesian space. This distance will be 0 when input geometries intersect tree geometries. If there are multiple equidistant or intersected geometries in tree and `all_matches` is True (the default), all matching tree geometries are returned; otherwise only the first matching tree geometry is returned. Tree indices are returned in the order they are visited for each input geometry and may not be in ascending index order; no meaningful order is implied. The max_distance used to search for nearest items in the tree may have a significant impact on performance by reducing the number of input geometries that are evaluated for nearest items in the tree. Only those input geometries with at least one tree geometry within +/- max_distance beyond their envelope will be evaluated. However, using a large max_distance may have a negative performance impact because many tree geometries will be queried for each input geometry. The distance, if returned, will be 0 for any intersected geometries in the tree. Any geometry that is None or empty in the input geometries is omitted from the output. Any Z values present in input geometries are ignored when finding nearest tree geometries. Parameters ---------- geometry : Geometry or array_like Input geometries to query the tree. max_distance : float, optional Maximum distance within which to query for nearest items in tree. Must be greater than 0. return_distance : bool, default False If True, will return distances in addition to indices. exclusive : bool, default False If True, the nearest tree geometries that are equal to the input geometry will not be returned. all_matches : bool, default True If True, all equidistant and intersected geometries will be returned for each input geometry. If False, only the first nearest geometry will be returned. Returns ------- tree indices or tuple of (tree indices, distances) if geometry is a scalar indices is an ndarray of shape (n, ) and distances (if present) an ndarray of shape (n, ) OR indices or tuple of (indices, distances) indices is an ndarray of shape (2,n) and distances (if present) an ndarray of shape (n). The first subarray of indices contains input geometry indices. The second subarray of indices contains tree geometry indices. See also -------- nearest: returns singular nearest geometry for each input Examples -------- >>> import numpy as np >>> from shapely import box, Point >>> points = [Point(0, 0), Point(1, 1), Point(2,2), Point(3, 3)] >>> tree = STRtree(points) Find the nearest tree geometries to a scalar geometry: >>> indices = tree.query_nearest(Point(0.25, 0.25)) >>> indices.tolist() [0] Retrieve the tree geometries by results of query: >>> tree.geometries.take(indices).tolist() [] Find the nearest tree geometries to an array of geometries: >>> query_points = np.array([Point(2.25, 2.25), Point(1, 1)]) >>> arr_indices = tree.query_nearest(query_points) >>> arr_indices.tolist() [[0, 1], [2, 1]] Or transpose to get all pairs of input and tree indices: >>> arr_indices.T.tolist() [[0, 2], [1, 1]] Retrieve all pairs of input and tree geometries: >>> list(zip(query_points.take(arr_indices[0]), tree.geometries.take(arr_indices[1]))) [(, ), (, )] All intersecting geometries in the tree are returned by default: >>> tree.query_nearest(box(1,1,3,3)).tolist() [1, 2, 3] Set all_matches to False to to return a single match per input geometry: >>> tree.query_nearest(box(1,1,3,3), all_matches=False).tolist() [1] Return the distance to each nearest tree geometry: >>> index, distance = tree.query_nearest(Point(0.5, 0.5), return_distance=True) >>> index.tolist() [0, 1] >>> distance.round(4).tolist() [0.7071, 0.7071] Return the distance for each input and nearest tree geometry for an array of geometries: >>> indices, distance = tree.query_nearest([Point(0.5, 0.5), Point(1, 1)], return_distance=True) >>> indices.tolist() [[0, 0, 1], [0, 1, 1]] >>> distance.round(4).tolist() [0.7071, 0.7071, 0.0] Retrieve custom items associated with tree geometries (records can be in whatever data structure so long as geometries and custom data can be extracted into arrays of the same length and order): >>> records = [ ... {"geometry": Point(0, 0), "value": "A"}, ... {"geometry": Point(2, 2), "value": "B"} ... ] >>> tree = STRtree([record["geometry"] for record in records]) >>> items = np.array([record["value"] for record in records]) >>> items.take(tree.query_nearest(Point(0.5, 0.5))).tolist() ['A'] """ geometry = np.asarray(geometry, dtype=object) is_scalar = False if geometry.ndim == 0: geometry = np.expand_dims(geometry, 0) is_scalar = True if max_distance is not None: if not np.isscalar(max_distance): raise ValueError("max_distance parameter only accepts scalar values") if max_distance <= 0: raise ValueError("max_distance must be greater than 0") # a distance of 0 means no max_distance is used max_distance = max_distance or 0 if not np.isscalar(exclusive): raise ValueError("exclusive parameter only accepts scalar values") if exclusive not in {True, False}: raise ValueError("exclusive parameter must be boolean") if not np.isscalar(all_matches): raise ValueError("all_matches parameter only accepts scalar values") if all_matches not in {True, False}: raise ValueError("all_matches parameter must be boolean") results = self._tree.query_nearest( geometry, max_distance, exclusive, all_matches ) # output indices are shape (n, ) if is_scalar: if not return_distance: return results[0][1] else: return (results[0][1], results[1]) # output indices are shape (2, n) if not return_distance: return results[0] return results shapely-2.0.3/shapely/testing.py000066400000000000000000000142751456366510000166740ustar00rootroot00000000000000from functools import partial import numpy as np import shapely __all__ = ["assert_geometries_equal"] def _equals_exact_with_ndim(x, y, tolerance): dimension_equals = shapely.get_coordinate_dimension( x ) == shapely.get_coordinate_dimension(y) with np.errstate(invalid="ignore"): # Suppress 'invalid value encountered in equals_exact' with nan coordinates geometry_equals = shapely.equals_exact(x, y, tolerance=tolerance) return dimension_equals & geometry_equals def _replace_nan(arr): return np.where(np.isnan(arr), 0.0, arr) def _assert_nan_coords_same(x, y, tolerance, err_msg, verbose): x, y = np.broadcast_arrays(x, y) x_coords = shapely.get_coordinates(x, include_z=True) y_coords = shapely.get_coordinates(y, include_z=True) # Check the shapes (condition is copied from numpy test_array_equal) if x_coords.shape != y_coords.shape: return False # Check NaN positional equality x_id = np.isnan(x_coords) y_id = np.isnan(y_coords) if not (x_id == y_id).all(): msg = build_err_msg( [x, y], err_msg + "\nx and y nan coordinate location mismatch:", verbose=verbose, ) raise AssertionError(msg) # If this passed, replace NaN with a number to be able to use equals_exact x_no_nan = shapely.transform(x, _replace_nan, include_z=True) y_no_nan = shapely.transform(y, _replace_nan, include_z=True) return _equals_exact_with_ndim(x_no_nan, y_no_nan, tolerance=tolerance) def _assert_none_same(x, y, err_msg, verbose): x_id = shapely.is_missing(x) y_id = shapely.is_missing(y) if not (x_id == y_id).all(): msg = build_err_msg( [x, y], err_msg + "\nx and y None location mismatch:", verbose=verbose, ) raise AssertionError(msg) # If there is a scalar, then here we know the array has the same # flag as it everywhere, so we should return the scalar flag. if x.ndim == 0: return bool(x_id) elif y.ndim == 0: return bool(y_id) else: return y_id def assert_geometries_equal( x, y, tolerance=1e-7, equal_none=True, equal_nan=True, normalize=False, err_msg="", verbose=True, ): """Raises an AssertionError if two geometry array_like objects are not equal. Given two array_like objects, check that the shape is equal and all elements of these objects are equal. An exception is raised at shape mismatch or conflicting values. In contrast to the standard usage in shapely, no assertion is raised if both objects have NaNs/Nones in the same positions. Parameters ---------- x : Geometry or array_like y : Geometry or array_like equal_none : bool, default True Whether to consider None elements equal to other None elements. equal_nan : bool, default True Whether to consider nan coordinates as equal to other nan coordinates. normalize : bool, default False Whether to normalize geometries prior to comparison. err_msg : str, optional The error message to be printed in case of failure. verbose : bool, optional If True, the conflicting values are appended to the error message. """ __tracebackhide__ = True # Hide traceback for py.test if normalize: x = shapely.normalize(x) y = shapely.normalize(y) x = np.array(x, copy=False) y = np.array(y, copy=False) is_scalar = x.ndim == 0 or y.ndim == 0 # Check the shapes (condition is copied from numpy test_array_equal) if not (is_scalar or x.shape == y.shape): msg = build_err_msg( [x, y], err_msg + f"\n(shapes {x.shape}, {y.shape} mismatch)", verbose=verbose, ) raise AssertionError(msg) flagged = False if equal_none: flagged = _assert_none_same(x, y, err_msg, verbose) if not np.isscalar(flagged): x, y = x[~flagged], y[~flagged] # Only do the comparison if actual values are left if x.size == 0: return elif flagged: # no sense doing comparison if everything is flagged. return is_equal = _equals_exact_with_ndim(x, y, tolerance=tolerance) if is_scalar and not np.isscalar(is_equal): is_equal = bool(is_equal[0]) if np.all(is_equal): return elif not equal_nan: msg = build_err_msg( [x, y], err_msg + f"\nNot equal to tolerance {tolerance:g}", verbose=verbose, ) raise AssertionError(msg) # Optionally refine failing elements if NaN should be considered equal if not np.isscalar(is_equal): x, y = x[~is_equal], y[~is_equal] # Only do the NaN check if actual values are left if x.size == 0: return elif is_equal: # no sense in checking for NaN if everything is equal. return is_equal = _assert_nan_coords_same(x, y, tolerance, err_msg, verbose) if not np.all(is_equal): msg = build_err_msg( [x, y], err_msg + f"\nNot equal to tolerance {tolerance:g}", verbose=verbose, ) raise AssertionError(msg) ## BELOW A COPY FROM numpy.testing._private.utils (numpy version 1.20.2) def build_err_msg( arrays, err_msg, header="Geometries are not equal:", verbose=True, names=("x", "y"), precision=8, ): msg = ["\n" + header] if err_msg: if err_msg.find("\n") == -1 and len(err_msg) < 79 - len(header): msg = [msg[0] + " " + err_msg] else: msg.append(err_msg) if verbose: for i, a in enumerate(arrays): if isinstance(a, np.ndarray): # precision argument is only needed if the objects are ndarrays r_func = partial(np.array_repr, precision=precision) else: r_func = repr try: r = r_func(a) except Exception as exc: r = f"[repr failed for <{type(a).__name__}>: {exc}]" if r.count("\n") > 3: r = "\n".join(r.splitlines()[:3]) r += "..." msg.append(f" {names[i]}: {r}") return "\n".join(msg) shapely-2.0.3/shapely/tests/000077500000000000000000000000001456366510000157765ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/__init__.py000066400000000000000000000000001456366510000200750ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/common.py000066400000000000000000000070101456366510000176360ustar00rootroot00000000000000from contextlib import contextmanager import numpy as np import pytest import shapely shapely20_todo = pytest.mark.xfail( strict=False, reason="Not yet implemented for Shapely 2.0" ) point_polygon_testdata = ( shapely.points(np.arange(6), np.arange(6)), shapely.box(2, 2, 4, 4), ) point = shapely.Point(2, 3) line_string = shapely.LineString([(0, 0), (1, 0), (1, 1)]) linear_ring = shapely.LinearRing([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) polygon = shapely.Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)]) multi_point = shapely.MultiPoint([(0, 0), (1, 2)]) multi_line_string = shapely.MultiLineString([[(0, 0), (1, 2)]]) multi_polygon = shapely.multipolygons( [ [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)], [(2.1, 2.1), (2.2, 2.1), (2.2, 2.2), (2.1, 2.2), (2.1, 2.1)], ] ) geometry_collection = shapely.GeometryCollection( [shapely.Point(51, -1), shapely.LineString([(52, -1), (49, 2)])] ) point_z = shapely.Point(2, 3, 4) line_string_z = shapely.LineString([(0, 0, 4), (1, 0, 4), (1, 1, 4)]) polygon_z = shapely.Polygon([(0, 0, 4), (2, 0, 4), (2, 2, 4), (0, 2, 4), (0, 0, 4)]) geometry_collection_z = shapely.GeometryCollection([point_z, line_string_z]) polygon_with_hole = shapely.Polygon( [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]], ) empty_point = shapely.from_wkt("POINT EMPTY") empty_point_z = shapely.from_wkt("POINT Z EMPTY") empty_line_string = shapely.from_wkt("LINESTRING EMPTY") empty_line_string_z = shapely.from_wkt("LINESTRING Z EMPTY") empty_polygon = shapely.from_wkt("POLYGON EMPTY") empty = shapely.from_wkt("GEOMETRYCOLLECTION EMPTY") multi_point_z = shapely.MultiPoint([(0, 0, 4), (1, 2, 4)]) multi_line_string_z = shapely.MultiLineString([[(0, 0, 4), (1, 2, 4)]]) multi_polygon_z = shapely.multipolygons( [ [(0, 0, 4), (1, 0, 4), (1, 1, 4), (0, 1, 4), (0, 0, 4)], [(2.1, 2.1, 4), (2.2, 2.1, 4), (2.2, 2.2, 4), (2.1, 2.2, 4), (2.1, 2.1, 4)], ] ) polygon_with_hole_z = shapely.Polygon( [(0, 0, 4), (0, 10, 4), (10, 10, 4), (10, 0, 4), (0, 0, 4)], holes=[[(2, 2, 4), (2, 4, 4), (4, 4, 4), (4, 2, 4), (2, 2, 4)]], ) all_types = ( point, line_string, linear_ring, polygon, multi_point, multi_line_string, multi_polygon, geometry_collection, empty, ) all_types_z = ( point_z, line_string_z, polygon_z, multi_point_z, multi_line_string_z, multi_polygon_z, polygon_with_hole_z, geometry_collection_z, empty_point_z, empty_line_string_z, ) @contextmanager def ignore_invalid(condition=True): if condition: with np.errstate(invalid="ignore"): yield else: yield with ignore_invalid(): line_string_nan = shapely.LineString([(np.nan, np.nan), (np.nan, np.nan)]) class ArrayLike: """ Simple numpy Array like class that implements the ufunc protocol. """ def __init__(self, array): self._array = np.asarray(array) def __len__(self): return len(self._array) def __getitem(self, key): return self._array[key] def __iter__(self): return self._array.__iter__() def __array__(self): return np.asarray(self._array) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if method == "__call__": inputs = [ arg._array if isinstance(arg, self.__class__) else arg for arg in inputs ] return self.__class__(ufunc(*inputs, **kwargs)) else: return NotImplemented shapely-2.0.3/shapely/tests/geometry/000077500000000000000000000000001456366510000176315ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/geometry/__init__.py000066400000000000000000000000001456366510000217300ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/geometry/test_collection.py000066400000000000000000000042661456366510000234050ustar00rootroot00000000000000import numpy as np import pytest from shapely import GeometryCollection, LineString, Point, wkt from shapely.geometry import shape @pytest.fixture() def geometrycollection_geojson(): return { "type": "GeometryCollection", "geometries": [ {"type": "Point", "coordinates": (0, 3, 0)}, {"type": "LineString", "coordinates": ((2, 0), (1, 0))}, ], } @pytest.mark.parametrize( "geom", [ GeometryCollection(), shape({"type": "GeometryCollection", "geometries": []}), wkt.loads("GEOMETRYCOLLECTION EMPTY"), ], ) def test_empty(geom): assert geom.geom_type == "GeometryCollection" assert geom.is_empty assert len(geom.geoms) == 0 assert list(geom.geoms) == [] def test_empty_subgeoms(): geom = GeometryCollection([Point(), LineString()]) assert geom.geom_type == "GeometryCollection" assert geom.is_empty assert len(geom.geoms) == 2 assert list(geom.geoms) == [Point(), LineString()] def test_child_with_deleted_parent(): # test that we can remove a collection while keeping # children around a = LineString([(0, 0), (1, 1), (1, 2), (2, 2)]) b = LineString([(0, 0), (1, 1), (2, 1), (2, 2)]) collection = a.intersection(b) child = collection.geoms[0] # delete parent of child del collection # access geometry, this should not seg fault as 1.2.15 did assert child.wkt is not None def test_from_geojson(geometrycollection_geojson): geom = shape(geometrycollection_geojson) assert geom.geom_type == "GeometryCollection" assert len(geom.geoms) == 2 geom_types = [g.geom_type for g in geom.geoms] assert "Point" in geom_types assert "LineString" in geom_types def test_geointerface(geometrycollection_geojson): geom = shape(geometrycollection_geojson) assert geom.__geo_interface__ == geometrycollection_geojson def test_len_raises(geometrycollection_geojson): geom = shape(geometrycollection_geojson) with pytest.raises(TypeError): len(geom) def test_numpy_object_array(): geom = GeometryCollection([LineString([(0, 0), (1, 1)])]) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom shapely-2.0.3/shapely/tests/geometry/test_coords.py000066400000000000000000000051771456366510000225450ustar00rootroot00000000000000import numpy as np import pytest from shapely import LineString class TestCoords: """ Shapely assumes contiguous C-order float64 data for internal ops. Data should be converted to contiguous float64 if numpy exists. c9a0707 broke this a little bit. """ def test_data_promotion(self): coords = np.array([[12, 34], [56, 78]], dtype=np.float32) processed_coords = np.array(LineString(coords).coords) assert coords.tolist() == processed_coords.tolist() def test_data_destriding(self): coords = np.array([[12, 34], [56, 78]], dtype=np.float32) # Easy way to introduce striding: reverse list order processed_coords = np.array(LineString(coords[::-1]).coords) assert coords[::-1].tolist() == processed_coords.tolist() class TestCoordsGetItem: def test_index_2d_coords(self): c = [(float(x), float(-x)) for x in range(4)] g = LineString(c) for i in range(-4, 4): assert g.coords[i] == c[i] with pytest.raises(IndexError): g.coords[4] with pytest.raises(IndexError): g.coords[-5] def test_index_3d_coords(self): c = [(float(x), float(-x), float(x * 2)) for x in range(4)] g = LineString(c) for i in range(-4, 4): assert g.coords[i] == c[i] with pytest.raises(IndexError): g.coords[4] with pytest.raises(IndexError): g.coords[-5] def test_index_coords_misc(self): g = LineString() # empty with pytest.raises(IndexError): g.coords[0] with pytest.raises(TypeError): g.coords[0.0] def test_slice_2d_coords(self): c = [(float(x), float(-x)) for x in range(4)] g = LineString(c) assert g.coords[1:] == c[1:] assert g.coords[:-1] == c[:-1] assert g.coords[::-1] == c[::-1] assert g.coords[::2] == c[::2] assert g.coords[:4] == c[:4] assert g.coords[4:] == c[4:] == [] def test_slice_3d_coords(self): c = [(float(x), float(-x), float(x * 2)) for x in range(4)] g = LineString(c) assert g.coords[1:] == c[1:] assert g.coords[:-1] == c[:-1] assert g.coords[::-1] == c[::-1] assert g.coords[::2] == c[::2] assert g.coords[:4] == c[:4] assert g.coords[4:] == c[4:] == [] class TestXY: """New geometry/coordseq method 'xy' makes numpy interop easier""" def test_arrays(self): x, y = LineString([(0, 0), (1, 1)]).xy assert len(x) == 2 assert list(x) == [0.0, 1.0] assert len(y) == 2 assert list(y) == [0.0, 1.0] shapely-2.0.3/shapely/tests/geometry/test_decimal.py000066400000000000000000000052761456366510000226520ustar00rootroot00000000000000from decimal import Decimal import pytest from shapely import ( GeometryCollection, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) items2d = [ [(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)], [(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)], ] items2d_mixed = [ [ (Decimal(0.0), Decimal(0.0)), (Decimal(70.0), 120.0), (140.0, Decimal(0.0)), (0.0, 0.0), ], [ (Decimal(60.0), Decimal(80.0)), (Decimal(80.0), 80.0), (70.0, Decimal(60.0)), (60.0, 80.0), ], ] items2d_decimal = [ [ (Decimal(0.0), Decimal(0.0)), (Decimal(70.0), Decimal(120.0)), (Decimal(140.0), Decimal(0.0)), (Decimal(0.0), Decimal(0.0)), ], [ (Decimal(60.0), Decimal(80.0)), (Decimal(80.0), Decimal(80.0)), (Decimal(70.0), Decimal(60.0)), (Decimal(60.0), Decimal(80.0)), ], ] items3d = [ [(0.0, 0.0, 1), (70.0, 120.0, 2), (140.0, 0.0, 3), (0.0, 0.0, 1)], [(60.0, 80.0, 1), (80.0, 80.0, 2), (70.0, 60.0, 3), (60.0, 80.0, 1)], ] items3d_mixed = [ [ (Decimal(0.0), Decimal(0.0), Decimal(1)), (Decimal(70.0), 120.0, Decimal(2)), (140.0, Decimal(0.0), 3), (0.0, 0.0, 1), ], [ (Decimal(60.0), Decimal(80.0), Decimal(1)), (Decimal(80.0), 80.0, 2), (70.0, Decimal(60.0), Decimal(3)), (60.0, 80.0, 1), ], ] items3d_decimal = [ [ (Decimal(0.0), Decimal(0.0), Decimal(1)), (Decimal(70.0), Decimal(120.0), Decimal(2)), (Decimal(140.0), Decimal(0.0), Decimal(3)), (Decimal(0.0), Decimal(0.0), Decimal(1)), ], [ (Decimal(60.0), Decimal(80.0), Decimal(1)), (Decimal(80.0), Decimal(80.0), Decimal(2)), (Decimal(70.0), Decimal(60.0), Decimal(3)), (Decimal(60.0), Decimal(80.0), Decimal(1)), ], ] all_geoms = [ [ Point(items[0][0]), Point(*items[0][0]), MultiPoint(items[0]), LinearRing(items[0]), LineString(items[0]), MultiLineString(items), Polygon(items[0]), MultiPolygon( [ Polygon(items[1]), Polygon(items[0], holes=items[1:]), ] ), GeometryCollection([Point(items[0][0]), Polygon(items[0])]), ] for items in [ items2d, items2d_mixed, items2d_decimal, items3d, items3d_mixed, items3d_decimal, ] ] @pytest.mark.parametrize("geoms", list(zip(*all_geoms))) def test_decimal(geoms): assert geoms[0] == geoms[1] == geoms[2] assert geoms[3] == geoms[4] == geoms[5] shapely-2.0.3/shapely/tests/geometry/test_emptiness.py000066400000000000000000000044721456366510000232600ustar00rootroot00000000000000import math import numpy as np import pytest from shapely import ( GeometryCollection, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.geometry import mapping, shape from shapely.geometry.base import BaseGeometry, EmptyGeometry def empty_generator(): return iter([]) class TestEmptiness: def test_empty_class(self): with pytest.warns(FutureWarning): g = EmptyGeometry() assert g.is_empty def test_empty_base(self): with pytest.warns(FutureWarning): g = BaseGeometry() assert g.is_empty def test_empty_point(self): assert Point().is_empty def test_empty_multipoint(self): assert MultiPoint().is_empty def test_empty_geometry_collection(self): assert GeometryCollection().is_empty def test_empty_linestring(self): assert LineString().is_empty assert LineString(None).is_empty assert LineString([]).is_empty assert LineString(empty_generator()).is_empty def test_empty_multilinestring(self): assert MultiLineString([]).is_empty def test_empty_polygon(self): assert Polygon().is_empty assert Polygon(None).is_empty assert Polygon([]).is_empty assert Polygon(empty_generator()).is_empty def test_empty_multipolygon(self): assert MultiPolygon([]).is_empty def test_empty_linear_ring(self): assert LinearRing().is_empty assert LinearRing(None).is_empty assert LinearRing([]).is_empty assert LinearRing(empty_generator()).is_empty def test_numpy_object_array(): geoms = [Point(), GeometryCollection()] arr = np.empty(2, object) arr[:] = geoms def test_shape_empty(): empty_mp = MultiPolygon() empty_json = mapping(empty_mp) empty_shape = shape(empty_json) assert empty_shape.is_empty @pytest.mark.parametrize( "geom", [ Point(), LineString(), Polygon(), MultiPoint(), MultiLineString(), MultiPolygon(), GeometryCollection(), LinearRing(), ], ) def test_empty_geometry_bounds(geom): """The bounds of an empty geometry is a tuple of NaNs""" assert len(geom.bounds) == 4 assert all(math.isnan(v) for v in geom.bounds) shapely-2.0.3/shapely/tests/geometry/test_equality.py000066400000000000000000000174561456366510000231140ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import LinearRing, LineString, MultiLineString, Point, Polygon from shapely.tests.common import all_types, all_types_z, ignore_invalid @pytest.mark.parametrize("geom", all_types + all_types_z) def test_equality(geom): assert geom == geom transformed = shapely.transform(geom, lambda x: x, include_z=True) assert geom == transformed assert not (geom != transformed) @pytest.mark.parametrize( "left, right", [ # (slightly) different coordinate values (LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 2)])), (LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 1 + 1e-12)])), # different coordinate order (LineString([(0, 0), (1, 1)]), LineString([(1, 1), (0, 0)])), # different number of coordinates (but spatially equal) (LineString([(0, 0), (1, 1)]), LineString([(0, 0), (1, 1), (1, 1)])), (LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0.5, 0.5), (1, 1)])), # different order of sub-geometries ( MultiLineString([[(1, 1), (2, 2)], [(2, 2), (3, 3)]]), MultiLineString([[(2, 2), (3, 3)], [(1, 1), (2, 2)]]), ), ], ) def test_equality_false(left, right): assert left != right with ignore_invalid(): cases1 = [ (LineString([(0, 1), (2, np.nan)]), LineString([(0, 1), (2, np.nan)])), ( LineString([(0, 1), (np.nan, np.nan)]), LineString([(0, 1), (np.nan, np.nan)]), ), (LineString([(np.nan, 1), (2, 3)]), LineString([(np.nan, 1), (2, 3)])), (LineString([(0, np.nan), (2, 3)]), LineString([(0, np.nan), (2, 3)])), ( LineString([(np.nan, np.nan), (np.nan, np.nan)]), LineString([(np.nan, np.nan), (np.nan, np.nan)]), ), # NaN as explicit Z coordinate # TODO: if first z is NaN -> considered as 2D -> tested below explicitly # ( # LineString([(0, 1, np.nan), (2, 3, np.nan)]), # LineString([(0, 1, np.nan), (2, 3, np.nan)]), # ), ( LineString([(0, 1, 2), (2, 3, np.nan)]), LineString([(0, 1, 2), (2, 3, np.nan)]), ), # ( # LineString([(0, 1, np.nan), (2, 3, 4)]), # LineString([(0, 1, np.nan), (2, 3, 4)]), # ), ] @pytest.mark.parametrize("left, right", cases1) def test_equality_with_nan(left, right): # TODO currently those evaluate as not equal, but we are considering to change this # assert left == right assert not (left == right) # assert not (left != right) assert left != right with ignore_invalid(): cases2 = [ ( LineString([(0, 1, np.nan), (2, 3, np.nan)]), LineString([(0, 1, np.nan), (2, 3, np.nan)]), ), ( LineString([(0, 1, np.nan), (2, 3, 4)]), LineString([(0, 1, np.nan), (2, 3, 4)]), ), ] @pytest.mark.parametrize("left, right", cases2) def test_equality_with_nan_z(left, right): # TODO: those are currently considered equal because z dimension is ignored if shapely.geos_version < (3, 12, 0): assert left == right assert not (left != right) else: # on GEOS main z dimension is not ignored -> NaNs cause inequality assert left != right with ignore_invalid(): cases3 = [ (LineString([(0, np.nan), (2, 3)]), LineString([(0, 1), (2, 3)])), (LineString([(0, 1), (2, np.nan)]), LineString([(0, 1), (2, 3)])), (LineString([(0, 1, np.nan), (2, 3, 4)]), LineString([(0, 1, 2), (2, 3, 4)])), (LineString([(0, 1, 2), (2, 3, np.nan)]), LineString([(0, 1, 2), (2, 3, 4)])), ] @pytest.mark.parametrize("left, right", cases3) def test_equality_with_nan_false(left, right): assert left != right def test_equality_with_nan_z_false(): with ignore_invalid(): left = LineString([(0, 1, np.nan), (2, 3, np.nan)]) right = LineString([(0, 1, np.nan), (2, 3, 4)]) if shapely.geos_version < (3, 10, 0): # GEOS <= 3.9 fill the NaN with 0, so the z dimension is different # assert left != right # however, has_z still returns False, so z dimension is ignored in .coords assert left == right elif shapely.geos_version < (3, 12, 0): # GEOS 3.10-3.11 ignore NaN for Z also when explicitly created with 3D # and so the geometries are considered as 2D (and thus z dimension is ignored) assert left == right else: assert left != right def test_equality_z(): # different dimensionality geom1 = Point(0, 1) geom2 = Point(0, 1, 0) assert geom1 != geom2 # different dimensionality with NaN z geom2 = Point(0, 1, np.nan) if shapely.geos_version < (3, 10, 0): # GEOS < 3.8 fill the NaN with 0, so the z dimension is different # assert geom1 != geom2 # however, has_z still returns False, so z dimension is ignored in .coords assert geom1 == geom2 elif shapely.geos_version < (3, 12, 0): # GEOS 3.10-3.11 ignore NaN for Z also when explicitly created with 3D # and so the geometries are considered as 2D (and thus z dimension is ignored) assert geom1 == geom2 else: assert geom1 != geom2 def test_equality_exact_type(): # geometries with different type but same coord seq are not equal geom1 = LineString([(0, 0), (1, 1), (0, 1), (0, 0)]) geom2 = LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]) geom3 = Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]) assert geom1 != geom2 assert geom1 != geom3 assert geom2 != geom3 # empty with different type geom1 = shapely.from_wkt("POINT EMPTY") geom2 = shapely.from_wkt("LINESTRING EMPTY") assert geom1 != geom2 def test_equality_polygon(): # different exterior rings geom1 = shapely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") geom2 = shapely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 15, 0 0))") assert geom1 != geom2 # different number of holes geom1 = shapely.from_wkt( "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1))" ) geom2 = shapely.from_wkt( "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), (3 3, 4 3, 4 4, 3 3))" ) assert geom1 != geom2 # different order of holes geom1 = shapely.from_wkt( "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (3 3, 4 3, 4 4, 3 3), (1 1, 2 1, 2 2, 1 1))" ) geom2 = shapely.from_wkt( "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 2 1, 2 2, 1 1), (3 3, 4 3, 4 4, 3 3))" ) assert geom1 != geom2 @pytest.mark.parametrize("geom", all_types) def test_comparison_notimplemented(geom): # comparing to a non-geometry class should return NotImplemented in __eq__ # to ensure proper delegation to other (eg to ensure comparison of scalar # with array works) # https://github.com/shapely/shapely/issues/1056 assert geom.__eq__(1) is NotImplemented # with array arr = np.array([geom, geom], dtype=object) result = arr == geom assert isinstance(result, np.ndarray) assert result.all() result = geom == arr assert isinstance(result, np.ndarray) assert result.all() result = arr != geom assert isinstance(result, np.ndarray) assert not result.any() result = geom != arr assert isinstance(result, np.ndarray) assert not result.any() def test_comparison_not_supported(): geom1 = Point(1, 1) geom2 = Point(2, 2) with pytest.raises(TypeError, match="not supported between instances"): geom1 > geom2 with pytest.raises(TypeError, match="not supported between instances"): geom1 < geom2 with pytest.raises(TypeError, match="not supported between instances"): geom1 >= geom2 with pytest.raises(TypeError, match="not supported between instances"): geom1 <= geom2 shapely-2.0.3/shapely/tests/geometry/test_format.py000066400000000000000000000107141456366510000225350ustar00rootroot00000000000000import pytest from shapely import Point, Polygon from shapely.geos import geos_version def test_format_invalid(): # check invalid spec formats pt = Point(1, 2) test_list = [ ("5G", ValueError, "invalid format specifier"), (".f", ValueError, "invalid format specifier"), ("0.2e", ValueError, "invalid format specifier"), (".1x", ValueError, "hex representation does not specify precision"), ] for format_spec, err, match in test_list: with pytest.raises(err, match=match): format(pt, format_spec) def test_format_point(): # example coordinate data xy1 = (0.12345678901234567, 1.2345678901234567e10) xy2 = (-169.910918, -18.997564) xyz3 = (630084, 4833438, 76) # list of tuples to test; see structure at top of the for-loop test_list = [ (".0f", xy1, "POINT (0 12345678901)", True), (".1f", xy1, "POINT (0.1 12345678901.2)", True), ("0.2f", xy2, "POINT (-169.91 -19.00)", True), (".3F", (float("inf"), -float("inf")), "POINT (INF -INF)", True), ] if geos_version < (3, 10, 0): # 'g' format varies depending on GEOS version test_list += [ (".1g", xy1, "POINT (0.1 1e+10)", True), (".6G", xy1, "POINT (0.123457 1.23457E+10)", True), ("0.12g", xy1, "POINT (0.123456789012 12345678901.2)", True), ("0.4g", xy2, "POINT (-169.9 -19)", True), ] else: test_list += [ (".1g", xy1, "POINT (0.1 12345678901.2)", False), (".6G", xy1, "POINT (0.123457 12345678901.234568)", False), ("0.12g", xy1, "POINT (0.123456789012 12345678901.234568)", False), ("g", xy2, "POINT (-169.910918 -18.997564)", False), ("0.2g", xy2, "POINT (-169.91 -19)", False), ] # without precsions test GEOS rounding_precision=-1; different than Python test_list += [ ("f", (1, 2), f"POINT ({1:.16f} {2:.16f})", False), ("F", xyz3, "POINT Z ({:.16f} {:.16f} {:.16f})".format(*xyz3), False), ("g", xyz3, "POINT Z (630084 4833438 76)", False), ] for format_spec, coords, expt_wkt, same_python_float in test_list: pt = Point(*coords) # basic checks assert f"{pt}" == pt.wkt assert format(pt, "") == pt.wkt assert format(pt, "x") == pt.wkb_hex.lower() assert format(pt, "X") == pt.wkb_hex # check formatted WKT to expected assert format(pt, format_spec) == expt_wkt, format_spec # check Python's format consistency text_coords = expt_wkt[expt_wkt.index("(") + 1 : expt_wkt.index(")")] is_same = [] for coord, expt_coord in zip(coords, text_coords.split()): py_fmt_float = format(float(coord), format_spec) if same_python_float: assert py_fmt_float == expt_coord, format_spec else: is_same.append(py_fmt_float == expt_coord) if not same_python_float: assert not all(is_same), f"{format_spec!r} with {expt_wkt}" def test_format_polygon(): # check basic cases poly = Point(0, 0).buffer(10, 2) assert f"{poly}" == poly.wkt assert format(poly, "") == poly.wkt assert format(poly, "x") == poly.wkb_hex.lower() assert format(poly, "X") == poly.wkb_hex # Use f-strings with extra characters and rounding precision if geos_version < (3, 13, 0): assert f"<{poly:.2f}>" == ( "" ) else: assert f"<{poly:.2f}>" == ( "" ) # 'g' format varies depending on GEOS version if geos_version < (3, 10, 0): assert f"{poly:.2G}" == ( "POLYGON ((10 0, 7.1 -7.1, 1.6E-14 -10, -7.1 -7.1, " "-10 -3.2E-14, -7.1 7.1, -4.6E-14 10, 7.1 7.1, 10 0))" ) else: assert f"{poly:.2G}" == ( "POLYGON ((10 0, 7.07 -7.07, 0 -10, -7.07 -7.07, " "-10 0, -7.07 7.07, 0 10, 7.07 7.07, 10 0))" ) # check empty empty = Polygon() assert f"{empty}" == "POLYGON EMPTY" assert format(empty, "") == empty.wkt assert format(empty, ".2G") == empty.wkt assert format(empty, "x") == empty.wkb_hex.lower() assert format(empty, "X") == empty.wkb_hex shapely-2.0.3/shapely/tests/geometry/test_geometry_base.py000066400000000000000000000155661456366510000241040ustar00rootroot00000000000000import platform import weakref import numpy as np import pytest import shapely from shapely import ( GeometryCollection, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.errors import ShapelyDeprecationWarning from shapely.testing import assert_geometries_equal def test_polygon(): assert bool(Polygon()) is False def test_linestring(): assert bool(LineString()) is False def test_point(): assert bool(Point()) is False def test_geometry_collection(): assert bool(GeometryCollection()) is False geometries_all_types = [ Point(1, 1), LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]), LineString([(0, 0), (1, 1), (0, 1), (0, 0)]), Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), MultiPoint([(1, 1)]), MultiLineString([[(0, 0), (1, 1), (0, 1), (0, 0)]]), MultiPolygon([Polygon([(0, 0), (1, 1), (0, 1), (0, 0)])]), GeometryCollection([Point(1, 1)]), ] @pytest.mark.skipif( platform.python_implementation() == "PyPy", reason="Setting custom attributes doesn't fail on PyPy", ) @pytest.mark.parametrize("geom", geometries_all_types) def test_setattr_disallowed(geom): with pytest.raises(AttributeError): geom.name = "test" @pytest.mark.parametrize("geom", geometries_all_types) def test_weakrefable(geom): _ = weakref.ref(geom) def test_base_class_not_callable(): with pytest.raises(TypeError): shapely.Geometry("POINT (1 1)") def test_GeometryType_deprecated(): geom = Point(1, 1) with pytest.warns(ShapelyDeprecationWarning): geom_type = geom.geometryType() assert geom_type == geom.geom_type def test_type_deprecated(): geom = Point(1, 1) with pytest.warns(ShapelyDeprecationWarning): geom_type = geom.type assert geom_type == geom.geom_type @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") def test_segmentize(): line = LineString([(0, 0), (0, 10)]) result = line.segmentize(max_segment_length=5) assert result.equals(LineString([(0, 0), (0, 5), (0, 10)])) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_reverse(): coords = [(0, 0), (1, 2)] line = LineString(coords) result = line.reverse() assert result.coords[:] == coords[::-1] @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize( "op", ["union", "intersection", "difference", "symmetric_difference"] ) @pytest.mark.parametrize("grid_size", [0, 1, 2]) def test_binary_op_grid_size(op, grid_size): geom1 = shapely.box(0, 0, 2.5, 2.5) geom2 = shapely.box(2, 2, 3, 3) result = getattr(geom1, op)(geom2, grid_size=grid_size) expected = getattr(shapely, op)(geom1, geom2, grid_size=grid_size) assert result == expected @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") def test_dwithin(): point = Point(1, 1) line = LineString([(0, 0), (0, 10)]) assert point.dwithin(line, 0.5) is False assert point.dwithin(line, 1.5) is True def test_contains_properly(): polygon = Polygon([(0, 0), (10, 10), (10, -10)]) line = LineString([(0, 0), (10, 0)]) assert polygon.contains_properly(line) is False assert polygon.contains(line) is True @pytest.mark.parametrize( "op", ["convex_hull", "envelope", "oriented_envelope", "minimum_rotated_rectangle"] ) def test_constructive_properties(op): geom = LineString([(0, 0), (0, 10), (10, 10)]) result = getattr(geom, op) expected = getattr(shapely, op)(geom) assert result == expected @pytest.mark.parametrize( "op", [ "crosses", "contains", "contains_properly", "covered_by", "covers", "disjoint", "equals", "intersects", "overlaps", "touches", "within", ], ) def test_array_argument_binary_predicates(op): polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)]) result = getattr(polygon, op)(points) assert isinstance(result, np.ndarray) expected = np.array([getattr(polygon, op)(p) for p in points], dtype=bool) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( "op, kwargs", [ pytest.param( "dwithin", dict(distance=0.5), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10" ), ), ("equals_exact", dict(tolerance=0.01)), ("relate_pattern", dict(pattern="T*F**F***")), ], ) def test_array_argument_binary_predicates2(op, kwargs): polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)]) result = getattr(polygon, op)(points, **kwargs) assert isinstance(result, np.ndarray) expected = np.array([getattr(polygon, op)(p, **kwargs) for p in points], dtype=bool) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( "op", [ "difference", "intersection", "symmetric_difference", "union", ], ) def test_array_argument_binary_geo(op): box = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) polygons = shapely.buffer(shapely.points([(0, 0), (0.5, 0.5), (1, 1)]), 0.5) result = getattr(box, op)(polygons) assert isinstance(result, np.ndarray) expected = np.array([getattr(box, op)(g) for g in polygons], dtype=object) assert_geometries_equal(result, expected) @pytest.mark.parametrize("op", ["distance", "hausdorff_distance"]) def test_array_argument_float(op): polygon = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)]) result = getattr(polygon, op)(points) assert isinstance(result, np.ndarray) expected = np.array([getattr(polygon, op)(p) for p in points], dtype="float64") np.testing.assert_array_equal(result, expected) def test_array_argument_linear(): line = LineString([(0, 0), (0, 1), (1, 1)]) distances = np.array([0, 0.5, 1]) result = line.line_interpolate_point(distances) assert isinstance(result, np.ndarray) expected = np.array( [line.line_interpolate_point(d) for d in distances], dtype=object ) assert_geometries_equal(result, expected) points = shapely.points([(0, 0), (0.5, 0.5), (1, 1)]) result = line.line_locate_point(points) assert isinstance(result, np.ndarray) expected = np.array([line.line_locate_point(p) for p in points], dtype="float64") np.testing.assert_array_equal(result, expected) def test_array_argument_buffer(): point = Point(1, 1) distances = np.array([0, 0.5, 1]) result = point.buffer(distances) assert isinstance(result, np.ndarray) expected = np.array([point.buffer(d) for d in distances], dtype=object) assert_geometries_equal(result, expected) shapely-2.0.3/shapely/tests/geometry/test_hash.py000066400000000000000000000012411456366510000221630ustar00rootroot00000000000000import pytest import shapely from shapely.affinity import translate from shapely.geometry import GeometryCollection, LineString, MultiPoint, Point @pytest.mark.parametrize( "geom", [ Point(1, 2), MultiPoint([(1, 2), (3, 4)]), LineString([(1, 2), (3, 4)]), Point(0, 0).buffer(1.0), GeometryCollection([Point(1, 2), LineString([(1, 2), (3, 4)])]), ], ids=[ "Point", "MultiPoint", "LineString", "Polygon", "GeometryCollection", ], ) def test_hash(geom): h1 = hash(geom) assert h1 == hash(shapely.from_wkb(geom.wkb)) assert h1 != hash(translate(geom, 1.0, 2.0)) shapely-2.0.3/shapely/tests/geometry/test_linestring.py000066400000000000000000000143031456366510000234210ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import LinearRing, LineString, Point from shapely.coords import CoordinateSequence def test_from_coordinate_sequence(): # From coordinate tuples line = LineString([(1.0, 2.0), (3.0, 4.0)]) assert len(line.coords) == 2 assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] line = LineString([(1.0, 2.0), (3.0, 4.0)]) assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] def test_from_coordinate_sequence_3D(): line = LineString([(1.0, 2.0, 3.0), (3.0, 4.0, 5.0)]) assert line.has_z assert line.coords[:] == [(1.0, 2.0, 3.0), (3.0, 4.0, 5.0)] def test_from_points(): # From Points line = LineString([Point(1.0, 2.0), Point(3.0, 4.0)]) assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] line = LineString([Point(1.0, 2.0), Point(3.0, 4.0)]) assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] def test_from_mix(): # From mix of tuples and Points line = LineString([Point(1.0, 2.0), (2.0, 3.0), Point(3.0, 4.0)]) assert line.coords[:] == [(1.0, 2.0), (2.0, 3.0), (3.0, 4.0)] def test_from_linestring(): # From another linestring line = LineString([(1.0, 2.0), (3.0, 4.0)]) copy = LineString(line) assert copy.coords[:] == [(1.0, 2.0), (3.0, 4.0)] assert copy.geom_type == "LineString" def test_from_linearring(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing(coords) copy = LineString(ring) assert copy.coords[:] == coords assert copy.geom_type == "LineString" def test_from_linestring_z(): coords = [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)] line = LineString(coords) copy = LineString(line) assert copy.coords[:] == coords assert copy.geom_type == "LineString" def test_from_generator(): gen = (coord for coord in [(1.0, 2.0), (3.0, 4.0)]) line = LineString(gen) assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] def test_from_empty(): line = LineString() assert line.is_empty assert isinstance(line.coords, CoordinateSequence) assert line.coords[:] == [] line = LineString([]) assert line.is_empty assert isinstance(line.coords, CoordinateSequence) assert line.coords[:] == [] def test_from_numpy(): # Construct from a numpy array line = LineString(np.array([[1.0, 2.0], [3.0, 4.0]])) assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] def test_numpy_empty_linestring_coords(): # Check empty line = LineString([]) la = np.asarray(line.coords) assert la.shape == (0, 2) def test_numpy_object_array(): geom = LineString([(0.0, 0.0), (0.0, 1.0)]) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:") def test_from_invalid_dim(): # TODO(shapely-2.0) better error message? # pytest.raises(ValueError, match="at least 2 coordinate tuples|at least 2 coordinates"): with pytest.raises(shapely.GEOSException): LineString([(1, 2)]) # exact error depends on numpy version with pytest.raises((ValueError, TypeError)): LineString([(1, 2, 3), (4, 5)]) with pytest.raises((ValueError, TypeError)): LineString([(1, 2), (3, 4, 5)]) msg = r"The ordinate \(last\) dimension should be 2 or 3, got {}" with pytest.raises(ValueError, match=msg.format(4)): LineString([(1, 2, 3, 4), (4, 5, 6, 7)]) with pytest.raises(ValueError, match=msg.format(1)): LineString([(1,), (4,)]) def test_from_single_coordinate(): """Test for issue #486""" coords = [[-122.185933073564, 37.3629353839073]] with pytest.raises(shapely.GEOSException): ls = LineString(coords) ls.geom_type # caused segfault before fix class TestLineString: def test_linestring(self): # From coordinate tuples line = LineString([(1.0, 2.0), (3.0, 4.0)]) assert len(line.coords) == 2 assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)] # Bounds assert line.bounds == (1.0, 2.0, 3.0, 4.0) # Coordinate access assert tuple(line.coords) == ((1.0, 2.0), (3.0, 4.0)) assert line.coords[0] == (1.0, 2.0) assert line.coords[1] == (3.0, 4.0) with pytest.raises(IndexError): line.coords[2] # index out of range # Geo interface assert line.__geo_interface__ == { "type": "LineString", "coordinates": ((1.0, 2.0), (3.0, 4.0)), } def test_linestring_empty(self): # Test Non-operability of Null geometry l_null = LineString() assert l_null.wkt == "LINESTRING EMPTY" assert l_null.length == 0.0 def test_equals_argument_order(self): """ Test equals predicate functions correctly regardless of the order of the inputs. See issue #317. """ coords = ((0, 0), (1, 0), (1, 1), (0, 0)) ls = LineString(coords) lr = LinearRing(coords) assert ls.__eq__(lr) is False # previously incorrectly returned True assert lr.__eq__(ls) is False assert (ls == lr) is False assert (lr == ls) is False ls_clone = LineString(coords) lr_clone = LinearRing(coords) assert ls.__eq__(ls_clone) is True assert lr.__eq__(lr_clone) is True assert (ls == ls_clone) is True assert (lr == lr_clone) is True def test_numpy_linestring_coords(self): from numpy.testing import assert_array_equal line = LineString([(1.0, 2.0), (3.0, 4.0)]) expected = np.array([[1.0, 2.0], [3.0, 4.0]]) # Coordinate sequences can be adapted as well la = np.asarray(line.coords) assert_array_equal(la, expected) def test_linestring_immutable(): line = LineString([(1.0, 2.0), (3.0, 4.0)]) with pytest.raises(AttributeError): line.coords = [(-1.0, -1.0), (1.0, 1.0)] with pytest.raises(TypeError): line.coords[0] = (-1.0, -1.0) def test_linestring_array_coercion(): # don't convert to array of coordinates, keep objects line = LineString([(1.0, 2.0), (3.0, 4.0)]) arr = np.array(line) assert arr.ndim == 0 assert arr.size == 1 assert arr.dtype == np.dtype("object") assert arr.item() == line shapely-2.0.3/shapely/tests/geometry/test_multi.py000066400000000000000000000004601456366510000223740ustar00rootroot00000000000000import numpy as np test_int_types = [int, np.int16, np.int32, np.int64] class MultiGeometryTestCase: def subgeom_access_test(self, cls, geoms): geom = cls(geoms) for t in test_int_types: for i, g in enumerate(geoms): assert geom.geoms[t(i)] == geoms[i] shapely-2.0.3/shapely/tests/geometry/test_multilinestring.py000066400000000000000000000054231456366510000244770ustar00rootroot00000000000000import numpy as np import pytest from shapely import LineString, MultiLineString from shapely.errors import EmptyPartError from shapely.geometry.base import dump_coords from shapely.tests.geometry.test_multi import MultiGeometryTestCase class TestMultiLineString(MultiGeometryTestCase): def test_multilinestring(self): # From coordinate tuples geom = MultiLineString([[(1.0, 2.0), (3.0, 4.0)]]) assert isinstance(geom, MultiLineString) assert len(geom.geoms) == 1 assert dump_coords(geom) == [[(1.0, 2.0), (3.0, 4.0)]] # From lines a = LineString([(1.0, 2.0), (3.0, 4.0)]) ml = MultiLineString([a]) assert len(ml.geoms) == 1 assert dump_coords(ml) == [[(1.0, 2.0), (3.0, 4.0)]] # From another multi-line ml2 = MultiLineString(ml) assert len(ml2.geoms) == 1 assert dump_coords(ml2) == [[(1.0, 2.0), (3.0, 4.0)]] # Sub-geometry Access geom = MultiLineString([(((0.0, 0.0), (1.0, 2.0)))]) assert isinstance(geom.geoms[0], LineString) assert dump_coords(geom.geoms[0]) == [(0.0, 0.0), (1.0, 2.0)] with pytest.raises(IndexError): # index out of range geom.geoms[1] # Geo interface assert geom.__geo_interface__ == { "type": "MultiLineString", "coordinates": (((0.0, 0.0), (1.0, 2.0)),), } def test_from_multilinestring_z(self): coords1 = [(0.0, 1.0, 2.0), (3.0, 4.0, 5.0)] coords2 = [(6.0, 7.0, 8.0), (9.0, 10.0, 11.0)] # From coordinate tuples ml = MultiLineString([coords1, coords2]) copy = MultiLineString(ml) assert isinstance(copy, MultiLineString) assert copy.geom_type == "MultiLineString" assert len(copy.geoms) == 2 assert dump_coords(copy.geoms[0]) == coords1 assert dump_coords(copy.geoms[1]) == coords2 def test_numpy(self): # Construct from a numpy array geom = MultiLineString([np.array(((0.0, 0.0), (1.0, 2.0)))]) assert isinstance(geom, MultiLineString) assert len(geom.geoms) == 1 assert dump_coords(geom) == [[(0.0, 0.0), (1.0, 2.0)]] def test_subgeom_access(self): line0 = LineString([(0.0, 1.0), (2.0, 3.0)]) line1 = LineString([(4.0, 5.0), (6.0, 7.0)]) self.subgeom_access_test(MultiLineString, [line0, line1]) def test_create_multi_with_empty_component(self): msg = "Can't create MultiLineString with empty component" with pytest.raises(EmptyPartError, match=msg): MultiLineString([LineString([(0, 0), (1, 1), (2, 2)]), LineString()]).wkt def test_numpy_object_array(): geom = MultiLineString([[[5.0, 6.0], [7.0, 8.0]]]) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom shapely-2.0.3/shapely/tests/geometry/test_multipoint.py000066400000000000000000000046031456366510000234510ustar00rootroot00000000000000import numpy as np import pytest from shapely import MultiPoint, Point from shapely.errors import EmptyPartError from shapely.geometry.base import dump_coords from shapely.tests.geometry.test_multi import MultiGeometryTestCase class TestMultiPoint(MultiGeometryTestCase): def test_multipoint(self): # From coordinate tuples geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)]) assert len(geom.geoms) == 2 assert dump_coords(geom) == [[(1.0, 2.0)], [(3.0, 4.0)]] # From points geom = MultiPoint([Point(1.0, 2.0), Point(3.0, 4.0)]) assert len(geom.geoms) == 2 assert dump_coords(geom) == [[(1.0, 2.0)], [(3.0, 4.0)]] # From another multi-point geom2 = MultiPoint(geom) assert len(geom2.geoms) == 2 assert dump_coords(geom2) == [[(1.0, 2.0)], [(3.0, 4.0)]] # Sub-geometry Access assert isinstance(geom.geoms[0], Point) assert geom.geoms[0].x == 1.0 assert geom.geoms[0].y == 2.0 with pytest.raises(IndexError): # index out of range geom.geoms[2] # Geo interface assert geom.__geo_interface__ == { "type": "MultiPoint", "coordinates": ((1.0, 2.0), (3.0, 4.0)), } def test_multipoint_from_numpy(self): # Construct from a numpy array geom = MultiPoint(np.array([[0.0, 0.0], [1.0, 2.0]])) assert isinstance(geom, MultiPoint) assert len(geom.geoms) == 2 assert dump_coords(geom) == [[(0.0, 0.0)], [(1.0, 2.0)]] def test_subgeom_access(self): p0 = Point(1.0, 2.0) p1 = Point(3.0, 4.0) self.subgeom_access_test(MultiPoint, [p0, p1]) def test_create_multi_with_empty_component(self): msg = "Can't create MultiPoint with empty component" with pytest.raises(EmptyPartError, match=msg): MultiPoint([Point(0, 0), Point()]).wkt def test_multipoint_array_coercion(): geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)]) arr = np.array(geom) assert arr.ndim == 0 assert arr.size == 1 assert arr.dtype == np.dtype("object") assert arr.item() == geom def test_numpy_object_array(): geom = MultiPoint([(1.0, 2.0), (3.0, 4.0)]) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom def test_len_raises(): geom = MultiPoint([[5.0, 6.0], [7.0, 8.0]]) with pytest.raises(TypeError): len(geom) shapely-2.0.3/shapely/tests/geometry/test_multipolygon.py000066400000000000000000000077521456366510000240170ustar00rootroot00000000000000import numpy as np import pytest from shapely import MultiPolygon, Polygon from shapely.geometry.base import dump_coords from shapely.tests.geometry.test_multi import MultiGeometryTestCase class TestMultiPolygon(MultiGeometryTestCase): def test_multipolygon(self): # From coordinate tuples coords = [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], ) ] geom = MultiPolygon(coords) assert isinstance(geom, MultiPolygon) assert len(geom.geoms) == 1 assert dump_coords(geom) == [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), [(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)], ] ] # Or without holes coords2 = [(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),)] geom = MultiPolygon(coords2) assert isinstance(geom, MultiPolygon) assert len(geom.geoms) == 1 assert dump_coords(geom) == [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), ] ] # Or from polygons p = Polygon( ((0, 0), (0, 1), (1, 1), (1, 0)), [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], ) geom = MultiPolygon([p]) assert len(geom.geoms) == 1 assert dump_coords(geom) == [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), [(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)], ] ] # Or from another multi-polygon geom2 = MultiPolygon(geom) assert len(geom2.geoms) == 1 assert dump_coords(geom2) == [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), [(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)], ] ] # Sub-geometry Access assert isinstance(geom.geoms[0], Polygon) assert dump_coords(geom.geoms[0]) == [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), [(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)], ] with pytest.raises(IndexError): # index out of range geom.geoms[1] # Geo interface assert geom.__geo_interface__ == { "type": "MultiPolygon", "coordinates": [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)), ) ], } def test_subgeom_access(self): poly0 = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]) poly1 = Polygon([(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]) self.subgeom_access_test(MultiPolygon, [poly0, poly1]) def test_fail_list_of_multipolygons(): """A list of multipolygons is not a valid multipolygon ctor argument""" multi = MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], ) ] ) with pytest.raises(ValueError): MultiPolygon([multi]) def test_numpy_object_array(): geom = MultiPolygon( [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], ) ] ) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom shapely-2.0.3/shapely/tests/geometry/test_point.py000066400000000000000000000111041456366510000223700ustar00rootroot00000000000000import numpy as np import pytest from shapely import Point from shapely.coords import CoordinateSequence from shapely.errors import DimensionError def test_from_coordinates(): # 2D points p = Point(1.0, 2.0) assert p.coords[:] == [(1.0, 2.0)] assert p.has_z is False # 3D Point p = Point(1.0, 2.0, 3.0) assert p.coords[:] == [(1.0, 2.0, 3.0)] assert p.has_z # empty p = Point() assert p.is_empty assert isinstance(p.coords, CoordinateSequence) assert p.coords[:] == [] def test_from_sequence(): # From single coordinate pair p = Point((3.0, 4.0)) assert p.coords[:] == [(3.0, 4.0)] p = Point([3.0, 4.0]) assert p.coords[:] == [(3.0, 4.0)] # From coordinate sequence p = Point([(3.0, 4.0)]) assert p.coords[:] == [(3.0, 4.0)] p = Point([[3.0, 4.0]]) assert p.coords[:] == [(3.0, 4.0)] # 3D p = Point((3.0, 4.0, 5.0)) assert p.coords[:] == [(3.0, 4.0, 5.0)] p = Point([3.0, 4.0, 5.0]) assert p.coords[:] == [(3.0, 4.0, 5.0)] p = Point([(3.0, 4.0, 5.0)]) assert p.coords[:] == [(3.0, 4.0, 5.0)] def test_from_numpy(): # Construct from a numpy array p = Point(np.array([1.0, 2.0])) assert p.coords[:] == [(1.0, 2.0)] p = Point(np.array([1.0, 2.0, 3.0])) assert p.coords[:] == [(1.0, 2.0, 3.0)] def test_from_numpy_xy(): # Construct from separate x, y numpy arrays - if those are length 1, # this is allowed for compat with shapely 1.8 # (https://github.com/shapely/shapely/issues/1587) p = Point(np.array([1.0]), np.array([2.0])) assert p.coords[:] == [(1.0, 2.0)] p = Point(np.array([1.0]), np.array([2.0]), np.array([3.0])) assert p.coords[:] == [(1.0, 2.0, 3.0)] def test_from_point(): # From another point p = Point(3.0, 4.0) q = Point(p) assert q.coords[:] == [(3.0, 4.0)] p = Point(3.0, 4.0, 5.0) q = Point(p) assert q.coords[:] == [(3.0, 4.0, 5.0)] def test_from_generator(): gen = (coord for coord in [(1.0, 2.0)]) p = Point(gen) assert p.coords[:] == [(1.0, 2.0)] def test_from_invalid(): with pytest.raises(TypeError, match="takes at most 3 arguments"): Point(1, 2, 3, 4) # this worked in shapely 1.x, just ignoring the other coords with pytest.raises( ValueError, match="takes only scalar or 1-size vector arguments" ): Point([(2, 3), (11, 4)]) class TestPoint: def test_point(self): # Test 2D points p = Point(1.0, 2.0) assert p.x == 1.0 assert p.y == 2.0 assert p.coords[:] == [(1.0, 2.0)] assert str(p) == p.wkt assert p.has_z is False with pytest.raises(DimensionError): p.z # Check 3D p = Point(1.0, 2.0, 3.0) assert p.coords[:] == [(1.0, 2.0, 3.0)] assert str(p) == p.wkt assert p.has_z is True assert p.z == 3.0 # Coordinate access p = Point((3.0, 4.0)) assert p.x == 3.0 assert p.y == 4.0 assert tuple(p.coords) == ((3.0, 4.0),) assert p.coords[0] == (3.0, 4.0) with pytest.raises(IndexError): # index out of range p.coords[1] # Bounds assert p.bounds == (3.0, 4.0, 3.0, 4.0) # Geo interface assert p.__geo_interface__ == {"type": "Point", "coordinates": (3.0, 4.0)} def test_point_empty(self): # Test Non-operability of Null geometry p_null = Point() assert p_null.wkt == "POINT EMPTY" assert p_null.coords[:] == [] assert p_null.area == 0.0 def test_coords(self): # From Array.txt p = Point(0.0, 0.0, 1.0) coords = p.coords[0] assert coords == (0.0, 0.0, 1.0) # Convert to Numpy array, passing through Python sequence a = np.asarray(coords) assert a.ndim == 1 assert a.size == 3 assert a.shape == (3,) def test_point_immutable(): p = Point(3.0, 4.0) with pytest.raises(AttributeError): p.coords = (2.0, 1.0) with pytest.raises(TypeError): p.coords[0] = (2.0, 1.0) def test_point_array_coercion(): # don't convert to array of coordinates, keep objects p = Point(3.0, 4.0) arr = np.array(p) assert arr.ndim == 0 assert arr.size == 1 assert arr.dtype == np.dtype("object") assert arr.item() == p def test_numpy_empty_point_coords(): pe = Point() # Access the coords a = np.asarray(pe.coords) assert a.shape == (0, 2) def test_numpy_object_array(): geom = Point(3.0, 4.0) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom shapely-2.0.3/shapely/tests/geometry/test_polygon.py000066400000000000000000000356751456366510000227510ustar00rootroot00000000000000"""Polygons and Linear Rings """ import numpy as np import pytest from shapely import LinearRing, LineString, Point, Polygon from shapely.coords import CoordinateSequence from shapely.errors import TopologicalError from shapely.wkb import loads as load_wkb def test_empty_linearring_coords(): assert LinearRing().coords[:] == [] def test_linearring_from_coordinate_sequence(): expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) assert ring.coords[:] == expected_coords ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) assert ring.coords[:] == expected_coords def test_linearring_from_points(): # From Points expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)]) assert ring.coords[:] == expected_coords def test_linearring_from_closed_linestring(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] line = LineString(coords) ring = LinearRing(line) assert len(ring.coords) == 4 assert ring.coords[:] == coords assert ring.geom_type == "LinearRing" def test_linearring_from_unclosed_linestring(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] line = LineString(coords[:-1]) # Pass in unclosed line ring = LinearRing(line) assert len(ring.coords) == 4 assert ring.coords[:] == coords assert ring.geom_type == "LinearRing" def test_linearring_from_invalid(): coords = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)] line = LineString(coords) assert not line.is_valid with pytest.raises(TopologicalError): LinearRing(line) def test_linearring_from_too_short_linestring(): # Creation of LinearRing request at least 3 coordinates (unclosed) or # 4 coordinates (closed) coords = [(0.0, 0.0), (1.0, 1.0)] line = LineString(coords) with pytest.raises(ValueError, match="requires at least 4 coordinates"): LinearRing(line) def test_linearring_from_linearring(): coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing(coords) assert ring.coords[:] == coords def test_linearring_from_generator(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] gen = (coord for coord in coords) ring = LinearRing(gen) assert ring.coords[:] == coords def test_linearring_from_empty(): ring = LinearRing() assert ring.is_empty assert isinstance(ring.coords, CoordinateSequence) assert ring.coords[:] == [] ring = LinearRing([]) assert ring.is_empty assert isinstance(ring.coords, CoordinateSequence) assert ring.coords[:] == [] def test_linearring_from_numpy(): # Construct from a numpy array coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing(np.array(coords)) assert ring.coords[:] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] def test_numpy_linearring_coords(): from numpy.testing import assert_array_equal ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) ra = np.asarray(ring.coords) expected = np.asarray([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)]) assert_array_equal(ra, expected) def test_numpy_empty_linearring_coords(): ring = LinearRing() assert np.asarray(ring.coords).shape == (0, 2) def test_numpy_object_array(): geom = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) ar = np.empty(1, object) ar[:] = [geom] assert ar[0] == geom def test_polygon_from_coordinate_sequence(): coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] # Construct a polygon, exterior ring only polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) assert polygon.exterior.coords[:] == coords assert len(polygon.interiors) == 0 polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]) assert polygon.exterior.coords[:] == coords assert len(polygon.interiors) == 0 def test_polygon_from_coordinate_sequence_with_holes(): coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] # Interior rings (holes) polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]]) assert polygon.exterior.coords[:] == coords assert len(polygon.interiors) == 1 assert len(polygon.interiors[0].coords) == 5 # Multiple interior rings with different length coords = [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)] holes = [ [(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)], [(3, 3), (3, 4), (4, 5), (5, 4), (5, 3), (3, 3)], ] polygon = Polygon(coords, holes) assert polygon.exterior.coords[:] == coords assert len(polygon.interiors) == 2 assert len(polygon.interiors[0].coords) == 5 assert len(polygon.interiors[1].coords) == 6 def test_polygon_from_linearring(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] ring = LinearRing(coords) polygon = Polygon(ring) assert polygon.exterior.coords[:] == coords assert len(polygon.interiors) == 0 # from shell and holes linearrings shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)]) holes = [ LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]), LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]), LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]), ] polygon = Polygon(shell, holes) assert polygon.exterior.coords[:] == shell.coords[:] assert len(polygon.interiors) == 3 for i in range(3): assert polygon.interiors[i].coords[:] == holes[i].coords[:] def test_polygon_from_linestring(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] line = LineString(coords) polygon = Polygon(line) assert polygon.exterior.coords[:] == coords # from unclosed linestring line = LineString(coords[:-1]) polygon = Polygon(line) assert polygon.exterior.coords[:] == coords def test_polygon_from_points(): polygon = Polygon([Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0)]) expected_coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)] assert polygon.exterior.coords[:] == expected_coords def test_polygon_from_polygon(): coords = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)] polygon = Polygon(coords, [[(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)]]) # Test from another Polygon copy = Polygon(polygon) assert len(copy.exterior.coords) == 5 assert len(copy.interiors) == 1 assert len(copy.interiors[0].coords) == 5 def test_polygon_from_invalid(): # Error handling with pytest.raises(ValueError): # A LinearRing must have at least 3 coordinate tuples Polygon([[1, 2], [2, 3]]) def test_polygon_from_empty(): polygon = Polygon() assert polygon.is_empty assert polygon.exterior.coords[:] == [] polygon = Polygon([]) assert polygon.is_empty assert polygon.exterior.coords[:] == [] def test_polygon_from_numpy(): a = np.array(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))) polygon = Polygon(a) assert len(polygon.exterior.coords) == 5 assert polygon.exterior.coords[:] == [ (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0), ] assert len(polygon.interiors) == 0 def test_polygon_from_generator(): coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] gen = (coord for coord in coords) polygon = Polygon(gen) assert polygon.exterior.coords[:] == coords class TestPolygon: def test_linearring(self): # Initialization # Linear rings won't usually be created by users, but by polygons coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)) ring = LinearRing(coords) assert len(ring.coords) == 5 assert ring.coords[0] == ring.coords[4] assert ring.coords[0] == ring.coords[-1] assert ring.is_ring is True def test_polygon(self): coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)) # Construct a polygon, exterior ring only polygon = Polygon(coords) assert len(polygon.exterior.coords) == 5 # Ring Access assert isinstance(polygon.exterior, LinearRing) ring = polygon.exterior assert len(ring.coords) == 5 assert ring.coords[0] == ring.coords[4] assert ring.coords[0] == (0.0, 0.0) assert ring.is_ring is True assert len(polygon.interiors) == 0 # Create a new polygon from WKB data = polygon.wkb polygon = None ring = None polygon = load_wkb(data) ring = polygon.exterior assert len(ring.coords) == 5 assert ring.coords[0] == ring.coords[4] assert ring.coords[0] == (0.0, 0.0) assert ring.is_ring is True polygon = None # Interior rings (holes) polygon = Polygon( coords, [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))] ) assert len(polygon.exterior.coords) == 5 assert len(polygon.interiors[0].coords) == 5 with pytest.raises(IndexError): # index out of range polygon.interiors[1] # Coordinate getter raises exceptions with pytest.raises(NotImplementedError): polygon.coords # Geo interface assert polygon.__geo_interface__ == { "type": "Polygon", "coordinates": ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)), ), } def test_linearring_empty(self): # Test Non-operability of Null rings r_null = LinearRing() assert r_null.wkt == "LINEARRING EMPTY" assert r_null.length == 0.0 def test_dimensions(self): # Background: see http://trac.gispython.org/lab/ticket/168 # http://lists.gispython.org/pipermail/community/2008-August/001859.html coords = ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0)) polygon = Polygon(coords) assert polygon._ndim == 3 gi = polygon.__geo_interface__ assert gi["coordinates"] == ( ( (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0), ), ) e = polygon.exterior assert e._ndim == 3 gi = e.__geo_interface__ assert gi["coordinates"] == ( (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0), ) def test_attribute_chains(self): # Attribute Chaining # See also ticket #151. p = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)]) assert list(p.boundary.coords) == [ (0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0), (0.0, 0.0), ] ec = list(Point(0.0, 0.0).buffer(1.0, 1).exterior.coords) assert isinstance(ec, list) # TODO: this is a poor test # Test chained access to interiors p = Polygon( [(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)], [[(-0.25, 0.25), (-0.25, 0.75), (-0.75, 0.75), (-0.75, 0.25)]], ) assert p.area == 0.75 """Not so much testing the exact values here, which are the responsibility of the geometry engine (GEOS), but that we can get chain functions and properties using anonymous references. """ assert list(p.interiors[0].coords) == [ (-0.25, 0.25), (-0.25, 0.75), (-0.75, 0.75), (-0.75, 0.25), (-0.25, 0.25), ] xy = list(p.interiors[0].buffer(1).exterior.coords)[0] assert len(xy) == 2 # Test multiple operators, boundary of a buffer ec = list(p.buffer(1).boundary.coords) assert isinstance(ec, list) # TODO: this is a poor test def test_empty_equality(self): # Test equals operator, including empty geometries # see issue #338 point1 = Point(0, 0) polygon1 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)]) polygon2 = Polygon([(0.0, 0.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0)]) polygon_empty1 = Polygon() polygon_empty2 = Polygon() assert point1 != polygon1 assert polygon_empty1 == polygon_empty2 assert polygon1 != polygon_empty1 assert polygon1 == polygon2 assert polygon_empty1 is not None def test_from_bounds(self): xmin, ymin, xmax, ymax = -180, -90, 180, 90 coords = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] assert Polygon(coords) == Polygon.from_bounds(xmin, ymin, xmax, ymax) def test_empty_polygon_exterior(self): p = Polygon() assert p.exterior == LinearRing() def test_linearring_immutable(): ring = LinearRing([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]) with pytest.raises(AttributeError): ring.coords = [(1.0, 1.0), (2.0, 2.0), (1.0, 2.0)] with pytest.raises(TypeError): ring.coords[0] = (1.0, 1.0) class TestLinearRingGetItem: def test_index_linearring(self): shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)]) holes = [ LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]), LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]), LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]), ] g = Polygon(shell, holes) for i in range(-3, 3): assert g.interiors[i].equals(holes[i]) with pytest.raises(IndexError): g.interiors[3] with pytest.raises(IndexError): g.interiors[-4] def test_index_linearring_misc(self): g = Polygon() # empty with pytest.raises(IndexError): g.interiors[0] with pytest.raises(TypeError): g.interiors[0.0] def test_slice_linearring(self): shell = LinearRing([(0.0, 0.0), (70.0, 120.0), (140.0, 0.0), (0.0, 0.0)]) holes = [ LinearRing([(60.0, 80.0), (80.0, 80.0), (70.0, 60.0), (60.0, 80.0)]), LinearRing([(30.0, 10.0), (50.0, 10.0), (40.0, 30.0), (30.0, 10.0)]), LinearRing([(90.0, 10), (110.0, 10.0), (100.0, 30.0), (90.0, 10.0)]), ] g = Polygon(shell, holes) t = [a.equals(b) for (a, b) in zip(g.interiors[1:], holes[1:])] assert all(t) t = [a.equals(b) for (a, b) in zip(g.interiors[:-1], holes[:-1])] assert all(t) t = [a.equals(b) for (a, b) in zip(g.interiors[::-1], holes[::-1])] assert all(t) t = [a.equals(b) for (a, b) in zip(g.interiors[::2], holes[::2])] assert all(t) t = [a.equals(b) for (a, b) in zip(g.interiors[:3], holes[:3])] assert all(t) assert g.interiors[3:] == holes[3:] == [] shapely-2.0.3/shapely/tests/legacy/000077500000000000000000000000001456366510000172425ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/legacy/__init__.py000066400000000000000000000004231456366510000213520ustar00rootroot00000000000000import sys import numpy from shapely.geos import geos_version_string # Show some diagnostic information; handy for CI print("Python version: " + sys.version.replace("\n", " ")) print("GEOS version: " + geos_version_string) print("Numpy version: " + numpy.version.version) shapely-2.0.3/shapely/tests/legacy/conftest.py000066400000000000000000000011141456366510000214360ustar00rootroot00000000000000import numpy import pytest from shapely.geos import geos_version requires_geos_38 = pytest.mark.skipif( geos_version < (3, 8, 0), reason="GEOS >= 3.8.0 is required." ) requires_geos_342 = pytest.mark.skipif( geos_version < (3, 4, 2), reason="GEOS > 3.4.2 is required." ) shapely20_todo = pytest.mark.xfail( strict=True, reason="Not yet implemented for Shapely 2.0" ) shapely20_wontfix = pytest.mark.xfail(strict=True, reason="Will fail for Shapely 2.0") def pytest_report_header(config): """Header for pytest.""" return f"dependencies: numpy-{numpy.__version__}" shapely-2.0.3/shapely/tests/legacy/data/000077500000000000000000000000001456366510000201535ustar00rootroot00000000000000shapely-2.0.3/shapely/tests/legacy/data/emptypoint_1.8.5.post1.pickle000066400000000000000000000001131456366510000253450ustar00rootroot00000000000000@shapely.geometry.pointPoint)RCb.shapely-2.0.3/shapely/tests/legacy/data/emptypolygon_1.8.5.post1.pickle000066400000000000000000000001031456366510000257020ustar00rootroot000000000000008shapely.geometry.polygonPolygon)RC b.shapely-2.0.3/shapely/tests/legacy/data/geometrycollection_1.8.5.post1.pickle000066400000000000000000000003031456366510000270450ustar00rootroot00000000000000shapely.geometry.collectionGeometryCollection)RC{?@?????b.shapely-2.0.3/shapely/tests/legacy/data/linearring_1.8.5.post1.pickle000066400000000000000000000002061456366510000252720ustar00rootroot00000000000000{shapely.geometry.polygon LinearRing)RCI???b.shapely-2.0.3/shapely/tests/legacy/data/linestring_1.8.5.post1.pickle000066400000000000000000000001711456366510000253170ustar00rootroot00000000000000nshapely.geometry.linestring LineString)RC9???b.shapely-2.0.3/shapely/tests/legacy/data/multilinestring_1.8.5.post1.pickle000066400000000000000000000002451456366510000263740ustar00rootroot00000000000000 shapely.geometry.multilinestringMultiLineString)RC[???@@@b.shapely-2.0.3/shapely/tests/legacy/data/multipoint_1.8.5.post1.pickle000066400000000000000000000002101456366510000253370ustar00rootroot00000000000000}shapely.geometry.multipoint MultiPoint)RCH?@@@@@b.shapely-2.0.3/shapely/tests/legacy/data/multipolygon_1.8.5.post1.pickle000066400000000000000000000004071456366510000257050ustar00rootroot00000000000000shapely.geometry.multipolygon MultiPolygon)RC?????@@@@@@@@@@b.shapely-2.0.3/shapely/tests/legacy/data/point2d_1.8.5.post1.pickle000066400000000000000000000001131456366510000245140ustar00rootroot00000000000000@shapely.geometry.pointPoint)RC?@b.shapely-2.0.3/shapely/tests/legacy/data/point3d_1.8.5.post1.pickle000066400000000000000000000001231456366510000245160ustar00rootroot00000000000000Hshapely.geometry.pointPoint)RC?@@b.shapely-2.0.3/shapely/tests/legacy/data/polygon_1.8.5.post1.pickle000066400000000000000000000002071456366510000246300ustar00rootroot00000000000000|shapely.geometry.polygonPolygon)RCM???b.shapely-2.0.3/shapely/tests/legacy/rungrind.dist000066400000000000000000000002251456366510000217560ustar00rootroot00000000000000#!/bin/sh #export PYTHONPATH=YOUR_CUSTOM_PATH valgrind --tool=memcheck --leak-check=yes --suppressions=valgrind-python.supp python test_doctests.py shapely-2.0.3/shapely/tests/legacy/test_affinity.py000066400000000000000000000303631456366510000224710ustar00rootroot00000000000000import unittest from math import pi import numpy as np import pytest from shapely import affinity from shapely.geometry import Point from shapely.wkt import loads as load_wkt class AffineTestCase(unittest.TestCase): def test_affine_params(self): g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)") with pytest.raises(TypeError): affinity.affine_transform(g, None) with pytest.raises(ValueError): affinity.affine_transform(g, [1, 2, 3, 4, 5, 6, 7, 8, 9]) with pytest.raises(AttributeError): affinity.affine_transform(None, [1, 2, 3, 4, 5, 6]) def test_affine_geom_types(self): # identity matrices, which should result with no transformation matrix2d = (1, 0, 0, 1, 0, 0) matrix3d = (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) # empty in, empty out empty2d = load_wkt("MULTIPOLYGON EMPTY") assert affinity.affine_transform(empty2d, matrix2d).is_empty def test_geom(g2, g3=None): assert not g2.has_z a2 = affinity.affine_transform(g2, matrix2d) assert not a2.has_z assert g2.equals(a2) if g3 is not None: assert g3.has_z a3 = affinity.affine_transform(g3, matrix3d) assert a3.has_z assert g3.equals(a3) return pt2d = load_wkt("POINT(12.3 45.6)") pt3d = load_wkt("POINT(12.3 45.6 7.89)") test_geom(pt2d, pt3d) ls2d = load_wkt("LINESTRING(0.9 3.4, 0.7 2, 2.5 2.7)") ls3d = load_wkt("LINESTRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5)") test_geom(ls2d, ls3d) lr2d = load_wkt("LINEARRING(0.9 3.4, 0.7 2, 2.5 2.7, 0.9 3.4)") lr3d = load_wkt("LINEARRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5, 0.9 3.4 3.3)") test_geom(lr2d, lr3d) test_geom( load_wkt( "POLYGON((0.9 2.3, 0.5 1.1, 2.4 0.8, 0.9 2.3), " "(1.1 1.7, 0.9 1.3, 1.4 1.2, 1.1 1.7), " "(1.6 1.3, 1.7 1, 1.9 1.1, 1.6 1.3))" ) ) test_geom( load_wkt("MULTIPOINT ((-300 300), (700 300), (-800 -1100), (200 -300))") ) test_geom( load_wkt( "MULTILINESTRING((0 0, -0.7 -0.7, 0.6 -1), " "(-0.5 0.5, 0.7 0.6, 0 -0.6))" ) ) test_geom( load_wkt( "MULTIPOLYGON(((900 4300, -1100 -400, 900 -800, 900 4300)), " "((1200 4300, 2300 4400, 1900 1000, 1200 4300)))" ) ) test_geom( load_wkt( "GEOMETRYCOLLECTION(POINT(20 70)," " POLYGON((60 70, 13 35, 60 -30, 60 70))," " LINESTRING(60 70, 50 100, 80 100))" ) ) def test_affine_2d(self): g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)") # custom scale and translate expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)") matrix2d = (2, 0, 0, 2.5, -5, 4.1) a2 = affinity.affine_transform(g, matrix2d) assert a2.equals_exact(expected2d, 1e-6) assert not a2.has_z # Make sure a 3D matrix does not make a 3D shape from a 2D input matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 10, -5, 4.1, 100) a3 = affinity.affine_transform(g, matrix3d) assert a3.equals_exact(expected2d, 1e-6) assert not a3.has_z def test_affine_3d(self): g2 = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)") g3 = load_wkt("LINESTRING(2.4 4.1 100.2, 2.4 3 132.8, 3 3 128.6)") # custom scale and translate matrix2d = (2, 0, 0, 2.5, -5, 4.1) matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 0.3048, -5, 4.1, 100) # Combinations of 2D and 3D geometries and matrices a22 = affinity.affine_transform(g2, matrix2d) a23 = affinity.affine_transform(g2, matrix3d) a32 = affinity.affine_transform(g3, matrix2d) a33 = affinity.affine_transform(g3, matrix3d) # Check dimensions assert not a22.has_z assert not a23.has_z assert a32.has_z assert a33.has_z # 2D equality checks expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)") expected3d = load_wkt( "LINESTRING(-0.2 14.35 130.54096, " "-0.2 11.6 140.47744, 1 11.6 139.19728)" ) expected32 = load_wkt( "LINESTRING(-0.2 14.35 100.2, " "-0.2 11.6 132.8, 1 11.6 128.6)" ) assert a22.equals_exact(expected2d, 1e-6) assert a23.equals_exact(expected2d, 1e-6) # Do explicit 3D check of coordinate values for a, e in zip(a32.coords, expected32.coords): for ap, ep in zip(a, e): self.assertAlmostEqual(ap, ep) for a, e in zip(a33.coords, expected3d.coords): for ap, ep in zip(a, e): self.assertAlmostEqual(ap, ep) class TransformOpsTestCase(unittest.TestCase): def test_rotate(self): ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)") # counter-clockwise degrees rls = affinity.rotate(ls, 90) els = load_wkt("LINESTRING(220 320, 320 320, 320 380)") assert rls.equals(els) # retest with named parameters for the same result rls = affinity.rotate(geom=ls, angle=90, origin="center") assert rls.equals(els) # clockwise radians rls = affinity.rotate(ls, -pi / 2, use_radians=True) els = load_wkt("LINESTRING(320 380, 220 380, 220 320)") assert rls.equals(els) ## other `origin` parameters # around the centroid rls = affinity.rotate(ls, 90, origin="centroid") els = load_wkt("LINESTRING(182.5 320, 282.5 320, 282.5 380)") assert rls.equals(els) # around the second coordinate tuple rls = affinity.rotate(ls, 90, origin=ls.coords[1]) els = load_wkt("LINESTRING(140 300, 240 300, 240 360)") assert rls.equals(els) # around the absolute Point of origin rls = affinity.rotate(ls, 90, origin=Point(0, 0)) els = load_wkt("LINESTRING(-400 240, -300 240, -300 300)") assert rls.equals(els) def test_rotate_empty(self): rls = affinity.rotate(load_wkt("LINESTRING EMPTY"), 90) els = load_wkt("LINESTRING EMPTY") assert rls.equals(els) def test_rotate_angle_array(self): ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)") els = load_wkt("LINESTRING(220 320, 320 320, 320 380)") # check with degrees theta = np.array(90.0) rls = affinity.rotate(ls, theta) assert theta.item() == 90.0 assert rls.equals(els) # check with radians theta = np.array(pi / 2) rls = affinity.rotate(ls, theta, use_radians=True) assert theta.item() == pi / 2 assert rls.equals(els) def test_scale(self): ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)") # test defaults of 1.0 sls = affinity.scale(ls) assert sls.equals(ls) # different scaling in different dimensions sls = affinity.scale(ls, 2, 3, 0.5) els = load_wkt("LINESTRING(210 500 5, 210 200 15, 330 200 10)") assert sls.equals(els) # Do explicit 3D check of coordinate values for a, b in zip(sls.coords, els.coords): for ap, bp in zip(a, b): self.assertEqual(ap, bp) # retest with named parameters for the same result sls = affinity.scale(geom=ls, xfact=2, yfact=3, zfact=0.5, origin="center") assert sls.equals(els) ## other `origin` parameters # around the centroid sls = affinity.scale(ls, 2, 3, 0.5, origin="centroid") els = load_wkt("LINESTRING(228.75 537.5, 228.75 237.5, 348.75 237.5)") assert sls.equals(els) # around the second coordinate tuple sls = affinity.scale(ls, 2, 3, 0.5, origin=ls.coords[1]) els = load_wkt("LINESTRING(240 600, 240 300, 360 300)") assert sls.equals(els) # around some other 3D Point of origin sls = affinity.scale(ls, 2, 3, 0.5, origin=Point(100, 200, 1000)) els = load_wkt("LINESTRING(380 800 505, 380 500 515, 500 500 510)") assert sls.equals(els) # Do explicit 3D check of coordinate values for a, b in zip(sls.coords, els.coords): for ap, bp in zip(a, b): assert ap == bp def test_scale_empty(self): sls = affinity.scale(load_wkt("LINESTRING EMPTY")) els = load_wkt("LINESTRING EMPTY") assert sls.equals(els) def test_skew(self): ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)") # test default shear angles of 0.0 sls = affinity.skew(ls) assert sls.equals(ls) # different shearing in x- and y-directions sls = affinity.skew(ls, 15, -30) els = load_wkt( "LINESTRING (253.39745962155615 417.3205080756888, " "226.60254037844385 317.3205080756888, " "286.60254037844385 282.67949192431126)" ) assert sls.equals_exact(els, 1e-6) # retest with radians for the same result sls = affinity.skew(ls, pi / 12, -pi / 6, use_radians=True) assert sls.equals_exact(els, 1e-6) # retest with named parameters for the same result sls = affinity.skew(geom=ls, xs=15, ys=-30, origin="center", use_radians=False) assert sls.equals_exact(els, 1e-6) ## other `origin` parameters # around the centroid sls = affinity.skew(ls, 15, -30, origin="centroid") els = load_wkt( "LINESTRING(258.42150697963973 406.49519052838332, " "231.6265877365273980 306.4951905283833185, " "291.6265877365274264 271.8541743770057337)" ) assert sls.equals_exact(els, 1e-6) # around the second coordinate tuple sls = affinity.skew(ls, 15, -30, origin=ls.coords[1]) els = load_wkt( "LINESTRING(266.7949192431123038 400, 240 300, " "300 265.3589838486224153)" ) assert sls.equals_exact(els, 1e-6) # around the absolute Point of origin sls = affinity.skew(ls, 15, -30, origin=Point(0, 0)) els = load_wkt( "LINESTRING(347.179676972449101 261.435935394489832, " "320.3847577293367976 161.4359353944898317, " "380.3847577293367976 126.7949192431122754)" ) assert sls.equals_exact(els, 1e-6) def test_skew_empty(self): sls = affinity.skew(load_wkt("LINESTRING EMPTY")) els = load_wkt("LINESTRING EMPTY") assert sls.equals(els) def test_skew_xs_ys_array(self): ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)") els = load_wkt( "LINESTRING (253.39745962155615 417.3205080756888, " "226.60254037844385 317.3205080756888, " "286.60254037844385 282.67949192431126)" ) # check with degrees xs_ys = np.array([15.0, -30.0]) sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...]) assert xs_ys[0] == 15.0 assert xs_ys[1] == -30.0 assert sls.equals_exact(els, 1e-6) # check with radians xs_ys = np.array([pi / 12, -pi / 6]) sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...], use_radians=True) assert xs_ys[0] == pi / 12 assert xs_ys[1] == -pi / 6 assert sls.equals_exact(els, 1e-6) def test_translate(self): ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)") # test default offset of 0.0 tls = affinity.translate(ls) assert tls.equals(ls) # test all offsets tls = affinity.translate(ls, 100, 400, -10) els = load_wkt("LINESTRING(340 800 0, 340 700 20, 400 700 10)") assert tls.equals(els) # Do explicit 3D check of coordinate values for a, b in zip(tls.coords, els.coords): for ap, bp in zip(a, b): assert ap == bp # retest with named parameters for the same result tls = affinity.translate(geom=ls, xoff=100, yoff=400, zoff=-10) assert tls.equals(els) def test_translate_empty(self): tls = affinity.translate(load_wkt("LINESTRING EMPTY")) els = load_wkt("LINESTRING EMPTY") self.assertTrue(tls.equals(els)) assert tls.equals(els) shapely-2.0.3/shapely/tests/legacy/test_box.py000066400000000000000000000011271456366510000214440ustar00rootroot00000000000000import unittest from shapely import geometry class BoxTestCase(unittest.TestCase): def test_ccw(self): b = geometry.box(0, 0, 1, 1, ccw=True) assert b.exterior.coords[0] == (1.0, 0.0) assert b.exterior.coords[1] == (1.0, 1.0) def test_ccw_default(self): b = geometry.box(0, 0, 1, 1) assert b.exterior.coords[0] == (1.0, 0.0) assert b.exterior.coords[1] == (1.0, 1.0) def test_cw(self): b = geometry.box(0, 0, 1, 1, ccw=False) assert b.exterior.coords[0] == (0.0, 0.0) assert b.exterior.coords[1] == (0.0, 1.0) shapely-2.0.3/shapely/tests/legacy/test_buffer.py000066400000000000000000000145711456366510000221340ustar00rootroot00000000000000import unittest import pytest from shapely import geometry from shapely.constructive import BufferCapStyle, BufferJoinStyle from shapely.geometry.base import CAP_STYLE, JOIN_STYLE @pytest.mark.parametrize("distance", [float("nan"), float("inf")]) def test_non_finite_distance(distance): g = geometry.Point(0, 0) with pytest.raises(ValueError, match="distance must be finite"): g.buffer(distance) class BufferTests(unittest.TestCase): """Test Buffer Point/Line/Polygon with and without single_sided params""" def test_empty(self): g = geometry.Point(0, 0) h = g.buffer(0) assert h.is_empty def test_point(self): g = geometry.Point(0, 0) h = g.buffer(1, quad_segs=1) assert h.geom_type == "Polygon" expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_point_single_sidedd(self): g = geometry.Point(0, 0) h = g.buffer(1, quad_segs=1, single_sided=True) assert h.geom_type == "Polygon" expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_line(self): g = geometry.LineString([[0, 0], [0, 1]]) h = g.buffer(1, quad_segs=1) assert h.geom_type == "Polygon" expected_coord = [ (-1.0, 1.0), (0, 2.0), (1.0, 1.0), (1.0, 0.0), (0, -1.0), (-1.0, 0.0), (-1.0, 1.0), ] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_line_single_sideded_left(self): g = geometry.LineString([[0, 0], [0, 1]]) h = g.buffer(1, quad_segs=1, single_sided=True) assert h.geom_type == "Polygon" expected_coord = [(0.0, 1.0), (0.0, 0.0), (-1.0, 0.0), (-1.0, 1.0), (0.0, 1.0)] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_line_single_sideded_right(self): g = geometry.LineString([[0, 0], [0, 1]]) h = g.buffer(-1, quad_segs=1, single_sided=True) assert h.geom_type == "Polygon" expected_coord = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_polygon(self): g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]) h = g.buffer(1, quad_segs=1) assert h.geom_type == "Polygon" expected_coord = [ (-1.0, 0.0), (-1.0, 1.0), (0.0, 2.0), (1.0, 2.0), (2.0, 1.0), (2.0, 0.0), (1.0, -1.0), (0.0, -1.0), (-1.0, 0.0), ] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_polygon_single_sideded(self): g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]) h = g.buffer(1, quad_segs=1, single_sided=True) assert h.geom_type == "Polygon" expected_coord = [ (-1.0, 0.0), (-1.0, 1.0), (0.0, 2.0), (1.0, 2.0), (2.0, 1.0), (2.0, 0.0), (1.0, -1.0), (0.0, -1.0), (-1.0, 0.0), ] for index, coord in enumerate(h.exterior.coords): assert coord[0] == pytest.approx(expected_coord[index][0]) assert coord[1] == pytest.approx(expected_coord[index][1]) def test_enum_values(self): assert CAP_STYLE.round == 1 assert CAP_STYLE.round == BufferCapStyle.round assert CAP_STYLE.flat == 2 assert CAP_STYLE.flat == BufferCapStyle.flat assert CAP_STYLE.square == 3 assert CAP_STYLE.square == BufferCapStyle.square assert JOIN_STYLE.round == 1 assert JOIN_STYLE.round == BufferJoinStyle.round assert JOIN_STYLE.mitre == 2 assert JOIN_STYLE.mitre == BufferJoinStyle.mitre assert JOIN_STYLE.bevel == 3 assert JOIN_STYLE.bevel == BufferJoinStyle.bevel def test_cap_style(self): g = geometry.LineString([[0, 0], [1, 0]]) h = g.buffer(1, cap_style=BufferCapStyle.round) assert h == g.buffer(1, cap_style=CAP_STYLE.round) assert h == g.buffer(1, cap_style="round") h = g.buffer(1, cap_style=BufferCapStyle.flat) assert h == g.buffer(1, cap_style=CAP_STYLE.flat) assert h == g.buffer(1, cap_style="flat") h = g.buffer(1, cap_style=BufferCapStyle.square) assert h == g.buffer(1, cap_style=CAP_STYLE.square) assert h == g.buffer(1, cap_style="square") def test_buffer_style(self): g = geometry.LineString([[0, 0], [1, 0]]) h = g.buffer(1, join_style=BufferJoinStyle.round) assert h == g.buffer(1, join_style=JOIN_STYLE.round) assert h == g.buffer(1, join_style="round") h = g.buffer(1, join_style=BufferJoinStyle.mitre) assert h == g.buffer(1, join_style=JOIN_STYLE.mitre) assert h == g.buffer(1, join_style="mitre") h = g.buffer(1, join_style=BufferJoinStyle.bevel) assert h == g.buffer(1, join_style=JOIN_STYLE.bevel) assert h == g.buffer(1, join_style="bevel") def test_deprecated_quadsegs(): point = geometry.Point(0, 0) with pytest.warns(FutureWarning): result = point.buffer(1, quadsegs=1) expected = point.buffer(1, quad_segs=1) assert result.equals(expected) def test_resolution_alias(): point = geometry.Point(0, 0) result = point.buffer(1, resolution=1) expected = point.buffer(1, quad_segs=1) assert result.equals(expected) shapely-2.0.3/shapely/tests/legacy/test_cga.py000066400000000000000000000027451456366510000214150ustar00rootroot00000000000000import unittest import pytest from shapely.geometry.polygon import LinearRing, orient, Polygon, signed_area class SignedAreaTestCase(unittest.TestCase): def test_triangle(self): tri = LinearRing([(0, 0), (2, 5), (7, 0)]) assert signed_area(tri) == pytest.approx(-7 * 5 / 2) def test_square(self): xmin, xmax = (-1, 1) ymin, ymax = (-2, 3) rect = LinearRing( [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax), (xmin, ymin)] ) assert signed_area(rect) == pytest.approx(10.0) class RingOrientationTestCase(unittest.TestCase): def test_ccw(self): ring = LinearRing([(1, 0), (0, 1), (0, 0)]) assert ring.is_ccw def test_cw(self): ring = LinearRing([(0, 0), (0, 1), (1, 0)]) assert not ring.is_ccw class PolygonOrienterTestCase(unittest.TestCase): def test_no_holes(self): ring = LinearRing([(0, 0), (0, 1), (1, 0)]) polygon = Polygon(ring) assert not polygon.exterior.is_ccw polygon = orient(polygon, 1) assert polygon.exterior.is_ccw def test_holes(self): # fmt: off polygon = Polygon( [(0, 0), (0, 1), (1, 0)], [[(0.5, 0.25), (0.25, 0.5), (0.25, 0.25)]] ) # fmt: on assert not polygon.exterior.is_ccw assert polygon.interiors[0].is_ccw polygon = orient(polygon, 1) assert polygon.exterior.is_ccw assert not polygon.interiors[0].is_ccw shapely-2.0.3/shapely/tests/legacy/test_clip_by_rect.py000066400000000000000000000101241456366510000233070ustar00rootroot00000000000000""" Tests for GEOSClipByRect based on unit tests from libgeos. There are some expected differences due to Shapely's handling of empty geometries. """ import pytest from shapely.ops import clip_by_rect from shapely.wkt import dumps as dump_wkt from shapely.wkt import loads as load_wkt def test_point_outside(): """Point outside""" geom1 = load_wkt("POINT (0 0)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def test_point_inside(): """Point inside""" geom1 = load_wkt("POINT (15 15)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "POINT (15 15)" def test_point_on_boundary(): """Point on boundary""" geom1 = load_wkt("POINT (15 10)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def test_line_outside(): """Line outside""" geom1 = load_wkt("LINESTRING (0 0, -5 5)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def test_line_inside(): """Line inside""" geom1 = load_wkt("LINESTRING (15 15, 16 15)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "LINESTRING (15 15, 16 15)" def test_line_on_boundary(): """Line on boundary""" geom1 = load_wkt("LINESTRING (10 15, 10 10, 15 10)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def test_line_splitting_rectangle(): """Line splitting rectangle""" geom1 = load_wkt("LINESTRING (10 5, 25 20)") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "LINESTRING (15 10, 20 15)" @pytest.mark.xfail(reason="TODO issue to CCW") def test_polygon_shell_ccw_fully_on_rectangle_boundary(): """Polygon shell (CCW) fully on rectangle boundary""" geom1 = load_wkt("POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert ( dump_wkt(geom2, rounding_precision=0) == "POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))" ) @pytest.mark.xfail(reason="TODO issue to CW") def test_polygon_shell_cc_fully_on_rectangle_boundary(): """Polygon shell (CW) fully on rectangle boundary""" geom1 = load_wkt("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))") geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert ( dump_wkt(geom2, rounding_precision=0) == "POLYGON ((10 10, 20 10, 20 20, 10 20, 10 10))" ) def polygon_hole_ccw_fully_on_rectangle_boundary(): """Polygon hole (CCW) fully on rectangle boundary""" geom1 = load_wkt( "POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 20 10, 20 20, 10 20, 10 10))" ) geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def polygon_hole_cw_fully_on_rectangle_boundary(): """Polygon hole (CW) fully on rectangle boundary""" geom1 = load_wkt( "POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 10 20, 20 20, 20 10, 10 10))" ) geom2 = clip_by_rect(geom1, 10, 10, 20, 20) assert dump_wkt(geom2, rounding_precision=0) == "GEOMETRYCOLLECTION EMPTY" def polygon_fully_within_rectangle(): """Polygon fully within rectangle""" wkt = "POLYGON ((1 1, 1 30, 30 30, 30 1, 1 1), (10 10, 20 10, 20 20, 10 20, 10 10))" geom1 = load_wkt(wkt) geom2 = clip_by_rect(geom1, 0, 0, 40, 40) assert dump_wkt(geom2, rounding_precision=0) == wkt def polygon_overlapping_rectangle(): """Polygon overlapping rectangle""" wkt = "POLYGON ((0 0, 0 30, 30 30, 30 0, 0 0), (10 10, 20 10, 20 20, 10 20, 10 10))" geom1 = load_wkt(wkt) geom2 = clip_by_rect(geom1, 5, 5, 15, 15) assert ( dump_wkt(geom2, rounding_precision=0) == "POLYGON ((5 5, 5 15, 10 15, 10 10, 15 10, 15 5, 5 5))" ) shapely-2.0.3/shapely/tests/legacy/test_create_inconsistent_dimensionality.py000066400000000000000000000246241456366510000300360ustar00rootroot00000000000000""" When a "context" passed to shape/asShape has a coordinate which is missing a dimension we should raise a descriptive error. When we use mixed dimensions in a WKT geometry, the parser strips any dimension which is not present in every coordinate. """ import pytest from shapely import wkt from shapely.errors import GEOSException from shapely.geometry import LineString, Polygon, shape from shapely.geos import geos_version geojson_cases = [ {"type": "LineString", "coordinates": [[1, 1, 1], [2, 2]]}, # Specific test case from #869 { "type": "Polygon", "coordinates": [ [ [55.12916764533149, 24.980385694214384, 2.5], [55.13098248044217, 24.979828079961905], [55.13966519231666, 24.97801442415322], [55.13966563924936, 24.97801442415322], [55.14139286840762, 24.982307444496097], [55.14169331277646, 24.983717465495562], [55.14203489144224, 24.985419446276566, 2.5], [55.14180327151276, 24.98428602667792, 2.5], [55.14170091915952, 24.984242720177235, 2.5], [55.14122966992623, 24.984954809433702, 2.5], [55.14134021791831, 24.985473928648396, 2.5], [55.141405876161286, 24.986090184809793, 2.5], [55.141361358941225, 24.986138101357326, 2.5], [55.14093322994411, 24.986218753894093, 2.5], [55.140897653420964, 24.986214283545635, 2.5], [55.14095492976058, 24.9863027591922, 2.5], [55.140900447388745, 24.98628436557094, 2.5], [55.140867059473706, 24.98628869622101, 2.5], [55.14089155325796, 24.986402364143782, 2.5], [55.14090938808566, 24.986479011993385, 2.5], [55.140943893587824, 24.986471188883584, 2.5], [55.1410161176551, 24.9864174050037, 2.5], [55.140996932409635, 24.986521806266644, 2.5], [55.14163554031332, 24.986910400619593, 2.5], [55.14095781686062, 24.987033474900578, 2.5], [55.14058258698692, 24.98693261266349, 2.5], [55.14032624044253, 24.98747538747211, 2.5], [55.14007240846915, 24.988001119077232, 2.5], [55.14013122149105, 24.98831115636925, 2.5], [55.13991827457961, 24.98834356639557, 2.5], [55.139779460946755, 24.988254625087706, 2.5], [55.13974742344948, 24.988261377176524, 2.5], [55.139515198160304, 24.98841811876934, 2.5], [55.13903617238334, 24.98817914139135, 2.5], [55.1391330764994, 24.988660542040925, 2.5], [55.13914369357698, 24.989438289540374, 2.5], [55.136431216517785, 24.98966711550207, 2.0], [55.13659028641709, 24.99041706302204, 2.0], [55.1355852030721, 24.990933481401207, 2.5], [55.13535549235394, 24.99110470506038, 2.5], [55.13512578163577, 24.99127592871955, 2.5], [55.129969653784556, 24.991440074326995, 2.5], [55.130221623112746, 24.988070688875112, 2.5], [55.130451333830905, 24.98789946521594, 2.5], [55.13089208224919, 24.98742639990359, 2.5], [55.132177586827666, 24.989003408454433, 2.5], [55.13238862452779, 24.988701566801254, 2.5], [55.132482594977674, 24.988501518707757, 2.5], [55.132525994610624, 24.988048802794115, 2.5], [55.13249018525683, 24.987180623870653, 2.5], [55.13253358488978, 24.986727907957015, 2.5], [55.1322761673244, 24.985827132742713, 2.5], [55.13163341503516, 24.98503862846729, 2.5], [55.131514764536504, 24.984469124700183, 2.5], [55.131275600894, 24.983796337257242, 2.0], [55.13066865795855, 24.98387601190528, 2.0], [55.13026930682963, 24.981537228037503, 2.0], [55.130260412698846, 24.981495691049748, 2.0], [55.13025151856806, 24.981454154061993, 2.0], [55.13022925995803, 24.98096497686874, 2.5], [55.12984453059386, 24.9804285816199, 2.5], [55.129998291954365, 24.98021419115843, 2.5], [55.12916764533149, 24.980385694214384, 2.5], ] ], }, ] direct_cases = [ (LineString, [[[0, 0, 0], [1, 1]]]), (Polygon, [[[0, 0, 0], [1, 1, 0], [1, 1], [0, 1, 0], [0, 0, 0]]]), # Specific test case from #869 ( Polygon, [ [ [55.12916764533149, 24.980385694214384, 2.5], [55.13098248044217, 24.979828079961905], [55.13966519231666, 24.97801442415322], [55.13966563924936, 24.97801442415322], [55.14139286840762, 24.982307444496097], [55.14169331277646, 24.983717465495562], [55.14203489144224, 24.985419446276566, 2.5], [55.14180327151276, 24.98428602667792, 2.5], [55.14170091915952, 24.984242720177235, 2.5], [55.14122966992623, 24.984954809433702, 2.5], [55.14134021791831, 24.985473928648396, 2.5], [55.141405876161286, 24.986090184809793, 2.5], [55.141361358941225, 24.986138101357326, 2.5], [55.14093322994411, 24.986218753894093, 2.5], [55.140897653420964, 24.986214283545635, 2.5], [55.14095492976058, 24.9863027591922, 2.5], [55.140900447388745, 24.98628436557094, 2.5], [55.140867059473706, 24.98628869622101, 2.5], [55.14089155325796, 24.986402364143782, 2.5], [55.14090938808566, 24.986479011993385, 2.5], [55.140943893587824, 24.986471188883584, 2.5], [55.1410161176551, 24.9864174050037, 2.5], [55.140996932409635, 24.986521806266644, 2.5], [55.14163554031332, 24.986910400619593, 2.5], [55.14095781686062, 24.987033474900578, 2.5], [55.14058258698692, 24.98693261266349, 2.5], [55.14032624044253, 24.98747538747211, 2.5], [55.14007240846915, 24.988001119077232, 2.5], [55.14013122149105, 24.98831115636925, 2.5], [55.13991827457961, 24.98834356639557, 2.5], [55.139779460946755, 24.988254625087706, 2.5], [55.13974742344948, 24.988261377176524, 2.5], [55.139515198160304, 24.98841811876934, 2.5], [55.13903617238334, 24.98817914139135, 2.5], [55.1391330764994, 24.988660542040925, 2.5], [55.13914369357698, 24.989438289540374, 2.5], [55.136431216517785, 24.98966711550207, 2.0], [55.13659028641709, 24.99041706302204, 2.0], [55.1355852030721, 24.990933481401207, 2.5], [55.13535549235394, 24.99110470506038, 2.5], [55.13512578163577, 24.99127592871955, 2.5], [55.129969653784556, 24.991440074326995, 2.5], [55.130221623112746, 24.988070688875112, 2.5], [55.130451333830905, 24.98789946521594, 2.5], [55.13089208224919, 24.98742639990359, 2.5], [55.132177586827666, 24.989003408454433, 2.5], [55.13238862452779, 24.988701566801254, 2.5], [55.132482594977674, 24.988501518707757, 2.5], [55.132525994610624, 24.988048802794115, 2.5], [55.13249018525683, 24.987180623870653, 2.5], [55.13253358488978, 24.986727907957015, 2.5], [55.1322761673244, 24.985827132742713, 2.5], [55.13163341503516, 24.98503862846729, 2.5], [55.131514764536504, 24.984469124700183, 2.5], [55.131275600894, 24.983796337257242, 2.0], [55.13066865795855, 24.98387601190528, 2.0], [55.13026930682963, 24.981537228037503, 2.0], [55.130260412698846, 24.981495691049748, 2.0], [55.13025151856806, 24.981454154061993, 2.0], [55.13022925995803, 24.98096497686874, 2.5], [55.12984453059386, 24.9804285816199, 2.5], [55.129998291954365, 24.98021419115843, 2.5], [55.12916764533149, 24.980385694214384, 2.5], ] ], ), ] wkt_cases = [ # preserve 3rd dimension ("MULTIPOINT (1 1 1, 2 2)", "MULTIPOINT Z (1 1 1, 2 2 0)"), ("MULTIPOINT (1 1, 2 2 2)", "MULTIPOINT Z (1 1 0, 2 2 2)"), ("LINESTRING (1 1 1, 2 2)", "LINESTRING Z (1 1 1, 2 2 0)"), ( "POLYGON ((0 0 0, 1 0 0, 1 1, 0 1 0, 0 0 0))", "POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 1 0, 0 0 0))", ), # drop 3rd dimension ("LINESTRING (1 1, 2 2 2)", "LINESTRING (1 1, 2 2)"), ("POLYGON ((0 0, 1 0 1, 1 1, 0 1, 0 0))", "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"), ] @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:") @pytest.mark.parametrize("geojson", geojson_cases) def test_create_from_geojson(geojson): # exact error depends on numpy version with pytest.raises((ValueError, TypeError)) as exc: shape(geojson).wkt assert exc.match( "Inconsistent coordinate dimensionality|Input operand 0 does not have enough dimensions|ufunc 'linestrings' not supported for the input types|setting an array element with a sequence. The requested array has an inhomogeneous shape" ) @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:") @pytest.mark.parametrize("constructor, args", direct_cases) def test_create_directly(constructor, args): with pytest.raises((ValueError, TypeError)) as exc: constructor(*args) assert exc.match( "Inconsistent coordinate dimensionality|Input operand 0 does not have enough dimensions|ufunc 'linestrings' not supported for the input types|setting an array element with a sequence. The requested array has an inhomogeneous shape" ) @pytest.mark.parametrize("wkt_geom,expected", wkt_cases) def test_create_from_wkt(wkt_geom, expected): if geos_version >= (3, 12, 0): # https://github.com/shapely/shapely/issues/1541 with pytest.raises(GEOSException): wkt.loads(wkt_geom) else: geom = wkt.loads(wkt_geom) assert geom.wkt == expected shapely-2.0.3/shapely/tests/legacy/test_delaunay.py000066400000000000000000000015061456366510000224570ustar00rootroot00000000000000import unittest from shapely.geometry import LineString, Point, Polygon from shapely.ops import triangulate class DelaunayTriangulation(unittest.TestCase): """ Only testing the number of triangles and their type here. This doesn't actually test the points in the resulting geometries. """ def setUp(self): self.p = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) def test_polys(self): polys = triangulate(self.p) assert len(polys) == 2 for p in polys: assert isinstance(p, Polygon) def test_lines(self): polys = triangulate(self.p, edges=True) assert len(polys) == 5 for p in polys: assert isinstance(p, LineString) def test_point(self): p = Point(1, 1) polys = triangulate(p) assert len(polys) == 0 shapely-2.0.3/shapely/tests/legacy/test_empty_polygons.py000066400000000000000000000012531456366510000237440ustar00rootroot00000000000000from shapely.geometry import MultiPolygon, Point, Polygon def test_empty_polygon(): """No constructor arg makes an empty polygon geometry.""" assert Polygon().is_empty def test_empty_multipolygon(): """No constructor arg makes an empty multipolygon geometry.""" assert MultiPolygon().is_empty def test_multipolygon_empty_polygon(): """An empty polygon passed to MultiPolygon() makes an empty multipolygon geometry.""" assert MultiPolygon([Polygon()]).is_empty def test_multipolygon_empty_among_polygon(): """An empty polygon passed to MultiPolygon() is ignored.""" assert len(MultiPolygon([Point(0, 0).buffer(1.0), Polygon()]).geoms) == 1 shapely-2.0.3/shapely/tests/legacy/test_equality.py000066400000000000000000000016431456366510000225140ustar00rootroot00000000000000import pytest from shapely import Point from shapely.errors import ShapelyDeprecationWarning def test_equals_exact(): p1 = Point(1.0, 1.0) p2 = Point(2.0, 2.0) assert not p1.equals(p2) assert not p1.equals_exact(p2, 0.001) assert p1.equals_exact(p2, 1.42) def test_almost_equals_default(): p1 = Point(1.0, 1.0) p2 = Point(1.0 + 1e-7, 1.0 + 1e-7) # almost equal to 6 places p3 = Point(1.0 + 1e-6, 1.0 + 1e-6) # not almost equal with pytest.warns(ShapelyDeprecationWarning): assert p1.almost_equals(p2) with pytest.warns(ShapelyDeprecationWarning): assert not p1.almost_equals(p3) def test_almost_equals(): p1 = Point(1.0, 1.0) p2 = Point(1.1, 1.1) assert not p1.equals(p2) with pytest.warns(ShapelyDeprecationWarning): assert p1.almost_equals(p2, 0) with pytest.warns(ShapelyDeprecationWarning): assert not p1.almost_equals(p2, 1) shapely-2.0.3/shapely/tests/legacy/test_geointerface.py000066400000000000000000000071001456366510000233040ustar00rootroot00000000000000import unittest from shapely import wkt from shapely.geometry import shape from shapely.geometry.linestring import LineString from shapely.geometry.multilinestring import MultiLineString from shapely.geometry.multipoint import MultiPoint from shapely.geometry.multipolygon import MultiPolygon from shapely.geometry.polygon import LinearRing, Polygon class GeoThing: def __init__(self, d): self.__geo_interface__ = d class GeoInterfaceTestCase(unittest.TestCase): def test_geointerface(self): # Convert a dictionary d = {"type": "Point", "coordinates": (0.0, 0.0)} geom = shape(d) assert geom.geom_type == "Point" assert tuple(geom.coords) == ((0.0, 0.0),) # Convert an object that implements the geo protocol geom = None thing = GeoThing({"type": "Point", "coordinates": (0.0, 0.0)}) geom = shape(thing) assert geom.geom_type == "Point" assert tuple(geom.coords) == ((0.0, 0.0),) # Check line string geom = shape({"type": "LineString", "coordinates": ((-1.0, -1.0), (1.0, 1.0))}) assert isinstance(geom, LineString) assert tuple(geom.coords) == ((-1.0, -1.0), (1.0, 1.0)) # Check linearring geom = shape( { "type": "LinearRing", "coordinates": ( (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0), (0.0, 0.0), ), } ) assert isinstance(geom, LinearRing) assert tuple(geom.coords) == ( (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0), (0.0, 0.0), ) # polygon geom = shape( { "type": "Polygon", "coordinates": ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0), (0.0, 0.0)), ((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)), ), } ) assert isinstance(geom, Polygon) assert tuple(geom.exterior.coords) == ( (0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0), (0.0, 0.0), ) assert len(geom.interiors) == 1 # multi point geom = shape({"type": "MultiPoint", "coordinates": ((1.0, 2.0), (3.0, 4.0))}) assert isinstance(geom, MultiPoint) assert len(geom.geoms) == 2 # multi line string geom = shape( {"type": "MultiLineString", "coordinates": (((0.0, 0.0), (1.0, 2.0)),)} ) assert isinstance(geom, MultiLineString) assert len(geom.geoms) == 1 # multi polygon geom = shape( { "type": "MultiPolygon", "coordinates": [ ( ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)), ((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1), (0.1, 0.1)), ) ], } ) assert isinstance(geom, MultiPolygon) assert len(geom.geoms) == 1 def test_empty_wkt_polygon(): """Confirm fix for issue #450""" g = wkt.loads("POLYGON EMPTY") assert g.__geo_interface__["type"] == "Polygon" assert g.__geo_interface__["coordinates"] == () def test_empty_polygon(): """Confirm fix for issue #450""" g = Polygon() assert g.__geo_interface__["type"] == "Polygon" assert g.__geo_interface__["coordinates"] == () shapely-2.0.3/shapely/tests/legacy/test_invalid_geometries.py000066400000000000000000000016131456366510000245250ustar00rootroot00000000000000"""Test recovery from operation on invalid geometries """ import unittest import pytest import shapely from shapely.errors import TopologicalError from shapely.geometry import Polygon class InvalidGeometriesTestCase(unittest.TestCase): def test_invalid_intersection(self): # Make a self-intersecting polygon polygon_invalid = Polygon([(0, 0), (1, 1), (1, -1), (0, 1), (0, 0)]) assert not polygon_invalid.is_valid # Intersect with a valid polygon polygon = Polygon([(-0.5, -0.5), (-0.5, 0.5), (0.5, 0.5), (0.5, -5)]) assert polygon.is_valid assert polygon_invalid.intersects(polygon) with pytest.raises((TopologicalError, shapely.GEOSException)): polygon_invalid.intersection(polygon) with pytest.raises((TopologicalError, shapely.GEOSException)): polygon.intersection(polygon_invalid) return shapely-2.0.3/shapely/tests/legacy/test_linear_referencing.py000066400000000000000000000055421456366510000245020ustar00rootroot00000000000000import unittest import pytest import shapely from shapely.geometry import LineString, MultiLineString, Point class LinearReferencingTestCase(unittest.TestCase): def setUp(self): self.point = Point(1, 1) self.line1 = LineString([(0, 0), (2, 0)]) self.line2 = LineString([(3, 0), (3, 6)]) self.multiline = MultiLineString( [list(self.line1.coords), list(self.line2.coords)] ) def test_line1_project(self): assert self.line1.project(self.point) == 1.0 assert self.line1.project(self.point, normalized=True) == 0.5 def test_alias_project(self): assert self.line1.line_locate_point(self.point) == 1.0 assert self.line1.line_locate_point(self.point, normalized=True) == 0.5 def test_line2_project(self): assert self.line2.project(self.point) == 1.0 assert self.line2.project(self.point, normalized=True) == pytest.approx( 0.16666666666, 8 ) def test_multiline_project(self): assert self.multiline.project(self.point) == 1.0 assert self.multiline.project(self.point, normalized=True) == 0.125 def test_not_supported_project(self): with pytest.raises(shapely.GEOSException, match="IllegalArgumentException"): self.point.buffer(1.0).project(self.point) def test_not_on_line_project(self): # Points that aren't on the line project to 0. assert self.line1.project(Point(-10, -10)) == 0.0 def test_line1_interpolate(self): assert self.line1.interpolate(0.5).equals(Point(0.5, 0.0)) assert self.line1.interpolate(-0.5).equals(Point(1.5, 0.0)) assert self.line1.interpolate(0.5, normalized=True).equals(Point(1, 0)) assert self.line1.interpolate(-0.5, normalized=True).equals(Point(1, 0)) def test_alias_interpolate(self): assert self.line1.line_interpolate_point(0.5).equals(Point(0.5, 0.0)) assert self.line1.line_interpolate_point(-0.5).equals(Point(1.5, 0.0)) assert self.line1.line_interpolate_point(0.5, normalized=True).equals( Point(1, 0) ) assert self.line1.line_interpolate_point(-0.5, normalized=True).equals( Point(1, 0) ) def test_line2_interpolate(self): assert self.line2.interpolate(0.5).equals(Point(3.0, 0.5)) assert self.line2.interpolate(0.5, normalized=True).equals(Point(3, 3)) def test_multiline_interpolate(self): assert self.multiline.interpolate(0.5).equals(Point(0.5, 0)) assert self.multiline.interpolate(0.5, normalized=True).equals(Point(3.0, 2.0)) def test_line_ends_interpolate(self): # Distances greater than length of the line or less than # zero yield the line's ends. assert self.line1.interpolate(-1000).equals(Point(0.0, 0.0)) assert self.line1.interpolate(1000).equals(Point(2.0, 0.0)) shapely-2.0.3/shapely/tests/legacy/test_linemerge.py000066400000000000000000000024621456366510000226260ustar00rootroot00000000000000import unittest from shapely.geometry import LineString, MultiLineString from shapely.ops import linemerge class LineMergeTestCase(unittest.TestCase): def test_linemerge(self): lines = MultiLineString([[(0, 0), (1, 1)], [(2, 0), (2, 1), (1, 1)]]) result = linemerge(lines) assert isinstance(result, LineString) assert not result.is_ring assert len(result.coords) == 4 assert result.coords[0] == (0.0, 0.0) assert result.coords[3] == (2.0, 0.0) lines2 = MultiLineString([((0, 0), (1, 1)), ((0, 0), (2, 0), (2, 1), (1, 1))]) result = linemerge(lines2) assert result.is_ring assert len(result.coords) == 5 lines3 = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), ] result = linemerge(lines3) assert not result.is_ring assert len(result.coords) == 3 assert result.coords[0] == (0.0, 1.0) assert result.coords[2] == (1.0, 1.0) lines4 = [ [(0, 0), (1, 1)], [(0, 0), (0, 1)], ] assert result.equals(linemerge(lines4)) lines5 = [ ((0, 0), (1, 1)), ((1, 0), (0, 1)), ] result = linemerge(lines5) assert result.geom_type == "MultiLineString" shapely-2.0.3/shapely/tests/legacy/test_locale.py000066400000000000000000000026341456366510000221170ustar00rootroot00000000000000"""Test locale independence of WKT """ import locale import sys import unittest from shapely.wkt import dumps, loads # Set locale to one that uses a comma as decimal separator # TODO: try a few other common locales if sys.platform == "win32": test_locales = {"Portuguese": "portuguese_brazil", "Italian": "italian_italy"} else: test_locales = { "Portuguese": "pt_BR.UTF-8", "Italian": "it_IT.UTF-8", } do_test_locale = False def setUpModule(): global do_test_locale for name in test_locales: try: test_locale = test_locales[name] locale.setlocale(locale.LC_ALL, test_locale) do_test_locale = True break except Exception: pass if not do_test_locale: raise unittest.SkipTest("test locale not found") def tearDownModule(): if sys.platform == "win32" or sys.version_info[0:2] >= (3, 11): locale.setlocale(locale.LC_ALL, "") else: # Deprecated since version 3.11, will be removed in version 3.13 locale.resetlocale() class LocaleTestCase(unittest.TestCase): # @unittest.skipIf(not do_test_locale, 'test locale not found') def test_wkt_locale(self): # Test reading and writing p = loads("POINT (0.0 0.0)") assert p.x == 0.0 assert p.y == 0.0 wkt = dumps(p) assert wkt.startswith("POINT") assert "," not in wkt shapely-2.0.3/shapely/tests/legacy/test_make_valid.py000066400000000000000000000010761456366510000227530ustar00rootroot00000000000000from shapely.geometry import Polygon from shapely.tests.legacy.conftest import requires_geos_38 from shapely.validation import make_valid @requires_geos_38 def test_make_valid_invalid_input(): geom = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]) valid = make_valid(geom) assert len(valid.geoms) == 2 assert all(geom.geom_type == "Polygon" for geom in valid.geoms) @requires_geos_38 def test_make_valid_input(): geom = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) valid = make_valid(geom) assert id(valid) == id(geom) shapely-2.0.3/shapely/tests/legacy/test_mapping.py000066400000000000000000000006131456366510000223060ustar00rootroot00000000000000import unittest from shapely.geometry import mapping, Point, Polygon class MappingTestCase(unittest.TestCase): def test_point(self): m = mapping(Point(0, 0)) assert m["type"] == "Point" assert m["coordinates"] == (0.0, 0.0) def test_empty_polygon(self): """Empty polygons will round trip without error""" assert mapping(Polygon()) is not None shapely-2.0.3/shapely/tests/legacy/test_minimum_clearance.py000066400000000000000000000016241456366510000243260ustar00rootroot00000000000000""" Tests for the minimum clearance property. """ import math import pytest from shapely.geos import geos_version from shapely.wkt import loads as load_wkt requires_geos_36 = pytest.mark.skipif( geos_version < (3, 6, 0), reason="GEOS >= 3.6.0 is required." ) @requires_geos_36 def test_point(): point = load_wkt("POINT (0 0)") assert point.minimum_clearance == math.inf @requires_geos_36 def test_linestring(): line = load_wkt("LINESTRING (0 0, 1 1, 2 2)") assert round(line.minimum_clearance, 6) == 1.414214 @requires_geos_36 def test_simple_polygon(): poly = load_wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))") assert poly.minimum_clearance == 1.0 @requires_geos_36 def test_more_complicated_polygon(): poly = load_wkt( "POLYGON ((20 20, 34 124, 70 140, 130 130, 70 100, 110 70, 170 20, 90 10, 20 20))" ) assert round(poly.minimum_clearance, 6) == 35.777088 shapely-2.0.3/shapely/tests/legacy/test_ndarrays.py000066400000000000000000000023341456366510000225000ustar00rootroot00000000000000# Tests of support for Numpy ndarrays. See # https://github.com/sgillies/shapely/issues/26 for discussion. import unittest from functools import reduce import numpy as np from shapely import geometry class TransposeTestCase(unittest.TestCase): def test_multipoint(self): arr = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]]) tarr = arr.T shape = geometry.MultiPoint(tarr) coords = reduce(lambda x, y: x + y, [list(g.coords) for g in shape.geoms]) assert coords == [(1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0)] def test_linestring(self): a = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]]) t = a.T s = geometry.LineString(t) assert list(s.coords) == [ (1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0), ] def test_polygon(self): a = np.array([[1.0, 1.0, 2.0, 2.0, 1.0], [3.0, 4.0, 4.0, 3.0, 3.0]]) t = a.T s = geometry.Polygon(t) assert list(s.exterior.coords) == [ (1.0, 3.0), (1.0, 4.0), (2.0, 4.0), (2.0, 3.0), (1.0, 3.0), ] shapely-2.0.3/shapely/tests/legacy/test_nearest.py000066400000000000000000000007341456366510000223200ustar00rootroot00000000000000import unittest import pytest from shapely.geometry import Point from shapely.ops import nearest_points class Nearest(unittest.TestCase): def test_nearest(self): first, second = nearest_points( Point(0, 0).buffer(1.0), Point(3, 0).buffer(1.0), ) assert first.x == pytest.approx(1.0) assert second.x == pytest.approx(2.0) assert first.y == pytest.approx(0.0) assert second.y == pytest.approx(0.0) shapely-2.0.3/shapely/tests/legacy/test_operations.py000066400000000000000000000077521456366510000230510ustar00rootroot00000000000000import unittest import pytest import shapely from shapely.errors import TopologicalError from shapely.geometry import GeometryCollection, LineString, MultiPoint, Point, Polygon from shapely.wkt import loads class OperationsTestCase(unittest.TestCase): def test_operations(self): point = Point(0.0, 0.0) # General geometry assert point.area == 0.0 assert point.length == 0.0 assert point.distance(Point(-1.0, -1.0)) == pytest.approx(1.4142135623730951) # Topology operations # Envelope assert isinstance(point.envelope, Point) # Intersection assert point.intersection(Point(-1, -1)).is_empty # Buffer assert isinstance(point.buffer(10.0), Polygon) assert isinstance(point.buffer(10.0, 32), Polygon) # Simplify p = loads( "POLYGON ((120 120, 140 199, 160 200, 180 199, 220 120, 122 122, 121 121, 120 120))" ) expected = loads( "POLYGON ((120 120, 140 199, 160 200, 180 199, 220 120, 120 120))" ) s = p.simplify(10.0, preserve_topology=False) assert s.equals_exact(expected, 0.001) p = loads( "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)," "(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))" ) expected = loads( "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)," "(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))" ) s = p.simplify(10.0, preserve_topology=True) assert s.equals_exact(expected, 0.001) # Convex Hull assert isinstance(point.convex_hull, Point) # Differences assert isinstance(point.difference(Point(-1, 1)), Point) assert isinstance(point.symmetric_difference(Point(-1, 1)), MultiPoint) # Boundary assert isinstance(point.boundary, GeometryCollection) # Union assert isinstance(point.union(Point(-1, 1)), MultiPoint) assert isinstance(point.representative_point(), Point) assert isinstance(point.point_on_surface(), Point) assert point.representative_point() == point.point_on_surface() assert isinstance(point.centroid, Point) def test_relate(self): # Relate assert Point(0, 0).relate(Point(-1, -1)) == "FF0FFF0F2" # issue #294: should raise TopologicalError on exception invalid_polygon = loads( "POLYGON ((40 100, 80 100, 80 60, 40 60, 40 100), (60 60, 80 60, 80 40, 60 40, 60 60))" ) assert not invalid_polygon.is_valid with pytest.raises((TopologicalError, shapely.GEOSException)): invalid_polygon.relate(invalid_polygon) def test_hausdorff_distance(self): point = Point(1, 1) line = LineString([(2, 0), (2, 4), (3, 4)]) distance = point.hausdorff_distance(line) assert distance == point.distance(Point(3, 4)) def test_interpolate(self): # successful interpolation test_line = LineString([(1, 1), (1, 2)]) known_point = Point(1, 1.5) interpolated_point = test_line.interpolate(0.5, normalized=True) assert interpolated_point == known_point # Issue #653; should nog segfault for empty geometries empty_line = loads("LINESTRING EMPTY") assert empty_line.is_empty interpolated_point = empty_line.interpolate(0.5, normalized=True) assert interpolated_point.is_empty # invalid geometry should raise TypeError on exception polygon = loads("POLYGON EMPTY") with pytest.raises(TypeError, match="incorrect geometry type"): polygon.interpolate(0.5, normalized=True) def test_normalize(self): point = Point(1, 1) result = point.normalize() assert result == point line = loads("MULTILINESTRING ((1 1, 0 0), (1 1, 1 2))") result = line.normalize() expected = loads("MULTILINESTRING ((1 1, 1 2), (0 0, 1 1))") assert result == expected shapely-2.0.3/shapely/tests/legacy/test_operators.py000066400000000000000000000036341456366510000226770ustar00rootroot00000000000000import unittest from shapely.geometry import LineString, MultiPoint, Point, Polygon class OperatorsTestCase(unittest.TestCase): def test_point(self): point = Point(0, 0) point2 = Point(-1, 1) assert point.union(point2).equals(point | point2) assert (point & point2).is_empty assert point.equals(point - point2) assert point.symmetric_difference(point2).equals(point ^ point2) assert point != point2 point_dupe = Point(0, 0) assert point, point_dupe def test_multipoint(self): mp1 = MultiPoint([(0, 0), (1, 1)]) mp1_dup = MultiPoint([(0, 0), (1, 1)]) mp1_rev = MultiPoint([(1, 1), (0, 0)]) mp2 = MultiPoint([(0, 0), (1, 1), (2, 2)]) mp3 = MultiPoint([(0, 0), (1, 1), (2, 3)]) assert mp1 == mp1_dup assert mp1 != mp1_rev assert mp1 != mp2 assert mp2 != mp3 p = Point(0, 0) mp = MultiPoint([(0, 0)]) assert p != mp assert mp != p def test_polygon(self): shell = ((0, 0), (3, 0), (3, 3), (0, 3)) hole = ((1, 1), (2, 1), (2, 2), (1, 2)) p_solid = Polygon(shell) p2_solid = Polygon(shell) p_hole = Polygon(shell, holes=[hole]) p2_hole = Polygon(shell, holes=[hole]) assert p_solid == p2_solid assert p_hole == p2_hole assert p_solid != p_hole shell2 = ((-5, 2), (10.5, 3), (7, 3)) p3_hole = Polygon(shell2, holes=[hole]) assert p_hole != p3_hole def test_linestring(self): line1 = LineString([(0, 0), (1, 1), (2, 2)]) line2 = LineString([(0, 0), (2, 2)]) line2_dup = LineString([(0, 0), (2, 2)]) # .equals() indicates these are the same assert line1.equals(line2) # but != indicates these are different assert line1 != line2 # but dupes are the same with == assert line2 == line2_dup shapely-2.0.3/shapely/tests/legacy/test_orient.py000066400000000000000000000045401456366510000221560ustar00rootroot00000000000000import unittest from shapely.geometry import ( GeometryCollection, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.ops import orient class OrientTestCase(unittest.TestCase): def test_point(self): point = Point(0, 0) assert orient(point, 1) == point assert orient(point, -1) == point def test_multipoint(self): multipoint = MultiPoint([(0, 0), (1, 1)]) assert orient(multipoint, 1) == multipoint assert orient(multipoint, -1) == multipoint def test_linestring(self): linestring = LineString([(0, 0), (1, 1)]) assert orient(linestring, 1) == linestring assert orient(linestring, -1) == linestring def test_multilinestring(self): multilinestring = MultiLineString([[(0, 0), (1, 1)], [(1, 0), (0, 1)]]) assert orient(multilinestring, 1) == multilinestring assert orient(multilinestring, -1) == multilinestring def test_linearring(self): linearring = LinearRing([(0, 0), (0, 1), (1, 0)]) assert orient(linearring, 1) == linearring assert orient(linearring, -1) == linearring def test_polygon(self): polygon = Polygon([(0, 0), (0, 1), (1, 0)]) polygon_reversed = Polygon(polygon.exterior.coords[::-1]) assert (orient(polygon, 1)) == polygon_reversed assert (orient(polygon, -1)) == polygon def test_multipolygon(self): polygon1 = Polygon([(0, 0), (0, 1), (1, 0)]) polygon2 = Polygon([(1, 0), (2, 0), (2, 1)]) polygon1_reversed = Polygon(polygon1.exterior.coords[::-1]) polygon2_reversed = Polygon(polygon2.exterior.coords[::-1]) multipolygon = MultiPolygon([polygon1, polygon2]) assert not polygon1.exterior.is_ccw assert polygon2.exterior.is_ccw assert orient(multipolygon, 1) == MultiPolygon([polygon1_reversed, polygon2]) assert orient(multipolygon, -1) == MultiPolygon([polygon1, polygon2_reversed]) def test_geometrycollection(self): polygon = Polygon([(0, 0), (0, 1), (1, 0)]) polygon_reversed = Polygon(polygon.exterior.coords[::-1]) collection = GeometryCollection([polygon]) assert orient(collection, 1) == GeometryCollection([polygon_reversed]) assert orient(collection, -1) == GeometryCollection([polygon]) shapely-2.0.3/shapely/tests/legacy/test_parallel_offset.py000066400000000000000000000044341456366510000240220ustar00rootroot00000000000000import unittest import pytest from shapely.geometry import LinearRing, LineString from shapely.testing import assert_geometries_equal @pytest.mark.parametrize("distance", [float("nan"), float("inf")]) def test_non_finite_distance(distance): g = LineString([(0, 0), (10, 0)]) with pytest.raises(ValueError, match="distance must be finite"): g.parallel_offset(distance) class OperationsTestCase(unittest.TestCase): def test_parallel_offset_linestring(self): line1 = LineString([(0, 0), (10, 0)]) left = line1.parallel_offset(5, "left") assert_geometries_equal(left, LineString([(0, 5), (10, 5)])) right = line1.parallel_offset(5, "right") assert_geometries_equal(right, LineString([(10, -5), (0, -5)]), normalize=True) right = line1.parallel_offset(-5, "left") assert_geometries_equal(right, LineString([(10, -5), (0, -5)]), normalize=True) left = line1.parallel_offset(-5, "right") assert_geometries_equal(left, LineString([(0, 5), (10, 5)])) # by default, parallel_offset is right-handed assert_geometries_equal(line1.parallel_offset(5), right) line2 = LineString([(0, 0), (5, 0), (5, -5)]) assert_geometries_equal( line2.parallel_offset(2, "left", join_style=3), LineString([(0, 2), (5, 2), (7, 0), (7, -5)]), ) assert_geometries_equal( line2.parallel_offset(2, "left", join_style=2), LineString([(0, 2), (7, 2), (7, -5)]), ) # offset_curve alias assert_geometries_equal( line1.offset_curve(2, quad_segs=10), line1.parallel_offset(2, "left", resolution=10), ) assert_geometries_equal( line1.offset_curve(-2, join_style="mitre"), line1.parallel_offset(2, "right", join_style=2), ) def test_parallel_offset_linear_ring(self): lr1 = LinearRing([(0, 0), (5, 0), (5, 5), (0, 5), (0, 0)]) assert_geometries_equal( lr1.parallel_offset(2, "left", resolution=1), LineString([(2, 2), (3, 2), (3, 3), (2, 3), (2, 2)]), ) # offset_curve alias assert_geometries_equal( lr1.offset_curve(2, quad_segs=1), lr1.parallel_offset(2, "left", resolution=1), ) shapely-2.0.3/shapely/tests/legacy/test_persist.py000066400000000000000000000032611456366510000223460ustar00rootroot00000000000000"""Persistence tests """ import pickle import struct import unittest from shapely import wkb, wkt from shapely.geometry import Point class PersistTestCase(unittest.TestCase): def test_pickle(self): p = Point(0.0, 0.0) data = pickle.dumps(p) q = pickle.loads(data) assert q.equals(p) def test_wkb(self): p = Point(0.0, 0.0) wkb_big_endian = wkb.dumps(p, big_endian=True) wkb_little_endian = wkb.dumps(p, big_endian=False) # Regardless of byte order, loads ought to correctly recover the # geometry assert p.equals(wkb.loads(wkb_big_endian)) assert p.equals(wkb.loads(wkb_little_endian)) def test_wkb_dumps_endianness(self): p = Point(0.5, 2.0) wkb_big_endian = wkb.dumps(p, big_endian=True) wkb_little_endian = wkb.dumps(p, big_endian=False) assert wkb_big_endian != wkb_little_endian # According to WKB specification in section 3.3 of OpenGIS # Simple Features Specification for SQL, revision 1.1, the # first byte of a WKB representation indicates byte order. # Big-endian is 0, little-endian is 1. assert wkb_big_endian[0] == 0 assert wkb_little_endian[0] == 1 # Check that the doubles (0.5, 2.0) are in correct byte order double_size = struct.calcsize("d") assert wkb_big_endian[(-2 * double_size) :] == struct.pack(">2d", p.x, p.y) assert wkb_little_endian[(-2 * double_size) :] == struct.pack("<2d", p.x, p.y) def test_wkt(self): p = Point(0.0, 0.0) text = wkt.dumps(p) assert text.startswith("POINT") pt = wkt.loads(text) assert pt.equals(p) shapely-2.0.3/shapely/tests/legacy/test_pickle.py000066400000000000000000000046611456366510000221310ustar00rootroot00000000000000import pathlib import pickle import warnings from pickle import dumps, HIGHEST_PROTOCOL, loads import pytest import shapely from shapely import wkt from shapely.geometry import ( box, GeometryCollection, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) HERE = pathlib.Path(__file__).parent TEST_DATA = { "point2d": Point([(1.0, 2.0)]), "point3d": Point([(1.0, 2.0, 3.0)]), "linestring": LineString([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)]), "linearring": LinearRing([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]), "polygon": Polygon([(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]), "multipoint": MultiPoint([(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]), "multilinestring": MultiLineString( [[(0.0, 0.0), (1.0, 1.0)], [(1.0, 2.0), (3.0, 3.0)]] ), "multipolygon": MultiPolygon([box(0, 0, 1, 1), box(2, 2, 3, 3)]), "geometrycollection": GeometryCollection([Point(1.0, 2.0), box(0, 0, 1, 1)]), "emptypoint": wkt.loads("POINT EMPTY"), "emptypolygon": wkt.loads("POLYGON EMPTY"), } TEST_NAMES, TEST_GEOMS = zip(*TEST_DATA.items()) @pytest.mark.parametrize("geom1", TEST_GEOMS, ids=TEST_NAMES) def test_pickle_round_trip(geom1): data = dumps(geom1, HIGHEST_PROTOCOL) with warnings.catch_warnings(): warnings.simplefilter("error") geom2 = loads(data) assert geom2.has_z == geom1.has_z assert type(geom2) is type(geom1) assert geom2.geom_type == geom1.geom_type assert geom2.wkt == geom1.wkt @pytest.mark.parametrize( "fname", (HERE / "data").glob("*.pickle"), ids=lambda fname: fname.name ) def test_unpickle_pre_20(fname): from shapely.testing import assert_geometries_equal geom_type = fname.name.split("_")[0] expected = TEST_DATA[geom_type] with open(fname, "rb") as f: with pytest.warns(UserWarning): result = pickle.load(f) assert_geometries_equal(result, expected) if __name__ == "__main__": datadir = HERE / "data" datadir.mkdir(exist_ok=True) shapely_version = shapely.__version__ print(shapely_version) print(shapely.geos.geos_version) for name, geom in TEST_DATA.items(): if name == "emptypoint" and shapely.geos.geos_version < (3, 9, 0): # Empty Points cannot be represented in WKB continue with open(datadir / f"{name}_{shapely_version}.pickle", "wb") as f: pickle.dump(geom, f) shapely-2.0.3/shapely/tests/legacy/test_polygonize.py000066400000000000000000000025551456366510000230610ustar00rootroot00000000000000import unittest from shapely.geometry import LineString, Point, Polygon from shapely.geometry.base import dump_coords from shapely.ops import polygonize, polygonize_full class PolygonizeTestCase(unittest.TestCase): def test_polygonize(self): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), LineString([(1, 1), (1, 0)]), LineString([(1, 0), (0, 0)]), LineString([(5, 5), (6, 6)]), Point(0, 0), ] result = list(polygonize(lines)) assert all(isinstance(x, Polygon) for x in result) def test_polygonize_full(self): lines2 = [ [(0, 0), (1, 1)], [(0, 0), (0, 1)], [(0, 1), (1, 1)], [(1, 1), (1, 0)], [(1, 0), (0, 0)], [(5, 5), (6, 6)], [(1, 1), (100, 100)], ] result2, cuts, dangles, invalids = polygonize_full(lines2) assert len(result2.geoms) == 2 assert all(isinstance(x, Polygon) for x in result2.geoms) assert list(cuts.geoms) == [] assert all(isinstance(x, LineString) for x in dangles.geoms) assert dump_coords(dangles) == [ [(1.0, 1.0), (100.0, 100.0)], [(5.0, 5.0), (6.0, 6.0)], ] assert list(invalids.geoms) == [] shapely-2.0.3/shapely/tests/legacy/test_polylabel.py000066400000000000000000000062311456366510000226400ustar00rootroot00000000000000import unittest import pytest from shapely.algorithms.polylabel import Cell, polylabel from shapely.errors import TopologicalError from shapely.geometry import LineString, Point, Polygon class PolylabelTestCase(unittest.TestCase): def test_polylabel(self): """ Finds pole of inaccessibility for a polygon with a tolerance of 10 """ polygon = LineString( [(0, 0), (50, 200), (100, 100), (20, 50), (-100, -20), (-150, -200)] ).buffer(100) label = polylabel(polygon, tolerance=10) expected = Point(59.35615556364569, 121.8391962974644) assert expected.equals_exact(label, 1e-6) def test_invalid_polygon(self): """ Makes sure that the polylabel function throws an exception when provided an invalid polygon. """ bowtie_polygon = Polygon( [(0, 0), (0, 20), (10, 10), (20, 20), (20, 0), (10, 10), (0, 0)] ) with pytest.raises(TopologicalError): polylabel(bowtie_polygon) def test_cell_sorting(self): """ Tests rich comparison operators of Cells for use in the polylabel minimum priority queue. """ polygon = Point(0, 0).buffer(100) cell1 = Cell(0, 0, 50, polygon) # closest cell2 = Cell(50, 50, 50, polygon) # furthest assert cell1 < cell2 assert cell1 <= cell2 assert (cell2 <= cell1) is False assert cell1 == cell1 assert (cell1 == cell2) is False assert cell1 != cell2 assert (cell1 != cell1) is False assert cell2 > cell1 assert (cell1 > cell2) is False assert cell2 >= cell1 assert (cell1 >= cell2) is False def test_concave_polygon(self): """ Finds pole of inaccessibility for a concave polygon and ensures that the point is inside. """ concave_polygon = LineString([(500, 0), (0, 0), (0, 500), (500, 500)]).buffer( 100 ) label = polylabel(concave_polygon) assert concave_polygon.contains(label) def test_rectangle_special_case(self): """ The centroid algorithm used is vulnerable to floating point errors and can give unexpected results for rectangular polygons. Test that this special case is handled correctly. https://github.com/mapbox/polylabel/issues/3 """ polygon = Polygon( [ (32.71997, -117.19310), (32.71997, -117.21065), (32.72408, -117.21065), (32.72408, -117.19310), ] ) label = polylabel(polygon) assert label.coords[:] == [(32.722025, -117.201875)] def test_polygon_with_hole(self): """ Finds pole of inaccessibility for a polygon with a hole https://github.com/shapely/shapely/issues/817 """ polygon = Polygon( shell=[(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)], holes=[[(2, 2), (6, 2), (6, 6), (2, 6), (2, 2)]], ) label = polylabel(polygon, 0.05) assert label.x == pytest.approx(7.65625) assert label.y == pytest.approx(7.65625) shapely-2.0.3/shapely/tests/legacy/test_predicates.py000066400000000000000000000052301456366510000227760ustar00rootroot00000000000000"""Test GEOS predicates """ import unittest import pytest import shapely from shapely.geometry import Point, Polygon class PredicatesTestCase(unittest.TestCase): def test_binary_predicates(self): point = Point(0.0, 0.0) point2 = Point(2.0, 2.0) assert point.disjoint(Point(-1.0, -1.0)) assert not point.touches(Point(-1.0, -1.0)) assert not point.crosses(Point(-1.0, -1.0)) assert not point.within(Point(-1.0, -1.0)) assert not point.contains(Point(-1.0, -1.0)) assert not point.equals(Point(-1.0, -1.0)) assert not point.touches(Point(-1.0, -1.0)) assert point.equals(Point(0.0, 0.0)) assert point.covers(Point(0.0, 0.0)) assert point.covered_by(Point(0.0, 0.0)) assert not point.covered_by(point2) assert not point2.covered_by(point) assert not point.covers(Point(-1.0, -1.0)) def test_unary_predicates(self): point = Point(0.0, 0.0) assert not point.is_empty assert point.is_valid assert point.is_simple assert not point.is_ring assert not point.has_z def test_binary_predicate_exceptions(self): p1 = [ (339, 346), (459, 346), (399, 311), (340, 277), (399, 173), (280, 242), (339, 415), (280, 381), (460, 207), (339, 346), ] p2 = [ (339, 207), (280, 311), (460, 138), (399, 242), (459, 277), (459, 415), (399, 381), (519, 311), (520, 242), (519, 173), (399, 450), (339, 207), ] with pytest.raises(shapely.GEOSException): Polygon(p1).within(Polygon(p2)) def test_relate_pattern(self): # a pair of partially overlapping polygons, and a nearby point g1 = Polygon([(0, 0), (0, 1), (3, 1), (3, 0), (0, 0)]) g2 = Polygon([(1, -1), (1, 2), (2, 2), (2, -1), (1, -1)]) g3 = Point(5, 5) assert g1.relate(g2) == "212101212" assert g1.relate_pattern(g2, "212101212") assert g1.relate_pattern(g2, "*********") assert g1.relate_pattern(g2, "2********") assert g1.relate_pattern(g2, "T********") assert not g1.relate_pattern(g2, "112101212") assert not g1.relate_pattern(g2, "1********") assert g1.relate_pattern(g3, "FF2FF10F2") # an invalid pattern should raise an exception with pytest.raises(shapely.GEOSException, match="IllegalArgumentException"): g1.relate_pattern(g2, "fail") shapely-2.0.3/shapely/tests/legacy/test_prepared.py000066400000000000000000000044531456366510000224630ustar00rootroot00000000000000import numpy as np import pytest from shapely.geometry import Point, Polygon from shapely.prepared import prep, PreparedGeometry def test_prepared_geometry(): polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) p = PreparedGeometry(polygon) assert p.contains(Point(0.5, 0.5)) assert not p.contains(Point(0.5, 1.5)) def test_prep(): polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) p = prep(polygon) assert p.contains(Point(0.5, 0.5)) assert not p.contains(Point(0.5, 1.5)) def test_op_not_allowed(): p = PreparedGeometry(Point(0.0, 0.0).buffer(1.0)) with pytest.raises(TypeError): Point(0.0, 0.0).union(p) def test_predicate_not_allowed(): p = PreparedGeometry(Point(0.0, 0.0).buffer(1.0)) with pytest.raises(TypeError): Point(0.0, 0.0).contains(p) def test_prepared_predicates(): # check prepared predicates give the same result as regular predicates polygon1 = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) polygon2 = Polygon([(0.5, 0.5), (1.5, 0.5), (1.0, 1.0), (0.5, 0.5)]) point2 = Point(0.5, 0.5) polygon_empty = Polygon() prepared_polygon1 = PreparedGeometry(polygon1) for geom2 in (polygon2, point2, polygon_empty): with np.errstate(invalid="ignore"): assert polygon1.disjoint(geom2) == prepared_polygon1.disjoint(geom2) assert polygon1.touches(geom2) == prepared_polygon1.touches(geom2) assert polygon1.intersects(geom2) == prepared_polygon1.intersects(geom2) assert polygon1.crosses(geom2) == prepared_polygon1.crosses(geom2) assert polygon1.within(geom2) == prepared_polygon1.within(geom2) assert polygon1.contains(geom2) == prepared_polygon1.contains(geom2) assert polygon1.overlaps(geom2) == prepared_polygon1.overlaps(geom2) def test_prepare_already_prepared(): polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) prepared = prep(polygon) # attempt to prepare an already prepared geometry with `prep` result = prep(prepared) assert isinstance(result, PreparedGeometry) assert result.context is polygon # attempt to prepare an already prepared geometry with `PreparedGeometry` result = PreparedGeometry(prepared) assert isinstance(result, PreparedGeometry) assert result.context is polygon shapely-2.0.3/shapely/tests/legacy/test_products_z.py000066400000000000000000000006041456366510000230470ustar00rootroot00000000000000import unittest from shapely.geometry import LineString class ProductZTestCase(unittest.TestCase): def test_line_intersection(self): line1 = LineString([(0, 0, 0), (1, 1, 1)]) line2 = LineString([(0, 1, 1), (1, 0, 0)]) interxn = line1.intersection(line2) assert interxn.has_z assert interxn._ndim == 3 assert 0.0 <= interxn.z <= 1.0 shapely-2.0.3/shapely/tests/legacy/test_shape.py000066400000000000000000000025741456366510000217630ustar00rootroot00000000000000import pytest from shapely.geometry import MultiLineString, Polygon, shape from shapely.geometry.geo import _is_coordinates_empty @pytest.mark.parametrize( "geom", [{"type": "Polygon", "coordinates": None}, {"type": "Polygon", "coordinates": []}], ) def test_polygon_no_coords(geom): assert shape(geom) == Polygon() def test_polygon_empty_np_array(): np = pytest.importorskip("numpy") geom = {"type": "Polygon", "coordinates": np.array([])} assert shape(geom) == Polygon() def test_polygon_with_coords_list(): geom = {"type": "Polygon", "coordinates": [[[5, 10], [10, 10], [10, 5]]]} obj = shape(geom) assert obj == Polygon([(5, 10), (10, 10), (10, 5)]) def test_polygon_not_empty_np_array(): np = pytest.importorskip("numpy") geom = {"type": "Polygon", "coordinates": np.array([[[5, 10], [10, 10], [10, 5]]])} obj = shape(geom) assert obj == Polygon([(5, 10), (10, 10), (10, 5)]) @pytest.mark.parametrize( "geom", [ {"type": "MultiLineString", "coordinates": []}, {"type": "MultiLineString", "coordinates": [[]]}, {"type": "MultiLineString", "coordinates": None}, ], ) def test_multilinestring_empty(geom): assert shape(geom) == MultiLineString() @pytest.mark.parametrize("coords", [[], [[]], [[], []], None, [[[]]]]) def test_is_coordinates_empty(coords): assert _is_coordinates_empty(coords) shapely-2.0.3/shapely/tests/legacy/test_shared_paths.py000066400000000000000000000026321456366510000233230ustar00rootroot00000000000000import unittest import pytest from shapely.errors import GeometryTypeError from shapely.geometry import GeometryCollection, LineString, MultiLineString, Point from shapely.ops import shared_paths class SharedPaths(unittest.TestCase): def test_shared_paths_forward(self): g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)]) g2 = LineString([(5, 0), (15, 0)]) result = shared_paths(g1, g2) assert isinstance(result, GeometryCollection) assert len(result.geoms) == 2 a, b = result.geoms assert isinstance(a, MultiLineString) assert len(a.geoms) == 1 assert a.geoms[0].coords[:] == [(5, 0), (10, 0)] assert b.is_empty def test_shared_paths_forward2(self): g1 = LineString([(0, 0), (10, 0), (10, 5), (20, 5)]) g2 = LineString([(15, 0), (5, 0)]) result = shared_paths(g1, g2) assert isinstance(result, GeometryCollection) assert len(result.geoms) == 2 a, b = result.geoms assert isinstance(b, MultiLineString) assert len(b.geoms) == 1 assert b.geoms[0].coords[:] == [(5, 0), (10, 0)] assert a.is_empty def test_wrong_type(self): g1 = Point(0, 0) g2 = LineString([(5, 0), (15, 0)]) with pytest.raises(GeometryTypeError): shared_paths(g1, g2) with pytest.raises(GeometryTypeError): shared_paths(g2, g1) shapely-2.0.3/shapely/tests/legacy/test_singularity.py000066400000000000000000000005751456366510000232340ustar00rootroot00000000000000import unittest from shapely.geometry import Polygon class PolygonTestCase(unittest.TestCase): def test_polygon_3(self): p = (1.0, 1.0) poly = Polygon([p, p, p]) assert poly.bounds == (1.0, 1.0, 1.0, 1.0) def test_polygon_5(self): p = (1.0, 1.0) poly = Polygon([p, p, p, p, p]) assert poly.bounds == (1.0, 1.0, 1.0, 1.0) shapely-2.0.3/shapely/tests/legacy/test_snap.py000066400000000000000000000013741456366510000216210ustar00rootroot00000000000000import unittest from shapely.geometry import LineString, Polygon from shapely.ops import snap class Snap(unittest.TestCase): def test_snap(self): # input geometries square = Polygon([(1, 1), (2, 1), (2, 2), (1, 2), (1, 1)]) line = LineString([(0, 0), (0.8, 0.8), (1.8, 0.95), (2.6, 0.5)]) square_coords = square.exterior.coords[:] line_coords = line.coords[:] result = snap(line, square, 0.5) # test result is correct assert isinstance(result, LineString) assert result.coords[:] == [(0.0, 0.0), (1.0, 1.0), (2.0, 1.0), (2.6, 0.5)] # test inputs have not been modified assert square.exterior.coords[:] == square_coords assert line.coords[:] == line_coords shapely-2.0.3/shapely/tests/legacy/test_split.py000066400000000000000000000227621456366510000220170ustar00rootroot00000000000000import unittest import pytest from shapely.errors import GeometryTypeError from shapely.geometry import ( LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.ops import linemerge, split, unary_union class TestSplitGeometry(unittest.TestCase): # helper class for testing below def helper(self, geom, splitter, expected_chunks): s = split(geom, splitter) assert s.geom_type == "GeometryCollection" assert len(s.geoms) == expected_chunks if expected_chunks > 1: # split --> expected collection that when merged is again equal to original geometry if s.geoms[0].geom_type == "LineString": self.assertTrue(linemerge(s).simplify(0.000001).equals(geom)) elif s.geoms[0].geom_type == "Polygon": union = unary_union(s).simplify(0.000001) assert union.equals(geom) assert union.area == geom.area else: raise ValueError elif expected_chunks == 1: # not split --> expected equal to line assert s.geoms[0].equals(geom) def test_split_closed_line_with_point(self): # point at start/end of closed ring -> return equal # see GH #524 ls = LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) splitter = Point(0, 0) self.helper(ls, splitter, 1) class TestSplitPolygon(TestSplitGeometry): poly_simple = Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)]) poly_hole = Polygon( [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)], [[(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]], ) def test_split_poly_with_line(self): # crossing at 2 points --> return 2 segments splitter = LineString([(1, 3), (1, -3)]) self.helper(self.poly_simple, splitter, 2) self.helper(self.poly_hole, splitter, 2) # touching the boundary--> return equal splitter = LineString([(0, 2), (5, 2)]) self.helper(self.poly_simple, splitter, 1) self.helper(self.poly_hole, splitter, 1) # inside the polygon --> return equal splitter = LineString([(0.2, 0.2), (1.7, 1.7), (3, 2)]) self.helper(self.poly_simple, splitter, 1) self.helper(self.poly_hole, splitter, 1) # outside the polygon --> return equal splitter = LineString([(0, 3), (3, 3), (3, 0)]) self.helper(self.poly_simple, splitter, 1) self.helper(self.poly_hole, splitter, 1) def test_split_poly_with_other(self): with pytest.raises(GeometryTypeError): split(self.poly_simple, Point(1, 1)) with pytest.raises(GeometryTypeError): split(self.poly_simple, MultiPoint([(1, 1), (3, 4)])) with pytest.raises(GeometryTypeError): split(self.poly_simple, self.poly_hole) class TestSplitLine(TestSplitGeometry): ls = LineString([(0, 0), (1.5, 1.5), (3.0, 4.0)]) def test_split_line_with_point(self): # point on line interior --> return 2 segments splitter = Point(1, 1) self.helper(self.ls, splitter, 2) # point on line point --> return 2 segments splitter = Point(1.5, 1.5) self.helper(self.ls, splitter, 2) # point on boundary --> return equal splitter = Point(3, 4) self.helper(self.ls, splitter, 1) # point on exterior of line --> return equal splitter = Point(2, 2) self.helper(self.ls, splitter, 1) def test_split_line_with_multipoint(self): # points on line interior --> return 4 segments splitter = MultiPoint([(1, 1), (1.5, 1.5), (0.5, 0.5)]) self.helper(self.ls, splitter, 4) # points on line interior and boundary -> return 2 segments splitter = MultiPoint([(1, 1), (3, 4)]) self.helper(self.ls, splitter, 2) # point on linear interior but twice --> return 2 segments splitter = MultiPoint([(1, 1), (1.5, 1.5), (1, 1)]) self.helper(self.ls, splitter, 3) def test_split_line_with_line(self): # crosses at one point --> return 2 segments splitter = LineString([(0, 1), (1, 0)]) self.helper(self.ls, splitter, 2) # crosses at two points --> return 3 segments splitter = LineString([(0, 1), (1, 0), (1, 2)]) self.helper(self.ls, splitter, 3) # overlaps --> raise splitter = LineString([(0, 0), (15, 15)]) with pytest.raises(ValueError): self.helper(self.ls, splitter, 1) # does not cross --> return equal splitter = LineString([(0, 1), (0, 2)]) self.helper(self.ls, splitter, 1) # is touching the boundary --> return equal splitter = LineString([(-1, 1), (1, -1)]) assert splitter.touches(self.ls) self.helper(self.ls, splitter, 1) # splitter boundary touches interior of line --> return 2 segments splitter = LineString([(0, 1), (1, 1)]) # touches at (1, 1) assert splitter.touches(self.ls) self.helper(self.ls, splitter, 2) def test_split_line_with_multiline(self): # crosses at one point --> return 2 segments splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 0), (2, -2)]]) self.helper(self.ls, splitter, 2) # crosses at two points --> return 3 segments splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 2), (2, 0)]]) self.helper(self.ls, splitter, 3) # crosses at three points --> return 4 segments splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 2), (2, 0), (2.2, 3.2)]]) self.helper(self.ls, splitter, 4) # overlaps --> raise splitter = MultiLineString([[(0, 0), (1.5, 1.5)], [(1.5, 1.5), (3, 4)]]) with pytest.raises(ValueError): self.helper(self.ls, splitter, 1) # does not cross --> return equal splitter = MultiLineString([[(0, 1), (0, 2)], [(1, 0), (2, 0)]]) self.helper(self.ls, splitter, 1) def test_split_line_with_polygon(self): # crosses at two points --> return 3 segments splitter = Polygon([(1, 0), (1, 2), (2, 2), (2, 0), (1, 0)]) self.helper(self.ls, splitter, 3) # crosses at one point and touches boundary --> return 2 segments splitter = Polygon([(0, 0), (1, 2), (2, 2), (1, 0), (0, 0)]) self.helper(self.ls, splitter, 2) # exterior crosses at one point and touches at (0, 0) # interior crosses at two points splitter = Polygon( [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)], [[(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]], ) self.helper(self.ls, splitter, 4) def test_split_line_with_multipolygon(self): poly1 = Polygon( [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)] ) # crosses at one point and touches at (0, 0) poly2 = Polygon( [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)] ) # crosses at two points poly3 = Polygon([(0, 0), (0, -2), (-2, -2), (-2, 0), (0, 0)]) # not crossing splitter = MultiPolygon([poly1, poly2, poly3]) self.helper(self.ls, splitter, 4) class TestSplitClosedRing(TestSplitGeometry): ls = LineString([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]) def test_split_closed_ring_with_point(self): splitter = Point([0.0, 0.0]) self.helper(self.ls, splitter, 1) splitter = Point([0.0, 0.5]) self.helper(self.ls, splitter, 2) result = split(self.ls, splitter) assert result.geoms[0].coords[:] == [(0, 0), (0.0, 0.5)] assert result.geoms[1].coords[:] == [(0.0, 0.5), (0, 1), (1, 1), (1, 0), (0, 0)] # previously failed, see GH#585 splitter = Point([0.5, 0.0]) self.helper(self.ls, splitter, 2) result = split(self.ls, splitter) assert result.geoms[0].coords[:] == [(0, 0), (0, 1), (1, 1), (1, 0), (0.5, 0)] assert result.geoms[1].coords[:] == [(0.5, 0), (0, 0)] splitter = Point([2.0, 2.0]) self.helper(self.ls, splitter, 1) class TestSplitMulti(TestSplitGeometry): def test_split_multiline_with_point(self): # a cross-like multilinestring with a point in the middle --> return 4 line segments l1 = LineString([(0, 1), (2, 1)]) l2 = LineString([(1, 0), (1, 2)]) ml = MultiLineString([l1, l2]) splitter = Point((1, 1)) self.helper(ml, splitter, 4) def test_split_multiline_with_multipoint(self): # a cross-like multilinestring with a point in middle, a point on one of the lines and a point in the exterior # --> return 4+1 line segments l1 = LineString([(0, 1), (3, 1)]) l2 = LineString([(1, 0), (1, 2)]) ml = MultiLineString([l1, l2]) splitter = MultiPoint([(1, 1), (2, 1), (4, 2)]) self.helper(ml, splitter, 5) def test_split_multipolygon_with_line(self): # two polygons with a crossing line --> return 4 triangles poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) poly2 = Polygon([(1, 1), (1, 2), (2, 2), (2, 1), (1, 1)]) mpoly = MultiPolygon([poly1, poly2]) ls = LineString([(-1, -1), (3, 3)]) self.helper(mpoly, ls, 4) # two polygons away from the crossing line --> return identity poly1 = Polygon([(10, 10), (10, 11), (11, 11), (11, 10), (10, 10)]) poly2 = Polygon([(-10, -10), (-10, -11), (-11, -11), (-11, -10), (-10, -10)]) mpoly = MultiPolygon([poly1, poly2]) ls = LineString([(-1, -1), (3, 3)]) self.helper(mpoly, ls, 2) shapely-2.0.3/shapely/tests/legacy/test_substring.py000066400000000000000000000512341456366510000227000ustar00rootroot00000000000000import json import unittest import pytest from shapely.errors import GeometryTypeError from shapely.geometry import LineString, Point, shape from shapely.ops import substring class SubstringTestCase(unittest.TestCase): def setUp(self): self.point = Point(1, 1) self.line1 = LineString([(0, 0), (2, 0)]) self.line2 = LineString([(3, 0), (3, 6), (4.5, 6)]) self.line3 = LineString((0, i) for i in range(5)) def test_return_startpoint(self): assert substring(self.line1, -500, -600).equals(Point(0, 0)) assert substring(self.line1, -500, -500).equals(Point(0, 0)) assert substring(self.line1, -1, -1.1, True).equals(Point(0, 0)) assert substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)) def test_return_endpoint(self): assert substring(self.line1, 500, 600).equals(Point(2, 0)) assert substring(self.line1, 500, 500).equals(Point(2, 0)) assert substring(self.line1, 1, 1.1, True).equals(Point(2, 0)) assert substring(self.line1, 1.1, 1.1, True).equals(Point(2, 0)) def test_return_midpoint(self): assert substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0)) assert substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0)) assert substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)) assert substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)) # Coming from opposite ends assert substring(self.line1, 1.5, -0.5).equals(Point(1.5, 0)) assert substring(self.line1, -0.5, 1.5).equals(Point(1.5, 0)) assert substring(self.line1, -0.7, 0.3, True).equals(Point(0.6, 0)) assert substring(self.line1, 0.3, -0.7, True).equals(Point(0.6, 0)) def test_return_startsubstring(self): assert ( substring(self.line1, -500, 0.6).wkt == LineString([(0, 0), (0.6, 0)]).wkt ) assert ( substring(self.line1, -1.1, 0.6, True).wkt == LineString([(0, 0), (1.2, 0)]).wkt ) def test_return_startsubstring_reversed(self): # not normalized assert substring(self.line1, -1, -500).wkt == LineString([(1, 0), (0, 0)]).wkt assert ( substring(self.line3, 3.5, 0).wkt == LineString([(0, 3.5), (0, 3), (0, 2), (0, 1), (0, 0)]).wkt ) assert ( substring(self.line3, -1.5, -500).wkt == LineString([(0, 2.5), (0, 2), (0, 1), (0, 0)]).wkt ) # normalized assert ( substring(self.line1, -0.5, -1.1, True).wkt == LineString([(1.0, 0), (0, 0)]).wkt ) assert ( substring(self.line3, 0.5, 0, True).wkt == LineString([(0, 2.0), (0, 1), (0, 0)]).wkt ) assert ( substring(self.line3, -0.5, -1.1, True).wkt == LineString([(0, 2.0), (0, 1), (0, 0)]).wkt ) def test_return_endsubstring(self): assert substring(self.line1, 0.6, 500).wkt == LineString([(0.6, 0), (2, 0)]).wkt assert ( substring(self.line1, 0.6, 1.1, True).wkt == LineString([(1.2, 0), (2, 0)]).wkt ) def test_return_endsubstring_reversed(self): # not normalized assert substring(self.line1, 500, -1).wkt == LineString([(2, 0), (1, 0)]).wkt assert ( substring(self.line3, 4, 2.5).wkt == LineString([(0, 4), (0, 3), (0, 2.5)]).wkt ) assert ( substring(self.line3, 500, -1.5).wkt == LineString([(0, 4), (0, 3), (0, 2.5)]).wkt ) # normalized assert ( substring(self.line1, 1.1, -0.5, True).wkt == LineString([(2, 0), (1.0, 0)]).wkt ) assert ( substring(self.line3, 1, 0.5, True).wkt == LineString([(0, 4), (0, 3), (0, 2.0)]).wkt ) assert ( substring(self.line3, 1.1, -0.5, True).wkt == LineString([(0, 4), (0, 3), (0, 2.0)]).wkt ) def test_return_midsubstring(self): assert ( substring(self.line1, 0.5, 0.6).wkt == LineString([(0.5, 0), (0.6, 0)]).wkt ) assert ( substring(self.line1, -0.6, -0.5).wkt == LineString([(1.4, 0), (1.5, 0)]).wkt ) assert ( substring(self.line1, 0.5, 0.6, True).wkt == LineString([(1, 0), (1.2, 0)]).wkt ) assert ( substring(self.line1, -0.6, -0.5, True).wkt == LineString([(0.8, 0), (1, 0)]).wkt ) def test_return_midsubstring_reversed(self): assert ( substring(self.line1, 0.6, 0.5).wkt == LineString([(0.6, 0), (0.5, 0)]).wkt ) assert ( substring(self.line1, -0.5, -0.6).wkt == LineString([(1.5, 0), (1.4, 0)]).wkt ) assert ( substring(self.line1, 0.6, 0.5, True).wkt == LineString([(1.2, 0), (1, 0)]).wkt ) assert ( substring(self.line1, -0.5, -0.6, True).wkt == LineString([(1, 0), (0.8, 0)]).wkt ) # with vertices # not normalized assert ( substring(self.line3, 3.5, 2.5).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) # (+, +) assert ( substring(self.line3, -0.5, -1.5).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) # (-, -) assert ( substring(self.line3, 3.5, -1.5).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) # (+, -) assert ( substring(self.line3, -0.5, 2.5).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) # (-, +) # normalized assert ( substring(self.line3, 0.875, 0.625, True).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) assert ( substring(self.line3, -0.125, -0.375, True).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) assert ( substring(self.line3, 0.875, -0.375, True).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) assert ( substring(self.line3, -0.125, 0.625, True).wkt == LineString([(0, 3.5), (0, 3), (0, 2.5)]).wkt ) def test_return_substring_with_vertices(self): assert ( substring(self.line2, 1, 7).wkt == LineString([(3, 1), (3, 6), (4, 6)]).wkt ) assert ( substring(self.line2, 0.2, 0.9, True).wkt == LineString([(3, 1.5), (3, 6), (3.75, 6)]).wkt ) assert ( substring(self.line2, 0, 0.9, True).wkt == LineString([(3, 0), (3, 6), (3.75, 6)]).wkt ) assert ( substring(self.line2, 0.2, 1, True).wkt == LineString([(3, 1.5), (3, 6), (4.5, 6)]).wkt ) def test_return_substring_issue682(self): assert list(substring(self.line2, 0.1, 0).coords) == [(3.0, 0.1), (3.0, 0.0)] def test_return_substring_issue848(self): line = shape(json.loads(data_issue_848)) cut_line = substring(line, 0.7, 0.8, normalized=True) assert len(cut_line.coords) == 53 def test_raise_type_error(self): with pytest.raises(GeometryTypeError): substring(Point(0, 0), 0, 0) def test_return_z_coord_issue1699(self): line_z = LineString([(0, 0, 0), (2, 0, 0)]) assert ( substring(line_z, 0, 0.5, True).wkt == LineString([(0, 0, 0), (1, 0, 0)]).wkt ) assert ( substring(line_z, 0.5, 0, True).wkt == LineString([(1, 0, 0), (0, 0, 0)]).wkt ) data_issue_848 = '{"type": "LineString", "coordinates": [[-87.71314, 41.96793], [-87.71312, 41.96667], [-87.71311, 41.96643], [-87.7131, 41.96635], [-87.71309, 41.9663], [-87.71303, 41.96621], [-87.71298, 41.96615], [-87.71292, 41.96611], [-87.7128, 41.96607], [-87.71268, 41.96605], [-87.71255, 41.96605], [-87.7124, 41.96605], [-87.71219, 41.96605], [-87.71173, 41.96606], [-87.71108, 41.96607], [-87.71027, 41.96607], [-87.70884, 41.96609], [-87.70763, 41.96611], [-87.70645, 41.96612], [-87.70399, 41.96613], [-87.70267, 41.96614], [-87.70166, 41.96615], [-87.70075, 41.96615], [-87.69954, 41.96615], [-87.69873, 41.96616], [-87.69789, 41.96618], [-87.69675, 41.9662], [-87.69502, 41.96621], [-87.69411, 41.96621], [-87.69145, 41.96623], [-87.69026, 41.96624], [-87.68946, 41.96625], [-87.6885, 41.96625], [-87.68718, 41.96628], [-87.68545, 41.96631], [-87.68399, 41.96632], [-87.68271, 41.96635], [-87.68159, 41.96636], [-87.68034, 41.96638], [-87.67863, 41.96641], [-87.67766, 41.96642], [-87.67741, 41.96641], [-87.67722, 41.9664], [-87.67695, 41.96638], [-87.67665, 41.96632], [-87.67638, 41.96623], [-87.67613, 41.96612], [-87.67589, 41.96596], [-87.6757, 41.96579], [-87.67557, 41.96565], [-87.67544, 41.96547], [-87.67539, 41.96536], [-87.6753, 41.96519], [-87.67524, 41.96503], [-87.67523, 41.96491], [-87.67522, 41.96477], [-87.67521, 41.96457], [-87.6752, 41.96434], [-87.67519, 41.96371], [-87.67517, 41.96175], [-87.67513, 41.96077], [-87.67505, 41.95798], [-87.67501, 41.95666], [-87.67497, 41.95513], [-87.67496, 41.95452], [-87.67491, 41.95392], [-87.67487, 41.95302], [-87.67485, 41.95202], [-87.67484, 41.95101], [-87.67479, 41.94959], [-87.67476, 41.94859], [-87.67474, 41.94703], [-87.67468, 41.94596], [-87.67466, 41.94513], [-87.67463, 41.94494], [-87.67457, 41.94474], [-87.6745, 41.94455], [-87.67442, 41.94438], [-87.6743, 41.94424], [-87.67419, 41.94414], [-87.67405, 41.94404], [-87.67386, 41.94393], [-87.67367, 41.94386], [-87.67348, 41.9438], [-87.67334, 41.94376], [-87.67311, 41.94373], [-87.67289, 41.9437], [-87.67263, 41.94369], [-87.67234, 41.94369], [-87.6715, 41.9437], [-87.67088, 41.94371], [-87.66938, 41.94373], [-87.66749, 41.94377], [-87.66585, 41.94378], [-87.66508, 41.94379], [-87.66361, 41.94381], [-87.6591, 41.94391], [-87.65767, 41.94391], [-87.65608, 41.94393], [-87.6555, 41.94394], [-87.65521, 41.94394], [-87.65503, 41.94393], [-87.65488, 41.9439], [-87.6547, 41.94386], [-87.65454, 41.9438], [-87.65441, 41.94375], [-87.65425, 41.94364], [-87.6541, 41.94351], [-87.654, 41.94342], [-87.65392, 41.94331], [-87.65382, 41.94319], [-87.65375, 41.94306], [-87.65367, 41.94292], [-87.65361, 41.9428], [-87.65355, 41.94269], [-87.65351, 41.94257], [-87.65347, 41.94238], [-87.65345, 41.94218], [-87.65338, 41.93975], [-87.65337, 41.93939], [-87.65337, 41.93893], [-87.65336, 41.93865], [-87.65333, 41.93763], [-87.65331, 41.93717], [-87.65328, 41.93627], [-87.65327, 41.93603], [-87.65323, 41.93532], [-87.65322, 41.93491], [-87.6532, 41.93445], [-87.65314, 41.93312], [-87.65313, 41.93273], [-87.6531, 41.93218], [-87.65307, 41.93151], [-87.65305, 41.9309], [-87.65302, 41.9303], [-87.65299, 41.92951], [-87.65296, 41.9287], [-87.65295, 41.92842], [-87.65294, 41.92768], [-87.65292, 41.92715], [-87.65289, 41.92599], [-87.65288, 41.92537], [-87.65287, 41.92505], [-87.65282, 41.92352], [-87.65276, 41.92172], [-87.65274, 41.92113], [-87.65264, 41.91822], [-87.65264, 41.91808], [-87.65262, 41.91763], [-87.65261, 41.91718], [-87.65255, 41.91563], [-87.6525, 41.91406], [-87.65242, 41.91377], [-87.65234, 41.91362], [-87.65223, 41.91351], [-87.65208, 41.91339], [-87.65183, 41.91322], [-87.65093, 41.9126], [-87.65017, 41.91203], [-87.64985, 41.9118], [-87.64971, 41.91171], [-87.64957, 41.91164], [-87.64948, 41.9116], [-87.64939, 41.91158], [-87.6492, 41.91153], [-87.649, 41.9115], [-87.64883, 41.9115], [-87.64863, 41.9115], [-87.64792, 41.91151], [-87.64781, 41.9115], [-87.64768, 41.91146], [-87.64756, 41.91139], [-87.64745, 41.91122], [-87.6474, 41.91112], [-87.64739, 41.91101], [-87.64738, 41.91086], [-87.64736, 41.91071], [-87.64734, 41.91061], [-87.64728, 41.91051], [-87.64718, 41.91044], [-87.64709, 41.9104], [-87.64697, 41.91036], [-87.64682, 41.91034], [-87.64664, 41.91033], [-87.64646, 41.91033], [-87.6458, 41.91034], [-87.64523, 41.91034], [-87.64348, 41.91036], [-87.64255, 41.91039], [-87.641, 41.9104], [-87.64038, 41.9104], [-87.63975, 41.9104], [-87.6393, 41.91041], [-87.63814, 41.91042], [-87.63798, 41.91041], [-87.63787, 41.91039], [-87.63771, 41.91034], [-87.63757, 41.91027], [-87.63746, 41.91021], [-87.63736, 41.91011], [-87.6373, 41.90999], [-87.63727, 41.90986], [-87.63726, 41.90973], [-87.63725, 41.90951], [-87.63723, 41.90874], [-87.63718, 41.90758], [-87.63713, 41.90607], [-87.63711, 41.90543], [-87.63702, 41.90381], [-87.63702, 41.90368], [-87.63701, 41.90334], [-87.63699, 41.90322], [-87.63694, 41.90312], [-87.63688, 41.90299], [-87.63682, 41.90292], [-87.63671, 41.90279], [-87.63659, 41.90265], [-87.63653, 41.90255], [-87.63649, 41.90245], [-87.63646, 41.90235], [-87.63647, 41.90221], [-87.63647, 41.90211], [-87.6365, 41.90202], [-87.63653, 41.9019], [-87.63659, 41.90177], [-87.63666, 41.90156], [-87.63669, 41.90143], [-87.6367, 41.90131], [-87.6367, 41.90119], [-87.63664, 41.90029], [-87.63664, 41.90008], [-87.63662, 41.89975], [-87.63658, 41.89892], [-87.63657, 41.89867], [-87.63654, 41.89761], [-87.63654, 41.89738], [-87.63649, 41.89726], [-87.63641, 41.89715], [-87.63634, 41.89708], [-87.63623, 41.89699], [-87.63595, 41.89677], [-87.63583, 41.89667], [-87.63574, 41.89654], [-87.63569, 41.89645], [-87.63568, 41.89633], [-87.63565, 41.89542], [-87.63563, 41.89434], [-87.6356, 41.89327], [-87.63558, 41.89261], [-87.63554, 41.89147], [-87.63553, 41.89051], [-87.63548, 41.8903], [-87.6354, 41.89021], [-87.63533, 41.89012], [-87.63524, 41.89007], [-87.63508, 41.89001], [-87.63493, 41.88997], [-87.63475, 41.88994], [-87.63462, 41.88991], [-87.63447, 41.88989], [-87.63436, 41.88984], [-87.63425, 41.88979], [-87.63414, 41.8897], [-87.63407, 41.88962], [-87.63402, 41.88952], [-87.63399, 41.88943], [-87.63397, 41.88897], [-87.63396, 41.88707], [-87.63391, 41.88572], [-87.63389, 41.88441], [-87.63385, 41.8827], [-87.63384, 41.88144], [-87.63378, 41.88014], [-87.63374, 41.87872], [-87.63369, 41.87726], [-87.63369, 41.87706], [-87.63365, 41.87695], [-87.63359, 41.87691], [-87.63353, 41.87688], [-87.63345, 41.87686], [-87.63338, 41.87685], [-87.63263, 41.87685], [-87.63173, 41.87686], [-87.62925, 41.87689], [-87.62821, 41.87691], [-87.62757, 41.87693], [-87.6265, 41.87696], [-87.62635, 41.87696], [-87.62603, 41.87697], [-87.62605, 41.87831], [-87.6261, 41.87951], [-87.62616, 41.88203], [-87.62619, 41.88322], [-87.62622, 41.88443], [-87.62626, 41.88534], [-87.62625, 41.88552], [-87.62625, 41.88557], [-87.62627, 41.88562], [-87.6263, 41.88566], [-87.62635, 41.88569], [-87.62642, 41.88572], [-87.6265, 41.88573], [-87.62655, 41.88574], [-87.62661, 41.88574], [-87.62683, 41.88574], [-87.62784, 41.88574], [-87.62887, 41.88574], [-87.62948, 41.88574], [-87.62982, 41.88574], [-87.62992, 41.88574], [-87.63011, 41.88574], [-87.6302, 41.88574], [-87.63089, 41.88574], [-87.63204, 41.88574], [-87.63285, 41.88573], [-87.63391, 41.88572], [-87.63396, 41.88707], [-87.63397, 41.88897], [-87.63399, 41.88943], [-87.63402, 41.88952], [-87.63407, 41.88962], [-87.63414, 41.8897], [-87.63425, 41.88979], [-87.63436, 41.88984], [-87.63447, 41.88989], [-87.63462, 41.88991], [-87.63475, 41.88994], [-87.63493, 41.88997], [-87.63508, 41.89001], [-87.63524, 41.89007], [-87.63533, 41.89012], [-87.6354, 41.89021], [-87.63548, 41.8903], [-87.63553, 41.89051], [-87.63554, 41.89147], [-87.63558, 41.89261], [-87.6356, 41.89327], [-87.63563, 41.89434], [-87.63565, 41.89542], [-87.63568, 41.89633], [-87.63569, 41.89645], [-87.63574, 41.89654], [-87.63583, 41.89667], [-87.63595, 41.89677], [-87.63623, 41.89699], [-87.63634, 41.89708], [-87.63641, 41.89715], [-87.63649, 41.89726], [-87.63654, 41.89738], [-87.63654, 41.89761], [-87.63657, 41.89867], [-87.63658, 41.89892], [-87.63662, 41.89975], [-87.63664, 41.90008], [-87.63664, 41.90029], [-87.6367, 41.90119], [-87.6367, 41.90131], [-87.63669, 41.90143], [-87.63666, 41.90156], [-87.63659, 41.90177], [-87.63653, 41.9019], [-87.6365, 41.90202], [-87.63647, 41.90211], [-87.63647, 41.90221], [-87.63646, 41.90235], [-87.63649, 41.90245], [-87.63653, 41.90255], [-87.63659, 41.90265], [-87.63671, 41.90279], [-87.63682, 41.90292], [-87.63688, 41.90299], [-87.63694, 41.90312], [-87.63699, 41.90322], [-87.63701, 41.90334], [-87.63702, 41.90368], [-87.63702, 41.90381], [-87.63711, 41.90543], [-87.63713, 41.90607], [-87.63718, 41.90758], [-87.63723, 41.90874], [-87.63725, 41.90951], [-87.63726, 41.90973], [-87.63727, 41.90986], [-87.6373, 41.90999], [-87.63736, 41.91011], [-87.63746, 41.91021], [-87.63757, 41.91027], [-87.63771, 41.91034], [-87.63787, 41.91039], [-87.63798, 41.91041], [-87.63814, 41.91042], [-87.6393, 41.91041], [-87.63975, 41.9104], [-87.64038, 41.9104], [-87.641, 41.9104], [-87.64255, 41.91039], [-87.64348, 41.91036], [-87.64523, 41.91034], [-87.6458, 41.91034], [-87.64646, 41.91033], [-87.64664, 41.91033], [-87.64682, 41.91034], [-87.64697, 41.91036], [-87.64709, 41.9104], [-87.64718, 41.91044], [-87.64728, 41.91051], [-87.64734, 41.91061], [-87.64736, 41.91071], [-87.64738, 41.91086], [-87.64739, 41.91101], [-87.6474, 41.91112], [-87.64745, 41.91122], [-87.64756, 41.91139], [-87.64768, 41.91146], [-87.64781, 41.9115], [-87.64792, 41.91151], [-87.64863, 41.9115], [-87.64883, 41.9115], [-87.649, 41.9115], [-87.6492, 41.91153], [-87.64939, 41.91158], [-87.64948, 41.9116], [-87.64957, 41.91164], [-87.64971, 41.91171], [-87.64985, 41.9118], [-87.65017, 41.91203], [-87.65093, 41.9126], [-87.65183, 41.91322], [-87.65208, 41.91339], [-87.65223, 41.91351], [-87.65234, 41.91362], [-87.65242, 41.91377], [-87.6525, 41.91406], [-87.65255, 41.91563], [-87.65261, 41.91718], [-87.65262, 41.91763], [-87.65264, 41.91808], [-87.65264, 41.91822], [-87.65274, 41.92113], [-87.65276, 41.92172], [-87.65282, 41.92352], [-87.65287, 41.92505], [-87.65288, 41.92537], [-87.65289, 41.92599], [-87.65292, 41.92715], [-87.65294, 41.92768], [-87.65295, 41.92842], [-87.65296, 41.9287], [-87.65299, 41.92951], [-87.65302, 41.9303], [-87.65305, 41.9309], [-87.65307, 41.93151], [-87.6531, 41.93218], [-87.65313, 41.93273], [-87.65314, 41.93312], [-87.6532, 41.93445], [-87.65322, 41.93491], [-87.65323, 41.93532], [-87.65327, 41.93603], [-87.65328, 41.93627], [-87.65331, 41.93717], [-87.65333, 41.93763], [-87.65336, 41.93865], [-87.65337, 41.93893], [-87.65337, 41.93939], [-87.65338, 41.93975], [-87.65345, 41.94218], [-87.65347, 41.94238], [-87.65351, 41.94257], [-87.65355, 41.94269], [-87.65361, 41.9428], [-87.65367, 41.94292], [-87.65375, 41.94306], [-87.65382, 41.94319], [-87.65392, 41.94331], [-87.654, 41.94342], [-87.6541, 41.94351], [-87.65425, 41.94364], [-87.65441, 41.94375], [-87.65454, 41.9438], [-87.6547, 41.94386], [-87.65488, 41.9439], [-87.65503, 41.94393], [-87.65521, 41.94394], [-87.6555, 41.94394], [-87.65608, 41.94393], [-87.65767, 41.94391], [-87.6591, 41.94391], [-87.66361, 41.94381], [-87.66508, 41.94379], [-87.66585, 41.94378], [-87.66749, 41.94377], [-87.66938, 41.94373], [-87.67088, 41.94371], [-87.6715, 41.9437], [-87.67234, 41.94369], [-87.67263, 41.94369], [-87.67289, 41.9437], [-87.67311, 41.94373], [-87.67334, 41.94376], [-87.67348, 41.9438], [-87.67367, 41.94386], [-87.67386, 41.94393], [-87.67405, 41.94404], [-87.67419, 41.94414], [-87.6743, 41.94424], [-87.67442, 41.94438], [-87.6745, 41.94455], [-87.67457, 41.94474], [-87.67463, 41.94494], [-87.67466, 41.94513], [-87.67468, 41.94596], [-87.67474, 41.94703], [-87.67476, 41.94859], [-87.67479, 41.94959], [-87.67484, 41.95101], [-87.67485, 41.95202], [-87.67487, 41.95302], [-87.67491, 41.95392], [-87.67496, 41.95452], [-87.67497, 41.95513], [-87.67501, 41.95666], [-87.67505, 41.95798], [-87.67513, 41.96077], [-87.67517, 41.96175], [-87.67519, 41.96371], [-87.6752, 41.96434], [-87.67521, 41.96457], [-87.67522, 41.96477], [-87.67523, 41.96491], [-87.67524, 41.96503], [-87.6753, 41.96519], [-87.67539, 41.96536], [-87.67544, 41.96547], [-87.67557, 41.96565], [-87.6757, 41.96579], [-87.67589, 41.96596], [-87.67613, 41.96612], [-87.67638, 41.96623], [-87.67665, 41.96632], [-87.67695, 41.96638], [-87.67722, 41.9664], [-87.67741, 41.96641], [-87.67766, 41.96642], [-87.67863, 41.96641], [-87.68034, 41.96638], [-87.68159, 41.96636], [-87.68271, 41.96635], [-87.68399, 41.96632], [-87.68545, 41.96631], [-87.68718, 41.96628], [-87.6885, 41.96625], [-87.68946, 41.96625], [-87.69026, 41.96624], [-87.69145, 41.96623], [-87.69411, 41.96621], [-87.69502, 41.96621], [-87.69675, 41.9662], [-87.69789, 41.96618], [-87.69873, 41.96616], [-87.69954, 41.96615], [-87.70075, 41.96615], [-87.70166, 41.96615], [-87.70267, 41.96614], [-87.70399, 41.96613], [-87.70645, 41.96612], [-87.70763, 41.96611], [-87.70884, 41.96609], [-87.71027, 41.96607], [-87.71108, 41.96607], [-87.71173, 41.96606], [-87.71219, 41.96605], [-87.7124, 41.96605], [-87.71255, 41.96605], [-87.71268, 41.96605], [-87.7128, 41.96607], [-87.71292, 41.96611], [-87.71298, 41.96615], [-87.71303, 41.96621], [-87.71309, 41.9663], [-87.7131, 41.96635], [-87.71311, 41.96643], [-87.71312, 41.96667], [-87.71314, 41.96793]]}' shapely-2.0.3/shapely/tests/legacy/test_svg.py000066400000000000000000000200751456366510000214560ustar00rootroot00000000000000# Tests SVG output and validity import os import unittest from xml.dom.minidom import parseString as parse_xml_string from shapely.geometry import ( LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.geometry.collection import GeometryCollection class SvgTestCase(unittest.TestCase): def assertSVG(self, geom, expected, **kwrds): """Helper function to check XML and debug SVG""" svg_elem = geom.svg(**kwrds) try: parse_xml_string(svg_elem) except Exception: raise AssertionError("XML is not valid for SVG element: " + str(svg_elem)) svg_doc = geom._repr_svg_() try: doc = parse_xml_string(svg_doc) except Exception: raise AssertionError("XML is not valid for SVG document: " + str(svg_doc)) svg_output_dir = None # svg_output_dir = '.' # useful for debugging SVG files if svg_output_dir: fname = geom.geom_type if geom.is_empty: fname += "_empty" if not geom.is_valid: fname += "_invalid" if kwrds: fname += "_" + ",".join(str(k) + "=" + str(kwrds[k]) for k in kwrds) svg_path = os.path.join(svg_output_dir, fname + ".svg") with open(svg_path, "w") as fp: fp.write(doc.toprettyxml()) assert svg_elem == expected def test_point(self): # Empty self.assertSVG(Point(), "") # Valid g = Point(6, 7) self.assertSVG( g, '', ) self.assertSVG( g, '', scale_factor=5, ) def test_multipoint(self): # Empty self.assertSVG(MultiPoint(), "") # Valid g = MultiPoint([(6, 7), (3, 4)]) self.assertSVG( g, '' '', ) self.assertSVG( g, '' '', scale_factor=5, ) def test_linestring(self): # Empty self.assertSVG(LineString(), "") # Valid g = LineString([(5, 8), (496, -6), (530, 20)]) self.assertSVG( g, '', ) self.assertSVG( g, '', scale_factor=5, ) # Invalid self.assertSVG( LineString([(0, 0), (0, 0)]), '', ) def test_multilinestring(self): # Empty self.assertSVG(MultiLineString(), "") # Valid self.assertSVG( MultiLineString([[(6, 7), (3, 4)], [(2, 8), (9, 1)]]), '' '', ) # Invalid self.assertSVG( MultiLineString([[(2, 3), (2, 3)], [(2, 8), (9, 1)]]), '' '', ) def test_polygon(self): # Empty self.assertSVG(Polygon(), "") # Valid g = Polygon( [(35, 10), (45, 45), (15, 40), (10, 20), (35, 10)], [[(20, 30), (35, 35), (30, 20), (20, 30)]], ) self.assertSVG( g, '', ) self.assertSVG( g, '', scale_factor=5, ) # Invalid self.assertSVG( Polygon([(0, 40), (0, 0), (40, 40), (40, 0), (0, 40)]), '', ) def test_multipolygon(self): # Empty self.assertSVG(MultiPolygon(), "") # Valid self.assertSVG( MultiPolygon( [ Polygon([(40, 40), (20, 45), (45, 30), (40, 40)]), Polygon( [(20, 35), (10, 30), (10, 10), (30, 5), (45, 20), (20, 35)], [[(30, 20), (20, 15), (20, 25), (30, 20)]], ), ] ), '' '', ) # Invalid self.assertSVG( MultiPolygon( [ Polygon([(140, 140), (120, 145), (145, 130), (140, 140)]), Polygon([(0, 40), (0, 0), (40, 40), (40, 0), (0, 40)]), ] ), '' '', ) def test_collection(self): # Empty self.assertSVG(GeometryCollection(), "") # Valid self.assertSVG( GeometryCollection([Point(7, 3), LineString([(4, 2), (8, 4)])]), '' '', ) # Invalid self.assertSVG( Point(7, 3).union(LineString([(4, 2), (4, 2)])), '' '', ) shapely-2.0.3/shapely/tests/legacy/test_transform.py000066400000000000000000000052211456366510000226660ustar00rootroot00000000000000import unittest import pytest from shapely import geometry from shapely.ops import transform class IdentityTestCase(unittest.TestCase): """New geometry/coordseq method 'xy' makes numpy interop easier""" def func(self, x, y, z=None): return tuple(c for c in [x, y, z] if c) def test_empty(self): g = geometry.Point() h = transform(self.func, g) assert h.is_empty def test_point(self): g = geometry.Point(0, 1) h = transform(self.func, g) assert h.geom_type == "Point" assert list(h.coords) == [(0, 1)] def test_line(self): g = geometry.LineString([(0, 1), (2, 3)]) h = transform(self.func, g) assert h.geom_type == "LineString" assert list(h.coords) == [(0, 1), (2, 3)] def test_linearring(self): g = geometry.LinearRing([(0, 1), (2, 3), (2, 2), (0, 1)]) h = transform(self.func, g) assert h.geom_type == "LinearRing" assert list(h.coords) == [(0, 1), (2, 3), (2, 2), (0, 1)] def test_polygon(self): g = geometry.Point(0, 1).buffer(1.0) h = transform(self.func, g) assert h.geom_type == "Polygon" assert g.area == pytest.approx(h.area) def test_multipolygon(self): g = geometry.MultiPoint([(0, 1), (0, 4)]).buffer(1.0) h = transform(self.func, g) assert h.geom_type == "MultiPolygon" assert g.area == pytest.approx(h.area) class LambdaTestCase(unittest.TestCase): """New geometry/coordseq method 'xy' makes numpy interop easier""" def test_point(self): g = geometry.Point(0, 1) h = transform(lambda x, y, z=None: (x + 1.0, y + 1.0), g) assert h.geom_type == "Point" assert list(h.coords) == [(1.0, 2.0)] def test_line(self): g = geometry.LineString([(0, 1), (2, 3)]) h = transform(lambda x, y, z=None: (x + 1.0, y + 1.0), g) assert h.geom_type == "LineString" assert list(h.coords) == [(1.0, 2.0), (3.0, 4.0)] def test_polygon(self): g = geometry.Point(0, 1).buffer(1.0) h = transform(lambda x, y, z=None: (x + 1.0, y + 1.0), g) assert h.geom_type == "Polygon" assert g.area == pytest.approx(h.area) assert h.centroid.x == pytest.approx(1.0) assert h.centroid.y == pytest.approx(2.0) def test_multipolygon(self): g = geometry.MultiPoint([(0, 1), (0, 4)]).buffer(1.0) h = transform(lambda x, y, z=None: (x + 1.0, y + 1.0), g) assert h.geom_type == "MultiPolygon" assert g.area == pytest.approx(h.area) assert h.centroid.x == pytest.approx(1.0) assert h.centroid.y == pytest.approx(3.5) shapely-2.0.3/shapely/tests/legacy/test_union.py000066400000000000000000000044171456366510000220110ustar00rootroot00000000000000import random import unittest from functools import partial from itertools import islice import pytest from shapely.errors import ShapelyDeprecationWarning from shapely.geometry import MultiPolygon, Point from shapely.ops import cascaded_union, unary_union def halton(base): """Returns an iterator over an infinite Halton sequence""" def value(index): result = 0.0 f = 1.0 / base i = index while i > 0: result += f * (i % base) i = i // base f = f / base return result i = 1 while i > 0: yield value(i) i += 1 class UnionTestCase(unittest.TestCase): def test_cascaded_union(self): # cascaded_union is deprecated, as it was superseded by unary_union # Use a partial function to make 100 points uniformly distributed # in a 40x40 box centered on 0,0. r = partial(random.uniform, -20.0, 20.0) points = [Point(r(), r()) for i in range(100)] # Buffer the points, producing 100 polygon spots spots = [p.buffer(2.5) for p in points] # Perform a cascaded union of the polygon spots, dissolving them # into a collection of polygon patches with pytest.warns(ShapelyDeprecationWarning, match="is deprecated"): u = cascaded_union(spots) assert u.geom_type in ("Polygon", "MultiPolygon") def setUp(self): # Instead of random points, use deterministic, pseudo-random Halton # sequences for repeatability sake. self.coords = zip( list(islice(halton(5), 20, 120)), list(islice(halton(7), 20, 120)), ) def test_unary_union(self): patches = [Point(xy).buffer(0.05) for xy in self.coords] u = unary_union(patches) assert u.geom_type == "MultiPolygon" assert u.area == pytest.approx(0.718572540569) def test_unary_union_multi(self): # Test of multipart input based on comment by @schwehr at # https://github.com/shapely/shapely/issues/47#issuecomment-21809308 patches = MultiPolygon([Point(xy).buffer(0.05) for xy in self.coords]) assert unary_union(patches).area == pytest.approx(0.71857254056) assert unary_union([patches, patches]).area == pytest.approx(0.71857254056) shapely-2.0.3/shapely/tests/legacy/test_validation.py000066400000000000000000000003561456366510000230110ustar00rootroot00000000000000import unittest from shapely.geometry import Point from shapely.validation import explain_validity class ValidationTestCase(unittest.TestCase): def test_valid(self): assert explain_validity(Point(0, 0)) == "Valid Geometry" shapely-2.0.3/shapely/tests/legacy/test_vectorized.py000066400000000000000000000067771456366510000230520ustar00rootroot00000000000000import unittest import numpy as np from shapely.geometry import box, MultiPolygon, Point class VectorizedContainsTestCase(unittest.TestCase): def assertContainsResults(self, geom, x, y): from shapely.vectorized import contains result = contains(geom, x, y) x = np.asanyarray(x) y = np.asanyarray(y) self.assertIsInstance(result, np.ndarray) self.assertEqual(result.dtype, bool) result_flat = result.flat x_flat, y_flat = x.flat, y.flat # Do the equivalent operation, only slowly, comparing the result # as we go. for idx in range(x.size): assert result_flat[idx] == geom.contains(Point(x_flat[idx], y_flat[idx])) return result def construct_torus(self): point = Point(0, 0) return point.buffer(5).symmetric_difference(point.buffer(2.5)) def test_contains_poly(self): y, x = np.mgrid[-10:10:5j], np.mgrid[-5:15:5j] self.assertContainsResults(self.construct_torus(), x, y) def test_contains_point(self): y, x = np.mgrid[-10:10:5j], np.mgrid[-5:15:5j] self.assertContainsResults(Point(x[0], y[0]), x, y) def test_contains_linestring(self): y, x = np.mgrid[-10:10:5j], np.mgrid[-5:15:5j] self.assertContainsResults(Point(x[0], y[0]), x, y) def test_contains_multipoly(self): y, x = np.mgrid[-10:10:5j], np.mgrid[-5:15:5j] # Construct a geometry of the torus cut in half vertically. cut_poly = box(-1, -10, -2.5, 10) geom = self.construct_torus().difference(cut_poly) assert isinstance(geom, MultiPolygon) self.assertContainsResults(geom, x, y) def test_y_array_order(self): y, x = np.mgrid[-10:10:5j, -5:15:5j] y = y.copy("f") self.assertContainsResults(self.construct_torus(), x, y) def test_x_array_order(self): y, x = np.mgrid[-10:10:5j, -5:15:5j] x = x.copy("f") self.assertContainsResults(self.construct_torus(), x, y) def test_xy_array_order(self): y, x = np.mgrid[-10:10:5j, -5:15:5j] x = x.copy("f") y = y.copy("f") result = self.assertContainsResults(self.construct_torus(), x, y) # Preserve the order assert result.flags["F_CONTIGUOUS"] def test_array_dtype(self): y, x = np.mgrid[-10:10:5j], np.mgrid[-5:15:5j] x = x.astype(np.int16) self.assertContainsResults(self.construct_torus(), x, y) def test_array_2d(self): y, x = np.mgrid[-10:10:15j, -5:15:16j] result = self.assertContainsResults(self.construct_torus(), x, y) assert result.shape == x.shape def test_shapely_xy_attr_contains(self): g = Point(0, 0).buffer(10.0) self.assertContainsResults(self.construct_torus(), *g.exterior.xy) class VectorizedTouchesTestCase(unittest.TestCase): def test_touches(self): from shapely.vectorized import touches y, x = np.mgrid[-2:3:6j, -1:3:5j] geom = box(0, -1, 2, 2) result = touches(geom, x, y) expected = np.array( [ [False, False, False, False, False], [False, True, True, True, False], [False, True, False, True, False], [False, True, False, True, False], [False, True, True, True, False], [False, False, False, False, False], ], dtype=bool, ) from numpy.testing import assert_array_equal assert_array_equal(result, expected) shapely-2.0.3/shapely/tests/legacy/test_voronoi_diagram.py000066400000000000000000000105341456366510000240350ustar00rootroot00000000000000""" Test cases for Voronoi Diagram creation. Overall, I'm trying less to test the correctness of the result and more to cover input cases and behavior, making sure that we return a sane result without error or raise a useful one. """ import numpy as np import pytest from shapely.geometry import MultiPoint from shapely.geos import geos_version from shapely.ops import voronoi_diagram from shapely.wkt import loads as load_wkt requires_geos_35 = pytest.mark.skipif( geos_version < (3, 5, 0), reason="GEOS >= 3.5.0 is required." ) @requires_geos_35 def test_no_regions(): mp = MultiPoint(points=[(0.5, 0.5)]) with np.errstate(invalid="ignore"): regions = voronoi_diagram(mp) assert len(regions.geoms) == 0 @requires_geos_35 def test_two_regions(): mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)]) regions = voronoi_diagram(mp) assert len(regions.geoms) == 2 @requires_geos_35 def test_edges(): mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)]) regions = voronoi_diagram(mp, edges=True) assert len(regions.geoms) == 1 # can be LineString or MultiLineString depending on the GEOS version assert all(r.geom_type.endswith("LineString") for r in regions.geoms) @requires_geos_35 def test_smaller_envelope(): mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)]) poly = load_wkt("POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))") regions = voronoi_diagram(mp, envelope=poly) assert len(regions.geoms) == 2 assert sum(r.area for r in regions.geoms) > poly.area @requires_geos_35 def test_larger_envelope(): """When the envelope we specify is larger than the area of the input feature, the created regions should expand to fill that area.""" mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)]) poly = load_wkt("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))") regions = voronoi_diagram(mp, envelope=poly) assert len(regions.geoms) == 2 assert sum(r.area for r in regions.geoms) == poly.area @requires_geos_35 def test_from_polygon(): poly = load_wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))") regions = voronoi_diagram(poly) assert len(regions.geoms) == 4 @requires_geos_35 def test_from_polygon_with_enough_tolerance(): poly = load_wkt("POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))") regions = voronoi_diagram(poly, tolerance=1.0) assert len(regions.geoms) == 2 @requires_geos_35 def test_from_polygon_without_enough_tolerance(): poly = load_wkt("POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))") with pytest.raises(ValueError) as exc: voronoi_diagram(poly, tolerance=0.6) assert "Could not create Voronoi Diagram with the specified inputs" in str( exc.value ) assert "Try running again with default tolerance value." in str(exc.value) @requires_geos_35 def test_from_polygon_without_floating_point_coordinates(): poly = load_wkt("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))") with pytest.raises(ValueError) as exc: voronoi_diagram(poly, tolerance=0.1) assert "Could not create Voronoi Diagram with the specified inputs" in str( exc.value ) assert "Try running again with default tolerance value." in str(exc.value) @requires_geos_35 def test_from_multipoint_without_floating_point_coordinates(): """A Multipoint with the same "shape" as the above Polygon raises the same error...""" mp = load_wkt("MULTIPOINT (0 0, 1 0, 1 1, 0 1)") with pytest.raises(ValueError) as exc: voronoi_diagram(mp, tolerance=0.1) assert "Could not create Voronoi Diagram with the specified inputs" in str( exc.value ) assert "Try running again with default tolerance value." in str(exc.value) @requires_geos_35 def test_from_multipoint_with_tolerace_without_floating_point_coordinates(): """This multipoint will not work with a tolerance value.""" mp = load_wkt("MULTIPOINT (0 0, 1 0, 1 2, 0 1)") with pytest.raises(ValueError) as exc: voronoi_diagram(mp, tolerance=0.1) assert "Could not create Voronoi Diagram with the specified inputs" in str( exc.value ) assert "Try running again with default tolerance value." in str(exc.value) @requires_geos_35 def test_from_multipoint_without_tolerace_without_floating_point_coordinates(): """But it's fine without it.""" mp = load_wkt("MULTIPOINT (0 0, 1 0, 1 2, 0 1)") regions = voronoi_diagram(mp) assert len(regions.geoms) == 4 shapely-2.0.3/shapely/tests/legacy/test_wkb.py000066400000000000000000000137511456366510000214450ustar00rootroot00000000000000import binascii import math import struct import sys import pytest from shapely import wkt from shapely.geometry import Point from shapely.geos import geos_version from shapely.tests.legacy.conftest import shapely20_todo from shapely.wkb import dump, dumps, load, loads @pytest.fixture(scope="module") def some_point(): return Point(1.2, 3.4) def bin2hex(value): return binascii.b2a_hex(value).upper().decode("utf-8") def hex2bin(value): return binascii.a2b_hex(value) def hostorder(fmt, value): """Re-pack a hex WKB value to native endianness if needed This routine does not understand WKB format, so it must be provided a struct module format string, without initial indicator character ("@=<>!"), which will be interpreted as big- or little-endian with standard sizes depending on the endian flag in the first byte of the value. """ if fmt and fmt[0] in "@=<>!": raise ValueError("Initial indicator character, one of @=<>!, in fmt") if not fmt or fmt[0] not in "cbB": raise ValueError("Missing endian flag in fmt") (hexendian,) = struct.unpack(fmt[0], hex2bin(value[:2])) hexorder = {0: ">", 1: "<"}[hexendian] sysorder = {"little": "<", "big": ">"}[sys.byteorder] if hexorder == sysorder: return value # Nothing to do return bin2hex( struct.pack( sysorder + fmt, {">": 0, "<": 1}[sysorder], *struct.unpack(hexorder + fmt, hex2bin(value))[1:] ) ) def test_dumps_srid(some_point): result = dumps(some_point) assert bin2hex(result) == hostorder( "BIdd", "0101000000333333333333F33F3333333333330B40" ) result = dumps(some_point, srid=4326) assert bin2hex(result) == hostorder( "BIIdd", "0101000020E6100000333333333333F33F3333333333330B40" ) def test_dumps_endianness(some_point): result = dumps(some_point) assert bin2hex(result) == hostorder( "BIdd", "0101000000333333333333F33F3333333333330B40" ) result = dumps(some_point, big_endian=False) assert bin2hex(result) == "0101000000333333333333F33F3333333333330B40" result = dumps(some_point, big_endian=True) assert bin2hex(result) == "00000000013FF3333333333333400B333333333333" def test_dumps_hex(some_point): result = dumps(some_point, hex=True) assert result == hostorder("BIdd", "0101000000333333333333F33F3333333333330B40") def test_loads_srid(): # load a geometry which includes an srid geom = loads(hex2bin("0101000020E6100000333333333333F33F3333333333330B40")) assert isinstance(geom, Point) assert geom.coords[:] == [(1.2, 3.4)] # by default srid is not exported result = dumps(geom) assert bin2hex(result) == hostorder( "BIdd", "0101000000333333333333F33F3333333333330B40" ) # include the srid in the output result = dumps(geom, include_srid=True) assert bin2hex(result) == hostorder( "BIIdd", "0101000020E6100000333333333333F33F3333333333330B40" ) # replace geometry srid with another result = dumps(geom, srid=27700) assert bin2hex(result) == hostorder( "BIIdd", "0101000020346C0000333333333333F33F3333333333330B40" ) def test_loads_hex(some_point): assert loads(dumps(some_point, hex=True), hex=True) == some_point def test_dump_load_binary(some_point, tmpdir): file = tmpdir.join("test.wkb") with open(file, "wb") as file_pointer: dump(some_point, file_pointer) with open(file, "rb") as file_pointer: restored = load(file_pointer) assert some_point == restored def test_dump_load_hex(some_point, tmpdir): file = tmpdir.join("test.wkb") with open(file, "w") as file_pointer: dump(some_point, file_pointer, hex=True) with open(file, "r") as file_pointer: restored = load(file_pointer, hex=True) assert some_point == restored # pygeos handles both bytes and str @shapely20_todo def test_dump_hex_load_binary(some_point, tmpdir): """Asserts that reading a binary file as text (hex mode) fails.""" file = tmpdir.join("test.wkb") with open(file, "w") as file_pointer: dump(some_point, file_pointer, hex=True) with pytest.raises(TypeError): with open(file, "rb") as file_pointer: load(file_pointer) def test_dump_binary_load_hex(some_point, tmpdir): """Asserts that reading a text file (hex mode) as binary fails.""" file = tmpdir.join("test.wkb") with open(file, "wb") as file_pointer: dump(some_point, file_pointer) # TODO(shapely-2.0) on windows this doesn't seem to error with pygeos, # but you get back a point with garbage coordinates if sys.platform == "win32": with open(file, "r") as file_pointer: restored = load(file_pointer, hex=True) assert some_point != restored return with pytest.raises((UnicodeEncodeError, UnicodeDecodeError)): with open(file, "r") as file_pointer: load(file_pointer, hex=True) requires_geos_380 = pytest.mark.xfail( geos_version < (3, 8, 0), reason="GEOS >= 3.8.0 is required", strict=True ) @requires_geos_380 def test_point_empty(): g = wkt.loads("POINT EMPTY") result = dumps(g, big_endian=False) # Use math.isnan for second part of the WKB representation there are # many byte representations for NaN) assert result[: -2 * 8] == b"\x01\x01\x00\x00\x00" coords = struct.unpack("<2d", result[-2 * 8 :]) assert len(coords) == 2 assert all(math.isnan(val) for val in coords) # Generally GEOS only serializes this correctly starting with GEOS 3.9 # For some reason MacOS has different behaviour (it's actually correct for # older GEOS versions, but not for GEOS 3.8) @pytest.mark.xfail( ( geos_version < (3, 9, 0) and not (geos_version < (3, 8, 0) and sys.platform == "darwin") ), reason="GEOS >= 3.9.0 is required", ) def test_point_z_empty(): g = wkt.loads("POINT Z EMPTY") assert g.wkb_hex == hostorder( "BIddd", "0101000080000000000000F87F000000000000F87F000000000000F87F" ) shapely-2.0.3/shapely/tests/legacy/test_wkt.py000066400000000000000000000031121456366510000214550ustar00rootroot00000000000000from math import pi import pytest from shapely.geometry import Point from shapely.wkt import dump, dumps, load, loads @pytest.fixture(scope="module") def some_point(): return Point(pi, -pi) @pytest.fixture(scope="module") def empty_geometry(): return Point() def test_wkt(some_point): """.wkt and wkt.dumps() both do not trim by default.""" assert some_point.wkt == f"POINT ({pi:.15f} {-pi:.15f})" def test_wkt_null(empty_geometry): assert empty_geometry.wkt == "POINT EMPTY" def test_dump_load(some_point, tmpdir): file = tmpdir.join("test.wkt") with open(file, "w") as file_pointer: dump(some_point, file_pointer) with open(file, "r") as file_pointer: restored = load(file_pointer) assert some_point == restored def test_dump_load_null_geometry(empty_geometry, tmpdir): file = tmpdir.join("test.wkt") with open(file, "w") as file_pointer: dump(empty_geometry, file_pointer) with open(file, "r") as file_pointer: restored = load(file_pointer) # This is does not work with __eq__(): assert empty_geometry.equals(restored) def test_dumps_loads(some_point): assert dumps(some_point) == f"POINT ({pi:.16f} {-pi:.16f})" assert loads(dumps(some_point)) == some_point def test_dumps_loads_null_geometry(empty_geometry): assert dumps(empty_geometry) == "POINT EMPTY" # This is does not work with __eq__(): assert loads(dumps(empty_geometry)).equals(empty_geometry) def test_dumps_precision(some_point): assert dumps(some_point, rounding_precision=4) == f"POINT ({pi:.4f} {-pi:.4f})" shapely-2.0.3/shapely/tests/legacy/threading_test.py000066400000000000000000000020101456366510000226110ustar00rootroot00000000000000import threading from binascii import b2a_hex def main(): num_threads = 10 use_threads = True if not use_threads: # Run core code runShapelyBuilding() else: threads = [ threading.Thread(target=runShapelyBuilding, name=str(i), args=(i,)) for i in range(num_threads) ] for t in threads: t.start() for t in threads: t.join() def runShapelyBuilding(num): print("%s: Running shapely tests on wkb" % num) import shapely.geos print("%s GEOS Handle: %s" % (num, shapely.geos.lgeos.geos_handle)) import shapely.wkb import shapely.wkt p = shapely.wkt.loads("POINT (0 0)") print("%s WKT: %s" % (num, shapely.wkt.dumps(p))) wkb = shapely.wkb.dumps(p) print("%s WKB: %s" % (num, b2a_hex(wkb))) for i in range(10): shapely.wkb.loads(wkb) print("%s GEOS Handle: %s" % (num, shapely.geos.lgeos.geos_handle)) print("Done %s" % num) if __name__ == "__main__": main() shapely-2.0.3/shapely/tests/legacy/valgrind-python.supp000066400000000000000000000073531456366510000233100ustar00rootroot00000000000000# # This is a valgrind suppression file that should be used when using valgrind. # # Here's an example of running valgrind: # # cd python/dist/src # valgrind --tool=memcheck --suppressions=Misc/valgrind-python.supp \ # ./python -E -tt ./Lib/test/regrtest.py -u bsddb,network # # You must edit Objects/obmalloc.c and uncomment Py_USING_MEMORY_DEBUGGER # to use the preferred suppressions with Py_ADDRESS_IN_RANGE. # # If you do not want to recompile Python, you can uncomment # suppressions for PyObject_Free and PyObject_Realloc. # # See Misc/README.valgrind for more information. # all tool names: Addrcheck,Memcheck,cachegrind,helgrind,massif { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Addr4 fun:Py_ADDRESS_IN_RANGE } { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Value4 fun:Py_ADDRESS_IN_RANGE } { ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value Memcheck:Cond fun:Py_ADDRESS_IN_RANGE } { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Addr4 fun:PyObject_Free } { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Value4 fun:PyObject_Free } { ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value Memcheck:Cond fun:PyObject_Free } { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Addr4 fun:PyObject_Realloc } { ADDRESS_IN_RANGE/Invalid read of size 4 Memcheck:Value4 fun:PyObject_Realloc } { ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value Memcheck:Cond fun:PyObject_Realloc } ### ### All the suppressions below are for errors that occur within libraries ### that Python uses. The problems to not appear to be related to Python's ### use of the libraries. ### { GDBM problems, see test_gdbm Memcheck:Param write(buf) fun:write fun:gdbm_open } ### ### These occur from somewhere within the SSL, when running ### test_socket_sll. They are too general to leave on by default. ### ###{ ### somewhere in SSL stuff ### Memcheck:Cond ### fun:memset ###} ###{ ### somewhere in SSL stuff ### Memcheck:Value4 ### fun:memset ###} ### ###{ ### somewhere in SSL stuff ### Memcheck:Cond ### fun:MD5_Update ###} ### ###{ ### somewhere in SSL stuff ### Memcheck:Value4 ### fun:MD5_Update ###} # # All of these problems come from using test_socket_ssl # { from test_socket_ssl Memcheck:Cond fun:BN_bin2bn } { from test_socket_ssl Memcheck:Cond fun:BN_num_bits_word } { from test_socket_ssl Memcheck:Value4 fun:BN_num_bits_word } { from test_socket_ssl Memcheck:Cond fun:BN_mod_exp_mont_word } { from test_socket_ssl Memcheck:Cond fun:BN_mod_exp_mont } { from test_socket_ssl Memcheck:Param write(buf) fun:write obj:/usr/lib/libcrypto.so.0.9.7 } { from test_socket_ssl Memcheck:Cond fun:RSA_verify } { from test_socket_ssl Memcheck:Value4 fun:RSA_verify } { from test_socket_ssl Memcheck:Value4 fun:DES_set_key_unchecked } { from test_socket_ssl Memcheck:Value4 fun:DES_encrypt2 } { from test_socket_ssl Memcheck:Cond obj:/usr/lib/libssl.so.0.9.7 } { from test_socket_ssl Memcheck:Value4 obj:/usr/lib/libssl.so.0.9.7 } { from test_socket_ssl Memcheck:Cond fun:BUF_MEM_grow_clean } { from test_socket_ssl Memcheck:Cond fun:memcpy fun:ssl3_read_bytes } { from test_socket_ssl Memcheck:Cond fun:SHA1_Update } { from test_socket_ssl Memcheck:Value4 fun:SHA1_Update } # some extra lxml specific (?) suppressions.. { Test Memcheck:Param sigaction(act) fun:__libc_sigaction } { ld Memcheck:Cond obj:/lib/ld-2.6.so obj:/lib/ld-2.6.so obj:* } { ld Memcheck:Addr4 obj:/lib/ld-2.6.so obj:/lib/ld-2.6.so obj:* } shapely-2.0.3/shapely/tests/test_constructive.py000066400000000000000000001004021456366510000221340ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import ( Geometry, GeometryCollection, GEOSException, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, ) from shapely.testing import assert_geometries_equal from shapely.tests.common import ( all_types, ArrayLike, empty, empty_line_string, empty_point, empty_polygon, ignore_invalid, line_string, multi_point, point, point_z, ) CONSTRUCTIVE_NO_ARGS = ( shapely.boundary, shapely.centroid, shapely.convex_hull, pytest.param( shapely.concave_hull, marks=pytest.mark.skipif( shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11" ), ), shapely.envelope, shapely.extract_unique_points, shapely.node, shapely.normalize, shapely.point_on_surface, ) CONSTRUCTIVE_FLOAT_ARG = ( shapely.buffer, shapely.offset_curve, shapely.delaunay_triangles, shapely.simplify, shapely.voronoi_polygons, ) @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("func", CONSTRUCTIVE_NO_ARGS) def test_no_args_array(geometry, func): actual = func([geometry, geometry]) assert actual.shape == (2,) assert actual[0] is None or isinstance(actual[0], Geometry) @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("func", CONSTRUCTIVE_FLOAT_ARG) def test_float_arg_array(geometry, func): if ( func is shapely.offset_curve and shapely.get_type_id(geometry) not in [1, 2] and shapely.geos_version < (3, 11, 0) ): with pytest.raises(GEOSException, match="only accept linestrings"): func([geometry, geometry], 0.0) return # voronoi_polygons emits an "invalid" warning when supplied with an empty # point (see https://github.com/libgeos/geos/issues/515) with ignore_invalid( func is shapely.voronoi_polygons and shapely.get_type_id(geometry) == 0 and shapely.geos_version < (3, 12, 0) ): actual = func([geometry, geometry], 0.0) assert actual.shape == (2,) assert isinstance(actual[0], Geometry) @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("reference", all_types) def test_snap_array(geometry, reference): actual = shapely.snap([geometry, geometry], [reference, reference], tolerance=1.0) assert actual.shape == (2,) assert isinstance(actual[0], Geometry) @pytest.mark.parametrize("func", CONSTRUCTIVE_NO_ARGS) def test_no_args_missing(func): actual = func(None) assert actual is None @pytest.mark.parametrize("func", CONSTRUCTIVE_FLOAT_ARG) def test_float_arg_missing(func): actual = func(None, 1.0) assert actual is None @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("func", CONSTRUCTIVE_FLOAT_ARG) def test_float_arg_nan(geometry, func): actual = func(geometry, float("nan")) assert actual is None def test_buffer_cap_style_invalid(): with pytest.raises(ValueError, match="'invalid' is not a valid option"): shapely.buffer(point, 1, cap_style="invalid") def test_buffer_join_style_invalid(): with pytest.raises(ValueError, match="'invalid' is not a valid option"): shapely.buffer(point, 1, join_style="invalid") def test_snap_none(): actual = shapely.snap(None, point, tolerance=1.0) assert actual is None @pytest.mark.parametrize("geometry", all_types) def test_snap_nan_float(geometry): actual = shapely.snap(geometry, point, tolerance=np.nan) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") def test_build_area_none(): actual = shapely.build_area(None) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geom,expected", [ (point, empty), # a point has no area (line_string, empty), # a line string has no area # geometry collection of two polygons are combined into one ( GeometryCollection( [ Polygon([(0, 0), (0, 3), (3, 3), (3, 0), (0, 0)]), Polygon([(1, 1), (2, 2), (1, 2), (1, 1)]), ] ), Polygon( [(0, 0), (0, 3), (3, 3), (3, 0), (0, 0)], holes=[[(1, 1), (2, 2), (1, 2), (1, 1)]], ), ), (empty, empty), ([empty], [empty]), ], ) def test_build_area(geom, expected): actual = shapely.build_area(geom) assert actual is not expected assert actual == expected @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") def test_make_valid_none(): actual = shapely.make_valid(None) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geom,expected", [ (point, point), # a valid geometry stays the same (but is copied) # an L shaped polygon without area is converted to a multilinestring ( Polygon([(0, 0), (1, 1), (1, 2), (1, 1), (0, 0)]), MultiLineString([((1, 1), (1, 2)), ((0, 0), (1, 1))]), ), # a polygon with self-intersection (bowtie) is converted into polygons ( Polygon([(0, 0), (2, 2), (2, 0), (0, 2), (0, 0)]), MultiPolygon( [ Polygon([(1, 1), (2, 2), (2, 0), (1, 1)]), Polygon([(0, 0), (0, 2), (1, 1), (0, 0)]), ] ), ), (empty, empty), ([empty], [empty]), ], ) def test_make_valid(geom, expected): actual = shapely.make_valid(geom) assert actual is not expected # normalize needed to handle variation in output across GEOS versions assert shapely.normalize(actual) == expected @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geom,expected", [ (all_types, all_types), # first polygon is valid, second polygon has self-intersection ( [ Polygon([(0, 0), (2, 2), (0, 2), (0, 0)]), Polygon([(0, 0), (2, 2), (2, 0), (0, 2), (0, 0)]), ], [ Polygon([(0, 0), (2, 2), (0, 2), (0, 0)]), MultiPolygon( [ Polygon([(1, 1), (0, 0), (0, 2), (1, 1)]), Polygon([(1, 1), (2, 2), (2, 0), (1, 1)]), ] ), ], ), ([point, None, empty], [point, None, empty]), ], ) def test_make_valid_1d(geom, expected): actual = shapely.make_valid(geom) # normalize needed to handle variation in output across GEOS versions assert np.all(shapely.normalize(actual) == shapely.normalize(expected)) @pytest.mark.parametrize( "geom,expected", [ (point, point), # a point is always in normalized form # order coordinates of linestrings and parts of multi-linestring ( MultiLineString([((1, 1), (0, 0)), ((1, 1), (1, 2))]), MultiLineString([((1, 1), (1, 2)), ((0, 0), (1, 1))]), ), ], ) def test_normalize(geom, expected): actual = shapely.normalize(geom) assert actual == expected def test_offset_curve_empty(): with ignore_invalid(shapely.geos_version < (3, 12, 0)): # Empty geometries emit an "invalid" warning # (see https://github.com/libgeos/geos/issues/515) actual = shapely.offset_curve(empty_line_string, 2.0) assert shapely.is_empty(actual) def test_offset_curve_distance_array(): # check that kwargs are passed through result = shapely.offset_curve([line_string, line_string], [-2.0, -3.0]) assert result[0] == shapely.offset_curve(line_string, -2.0) assert result[1] == shapely.offset_curve(line_string, -3.0) def test_offset_curve_kwargs(): # check that kwargs are passed through result1 = shapely.offset_curve( line_string, -2.0, quad_segs=2, join_style="mitre", mitre_limit=2.0 ) result2 = shapely.offset_curve(line_string, -2.0) assert result1 != result2 def test_offset_curve_non_scalar_kwargs(): msg = "only accepts scalar values" with pytest.raises(TypeError, match=msg): shapely.offset_curve([line_string, line_string], 1, quad_segs=np.array([8, 9])) with pytest.raises(TypeError, match=msg): shapely.offset_curve( [line_string, line_string], 1, join_style=["round", "bevel"] ) with pytest.raises(TypeError, match=msg): shapely.offset_curve([line_string, line_string], 1, mitre_limit=[5.0, 6.0]) def test_offset_curve_join_style_invalid(): with pytest.raises(ValueError, match="'invalid' is not a valid option"): shapely.offset_curve(line_string, 1.0, join_style="invalid") @pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11") @pytest.mark.parametrize( "geom,expected", [ (LineString([(0, 0), (0, 0), (1, 0)]), LineString([(0, 0), (1, 0)])), ( LinearRing([(0, 0), (1, 2), (1, 2), (1, 3), (0, 0)]), LinearRing([(0, 0), (1, 2), (1, 3), (0, 0)]), ), ( Polygon([(0, 0), (0, 0), (1, 0), (1, 1), (1, 0), (0, 0)]), Polygon([(0, 0), (1, 0), (1, 1), (1, 0), (0, 0)]), ), ( Polygon( [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)], holes=[[(2, 2), (2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]], ), Polygon( [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)], holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]], ), ), ( MultiPolygon( [ Polygon([(0, 0), (0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]), Polygon([(2, 2), (2, 2), (2, 3), (3, 3), (3, 2), (2, 2)]), ] ), MultiPolygon( [ Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]), Polygon([(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)]), ] ), ), # points are unchanged (point, point), (point_z, point_z), (multi_point, multi_point), # empty geometries are unchanged (empty_point, empty_point), (empty_line_string, empty_line_string), (empty, empty), (empty_polygon, empty_polygon), ], ) def test_remove_repeated_points(geom, expected): assert_geometries_equal(shapely.remove_repeated_points(geom, 0), expected) @pytest.mark.skipif(shapely.geos_version < (3, 12, 0), reason="GEOS < 3.12") @pytest.mark.parametrize( "geom, tolerance", [[Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]), 2]] ) def test_remove_repeated_points_invalid_result(geom, tolerance): # Requiring GEOS 3.12 instead of 3.11 # (GEOS 3.11 had a bug causing this to intermittently not fail) with pytest.raises(shapely.GEOSException, match="Invalid number of points"): shapely.remove_repeated_points(geom, tolerance) @pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11") def test_remove_repeated_points_none(): assert shapely.remove_repeated_points(None, 1) is None assert shapely.remove_repeated_points([None], 1).tolist() == [None] geometry = LineString([(0, 0), (0, 0), (1, 1)]) expected = LineString([(0, 0), (1, 1)]) result = shapely.remove_repeated_points([None, geometry], 1) assert result[0] is None assert_geometries_equal(result[1], expected) @pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11") @pytest.mark.parametrize("geom, tolerance", [("Not a geometry", 1), (1, 1)]) def test_remove_repeated_points_invalid_type(geom, tolerance): with pytest.raises(TypeError, match="One of the arguments is of incorrect type"): shapely.remove_repeated_points(geom, tolerance) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize( "geom,expected", [ (LineString([(0, 0), (1, 2)]), LineString([(1, 2), (0, 0)])), ( LinearRing([(0, 0), (1, 2), (1, 3), (0, 0)]), LinearRing([(0, 0), (1, 3), (1, 2), (0, 0)]), ), ( Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]), Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), ), ( Polygon( [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)], holes=[[(2, 2), (2, 4), (4, 4), (4, 2), (2, 2)]], ), Polygon( [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], holes=[[(2, 2), (4, 2), (4, 4), (2, 4), (2, 2)]], ), ), pytest.param( MultiLineString([[(0, 0), (1, 2)], [(3, 3), (4, 4)]]), MultiLineString([[(1, 2), (0, 0)], [(4, 4), (3, 3)]]), marks=pytest.mark.skipif( shapely.geos_version < (3, 8, 1), reason="GEOS < 3.8.1" ), ), ( MultiPolygon( [ Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]), Polygon([(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)]), ] ), MultiPolygon( [ Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), Polygon([(2, 2), (3, 2), (3, 3), (2, 3), (2, 2)]), ] ), ), # points are unchanged (point, point), (point_z, point_z), (multi_point, multi_point), # empty geometries are unchanged (empty_point, empty_point), (empty_line_string, empty_line_string), (empty, empty), (empty_polygon, empty_polygon), ], ) def test_reverse(geom, expected): assert_geometries_equal(shapely.reverse(geom), expected) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_reverse_none(): assert shapely.reverse(None) is None assert shapely.reverse([None]).tolist() == [None] geometry = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]) expected = Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) result = shapely.reverse([None, geometry]) assert result[0] is None assert_geometries_equal(result[1], expected) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize("geom", ["Not a geometry", 1]) def test_reverse_invalid_type(geom): with pytest.raises(TypeError, match="One of the arguments is of incorrect type"): shapely.reverse(geom) @pytest.mark.parametrize( "geom,expected", [ # Point outside (Point(0, 0), GeometryCollection()), # Point inside (Point(15, 15), Point(15, 15)), # Point on boundary (Point(15, 10), GeometryCollection()), # Line outside (LineString([(0, 0), (-5, 5)]), GeometryCollection()), # Line inside (LineString([(15, 15), (16, 15)]), LineString([(15, 15), (16, 15)])), # Line on boundary (LineString([(10, 15), (10, 10), (15, 10)]), GeometryCollection()), # Line splitting rectangle (LineString([(10, 5), (25, 20)]), LineString([(15, 10), (20, 15)])), ], ) def test_clip_by_rect(geom, expected): actual = shapely.clip_by_rect(geom, 10, 10, 20, 20) assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "geom, rect, expected", [ # Polygon hole (CCW) fully on rectangle boundary""" ( Polygon( ((0, 0), (0, 30), (30, 30), (30, 0), (0, 0)), holes=[((10, 10), (20, 10), (20, 20), (10, 20), (10, 10))], ), (10, 10, 20, 20), GeometryCollection(), ), # Polygon hole (CW) fully on rectangle boundary""" ( Polygon( ((0, 0), (0, 30), (30, 30), (30, 0), (0, 0)), holes=[((10, 10), (10, 20), (20, 20), (20, 10), (10, 10))], ), (10, 10, 20, 20), GeometryCollection(), ), # Polygon fully within rectangle""" ( Polygon( ((1, 1), (1, 30), (30, 30), (30, 1), (1, 1)), holes=[((10, 10), (20, 10), (20, 20), (10, 20), (10, 10))], ), (0, 0, 40, 40), Polygon( ((1, 1), (1, 30), (30, 30), (30, 1), (1, 1)), holes=[((10, 10), (20, 10), (20, 20), (10, 20), (10, 10))], ), ), # Polygon overlapping rectanglez ( Polygon( [(0, 0), (0, 30), (30, 30), (30, 0), (0, 0)], holes=[[(10, 10), (20, 10), (20, 20), (10, 20), (10, 10)]], ), (5, 5, 15, 15), Polygon([(5, 5), (5, 15), (10, 15), (10, 10), (15, 10), (15, 5), (5, 5)]), ), ], ) def test_clip_by_rect_polygon(geom, rect, expected): actual = shapely.clip_by_rect(geom, *rect) assert_geometries_equal(actual, expected) @pytest.mark.parametrize("geometry", all_types) def test_clip_by_rect_array(geometry): actual = shapely.clip_by_rect([geometry, geometry], 0.0, 0.0, 1.0, 1.0) assert actual.shape == (2,) assert actual[0] is None or isinstance(actual[0], Geometry) def test_clip_by_rect_missing(): actual = shapely.clip_by_rect(None, 0, 0, 1, 1) assert actual is None @pytest.mark.parametrize("geom", [empty, empty_line_string, empty_polygon]) def test_clip_by_rect_empty(geom): # TODO empty point actual = shapely.clip_by_rect(geom, 0, 0, 1, 1) assert actual == GeometryCollection() def test_clip_by_rect_non_scalar_kwargs(): msg = "only accepts scalar values" with pytest.raises(TypeError, match=msg): shapely.clip_by_rect([line_string, line_string], 0, 0, 1, np.array([0, 1])) def test_polygonize(): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), LineString([(1, 1), (1, 0)]), LineString([(1, 0), (0, 0)]), LineString([(5, 5), (6, 6)]), Point(0, 0), None, ] result = shapely.polygonize(lines) assert shapely.get_type_id(result) == 7 # GeometryCollection expected = GeometryCollection( [ Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), Polygon([(1, 1), (0, 0), (0, 1), (1, 1)]), ] ) assert result == expected def test_polygonize_array(): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), ] expected = GeometryCollection([Polygon([(1, 1), (0, 0), (0, 1), (1, 1)])]) result = shapely.polygonize(np.array(lines)) assert isinstance(result, shapely.Geometry) assert result == expected result = shapely.polygonize(np.array([lines])) assert isinstance(result, np.ndarray) assert result.shape == (1,) assert result[0] == expected arr = np.array([lines, lines]) assert arr.shape == (2, 3) result = shapely.polygonize(arr) assert isinstance(result, np.ndarray) assert result.shape == (2,) assert result[0] == expected assert result[1] == expected arr = np.array([[lines, lines], [lines, lines], [lines, lines]]) assert arr.shape == (3, 2, 3) result = shapely.polygonize(arr) assert isinstance(result, np.ndarray) assert result.shape == (3, 2) for res in result.flatten(): assert res == expected @pytest.mark.skipif( np.__version__ < "1.15", reason="axis keyword for generalized ufunc introduced in np 1.15", ) def test_polygonize_array_axis(): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), ] arr = np.array([lines, lines]) # shape (2, 3) result = shapely.polygonize(arr, axis=1) assert result.shape == (2,) result = shapely.polygonize(arr, axis=0) assert result.shape == (3,) def test_polygonize_missing(): # set of geometries that is all missing result = shapely.polygonize([None, None]) assert result == GeometryCollection() def test_polygonize_full(): lines = [ None, LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), LineString([(1, 1), (1, 0)]), None, LineString([(1, 0), (0, 0)]), LineString([(5, 5), (6, 6)]), LineString([(1, 1), (100, 100)]), Point(0, 0), None, ] result = shapely.polygonize_full(lines) assert len(result) == 4 assert all(shapely.get_type_id(geom) == 7 for geom in result) # GeometryCollection polygons, cuts, dangles, invalid = result expected_polygons = GeometryCollection( [ Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]), Polygon([(1, 1), (0, 0), (0, 1), (1, 1)]), ] ) assert polygons == expected_polygons assert cuts == GeometryCollection() expected_dangles = GeometryCollection( [LineString([(1, 1), (100, 100)]), LineString([(5, 5), (6, 6)])] ) assert dangles == expected_dangles assert invalid == GeometryCollection() def test_polygonize_full_array(): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), ] expected = GeometryCollection([Polygon([(1, 1), (0, 0), (0, 1), (1, 1)])]) result = shapely.polygonize_full(np.array(lines)) assert len(result) == 4 assert all(isinstance(geom, shapely.Geometry) for geom in result) assert result[0] == expected assert all(geom == GeometryCollection() for geom in result[1:]) result = shapely.polygonize_full(np.array([lines])) assert len(result) == 4 assert all(isinstance(geom, np.ndarray) for geom in result) assert all(geom.shape == (1,) for geom in result) assert result[0][0] == expected assert all(geom[0] == GeometryCollection() for geom in result[1:]) arr = np.array([lines, lines]) assert arr.shape == (2, 3) result = shapely.polygonize_full(arr) assert len(result) == 4 assert all(isinstance(arr, np.ndarray) for arr in result) assert all(arr.shape == (2,) for arr in result) assert result[0][0] == expected assert result[0][1] == expected assert all(g == GeometryCollection() for geom in result[1:] for g in geom) arr = np.array([[lines, lines], [lines, lines], [lines, lines]]) assert arr.shape == (3, 2, 3) result = shapely.polygonize_full(arr) assert len(result) == 4 assert all(isinstance(arr, np.ndarray) for arr in result) assert all(arr.shape == (3, 2) for arr in result) for res in result[0].flatten(): assert res == expected for arr in result[1:]: for res in arr.flatten(): assert res == GeometryCollection() @pytest.mark.skipif( np.__version__ < "1.15", reason="axis keyword for generalized ufunc introduced in np 1.15", ) def test_polygonize_full_array_axis(): lines = [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0, 1)]), LineString([(0, 1), (1, 1)]), ] arr = np.array([lines, lines]) # shape (2, 3) result = shapely.polygonize_full(arr, axis=1) assert len(result) == 4 assert all(arr.shape == (2,) for arr in result) result = shapely.polygonize_full(arr, axis=0) assert len(result) == 4 assert all(arr.shape == (3,) for arr in result) def test_polygonize_full_missing(): # set of geometries that is all missing result = shapely.polygonize_full([None, None]) assert len(result) == 4 assert all(geom == GeometryCollection() for geom in result) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("max_segment_length", [-1, 0]) def test_segmentize_invalid_max_segment_length(geometry, max_segment_length): with pytest.raises(GEOSException, match="IllegalArgumentException"): shapely.segmentize(geometry, max_segment_length=max_segment_length) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize("geometry", all_types) def test_segmentize_max_segment_length_nan(geometry): actual = shapely.segmentize(geometry, max_segment_length=np.nan) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry", [empty, empty_point, empty_line_string, empty_polygon] ) def test_segmentize_empty(geometry): actual = shapely.segmentize(geometry, max_segment_length=5) assert_geometries_equal(actual, geometry) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize("geometry", [point, point_z, multi_point]) def test_segmentize_no_change(geometry): actual = shapely.segmentize(geometry, max_segment_length=5) assert_geometries_equal(actual, geometry) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") def test_segmentize_none(): assert shapely.segmentize(None, max_segment_length=5) is None @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry,tolerance, expected", [ # tolerance greater than max edge length, no change ( LineString([(0, 0), (0, 10)]), 20, LineString([(0, 0), (0, 10)]), ), ( Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]), 20, Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]), ), # tolerance causes one vertex per segment ( LineString([(0, 0), (0, 10)]), 5, LineString([(0, 0), (0, 5), (0, 10)]), ), ( Polygon([(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]), 5, Polygon( [ (0, 0), (5, 0), (10, 0), (10, 5), (10, 10), (5, 10), (0, 10), (0, 5), (0, 0), ] ), ), # ensure input arrays are broadcast correctly ( [ LineString([(0, 0), (0, 10)]), LineString([(0, 0), (0, 2)]), ], 5, [ LineString([(0, 0), (0, 5), (0, 10)]), LineString([(0, 0), (0, 2)]), ], ), ( [ LineString([(0, 0), (0, 10)]), LineString([(0, 0), (0, 2)]), ], [5], [ LineString([(0, 0), (0, 5), (0, 10)]), LineString([(0, 0), (0, 2)]), ], ), ( [ LineString([(0, 0), (0, 10)]), LineString([(0, 0), (0, 2)]), ], [5, 1.5], [ LineString([(0, 0), (0, 5), (0, 10)]), LineString([(0, 0), (0, 1), (0, 2)]), ], ), ], ) def test_segmentize(geometry, tolerance, expected): actual = shapely.segmentize(geometry, tolerance) assert_geometries_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize("geometry", all_types) def test_minimum_bounding_circle_all_types(geometry): actual = shapely.minimum_bounding_circle([geometry, geometry]) assert actual.shape == (2,) assert actual[0] is None or isinstance(actual[0], Geometry) actual = shapely.minimum_bounding_circle(None) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geometry, expected", [ ( Polygon([(0, 5), (5, 10), (10, 5), (5, 0), (0, 5)]), shapely.buffer(Point(5, 5), 5), ), ( LineString([(1, 0), (1, 10)]), shapely.buffer(Point(1, 5), 5), ), ( MultiPoint([(2, 2), (4, 2)]), shapely.buffer(Point(3, 2), 1), ), ( Point(2, 2), Point(2, 2), ), ( GeometryCollection(), Polygon(), ), ], ) def test_minimum_bounding_circle(geometry, expected): actual = shapely.minimum_bounding_circle(geometry) assert_geometries_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") @pytest.mark.parametrize("geometry", all_types) def test_oriented_envelope_all_types(geometry): actual = shapely.oriented_envelope([geometry, geometry]) assert actual.shape == (2,) assert actual[0] is None or isinstance(actual[0], Geometry) actual = shapely.oriented_envelope(None) assert actual is None @pytest.mark.parametrize( "func", [shapely.oriented_envelope, shapely.minimum_rotated_rectangle] ) @pytest.mark.parametrize( "geometry, expected", [ ( MultiPoint([(1.0, 1.0), (1.0, 5.0), (3.0, 6.0), (4.0, 2.0), (5.0, 5.0)]), Polygon([(1.0, 1.0), (1.0, 6.0), (5.0, 6.0), (5.0, 1.0), (1.0, 1.0)]), ), ( LineString([(1, 1), (5, 1), (10, 10)]), Polygon([(1, 1), (3, -1), (12, 8), (10, 10), (1, 1)]), ), ( Polygon([(1, 1), (15, 1), (5, 9), (1, 1)]), Polygon([(1.0, 1.0), (5.0, 9.0), (16.2, 3.4), (12.2, -4.6), (1.0, 1.0)]), ), ( LineString([(1, 1), (10, 1)]), LineString([(1, 1), (10, 1)]), ), ( Point(2, 2), Point(2, 2), ), ( GeometryCollection(), Polygon(), ), ], ) def test_oriented_envelope(geometry, expected, func): actual = func(geometry) assert_geometries_equal(actual, expected, normalize=True, tolerance=1e-3) @pytest.mark.skipif( shapely.geos_version >= (3, 12, 0) or shapely.geos_version < (3, 8, 0), reason="GEOS >= 3.12", ) @pytest.mark.parametrize( "geometry, expected", [ ( MultiPoint([(1.0, 1.0), (1.0, 5.0), (3.0, 6.0), (4.0, 2.0), (5.0, 5.0)]), Polygon([(-0.2, 1.4), (1.5, 6.5), (5.1, 5.3), (3.4, 0.2), (-0.2, 1.4)]), ), ( LineString([(1, 1), (5, 1), (10, 10)]), Polygon([(1, 1), (3, -1), (12, 8), (10, 10), (1, 1)]), ), ( Polygon([(1, 1), (15, 1), (5, 9), (1, 1)]), Polygon([(1.0, 1.0), (1.0, 9.0), (15.0, 9.0), (15.0, 1.0), (1.0, 1.0)]), ), ( LineString([(1, 1), (10, 1)]), LineString([(1, 1), (10, 1)]), ), ( Point(2, 2), Point(2, 2), ), ( GeometryCollection(), Polygon(), ), ], ) def test_oriented_envelope_pre_geos_312(geometry, expected): # use private method (similar as direct shapely.lib.oriented_envelope) # to cover the C code for older GEOS versions actual = shapely.constructive._oriented_envelope_geos(geometry) if shapely.geos_version < (3, 8, 0): # For GEOS 3.7, the function returns 3D which was ignored in the old test: assert shapely.equals(actual, expected).all() else: assert_geometries_equal(actual, expected, normalize=True, tolerance=1e-3) def test_oriented_evelope_array_like(): # https://github.com/shapely/shapely/issues/1929 # because we have a custom python implementation, need to ensure this has # the same capabilities as numpy ufuncs to work with array-likes geometries = [Point(1, 1).buffer(1), Point(2, 2).buffer(1)] actual = shapely.oriented_envelope(ArrayLike(geometries)) assert isinstance(actual, ArrayLike) expected = shapely.oriented_envelope(geometries) assert_geometries_equal(np.asarray(actual), expected) @pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11") def test_concave_hull_kwargs(): p = Point(10, 10) mp = MultiPoint(p.buffer(5).exterior.coords[:] + p.buffer(4).exterior.coords[:]) result1 = shapely.concave_hull(mp, ratio=0.5) assert len(result1.interiors) == 0 result2 = shapely.concave_hull(mp, ratio=0.5, allow_holes=True) assert len(result2.interiors) == 1 result3 = shapely.concave_hull(mp, ratio=0) result4 = shapely.concave_hull(mp, ratio=1) assert shapely.get_num_coordinates(result4) < shapely.get_num_coordinates(result3) shapely-2.0.3/shapely/tests/test_coordinates.py000066400000000000000000000215541456366510000217300ustar00rootroot00000000000000import numpy as np import pytest from numpy.testing import assert_allclose, assert_equal import shapely from shapely import count_coordinates, get_coordinates, set_coordinates, transform from shapely.tests.common import ( empty, empty_line_string_z, empty_point, empty_point_z, geometry_collection, geometry_collection_z, line_string, line_string_z, linear_ring, multi_line_string, multi_point, multi_polygon, point, point_z, polygon, polygon_with_hole, polygon_z, ) nested_2 = shapely.geometrycollections([geometry_collection, point]) nested_3 = shapely.geometrycollections([nested_2, point]) @pytest.mark.parametrize( "geoms,count", [ ([], 0), ([empty], 0), ([point, empty], 1), ([empty, point, empty], 1), ([point, None], 1), ([None, point, None], 1), ([point, point], 2), ([point, point_z], 2), ([line_string, linear_ring], 8), ([polygon], 5), ([polygon_with_hole], 10), ([multi_point, multi_line_string], 4), ([multi_polygon], 10), ([geometry_collection], 3), ([nested_2], 4), ([nested_3], 5), ], ) def test_count_coords(geoms, count): actual = count_coordinates(np.array(geoms, np.object_)) assert actual == count # fmt: off @pytest.mark.parametrize("include_z", [True, False]) @pytest.mark.parametrize( "geoms,x,y", [ ([], [], []), ([empty], [], []), ([point, empty], [2], [3]), ([empty, point, empty], [2], [3]), ([point, None], [2], [3]), ([None, point, None], [2], [3]), ([point, point], [2, 2], [3, 3]), ([line_string, linear_ring], [0, 1, 1, 0, 1, 1, 0, 0], [0, 0, 1, 0, 0, 1, 1, 0]), ([polygon], [0, 2, 2, 0, 0], [0, 0, 2, 2, 0]), ([polygon_with_hole], [0, 0, 10, 10, 0, 2, 2, 4, 4, 2], [0, 10, 10, 0, 0, 2, 4, 4, 2, 2]), ([multi_point, multi_line_string], [0, 1, 0, 1], [0, 2, 0, 2]), ([multi_polygon], [0, 1, 1, 0, 0, 2.1, 2.2, 2.2, 2.1, 2.1], [0, 0, 1, 1, 0, 2.1, 2.1, 2.2, 2.2, 2.1]), ([geometry_collection], [51, 52, 49], [-1, -1, 2]), ([nested_2], [51, 52, 49, 2], [-1, -1, 2, 3]), ([nested_3], [51, 52, 49, 2, 2], [-1, -1, 2, 3, 3]), ], ) # fmt: on def test_get_coords(geoms, x, y, include_z): actual = get_coordinates(np.array(geoms, np.object_), include_z=include_z) if not include_z: expected = np.array([x, y], np.float64).T else: expected = np.array([x, y, [np.nan] * len(x)], np.float64).T assert_equal(actual, expected) # fmt: off @pytest.mark.parametrize( "geoms,index", [ ([], []), ([empty], []), ([point, empty], [0]), ([empty, point, empty], [1]), ([point, None], [0]), ([None, point, None], [1]), ([point, point], [0, 1]), ([point, line_string], [0, 1, 1, 1]), ([line_string, point], [0, 0, 0, 1]), ([line_string, linear_ring], [0, 0, 0, 1, 1, 1, 1, 1]), ], ) # fmt: on def test_get_coords_index(geoms, index): _, actual = get_coordinates(np.array(geoms, np.object_), return_index=True) expected = np.array(index, dtype=np.intp) assert_equal(actual, expected) @pytest.mark.parametrize("order", ["C", "F"]) def test_get_coords_index_multidim(order): geometry = np.array([[point, line_string], [empty, empty]], order=order) expected = [0, 1, 1, 1] # would be [0, 2, 2, 2] with fortran order _, actual = get_coordinates(geometry, return_index=True) assert_equal(actual, expected) # fmt: off @pytest.mark.parametrize("include_z", [True, False]) @pytest.mark.parametrize( "geoms,x,y,z", [ ([point, point_z], [2, 2], [3, 3], [np.nan, 4]), ([line_string_z], [0, 1, 1], [0, 0, 1], [4, 4, 4]), ([polygon_z], [0, 2, 2, 0, 0], [0, 0, 2, 2, 0], [4, 4, 4, 4, 4]), ([geometry_collection_z], [2, 0, 1, 1], [3, 0, 0, 1], [4, 4, 4, 4]), ([point, empty_point], [2], [3], [np.nan]), ], ) # fmt: on def test_get_coords_3d(geoms, x, y, z, include_z): actual = get_coordinates(np.array(geoms, np.object_), include_z=include_z) if include_z: expected = np.array([x, y, z], np.float64).T else: expected = np.array([x, y], np.float64).T assert_equal(actual, expected) @pytest.mark.parametrize("include_z", [True, False]) @pytest.mark.parametrize( "geoms,count,has_ring", [ ([], 0, False), ([empty], 0, False), ([empty_point], 0, False), ([point, empty], 1, False), ([empty, point, empty], 1, False), ([point, None], 1, False), ([None, point, None], 1, False), ([point, point], 2, False), ([point, point_z], 2, False), ([line_string, linear_ring], 8, True), ([line_string_z], 3, True), ([polygon], 5, True), ([polygon_z], 5, True), ([polygon_with_hole], 10, True), ([multi_point, multi_line_string], 4, False), ([multi_polygon], 10, True), ([geometry_collection], 3, False), ([geometry_collection_z], 3, False), ([nested_2], 4, False), ([nested_3], 5, False), ], ) def test_set_coords(geoms, count, has_ring, include_z): arr_geoms = np.array(geoms, np.object_) n = 3 if include_z else 2 coords = get_coordinates(arr_geoms, include_z=include_z) + np.random.random((1, n)) new_geoms = set_coordinates(arr_geoms, coords) assert_equal(coords, get_coordinates(new_geoms, include_z=include_z)) def test_set_coords_nan(): geoms = np.array([point]) coords = np.array([[np.nan, np.inf]]) new_geoms = set_coordinates(geoms, coords) assert_equal(coords, get_coordinates(new_geoms)) def test_set_coords_breaks_ring(): with pytest.raises(shapely.GEOSException): set_coordinates(linear_ring, np.random.random((5, 2))) def test_set_coords_0dim(): # a geometry input returns a geometry actual = set_coordinates(point, [[1, 1]]) assert isinstance(actual, shapely.Geometry) # a 0-dim array input returns a 0-dim array actual = set_coordinates(np.asarray(point), [[1, 1]]) assert isinstance(actual, np.ndarray) assert actual.ndim == 0 @pytest.mark.parametrize("include_z", [True, False]) def test_set_coords_mixed_dimension(include_z): geoms = np.array([point, point_z], dtype=object) coords = get_coordinates(geoms, include_z=include_z) new_geoms = set_coordinates(geoms, coords * 2) if include_z: # preserve original dimensionality assert not shapely.has_z(new_geoms[0]) assert shapely.has_z(new_geoms[1]) else: # all 2D assert not shapely.has_z(new_geoms).any() @pytest.mark.parametrize("include_z", [True, False]) @pytest.mark.parametrize( "geoms", [[], [empty], [None, point, None], [nested_3], [point, point_z], [line_string_z]], ) def test_transform(geoms, include_z): geoms = np.array(geoms, np.object_) coordinates_before = get_coordinates(geoms, include_z=include_z) new_geoms = transform(geoms, lambda x: x + 1, include_z=include_z) assert new_geoms is not geoms coordinates_after = get_coordinates(new_geoms, include_z=include_z) assert_allclose(coordinates_before + 1, coordinates_after, equal_nan=True) def test_transform_0dim(): # a geometry input returns a geometry actual = transform(point, lambda x: x + 1) assert isinstance(actual, shapely.Geometry) # a 0-dim array input returns a 0-dim array actual = transform(np.asarray(point), lambda x: x + 1) assert isinstance(actual, np.ndarray) assert actual.ndim == 0 def test_transform_check_shape(): def remove_coord(arr): return arr[:-1] with pytest.raises(ValueError): transform(linear_ring, remove_coord) def test_transform_correct_coordinate_dimension(): # ensure that new geometry is 2D with include_z=False geom = line_string_z assert shapely.get_coordinate_dimension(geom) == 3 new_geom = transform(geom, lambda x: x + 1, include_z=False) assert shapely.get_coordinate_dimension(new_geom) == 2 @pytest.mark.parametrize("geom", [ pytest.param(empty_point_z, marks=pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="Empty points don't have a dimensionality before GEOS 3.9")), empty_line_string_z, ]) def test_transform_empty_preserve_z(geom): assert shapely.get_coordinate_dimension(geom) == 3 new_geom = transform(geom, lambda x: x + 1, include_z=True) assert shapely.get_coordinate_dimension(new_geom) == 3 @pytest.mark.parametrize("geom", [ pytest.param(empty_point_z, marks=pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="Empty points don't have a dimensionality before GEOS 3.9")), empty_line_string_z, ]) def test_transform_remove_z(geom): assert shapely.get_coordinate_dimension(geom) == 3 new_geom = transform(geom, lambda x: x + 1, include_z=False) assert shapely.get_coordinate_dimension(new_geom) == 2 shapely-2.0.3/shapely/tests/test_creation.py000066400000000000000000000427141456366510000212230ustar00rootroot00000000000000import numpy as np import pytest import shapely # Note: Point is not imported because it is overridden for testing from shapely import ( GeometryCollection, GeometryType, LinearRing, LineString, MultiLineString, MultiPoint, MultiPolygon, Polygon, ) from shapely.testing import assert_geometries_equal from shapely.tests.common import ( empty_polygon, geometry_collection, line_string, linear_ring, multi_line_string, multi_point, multi_polygon, point, polygon, ) def box_tpl(x1, y1, x2, y2): return (x2, y1), (x2, y2), (x1, y2), (x1, y1), (x2, y1) def test_points_from_coords(): actual = shapely.points([[0, 0], [2, 2]]) assert_geometries_equal(actual, [shapely.Point(0, 0), shapely.Point(2, 2)]) def test_points_from_xy(): actual = shapely.points(2, [0, 1]) assert_geometries_equal(actual, [shapely.Point(2, 0), shapely.Point(2, 1)]) def test_points_from_xyz(): actual = shapely.points(1, 1, [0, 1]) assert_geometries_equal(actual, [shapely.Point(1, 1, 0), shapely.Point(1, 1, 1)]) def test_points_invalid_ndim(): with pytest.raises(ValueError, match="dimension should be 2 or 3, got 4"): shapely.points([0, 1, 2, 3]) with pytest.raises(ValueError, match="dimension should be 2 or 3, got 1"): shapely.points([0]) @pytest.mark.skipif( shapely.geos_version[:2] not in ((3, 10), (3, 11), (3, 12)), reason="GEOS not in 3.10, 3.11, 3.12", ) def test_points_nan_all_nan_becomes_empty(): actual = shapely.points(np.nan, np.nan) assert actual.wkt == "POINT EMPTY" @pytest.mark.skipif( shapely.geos_version[:2] not in ((3, 10), (3, 11)), reason="GEOS not in 3.10, 3.11", ) def test_points_nan_3D_all_nan_becomes_empty_2D(): actual = shapely.points(np.nan, np.nan, np.nan) assert actual.wkt == "POINT EMPTY" @pytest.mark.skipif(shapely.geos_version[:2] != (3, 12), reason="GEOS != 3.12") def test_points_nan_3D_all_nan_becomes_empty(): actual = shapely.points(np.nan, np.nan, np.nan) assert actual.wkt == "POINT Z EMPTY" @pytest.mark.skipif(shapely.geos_version < (3, 12, 0), reason="GEOS < 3.12") @pytest.mark.parametrize( "coords,expected_wkt", [ pytest.param( [np.nan, np.nan], "POINT (NaN NaN)", marks=pytest.mark.skipif( shapely.geos_version < (3, 13, 0), reason="GEOS < 3.13" ), ), pytest.param( [np.nan, np.nan, np.nan], "POINT Z (NaN NaN NaN)", marks=pytest.mark.skipif( shapely.geos_version < (3, 13, 0), reason="GEOS < 3.13" ), ), ([1, np.nan], "POINT (1 NaN)"), ([np.nan, 1], "POINT (NaN 1)"), ([np.nan, 1, np.nan], "POINT Z (NaN 1 NaN)"), ([np.nan, np.nan, 1], "POINT Z (NaN NaN 1)"), ], ) def test_points_handle_nan_allow(coords, expected_wkt): actual = shapely.points(coords) assert actual.wkt == expected_wkt def test_linestrings_from_coords(): actual = shapely.linestrings([[[0, 0], [1, 1]], [[0, 0], [2, 2]]]) assert_geometries_equal( actual, [ LineString([(0, 0), (1, 1)]), LineString([(0, 0), (2, 2)]), ], ) def test_linestrings_from_xy(): actual = shapely.linestrings([0, 1], [2, 3]) assert_geometries_equal(actual, LineString([(0, 2), (1, 3)])) def test_linestrings_from_xy_broadcast(): x = [0, 1] # the same X coordinates for both linestrings y = [2, 3], [4, 5] # each linestring has a different set of Y coordinates actual = shapely.linestrings(x, y) assert_geometries_equal( actual, [ LineString([(0, 2), (1, 3)]), LineString([(0, 4), (1, 5)]), ], ) def test_linestrings_from_xyz(): actual = shapely.linestrings([0, 1], [2, 3], 0) assert_geometries_equal(actual, LineString([(0, 2, 0), (1, 3, 0)])) @pytest.mark.parametrize("dim", [2, 3]) def test_linestrings_buffer(dim): coords = np.random.randn(10, 3, dim) coords1 = np.asarray(coords, order="C") result1 = shapely.linestrings(coords1) coords2 = np.asarray(coords1, order="F") result2 = shapely.linestrings(coords2) assert_geometries_equal(result1, result2) # creating (.., 8, 8*3) strided array so it uses copyFromArrays coords3 = np.asarray(np.swapaxes(np.swapaxes(coords, 0, 2), 1, 0), order="F") coords3 = np.swapaxes(np.swapaxes(coords3, 0, 2), 1, 2) result3 = shapely.linestrings(coords3) assert_geometries_equal(result1, result3) def test_linestrings_invalid_shape_scalar(): with pytest.raises(ValueError): shapely.linestrings((1, 1)) @pytest.mark.parametrize( "shape", [ (2, 1, 2), # 2 linestrings of 1 2D point (1, 1, 2), # 1 linestring of 1 2D point (1, 2), # 1 linestring of 1 2D point (scalar) ], ) def test_linestrings_invalid_shape(shape): with pytest.raises(shapely.GEOSException): shapely.linestrings(np.ones(shape)) def test_linestrings_invalid_ndim(): msg = r"The ordinate \(last\) dimension should be 2 or 3, got {}" coords = np.ones((10, 2, 4), order="C") with pytest.raises(ValueError, match=msg.format(4)): shapely.linestrings(coords) coords = np.ones((10, 2, 4), order="F") with pytest.raises(ValueError, match=msg.format(4)): shapely.linestrings(coords) coords = np.swapaxes(np.swapaxes(np.ones((10, 2, 4)), 0, 2), 1, 0) coords = np.swapaxes(np.swapaxes(np.asarray(coords, order="F"), 0, 2), 1, 2) with pytest.raises(ValueError, match=msg.format(4)): shapely.linestrings(coords) # too few ordinates coords = np.ones((10, 2, 1)) with pytest.raises(ValueError, match=msg.format(1)): shapely.linestrings(coords) def test_linearrings(): actual = shapely.linearrings(box_tpl(0, 0, 1, 1)) assert_geometries_equal( actual, LinearRing([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)]) ) def test_linearrings_from_xy(): actual = shapely.linearrings([0, 1, 2, 0], [3, 4, 5, 3]) assert_geometries_equal(actual, LinearRing([(0, 3), (1, 4), (2, 5), (0, 3)])) def test_linearrings_unclosed(): actual = shapely.linearrings(box_tpl(0, 0, 1, 1)[:-1]) assert_geometries_equal( actual, LinearRing([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)]) ) def test_linearrings_unclosed_all_coords_equal(): actual = shapely.linearrings([(0, 0), (0, 0), (0, 0)]) assert_geometries_equal(actual, LinearRing([(0, 0), (0, 0), (0, 0), (0, 0)])) def test_linearrings_invalid_shape_scalar(): with pytest.raises(ValueError): shapely.linearrings((1, 1)) @pytest.mark.parametrize( "shape", [ (2, 1, 2), # 2 linearrings of 1 2D point (1, 1, 2), # 1 linearring of 1 2D point (1, 2), # 1 linearring of 1 2D point (scalar) (2, 2, 2), # 2 linearrings of 2 2D points (1, 2, 2), # 1 linearring of 2 2D points (2, 2), # 1 linearring of 2 2D points (scalar) ], ) def test_linearrings_invalid_shape(shape): coords = np.ones(shape) with pytest.raises(ValueError): shapely.linearrings(coords) # make sure the first coordinate != second coordinate coords[..., 1] += 1 with pytest.raises(ValueError): shapely.linearrings(coords) def test_linearrings_invalid_ndim(): msg = r"The ordinate \(last\) dimension should be 2 or 3, got {}" coords1 = np.random.randn(10, 3, 4) with pytest.raises(ValueError, match=msg.format(4)): shapely.linearrings(coords1) coords2 = np.hstack((coords1, coords1[:, [0], :])) with pytest.raises(ValueError, match=msg.format(4)): shapely.linearrings(coords2) # too few ordinates coords3 = np.random.randn(10, 3, 1) with pytest.raises(ValueError, match=msg.format(1)): shapely.linestrings(coords3) def test_linearrings_all_nan(): coords = np.full((4, 2), np.nan) with pytest.raises(shapely.GEOSException): shapely.linearrings(coords) @pytest.mark.parametrize("dim", [2, 3]) @pytest.mark.parametrize("order", ["C", "F"]) def test_linearrings_buffer(dim, order): coords1 = np.random.randn(10, 4, dim) coords1 = np.asarray(coords1, order=order) result1 = shapely.linearrings(coords1) # with manual closure -> can directly copy from buffer if C order coords2 = np.hstack((coords1, coords1[:, [0], :])) coords2 = np.asarray(coords2, order=order) result2 = shapely.linearrings(coords2) assert_geometries_equal(result1, result2) # create scalar -> can also directly copy from buffer if F order coords3 = np.asarray(coords2[0], order=order) result3 = shapely.linearrings(coords3) assert_geometries_equal(result3, result1[0]) def test_polygon_from_linearring(): actual = shapely.polygons(shapely.linearrings(box_tpl(0, 0, 1, 1))) assert_geometries_equal(actual, Polygon([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)])) def test_polygons_none(): assert_geometries_equal(shapely.polygons(None), empty_polygon) assert_geometries_equal(shapely.polygons(None, holes=[linear_ring]), empty_polygon) def test_polygons(): actual = shapely.polygons(box_tpl(0, 0, 1, 1)) assert_geometries_equal(actual, Polygon([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)])) def test_polygon_no_hole_list_raises(): with pytest.raises(ValueError): shapely.polygons(box_tpl(0, 0, 10, 10), box_tpl(1, 1, 2, 2)) def test_polygon_no_hole_wrong_type(): with pytest.raises((TypeError, shapely.GEOSException)): shapely.polygons(point) def test_polygon_with_hole_wrong_type(): with pytest.raises((TypeError, shapely.GEOSException)): shapely.polygons(point, [linear_ring]) def test_polygon_wrong_hole_type(): with pytest.raises((TypeError, shapely.GEOSException)): shapely.polygons(linear_ring, [point]) def test_polygon_with_1_hole(): actual = shapely.polygons(box_tpl(0, 0, 10, 10), [box_tpl(1, 1, 2, 2)]) assert shapely.area(actual) == 99.0 def test_polygon_with_2_holes(): actual = shapely.polygons( box_tpl(0, 0, 10, 10), [box_tpl(1, 1, 2, 2), box_tpl(3, 3, 4, 4)] ) assert shapely.area(actual) == 98.0 def test_polygon_with_none_hole(): actual = shapely.polygons( shapely.linearrings(box_tpl(0, 0, 10, 10)), [ shapely.linearrings(box_tpl(1, 1, 2, 2)), None, shapely.linearrings(box_tpl(3, 3, 4, 4)), ], ) assert shapely.area(actual) == 98.0 def test_2_polygons_with_same_hole(): actual = shapely.polygons( [box_tpl(0, 0, 10, 10), box_tpl(0, 0, 5, 5)], [box_tpl(1, 1, 2, 2)] ) assert shapely.area(actual).tolist() == [99.0, 24.0] def test_2_polygons_with_2_same_holes(): actual = shapely.polygons( [box_tpl(0, 0, 10, 10), box_tpl(0, 0, 5, 5)], [box_tpl(1, 1, 2, 2), box_tpl(3, 3, 4, 4)], ) assert shapely.area(actual).tolist() == [98.0, 23.0] def test_2_polygons_with_different_holes(): actual = shapely.polygons( [box_tpl(0, 0, 10, 10), box_tpl(0, 0, 5, 5)], [[box_tpl(1, 1, 3, 3)], [box_tpl(1, 1, 2, 2)]], ) assert shapely.area(actual).tolist() == [96.0, 24.0] def test_polygons_not_enough_points_in_shell_scalar(): with pytest.raises(ValueError): shapely.polygons((1, 1)) @pytest.mark.parametrize( "shape", [ (2, 1, 2), # 2 linearrings of 1 2D point (1, 1, 2), # 1 linearring of 1 2D point (1, 2), # 1 linearring of 1 2D point (scalar) (2, 2, 2), # 2 linearrings of 2 2D points (1, 2, 2), # 1 linearring of 2 2D points (2, 2), # 1 linearring of 2 2D points (scalar) ], ) def test_polygons_not_enough_points_in_shell(shape): coords = np.ones(shape) with pytest.raises(ValueError): shapely.polygons(coords) # make sure the first coordinate != second coordinate coords[..., 1] += 1 with pytest.raises(ValueError): shapely.polygons(coords) def test_polygons_not_enough_points_in_holes_scalar(): with pytest.raises(ValueError): shapely.polygons(np.ones((1, 4, 2)), (1, 1)) @pytest.mark.parametrize( "shape", [ (2, 1, 2), # 2 linearrings of 1 2D point (1, 1, 2), # 1 linearring of 1 2D point (1, 2), # 1 linearring of 1 2D point (scalar) (2, 2, 2), # 2 linearrings of 2 2D points (1, 2, 2), # 1 linearring of 2 2D points (2, 2), # 1 linearring of 2 2D points (scalar) ], ) def test_polygons_not_enough_points_in_holes(shape): coords = np.ones(shape) with pytest.raises(ValueError): shapely.polygons(np.ones((1, 4, 2)), coords) # make sure the first coordinate != second coordinate coords[..., 1] += 1 with pytest.raises(ValueError): shapely.polygons(np.ones((1, 4, 2)), coords) @pytest.mark.parametrize( "func,expected", [ (shapely.multipoints, MultiPoint()), (shapely.multilinestrings, MultiLineString()), (shapely.multipolygons, MultiPolygon()), (shapely.geometrycollections, GeometryCollection()), ], ) def test_create_collection_only_none(func, expected): actual = func(np.array([None], dtype=object)) assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "func,sub_geom", [ (shapely.multipoints, point), (shapely.multilinestrings, line_string), (shapely.multilinestrings, linear_ring), (shapely.multipolygons, polygon), (shapely.geometrycollections, point), (shapely.geometrycollections, line_string), (shapely.geometrycollections, linear_ring), (shapely.geometrycollections, polygon), (shapely.geometrycollections, multi_point), (shapely.geometrycollections, multi_line_string), (shapely.geometrycollections, multi_polygon), (shapely.geometrycollections, geometry_collection), ], ) def test_create_collection(func, sub_geom): actual = func([sub_geom, sub_geom]) assert shapely.get_num_geometries(actual) == 2 @pytest.mark.parametrize( "func,sub_geom", [ (shapely.multipoints, point), (shapely.multilinestrings, line_string), (shapely.multipolygons, polygon), (shapely.geometrycollections, polygon), ], ) def test_create_collection_skips_none(func, sub_geom): actual = func([sub_geom, None, None, sub_geom]) assert shapely.get_num_geometries(actual) == 2 @pytest.mark.parametrize( "func,sub_geom", [ (shapely.multipoints, line_string), (shapely.multipoints, geometry_collection), (shapely.multipoints, multi_point), (shapely.multilinestrings, point), (shapely.multilinestrings, polygon), (shapely.multilinestrings, multi_line_string), (shapely.multipolygons, linear_ring), (shapely.multipolygons, multi_point), (shapely.multipolygons, multi_polygon), ], ) def test_create_collection_wrong_geom_type(func, sub_geom): with pytest.raises(TypeError): func([sub_geom]) @pytest.mark.parametrize( "coords,ccw,expected", [ ((0, 0, 1, 1), True, Polygon([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)])), ( (0, 0, 1, 1), False, Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]), ), ], ) def test_box(coords, ccw, expected): actual = shapely.box(*coords, ccw=ccw) assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "coords,ccw,expected", [ ( (0, 0, [1, 2], [1, 2]), True, [ Polygon([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)]), Polygon([(2, 0), (2, 2), (0, 2), (0, 0), (2, 0)]), ], ), ( (0, 0, [1, 2], [1, 2]), [True, False], [ Polygon([(1, 0), (1, 1), (0, 1), (0, 0), (1, 0)]), Polygon([(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]), ], ), ], ) def test_box_array(coords, ccw, expected): actual = shapely.box(*coords, ccw=ccw) assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "coords", [ [np.nan, np.nan, np.nan, np.nan], [np.nan, 0, 1, 1], [0, np.nan, 1, 1], [0, 0, np.nan, 1], [0, 0, 1, np.nan], ], ) def test_box_nan(coords): assert shapely.box(*coords) is None def test_prepare(): arr = np.array([shapely.points(1, 1), None, shapely.box(0, 0, 1, 1)]) assert arr[0]._geom_prepared == 0 assert arr[2]._geom_prepared == 0 shapely.prepare(arr) assert arr[0]._geom_prepared != 0 assert arr[1] is None assert arr[2]._geom_prepared != 0 # preparing again actually does nothing original = arr[0]._geom_prepared shapely.prepare(arr) assert arr[0]._geom_prepared == original def test_destroy_prepared(): arr = np.array([shapely.points(1, 1), None, shapely.box(0, 0, 1, 1)]) shapely.prepare(arr) assert arr[0]._geom_prepared != 0 assert arr[2]._geom_prepared != 0 shapely.destroy_prepared(arr) assert arr[0]._geom_prepared == 0 assert arr[1] is None assert arr[2]._geom_prepared == 0 shapely.destroy_prepared(arr) # does not error @pytest.mark.parametrize("geom_type", [None, GeometryType.MISSING, -1]) def test_empty_missing(geom_type): actual = shapely.empty((2,), geom_type=geom_type) assert shapely.is_missing(actual).all() @pytest.mark.parametrize("geom_type", range(8)) def test_empty(geom_type): actual = shapely.empty((2,), geom_type=geom_type) assert (~shapely.is_missing(actual)).all() assert shapely.is_empty(actual).all() assert (shapely.get_type_id(actual) == geom_type).all() shapely-2.0.3/shapely/tests/test_creation_indices.py000066400000000000000000000321261456366510000227150ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import LinearRing, Polygon from shapely.testing import assert_geometries_equal from shapely.tests.common import empty_point, line_string, linear_ring, point, polygon pnts = shapely.points lstrs = shapely.linestrings geom_coll = shapely.geometrycollections @pytest.mark.parametrize( "func", [shapely.points, shapely.linestrings, shapely.linearrings] ) @pytest.mark.parametrize( "coordinates", [ np.empty((2,)), # not enough dimensions np.empty((2, 4, 1)), # too many dimensions np.empty((2, 4)), # wrong inner dimension size None, np.full((2, 2), "foo", dtype=object), # wrong type ], ) def test_invalid_coordinates(func, coordinates): with pytest.raises((TypeError, ValueError)): func(coordinates, indices=[0, 1]) @pytest.mark.parametrize( "func", [ shapely.multipoints, shapely.multilinestrings, shapely.multipolygons, shapely.geometrycollections, ], ) @pytest.mark.parametrize( "geometries", [np.array([1, 2], dtype=np.intp), None, np.array([[point]]), "hello"] ) def test_invalid_geometries(func, geometries): with pytest.raises((TypeError, ValueError)): func(geometries, indices=[0, 1]) @pytest.mark.parametrize( "func", [shapely.points, shapely.linestrings, shapely.linearrings] ) @pytest.mark.parametrize("indices", [[point], " hello", [0, 1], [-1]]) def test_invalid_indices_simple(func, indices): with pytest.raises((TypeError, ValueError)): func([[0.2, 0.3]], indices=indices) non_writeable = np.empty(3, dtype=object) non_writeable.flags.writeable = False @pytest.mark.parametrize( "func", [shapely.points, shapely.linestrings, shapely.geometrycollections] ) @pytest.mark.parametrize( "out", [ [None, None, None], # not an ndarray np.empty(3), # wrong dtype non_writeable, # not writeable np.empty((3, 2), dtype=object), # too many dimensions np.empty((), dtype=object), # too few dimensions np.empty((2,), dtype=object), # too small ], ) def test_invalid_out(func, out): if func is shapely.points: x = [[0.2, 0.3], [0.4, 0.5]] indices = [0, 2] elif func is shapely.linestrings: x = [[1, 1], [2, 1], [2, 2], [3, 3], [3, 4], [4, 4]] indices = [0, 0, 0, 2, 2, 2] else: x = [point, line_string] indices = [0, 2] with pytest.raises((TypeError, ValueError)): func(x, indices=indices, out=out) def test_points_invalid(): # attempt to construct a point with 2 coordinates with pytest.raises(shapely.GEOSException): shapely.points([[1, 1], [2, 2]], indices=[0, 0]) def test_points(): actual = shapely.points( np.array([[2, 3], [2, 3]], dtype=float), indices=np.array([0, 1], dtype=np.intp), ) assert_geometries_equal(actual, [point, point]) def test_points_no_index_raises(): with pytest.raises(ValueError): shapely.points( np.array([[2, 3], [2, 3]], dtype=float), indices=np.array([0, 2], dtype=np.intp), ) @pytest.mark.parametrize( "indices,expected", [ ([0, 1], [point, point, empty_point, None]), ([0, 3], [point, None, empty_point, point]), ([2, 3], [None, None, point, point]), ], ) def test_points_out(indices, expected): out = np.empty(4, dtype=object) out[2] = empty_point actual = shapely.points( [[2, 3], [2, 3]], indices=indices, out=out, ) assert_geometries_equal(out, expected) assert actual is out @pytest.mark.parametrize( "coordinates,indices,expected", [ ([[1, 1], [2, 2]], [0, 0], [lstrs([[1, 1], [2, 2]])]), ([[1, 1, 1], [2, 2, 2]], [0, 0], [lstrs([[1, 1, 1], [2, 2, 2]])]), ( [[1, 1], [2, 2], [2, 2], [3, 3]], [0, 0, 1, 1], [lstrs([[1, 1], [2, 2]]), lstrs([[2, 2], [3, 3]])], ), ], ) def test_linestrings(coordinates, indices, expected): actual = shapely.linestrings( np.array(coordinates, dtype=float), indices=np.array(indices, dtype=np.intp) ) assert_geometries_equal(actual, expected) def test_linestrings_invalid(): # attempt to construct linestrings with 1 coordinate with pytest.raises(shapely.GEOSException): shapely.linestrings([[1, 1], [2, 2]], indices=[0, 1]) @pytest.mark.parametrize( "indices,expected", [ ([0, 0, 0, 1, 1, 1], [line_string, line_string, empty_point, None]), ([0, 0, 0, 3, 3, 3], [line_string, None, empty_point, line_string]), ([2, 2, 2, 3, 3, 3], [None, None, line_string, line_string]), ], ) def test_linestrings_out(indices, expected): out = np.empty(4, dtype=object) out[2] = empty_point actual = shapely.linestrings( [(0, 0), (1, 0), (1, 1), (0, 0), (1, 0), (1, 1)], indices=indices, out=out, ) assert_geometries_equal(out, expected) assert actual is out @pytest.mark.parametrize( "coordinates", [([[1, 1], [2, 1], [2, 2], [1, 1]]), ([[1, 1], [2, 1], [2, 2]])] ) def test_linearrings(coordinates): actual = shapely.linearrings( np.array(coordinates, dtype=np.float64), indices=np.zeros(len(coordinates), dtype=np.intp), ) assert_geometries_equal(actual, shapely.linearrings(coordinates)) @pytest.mark.parametrize( "coordinates", [ ([[1, np.nan], [2, 1], [2, 2], [1, 1]]), # starting with nan ], ) def test_linearrings_invalid(coordinates): # attempt to construct linestrings with 1 coordinate with pytest.raises((shapely.GEOSException, ValueError)): shapely.linearrings(coordinates, indices=np.zeros(len(coordinates))) def test_linearrings_unclosed_all_coords_equal(): actual = shapely.linearrings([(0, 0), (0, 0), (0, 0)], indices=np.zeros(3)) assert_geometries_equal(actual, LinearRing([(0, 0), (0, 0), (0, 0), (0, 0)])) @pytest.mark.parametrize( "indices,expected", [ ([0, 0, 0, 0, 0], [linear_ring, None, None, empty_point]), ([1, 1, 1, 1, 1], [None, linear_ring, None, empty_point]), ([3, 3, 3, 3, 3], [None, None, None, linear_ring]), ], ) def test_linearrings_out(indices, expected): out = np.empty(4, dtype=object) out[3] = empty_point actual = shapely.linearrings( [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)], indices=indices, out=out, ) assert_geometries_equal(out, expected) assert actual is out @pytest.mark.parametrize("dim", [2, 3]) @pytest.mark.parametrize("order", ["C", "F"]) def test_linearrings_buffer(dim, order): coords = np.random.randn(10, 4, dim) coords1 = np.asarray(coords.reshape(10 * 4, dim), order=order) indices1 = np.repeat(range(10), 4) result1 = shapely.linearrings(coords1, indices=indices1) # with manual closure -> can directly copy from buffer if C order coords2 = np.hstack((coords, coords[:, [0], :])) coords2 = np.asarray(coords2.reshape(10 * 5, dim), order=order) indices2 = np.repeat(range(10), 5) result2 = shapely.linearrings(coords2, indices=indices2) assert_geometries_equal(result1, result2) hole_1 = shapely.linearrings([(0.2, 0.2), (0.2, 0.4), (0.4, 0.4)]) hole_2 = shapely.linearrings([(0.6, 0.6), (0.6, 0.8), (0.8, 0.8)]) poly = shapely.polygons(linear_ring) poly_empty = Polygon() poly_hole_1 = shapely.polygons(linear_ring, holes=[hole_1]) poly_hole_2 = shapely.polygons(linear_ring, holes=[hole_2]) poly_hole_1_2 = shapely.polygons(linear_ring, holes=[hole_1, hole_2]) @pytest.mark.parametrize( "rings,indices,expected", [ ([linear_ring, linear_ring], [0, 1], [poly, poly]), ([None, linear_ring], [0, 1], [poly_empty, poly]), ([None, linear_ring, None, None], [0, 0, 1, 1], [poly, poly_empty]), ([linear_ring, hole_1, linear_ring], [0, 0, 1], [poly_hole_1, poly]), ([linear_ring, linear_ring, hole_1], [0, 1, 1], [poly, poly_hole_1]), ([None, linear_ring, linear_ring, hole_1], [0, 0, 1, 1], [poly, poly_hole_1]), ([linear_ring, None, linear_ring, hole_1], [0, 0, 1, 1], [poly, poly_hole_1]), ([linear_ring, None, linear_ring, hole_1], [0, 1, 1, 1], [poly, poly_hole_1]), ([linear_ring, linear_ring, None, hole_1], [0, 1, 1, 1], [poly, poly_hole_1]), ([linear_ring, linear_ring, hole_1, None], [0, 1, 1, 1], [poly, poly_hole_1]), ( [linear_ring, hole_1, hole_2, linear_ring], [0, 0, 0, 1], [poly_hole_1_2, poly], ), ( [linear_ring, hole_1, linear_ring, hole_2], [0, 0, 1, 1], [poly_hole_1, poly_hole_2], ), ( [linear_ring, linear_ring, hole_1, hole_2], [0, 1, 1, 1], [poly, poly_hole_1_2], ), ( [linear_ring, hole_1, None, hole_2, linear_ring], [0, 0, 0, 0, 1], [poly_hole_1_2, poly], ), ( [linear_ring, hole_1, None, linear_ring, hole_2], [0, 0, 0, 1, 1], [poly_hole_1, poly_hole_2], ), ( [linear_ring, hole_1, linear_ring, None, hole_2], [0, 0, 1, 1, 1], [poly_hole_1, poly_hole_2], ), ], ) def test_polygons(rings, indices, expected): actual = shapely.polygons( np.array(rings, dtype=object), indices=np.array(indices, dtype=np.intp) ) assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "indices,expected", [ ([0, 1], [poly, poly, empty_point, None]), ([0, 3], [poly, None, empty_point, poly]), ([2, 3], [None, None, poly, poly]), ], ) def test_polygons_out(indices, expected): out = np.empty(4, dtype=object) out[2] = empty_point actual = shapely.polygons([linear_ring, linear_ring], indices=indices, out=out) assert_geometries_equal(out, expected) assert actual is out @pytest.mark.parametrize( "func", [ shapely.polygons, shapely.multipoints, shapely.multilinestrings, shapely.multipolygons, shapely.geometrycollections, ], ) @pytest.mark.parametrize("indices", [np.array([point]), " hello", [0, 1], [-1]]) def test_invalid_indices_collections(func, indices): with pytest.raises((TypeError, ValueError)): func([point], indices=indices) @pytest.mark.parametrize( "geometries,indices,expected", [ ([point, line_string], [0, 0], [geom_coll([point, line_string])]), ([point, line_string], [0, 1], [geom_coll([point]), geom_coll([line_string])]), ([point, None], [0, 0], [geom_coll([point])]), ([point, None], [0, 1], [geom_coll([point]), geom_coll([])]), ([None, point, None, None], [0, 0, 1, 1], [geom_coll([point]), geom_coll([])]), ([point, None, line_string], [0, 0, 0], [geom_coll([point, line_string])]), ], ) def test_geometrycollections(geometries, indices, expected): actual = shapely.geometrycollections( np.array(geometries, dtype=object), indices=indices ) assert_geometries_equal(actual, expected) def test_geometrycollections_no_index_raises(): with pytest.raises(ValueError): shapely.geometrycollections( np.array([point, line_string], dtype=object), indices=[0, 2] ) @pytest.mark.parametrize( "indices,expected", [ ([0, 0], [geom_coll([point, line_string]), None, None, empty_point]), ([3, 3], [None, None, None, geom_coll([point, line_string])]), ], ) def test_geometrycollections_out(indices, expected): out = np.empty(4, dtype=object) out[3] = empty_point actual = shapely.geometrycollections([point, line_string], indices=indices, out=out) assert_geometries_equal(out, expected) assert actual is out def test_multipoints(): actual = shapely.multipoints( np.array([point], dtype=object), indices=np.zeros(1, dtype=np.intp) ) assert_geometries_equal(actual, shapely.multipoints([point])) def test_multilinestrings(): actual = shapely.multilinestrings( np.array([line_string], dtype=object), indices=np.zeros(1, dtype=np.intp) ) assert_geometries_equal(actual, shapely.multilinestrings([line_string])) def test_multilinearrings(): actual = shapely.multilinestrings( np.array([linear_ring], dtype=object), indices=np.zeros(1, dtype=np.intp) ) assert_geometries_equal(actual, shapely.multilinestrings([linear_ring])) def test_multipolygons(): actual = shapely.multipolygons( np.array([polygon], dtype=object), indices=np.zeros(1, dtype=np.intp) ) assert_geometries_equal(actual, shapely.multipolygons([polygon])) @pytest.mark.parametrize( "geometries,func", [ ([point], shapely.polygons), ([line_string], shapely.polygons), ([polygon], shapely.polygons), ([line_string], shapely.multipoints), ([polygon], shapely.multipoints), ([point], shapely.multilinestrings), ([polygon], shapely.multilinestrings), ([point], shapely.multipolygons), ([line_string], shapely.multipolygons), ], ) def test_incompatible_types(geometries, func): with pytest.raises(TypeError): func(geometries, indices=[0]) shapely-2.0.3/shapely/tests/test_geometry.py000066400000000000000000000576051456366510000212570ustar00rootroot00000000000000import warnings import numpy as np import pytest import shapely from shapely import LinearRing, LineString, MultiPolygon, Point, Polygon from shapely.testing import assert_geometries_equal from shapely.tests.common import all_types from shapely.tests.common import empty as empty_geometry_collection from shapely.tests.common import ( empty_line_string, empty_line_string_z, empty_point, empty_point_z, empty_polygon, geometry_collection, geometry_collection_z, ignore_invalid, line_string, line_string_nan, line_string_z, linear_ring, multi_line_string, multi_line_string_z, multi_point, multi_point_z, multi_polygon, multi_polygon_z, point, point_z, polygon, polygon_with_hole, polygon_with_hole_z, polygon_z, ) def test_get_num_points(): actual = shapely.get_num_points(all_types + (None,)).tolist() assert actual == [0, 3, 5, 0, 0, 0, 0, 0, 0, 0] def test_get_num_interior_rings(): actual = shapely.get_num_interior_rings(all_types + (polygon_with_hole, None)) assert actual.tolist() == [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0] def test_get_num_geometries(): actual = shapely.get_num_geometries(all_types + (None,)).tolist() assert actual == [1, 1, 1, 1, 2, 1, 2, 2, 0, 0] @pytest.mark.parametrize( "geom", [ point, polygon, multi_point, multi_line_string, multi_polygon, geometry_collection, ], ) def test_get_point_non_linestring(geom): actual = shapely.get_point(geom, [0, 2, -1]) assert shapely.is_missing(actual).all() @pytest.mark.parametrize("geom", [line_string, linear_ring]) def test_get_point(geom): n = shapely.get_num_points(geom) actual = shapely.get_point(geom, [0, -n, n, -(n + 1)]) assert_geometries_equal(actual[0], actual[1]) assert shapely.is_missing(actual[2:4]).all() @pytest.mark.parametrize( "geom", [ point, line_string, linear_ring, multi_point, multi_line_string, multi_polygon, geometry_collection, ], ) def test_get_exterior_ring_non_polygon(geom): actual = shapely.get_exterior_ring(geom) assert shapely.is_missing(actual).all() def test_get_exterior_ring(): actual = shapely.get_exterior_ring([polygon, polygon_with_hole]) assert (shapely.get_type_id(actual) == 2).all() @pytest.mark.parametrize( "geom", [ point, line_string, linear_ring, multi_point, multi_line_string, multi_polygon, geometry_collection, ], ) def test_get_interior_ring_non_polygon(geom): actual = shapely.get_interior_ring(geom, [0, 2, -1]) assert shapely.is_missing(actual).all() def test_get_interior_ring(): actual = shapely.get_interior_ring(polygon_with_hole, [0, -1, 1, -2]) assert_geometries_equal(actual[0], actual[1]) assert shapely.is_missing(actual[2:4]).all() @pytest.mark.parametrize("geom", [point, line_string, linear_ring, polygon]) def test_get_geometry_simple(geom): actual = shapely.get_geometry(geom, [0, -1, 1, -2]) assert_geometries_equal(actual[0], actual[1]) assert shapely.is_missing(actual[2:4]).all() @pytest.mark.parametrize( "geom", [multi_point, multi_line_string, multi_polygon, geometry_collection] ) def test_get_geometry_collection(geom): n = shapely.get_num_geometries(geom) actual = shapely.get_geometry(geom, [0, -n, n, -(n + 1)]) assert_geometries_equal(actual[0], actual[1]) assert shapely.is_missing(actual[2:4]).all() def test_get_type_id(): actual = shapely.get_type_id(all_types).tolist() assert actual == [0, 1, 2, 3, 4, 5, 6, 7, 7] def test_get_dimensions(): actual = shapely.get_dimensions(all_types).tolist() assert actual == [0, 1, 1, 2, 0, 1, 2, 1, -1] def test_get_coordinate_dimension(): actual = shapely.get_coordinate_dimension([point, point_z, None]).tolist() assert actual == [2, 3, -1] def test_get_num_coordinates(): actual = shapely.get_num_coordinates(all_types + (None,)).tolist() assert actual == [1, 3, 5, 5, 2, 2, 10, 3, 0, 0] def test_get_srid(): """All geometry types have no SRID by default; None returns -1""" actual = shapely.get_srid(all_types + (None,)).tolist() assert actual == [0, 0, 0, 0, 0, 0, 0, 0, 0, -1] def test_get_set_srid(): actual = shapely.set_srid(point, 4326) assert shapely.get_srid(point) == 0 assert shapely.get_srid(actual) == 4326 @pytest.mark.parametrize( "func", [ shapely.get_x, shapely.get_y, pytest.param( shapely.get_z, marks=pytest.mark.skipif( shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7" ), ), ], ) @pytest.mark.parametrize("geom", all_types[1:]) def test_get_xyz_no_point(func, geom): assert np.isnan(func(geom)) def test_get_x(): assert shapely.get_x([point, point_z]).tolist() == [2.0, 2.0] def test_get_y(): assert shapely.get_y([point, point_z]).tolist() == [3.0, 3.0] @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_get_z(): assert shapely.get_z([point_z]).tolist() == [4.0] @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_get_z_2d(): assert np.isnan(shapely.get_z(point)) @pytest.mark.parametrize("geom", all_types) def test_new_from_wkt(geom): actual = shapely.from_wkt(str(geom)) assert_geometries_equal(actual, geom) def test_adapt_ptr_raises(): point = Point(2, 2) with pytest.raises(AttributeError): point._geom += 1 @pytest.mark.parametrize( "geom", all_types + (shapely.points(np.nan, np.nan), empty_point) ) def test_hash_same_equal(geom): assert hash(geom) == hash(shapely.transform(geom, lambda x: x)) @pytest.mark.parametrize("geom", all_types[:-1]) def test_hash_same_not_equal(geom): assert hash(geom) != hash(shapely.transform(geom, lambda x: x + 1)) @pytest.mark.parametrize("geom", all_types) def test_eq(geom): assert geom == shapely.transform(geom, lambda x: x) @pytest.mark.parametrize("geom", all_types[:-1]) def test_neq(geom): assert geom != shapely.transform(geom, lambda x: x + 1) @pytest.mark.parametrize("geom", all_types) def test_set_unique(geom): a = {geom, shapely.transform(geom, lambda x: x)} assert len(a) == 1 def test_set_nan(): # As NaN != NaN, you can have multiple "NaN" points in a set # set([float("nan"), float("nan")]) also returns a set with 2 elements with ignore_invalid(): a = set(shapely.linestrings([[[np.nan, np.nan], [np.nan, np.nan]]] * 10)) assert len(a) == 10 # different objects: NaN != NaN def test_set_nan_same_objects(): # You can't put identical objects in a set. # x = float("nan"); set([x, x]) also retuns a set with 1 element a = set([line_string_nan] * 10) assert len(a) == 1 @pytest.mark.parametrize( "geom", [ point, multi_point, line_string, multi_line_string, polygon, multi_polygon, geometry_collection, empty_point, empty_line_string, empty_polygon, empty_geometry_collection, np.array([None]), np.empty_like(np.array([None])), ], ) def test_get_parts(geom): expected_num_parts = shapely.get_num_geometries(geom) if expected_num_parts == 0: expected_parts = [] else: expected_parts = shapely.get_geometry(geom, range(0, expected_num_parts)) parts = shapely.get_parts(geom) assert len(parts) == expected_num_parts assert_geometries_equal(parts, expected_parts) def test_get_parts_array(): # note: this also verifies that None is handled correctly # in the mix; internally it returns -1 for count of geometries geom = np.array([None, empty_line_string, multi_point, point, multi_polygon]) expected_parts = [] for g in geom: for i in range(0, shapely.get_num_geometries(g)): expected_parts.append(shapely.get_geometry(g, i)) parts = shapely.get_parts(geom) assert len(parts) == len(expected_parts) assert_geometries_equal(parts, expected_parts) def test_get_parts_geometry_collection_multi(): """On the first pass, the individual Multi* geometry objects are returned from the collection. On the second pass, the individual singular geometry objects within those are returned. """ geom = shapely.geometrycollections([multi_point, multi_line_string, multi_polygon]) expected_num_parts = shapely.get_num_geometries(geom) expected_parts = shapely.get_geometry(geom, range(0, expected_num_parts)) parts = shapely.get_parts(geom) assert len(parts) == expected_num_parts assert_geometries_equal(parts, expected_parts) expected_subparts = [] for g in np.asarray(expected_parts): for i in range(0, shapely.get_num_geometries(g)): expected_subparts.append(shapely.get_geometry(g, i)) subparts = shapely.get_parts(parts) assert len(subparts) == len(expected_subparts) assert_geometries_equal(subparts, expected_subparts) def test_get_parts_return_index(): geom = np.array([multi_point, point, multi_polygon]) expected_parts = [] expected_index = [] for i, g in enumerate(geom): for j in range(0, shapely.get_num_geometries(g)): expected_parts.append(shapely.get_geometry(g, j)) expected_index.append(i) parts, index = shapely.get_parts(geom, return_index=True) assert len(parts) == len(expected_parts) assert_geometries_equal(parts, expected_parts) assert np.array_equal(index, expected_index) @pytest.mark.parametrize( "geom", ([[None]], [[empty_point]], [[multi_point]], [[multi_point, multi_line_string]]), ) def test_get_parts_invalid_dimensions(geom): """Only 1D inputs are supported""" with pytest.raises(ValueError, match="Array should be one dimensional"): shapely.get_parts(geom) @pytest.mark.parametrize("geom", [point, line_string, polygon]) def test_get_parts_non_multi(geom): """Non-multipart geometries should be returned identical to inputs""" assert_geometries_equal(geom, shapely.get_parts(geom)) @pytest.mark.parametrize("geom", [None, [None], []]) def test_get_parts_None(geom): assert len(shapely.get_parts(geom)) == 0 @pytest.mark.parametrize("geom", ["foo", ["foo"], 42]) def test_get_parts_invalid_geometry(geom): with pytest.raises(TypeError, match="One of the arguments is of incorrect type."): shapely.get_parts(geom) @pytest.mark.parametrize( "geom", [ point, multi_point, line_string, multi_line_string, polygon, multi_polygon, geometry_collection, empty_point, empty_line_string, empty_polygon, empty_geometry_collection, None, ], ) def test_get_rings(geom): if (shapely.get_type_id(geom) != shapely.GeometryType.POLYGON) or shapely.is_empty( geom ): rings = shapely.get_rings(geom) assert len(rings) == 0 else: rings = shapely.get_rings(geom) assert len(rings) == 1 assert rings[0] == shapely.get_exterior_ring(geom) def test_get_rings_holes(): rings = shapely.get_rings(polygon_with_hole) assert len(rings) == 2 assert rings[0] == shapely.get_exterior_ring(polygon_with_hole) assert rings[1] == shapely.get_interior_ring(polygon_with_hole, 0) def test_get_rings_return_index(): geom = np.array([polygon, None, empty_polygon, polygon_with_hole]) expected_parts = [] expected_index = [] for i, g in enumerate(geom): if g is None or shapely.is_empty(g): continue expected_parts.append(shapely.get_exterior_ring(g)) expected_index.append(i) for j in range(0, shapely.get_num_interior_rings(g)): expected_parts.append(shapely.get_interior_ring(g, j)) expected_index.append(i) parts, index = shapely.get_rings(geom, return_index=True) assert len(parts) == len(expected_parts) assert_geometries_equal(parts, expected_parts) assert np.array_equal(index, expected_index) @pytest.mark.parametrize("geom", [[[None]], [[polygon]]]) def test_get_rings_invalid_dimensions(geom): """Only 1D inputs are supported""" with pytest.raises(ValueError, match="Array should be one dimensional"): shapely.get_parts(geom) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_get_precision(): geometries = all_types + (point_z, empty_point, empty_line_string, empty_polygon) # default is 0 actual = shapely.get_precision(geometries).tolist() assert actual == [0] * len(geometries) geometry = shapely.set_precision(geometries, 1) actual = shapely.get_precision(geometry).tolist() assert actual == [1] * len(geometries) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_get_precision_none(): assert np.all(np.isnan(shapely.get_precision([None]))) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") @pytest.mark.parametrize("mode", ("valid_output", "pointwise", "keep_collapsed")) def test_set_precision(mode): initial_geometry = Point(0.9, 0.9) assert shapely.get_precision(initial_geometry) == 0 geometry = shapely.set_precision(initial_geometry, 0, mode=mode) assert shapely.get_precision(geometry) == 0 assert_geometries_equal(geometry, initial_geometry) geometry = shapely.set_precision(initial_geometry, 1, mode=mode) assert shapely.get_precision(geometry) == 1 assert_geometries_equal(geometry, Point(1, 1)) # original should remain unchanged assert_geometries_equal(initial_geometry, Point(0.9, 0.9)) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_set_precision_drop_coords(): # setting precision of 0 will not drop duplicated points in original geometry = shapely.set_precision(LineString([(0, 0), (0, 0), (0, 1), (1, 1)]), 0) assert_geometries_equal(geometry, LineString([(0, 0), (0, 0), (0, 1), (1, 1)])) # setting precision will remove duplicated points geometry = shapely.set_precision(geometry, 1) assert_geometries_equal(geometry, LineString([(0, 0), (0, 1), (1, 1)])) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") @pytest.mark.parametrize("mode", ("valid_output", "pointwise", "keep_collapsed")) def test_set_precision_z(mode): with warnings.catch_warnings(): warnings.simplefilter("ignore") # GEOS <= 3.9 emits warning for 'pointwise' geometry = shapely.set_precision(Point(0.9, 0.9, 0.9), 1, mode=mode) assert shapely.get_precision(geometry) == 1 assert_geometries_equal(geometry, Point(1, 1, 0.9)) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") @pytest.mark.parametrize("mode", ("valid_output", "pointwise", "keep_collapsed")) def test_set_precision_nan(mode): with warnings.catch_warnings(): warnings.simplefilter("ignore") # GEOS <= 3.9 emits warning for 'pointwise' actual = shapely.set_precision(line_string_nan, 1, mode=mode) assert_geometries_equal(actual, line_string_nan) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_set_precision_none(): assert shapely.set_precision(None, 0) is None @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_set_precision_grid_size_nan(): assert shapely.set_precision(Point(0.9, 0.9), np.nan) is None @pytest.mark.parametrize( "geometry,mode,expected", [ ( Polygon([(2, 2), (4, 2), (3.2, 3), (4, 4), (2, 4), (2.8, 3), (2, 2)]), "valid_output", MultiPolygon( [ Polygon([(4, 2), (2, 2), (3, 3), (4, 2)]), Polygon([(2, 4), (4, 4), (3, 3), (2, 4)]), ] ), ), pytest.param( Polygon([(2, 2), (4, 2), (3.2, 3), (4, 4), (2, 4), (2.8, 3), (2, 2)]), "pointwise", Polygon([(2, 2), (4, 2), (3, 3), (4, 4), (2, 4), (3, 3), (2, 2)]), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="pointwise does not work pre-GEOS 3.10", ), ), ( Polygon([(2, 2), (4, 2), (3.2, 3), (4, 4), (2, 4), (2.8, 3), (2, 2)]), "keep_collapsed", MultiPolygon( [ Polygon([(4, 2), (2, 2), (3, 3), (4, 2)]), Polygon([(2, 4), (4, 4), (3, 3), (2, 4)]), ] ), ), (LineString([(0, 0), (0.1, 0.1)]), "valid_output", LineString()), pytest.param( LineString([(0, 0), (0.1, 0.1)]), "pointwise", LineString([(0, 0), (0, 0)]), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="pointwise does not work pre-GEOS 3.10", ), ), ( LineString([(0, 0), (0.1, 0.1)]), "keep_collapsed", LineString([(0, 0), (0, 0)]), ), pytest.param( LinearRing([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "valid_output", LinearRing(), marks=pytest.mark.skipif( shapely.geos_version == (3, 10, 0), reason="Segfaults on GEOS 3.10.0" ), ), pytest.param( LinearRing([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "pointwise", LinearRing([(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="pointwise does not work pre-GEOS 3.10", ), ), pytest.param( LinearRing([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "keep_collapsed", # See https://trac.osgeo.org/geos/ticket/1135#comment:5 LineString([(0, 0), (0, 0), (0, 0)]), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="this collapsed into an invalid linearring pre-GEOS 3.10", ), ), ( Polygon([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "valid_output", Polygon(), ), pytest.param( Polygon([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "pointwise", Polygon([(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="pointwise does not work pre-GEOS 3.10", ), ), ( Polygon([(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]), "keep_collapsed", Polygon(), ), ], ) def test_set_precision_collapse(geometry, mode, expected): """Lines and polygons collapse to empty geometries if vertices are too close""" actual = shapely.set_precision(geometry, 1, mode=mode) if shapely.geos_version < (3, 9, 0): # pre GEOS 3.9 has difficulty comparing empty geometries exactly # normalize and compare by WKT instead assert shapely.to_wkt(shapely.normalize(actual)) == shapely.to_wkt( shapely.normalize(expected) ) else: # force to 2D because GEOS 3.10 yields 3D geometries when they are empty. assert_geometries_equal(shapely.force_2d(actual), expected) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_set_precision_intersection(): """Operations should use the most precise presision grid size of the inputs""" box1 = shapely.normalize(shapely.box(0, 0, 0.9, 0.9)) box2 = shapely.normalize(shapely.box(0.75, 0, 1.75, 0.75)) assert shapely.get_precision(shapely.intersection(box1, box2)) == 0 # GEOS will use and keep the most precise precision grid size box1 = shapely.set_precision(box1, 0.5) box2 = shapely.set_precision(box2, 1) out = shapely.intersection(box1, box2) assert shapely.get_precision(out) == 0.5 assert_geometries_equal(out, LineString([(1, 1), (1, 0)])) @pytest.mark.parametrize("preserve_topology", [False, True]) def set_precision_preserve_topology(preserve_topology): # the preserve_topology kwarg is deprecated (ignored) with pytest.warns(UserWarning): actual = shapely.set_precision( LineString([(0, 0), (0.1, 0.1)]), 1.0, preserve_topology=preserve_topology, ) assert_geometries_equal(shapely.force_2d(actual), LineString()) @pytest.mark.skipif(shapely.geos_version >= (3, 10, 0), reason="GEOS >= 3.10") def set_precision_pointwise_pre_310(): # using 'pointwise' emits a warning with pytest.warns(UserWarning): actual = shapely.set_precision( LineString([(0, 0), (0.1, 0.1)]), 1.0, mode="pointwise", ) assert_geometries_equal(shapely.force_2d(actual), LineString()) @pytest.mark.parametrize("flags", [np.array([0, 1]), 4, "foo"]) def set_precision_illegal_flags(flags): # the preserve_topology kwarg is deprecated (ignored) with pytest.raises((ValueError, TypeError)): shapely.lib.set_precision(line_string, 1.0, flags) def test_empty(): """Compatibility with empty_like, see GH373""" g = np.empty_like(np.array([None, None])) assert shapely.is_missing(g).all() # corresponding to geometry_collection_z: geometry_collection_2 = shapely.geometrycollections([point, line_string]) empty_geom_mark = pytest.mark.skipif( shapely.geos_version < (3, 9, 0), reason="Empty points don't have a dimensionality before GEOS 3.9", ) @pytest.mark.parametrize( "geom,expected", [ (point, point), (point_z, point), pytest.param(empty_point, empty_point, marks=empty_geom_mark), pytest.param(empty_point_z, empty_point, marks=empty_geom_mark), (line_string, line_string), (line_string_z, line_string), pytest.param(empty_line_string, empty_line_string, marks=empty_geom_mark), pytest.param(empty_line_string_z, empty_line_string, marks=empty_geom_mark), (polygon, polygon), (polygon_z, polygon), (polygon_with_hole, polygon_with_hole), (polygon_with_hole_z, polygon_with_hole), (multi_point, multi_point), (multi_point_z, multi_point), (multi_line_string, multi_line_string), (multi_line_string_z, multi_line_string), (multi_polygon, multi_polygon), (multi_polygon_z, multi_polygon), (geometry_collection_2, geometry_collection_2), (geometry_collection_z, geometry_collection_2), ], ) def test_force_2d(geom, expected): actual = shapely.force_2d(geom) assert shapely.get_coordinate_dimension(actual) == 2 assert_geometries_equal(actual, expected) @pytest.mark.parametrize( "geom,expected", [ (point, point_z), (point_z, point_z), pytest.param(empty_point, empty_point_z, marks=empty_geom_mark), pytest.param(empty_point_z, empty_point_z, marks=empty_geom_mark), (line_string, line_string_z), (line_string_z, line_string_z), pytest.param(empty_line_string, empty_line_string_z, marks=empty_geom_mark), pytest.param(empty_line_string_z, empty_line_string_z, marks=empty_geom_mark), (polygon, polygon_z), (polygon_z, polygon_z), (polygon_with_hole, polygon_with_hole_z), (polygon_with_hole_z, polygon_with_hole_z), (multi_point, multi_point_z), (multi_point_z, multi_point_z), (multi_line_string, multi_line_string_z), (multi_line_string_z, multi_line_string_z), (multi_polygon, multi_polygon_z), (multi_polygon_z, multi_polygon_z), (geometry_collection_2, geometry_collection_z), (geometry_collection_z, geometry_collection_z), ], ) def test_force_3d(geom, expected): actual = shapely.force_3d(geom, z=4) assert shapely.get_coordinate_dimension(actual) == 3 assert_geometries_equal(actual, expected) shapely-2.0.3/shapely/tests/test_io.py000066400000000000000000000605701456366510000200260ustar00rootroot00000000000000import json import pickle import struct import warnings import numpy as np import pytest import shapely from shapely import GeometryCollection, LineString, Point, Polygon from shapely.errors import UnsupportedGEOSVersionError from shapely.testing import assert_geometries_equal from shapely.tests.common import all_types, empty_point, empty_point_z, point, point_z # fmt: off POINT11_WKB = b"\x01\x01\x00\x00\x00" + struct.pack("<2d", 1.0, 1.0) NAN = struct.pack("= (3, 12, 0): expected = "MULTIPOINT (EMPTY, (2 3))" else: # invalid WKT form expected = "MULTIPOINT (EMPTY, 2 3)" assert shapely.to_wkt(geom) == expected @pytest.mark.skipif( shapely.geos_version >= (3, 9, 0), reason="MULTIPOINT (EMPTY, 2 3) gives ValueError on GEOS < 3.9", ) def test_to_wkt_multipoint_with_point_empty_errors(): # test if segfault is prevented geom = shapely.multipoints([empty_point, point]) with pytest.raises(ValueError): shapely.to_wkt(geom) def test_repr(): assert repr(point) == "" def test_repr_max_length(): # the repr is limited to 80 characters geom = shapely.linestrings(np.arange(1000), np.arange(1000)) representation = repr(geom) assert len(representation) == 80 assert representation.endswith("...>") @pytest.mark.skipif( shapely.geos_version >= (3, 9, 0), reason="MULTIPOINT (EMPTY, 2 3) gives Exception on GEOS < 3.9", ) def test_repr_multipoint_with_point_empty(): # Test if segfault is prevented geom = shapely.multipoints([point, empty_point]) assert repr(geom) == "" @pytest.mark.skipif( shapely.geos_version < (3, 9, 0), reason="Empty geometries have no dimensionality on GEOS < 3.9", ) def test_repr_point_z_empty(): assert repr(empty_point_z) == "" def test_to_wkb(): point = shapely.points(1, 1) actual = shapely.to_wkb(point, byte_order=1) assert actual == POINT11_WKB def test_to_wkb_hex(): point = shapely.points(1, 1) actual = shapely.to_wkb(point, hex=True, byte_order=1) le = "01" point_type = "01000000" coord = "000000000000F03F" # 1.0 as double (LE) assert actual == le + point_type + 2 * coord def test_to_wkb_3D(): point_z = shapely.points(1, 1, 1) actual = shapely.to_wkb(point_z, byte_order=1) # fmt: off assert actual == b"\x01\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?" # noqa # fmt: on actual = shapely.to_wkb(point_z, output_dimension=2, byte_order=1) assert actual == POINT11_WKB def test_to_wkb_none(): # None propagates assert shapely.to_wkb(None) is None def test_to_wkb_exceptions(): with pytest.raises(TypeError): shapely.to_wkb(1) with pytest.raises(shapely.GEOSException): shapely.to_wkb(point, output_dimension=5) with pytest.raises(ValueError): shapely.to_wkb(point, flavor="other") def test_to_wkb_byte_order(): point = shapely.points(1.0, 1.0) be = b"\x00" le = b"\x01" point_type = b"\x01\x00\x00\x00" # 1 as 32-bit uint (LE) coord = b"\x00\x00\x00\x00\x00\x00\xf0?" # 1.0 as double (LE) assert shapely.to_wkb(point, byte_order=1) == le + point_type + 2 * coord assert ( shapely.to_wkb(point, byte_order=0) == be + point_type[::-1] + 2 * coord[::-1] ) def test_to_wkb_srid(): # hex representation of POINT (0 0) with SRID=4 ewkb = "01010000200400000000000000000000000000000000000000" wkb = "010100000000000000000000000000000000000000" actual = shapely.from_wkb(ewkb) assert shapely.to_wkt(actual, trim=True) == "POINT (0 0)" assert shapely.to_wkb(actual, hex=True, byte_order=1) == wkb assert shapely.to_wkb(actual, hex=True, include_srid=True, byte_order=1) == ewkb point = shapely.points(1, 1) point_with_srid = shapely.set_srid(point, np.int32(4326)) result = shapely.to_wkb(point_with_srid, include_srid=True, byte_order=1) assert np.frombuffer(result[5:9], "= (3, 10, 0), reason="GEOS < 3.10.0") def test_to_wkb_flavor_unsupported_geos(): with pytest.raises(UnsupportedGEOSVersionError): shapely.to_wkb(point_z, flavor="iso") @pytest.mark.parametrize( "geom,expected", [ (empty_point, POINT_NAN_WKB), (empty_point_z, POINT_NAN_WKB), (shapely.multipoints([empty_point]), MULTIPOINT_NAN_WKB), (shapely.multipoints([empty_point_z]), MULTIPOINT_NAN_WKB), (shapely.geometrycollections([empty_point]), GEOMETRYCOLLECTION_NAN_WKB), (shapely.geometrycollections([empty_point_z]), GEOMETRYCOLLECTION_NAN_WKB), ( shapely.geometrycollections([shapely.multipoints([empty_point])]), NESTED_COLLECTION_NAN_WKB, ), ( shapely.geometrycollections([shapely.multipoints([empty_point_z])]), NESTED_COLLECTION_NAN_WKB, ), ], ) def test_to_wkb_point_empty_2d(geom, expected): actual = shapely.to_wkb(geom, output_dimension=2, byte_order=1) # Split 'actual' into header and coordinates coordinate_length = 16 header_length = len(expected) - coordinate_length # Check the total length (this checks the correct dimensionality) assert len(actual) == header_length + coordinate_length # Check the header assert actual[:header_length] == expected[:header_length] # Check the coordinates (using numpy.isnan; there are many byte representations for NaN) assert np.isnan(struct.unpack("<2d", actual[header_length:])).all() @pytest.mark.xfail( shapely.geos_version[:2] == (3, 8), reason="GEOS==3.8 never outputs 3D empty points" ) @pytest.mark.parametrize( "geom,expected", [ (empty_point_z, POINTZ_NAN_WKB), (shapely.multipoints([empty_point_z]), MULTIPOINTZ_NAN_WKB), (shapely.geometrycollections([empty_point_z]), GEOMETRYCOLLECTIONZ_NAN_WKB), ( shapely.geometrycollections([shapely.multipoints([empty_point_z])]), NESTED_COLLECTIONZ_NAN_WKB, ), ], ) def test_to_wkb_point_empty_3d(geom, expected): actual = shapely.to_wkb(geom, output_dimension=3, byte_order=1) # Split 'actual' into header and coordinates coordinate_length = 24 header_length = len(expected) - coordinate_length # Check the total length (this checks the correct dimensionality) assert len(actual) == header_length + coordinate_length # Check the header assert actual[:header_length] == expected[:header_length] # Check the coordinates (using numpy.isnan; there are many byte representations for NaN) assert np.isnan(struct.unpack("<3d", actual[header_length:])).all() @pytest.mark.xfail( shapely.geos_version < (3, 8, 0), reason="GEOS<3.8 always outputs 3D empty points if output_dimension=3", ) @pytest.mark.parametrize( "geom,expected", [ (empty_point, POINT_NAN_WKB), (shapely.multipoints([empty_point]), MULTIPOINT_NAN_WKB), (shapely.geometrycollections([empty_point]), GEOMETRYCOLLECTION_NAN_WKB), ( shapely.geometrycollections([shapely.multipoints([empty_point])]), NESTED_COLLECTION_NAN_WKB, ), ], ) def test_to_wkb_point_empty_2d_output_dim_3(geom, expected): actual = shapely.to_wkb(geom, output_dimension=3, byte_order=1) # Split 'actual' into header and coordinates coordinate_length = 16 header_length = len(expected) - coordinate_length # Check the total length (this checks the correct dimensionality) assert len(actual) == header_length + coordinate_length # Check the header assert actual[:header_length] == expected[:header_length] # Check the coordinates (using numpy.isnan; there are many byte representations for NaN) assert np.isnan(struct.unpack("<2d", actual[header_length:])).all() @pytest.mark.parametrize( "wkb,expected_type,expected_dim", [ (POINT_NAN_WKB, 0, 2), (POINTZ_NAN_WKB, 0, 3), (MULTIPOINT_NAN_WKB, 4, 2), (MULTIPOINTZ_NAN_WKB, 4, 3), (GEOMETRYCOLLECTION_NAN_WKB, 7, 2), (GEOMETRYCOLLECTIONZ_NAN_WKB, 7, 3), (NESTED_COLLECTION_NAN_WKB, 7, 2), (NESTED_COLLECTIONZ_NAN_WKB, 7, 3), ], ) def test_from_wkb_point_empty(wkb, expected_type, expected_dim): geom = shapely.from_wkb(wkb) # POINT (nan nan) transforms to an empty point assert shapely.is_empty(geom) assert shapely.get_type_id(geom) == expected_type # The dimensionality (2D/3D) is only read correctly for GEOS >= 3.9.0 if shapely.geos_version >= (3, 9, 0): assert shapely.get_coordinate_dimension(geom) == expected_dim def test_to_wkb_point_empty_srid(): expected = shapely.set_srid(empty_point, 4236) wkb = shapely.to_wkb(expected, include_srid=True) actual = shapely.from_wkb(wkb) assert shapely.get_srid(actual) == 4236 @pytest.mark.parametrize("geom", all_types + (point_z, empty_point)) def test_pickle(geom): pickled = pickle.dumps(geom) assert_geometries_equal(pickle.loads(pickled), geom, tolerance=0) @pytest.mark.parametrize("geom", all_types + (point_z, empty_point)) def test_pickle_with_srid(geom): geom = shapely.set_srid(geom, 4326) pickled = pickle.dumps(geom) assert shapely.get_srid(pickle.loads(pickled)) == 4326 @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") @pytest.mark.parametrize( "geojson,expected", [ (GEOJSON_GEOMETRY, GEOJSON_GEOMETRY_EXPECTED), (GEOJSON_FEATURE, GEOJSON_GEOMETRY_EXPECTED), ( GEOJSON_FEATURECOLECTION, shapely.geometrycollections(GEOJSON_COLLECTION_EXPECTED), ), ([GEOJSON_GEOMETRY] * 2, [GEOJSON_GEOMETRY_EXPECTED] * 2), (None, None), ([GEOJSON_GEOMETRY, None], [GEOJSON_GEOMETRY_EXPECTED, None]), ], ) def test_from_geojson(geojson, expected): actual = shapely.from_geojson(geojson) assert_geometries_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") def test_from_geojson_exceptions(): with pytest.raises(TypeError, match="Expected bytes or string, got int"): shapely.from_geojson(1) with pytest.raises(shapely.GEOSException, match="Error parsing JSON"): shapely.from_geojson("") with pytest.raises(shapely.GEOSException, match="Unknown geometry type"): shapely.from_geojson('{"type": "NoGeometry", "coordinates": []}') with pytest.raises(shapely.GEOSException, match="type must be array, but is null"): shapely.from_geojson('{"type": "LineString", "coordinates": null}') # Note: The two below tests are the reason that from_geojson is disabled for # GEOS 3.10.0 See https://trac.osgeo.org/geos/ticket/1138 with pytest.raises(shapely.GEOSException, match="key 'type' not found"): shapely.from_geojson('{"geometry": null, "properties": []}') with pytest.raises(shapely.GEOSException, match="key 'type' not found"): shapely.from_geojson('{"no": "geojson"}') @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") def test_from_geojson_warn_on_invalid(): with pytest.warns(Warning, match="Invalid GeoJSON"): assert shapely.from_geojson("", on_invalid="warn") is None @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") def test_from_geojson_ignore_on_invalid(): with warnings.catch_warnings(): warnings.simplefilter("error") assert shapely.from_geojson("", on_invalid="ignore") is None @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") def test_from_geojson_on_invalid_unsupported_option(): with pytest.raises(ValueError, match="not a valid option"): shapely.from_geojson(GEOJSON_GEOMETRY, on_invalid="unsupported_option") @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "expected,geometry", [ (GEOJSON_GEOMETRY, GEOJSON_GEOMETRY_EXPECTED), ([GEOJSON_GEOMETRY] * 2, [GEOJSON_GEOMETRY_EXPECTED] * 2), (None, None), ([GEOJSON_GEOMETRY, None], [GEOJSON_GEOMETRY_EXPECTED, None]), ], ) def test_to_geojson(geometry, expected): actual = shapely.to_geojson(geometry, indent=4) assert np.all(actual == np.asarray(expected)) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize("indent", [None, 0, 4]) def test_to_geojson_indent(indent): separators = (",", ":") if indent is None else (",", ": ") expected = json.dumps( json.loads(GEOJSON_GEOMETRY), indent=indent, separators=separators ) actual = shapely.to_geojson(GEOJSON_GEOMETRY_EXPECTED, indent=indent) assert actual == expected @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") def test_to_geojson_exceptions(): with pytest.raises(TypeError): shapely.to_geojson(1) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geom", [ empty_point, shapely.multipoints([empty_point, point]), shapely.geometrycollections([empty_point, point]), shapely.geometrycollections( [shapely.geometrycollections([empty_point]), point] ), ], ) def test_to_geojson_point_empty(geom): # Pending GEOS ticket: https://trac.osgeo.org/geos/ticket/1139 with pytest.raises(ValueError): assert shapely.to_geojson(geom) @pytest.mark.skipif(shapely.geos_version < (3, 10, 1), reason="GEOS < 3.10.1") @pytest.mark.parametrize("geom", all_types) def test_geojson_all_types(geom): if shapely.get_type_id(geom) == shapely.GeometryType.LINEARRING: pytest.skip("Linearrings are not preserved in GeoJSON") geojson = shapely.to_geojson(geom) actual = shapely.from_geojson(geojson) assert_geometries_equal(actual, geom) shapely-2.0.3/shapely/tests/test_linear.py000066400000000000000000000164201456366510000206640ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import GeometryCollection, LinearRing, LineString, MultiLineString, Point from shapely.errors import UnsupportedGEOSVersionError from shapely.testing import assert_geometries_equal from shapely.tests.common import ( empty_line_string, empty_point, line_string, linear_ring, multi_line_string, multi_point, multi_polygon, point, polygon, ) def test_line_interpolate_point_geom_array(): actual = shapely.line_interpolate_point( [line_string, linear_ring, multi_line_string], -1 ) assert_geometries_equal(actual[0], Point(1, 0)) assert_geometries_equal(actual[1], Point(0, 1)) assert_geometries_equal(actual[2], Point(0.5528, 1.1056), tolerance=0.001) def test_line_interpolate_point_geom_array_normalized(): actual = shapely.line_interpolate_point( [line_string, linear_ring, multi_line_string], 1, normalized=True ) assert_geometries_equal(actual[0], Point(1, 1)) assert_geometries_equal(actual[1], Point(0, 0)) assert_geometries_equal(actual[2], Point(1, 2)) def test_line_interpolate_point_float_array(): actual = shapely.line_interpolate_point(line_string, [0.2, 1.5, -0.2]) assert_geometries_equal(actual[0], Point(0.2, 0)) assert_geometries_equal(actual[1], Point(1, 0.5)) assert_geometries_equal(actual[2], Point(1, 0.8)) @pytest.mark.parametrize("normalized", [False, True]) @pytest.mark.parametrize( "geom", [ LineString(), LinearRing(), MultiLineString(), shapely.from_wkt("MULTILINESTRING (EMPTY, (0 0, 1 1))"), GeometryCollection(), GeometryCollection([LineString(), Point(1, 1)]), ], ) def test_line_interpolate_point_empty(geom, normalized): # These geometries segfault in some versions of GEOS (in 3.8.0, still # some of them segfault). Instead, we patched this to return POINT EMPTY. # This matches GEOS 3.8.0 behavior on simple empty geometries. assert_geometries_equal( shapely.line_interpolate_point(geom, 0.2, normalized=normalized), empty_point ) @pytest.mark.parametrize("normalized", [False, True]) @pytest.mark.parametrize( "geom", [ empty_point, point, polygon, multi_point, multi_polygon, shapely.geometrycollections([point]), shapely.geometrycollections([polygon]), shapely.geometrycollections([multi_line_string]), shapely.geometrycollections([multi_point]), shapely.geometrycollections([multi_polygon]), ], ) def test_line_interpolate_point_invalid_type(geom, normalized): with pytest.raises(TypeError): assert shapely.line_interpolate_point(geom, 0.2, normalized=normalized) def test_line_interpolate_point_none(): assert shapely.line_interpolate_point(None, 0.2) is None def test_line_interpolate_point_nan(): assert shapely.line_interpolate_point(line_string, np.nan) is None def test_line_locate_point_geom_array(): point = shapely.points(0, 1) actual = shapely.line_locate_point([line_string, linear_ring], point) np.testing.assert_allclose(actual, [0.0, 3.0]) def test_line_locate_point_geom_array2(): points = shapely.points([[0, 0], [1, 0]]) actual = shapely.line_locate_point(line_string, points) np.testing.assert_allclose(actual, [0.0, 1.0]) @pytest.mark.parametrize("normalized", [False, True]) def test_line_locate_point_none(normalized): assert np.isnan(shapely.line_locate_point(line_string, None, normalized=normalized)) assert np.isnan(shapely.line_locate_point(None, point, normalized=normalized)) @pytest.mark.parametrize("normalized", [False, True]) def test_line_locate_point_empty(normalized): assert np.isnan( shapely.line_locate_point(line_string, empty_point, normalized=normalized) ) assert np.isnan( shapely.line_locate_point(empty_line_string, point, normalized=normalized) ) @pytest.mark.parametrize("normalized", [False, True]) def test_line_locate_point_invalid_geometry(normalized): with pytest.raises(shapely.GEOSException): shapely.line_locate_point(line_string, line_string, normalized=normalized) with pytest.raises(shapely.GEOSException): shapely.line_locate_point(polygon, point, normalized=normalized) def test_line_merge_geom_array(): actual = shapely.line_merge([line_string, multi_line_string]) assert_geometries_equal(actual[0], line_string) assert_geometries_equal(actual[1], LineString([(0, 0), (1, 2)])) @pytest.mark.skipif(shapely.geos_version < (3, 11, 0), reason="GEOS < 3.11.0") def test_line_merge_directed(): lines = MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]]) # Merge lines without directed, this requires changing the vertex ordering result = shapely.line_merge(lines) assert_geometries_equal(result, LineString([(1, 0), (0, 0), (3, 0)])) # Since the lines can't be merged when directed is specified # the original geometry is returned result = shapely.line_merge(lines, directed=True) assert_geometries_equal(result, lines) @pytest.mark.skipif(shapely.geos_version >= (3, 11, 0), reason="GEOS >= 3.11.0") def test_line_merge_error(): lines = MultiLineString([[(0, 0), (1, 0)], [(0, 0), (3, 0)]]) with pytest.raises(UnsupportedGEOSVersionError): shapely.line_merge(lines, directed=True) def test_shared_paths_linestring(): g1 = shapely.linestrings([(0, 0), (1, 0), (1, 1)]) g2 = shapely.linestrings([(0, 0), (1, 0)]) actual1 = shapely.shared_paths(g1, g2) assert_geometries_equal( shapely.get_geometry(actual1, 0), shapely.multilinestrings([g2]) ) def test_shared_paths_none(): assert shapely.shared_paths(line_string, None) is None assert shapely.shared_paths(None, line_string) is None assert shapely.shared_paths(None, None) is None def test_shared_paths_non_linestring(): g1 = shapely.linestrings([(0, 0), (1, 0), (1, 1)]) g2 = shapely.points(0, 1) with pytest.raises(shapely.GEOSException): shapely.shared_paths(g1, g2) def _prepare_input(geometry, prepare): """Prepare without modifying inplace""" if prepare: geometry = shapely.transform(geometry, lambda x: x) # makes a copy shapely.prepare(geometry) return geometry else: return geometry @pytest.mark.parametrize("prepare", [True, False]) def test_shortest_line(prepare): g1 = shapely.linestrings([(0, 0), (1, 0), (1, 1)]) g2 = shapely.linestrings([(0, 3), (3, 0)]) actual = shapely.shortest_line(_prepare_input(g1, prepare), g2) expected = shapely.linestrings([(1, 1), (1.5, 1.5)]) assert shapely.equals(actual, expected) @pytest.mark.parametrize("prepare", [True, False]) def test_shortest_line_none(prepare): assert shapely.shortest_line(_prepare_input(line_string, prepare), None) is None assert shapely.shortest_line(None, line_string) is None assert shapely.shortest_line(None, None) is None @pytest.mark.parametrize("prepare", [True, False]) def test_shortest_line_empty(prepare): g1 = _prepare_input(line_string, prepare) assert shapely.shortest_line(g1, empty_line_string) is None g1_empty = _prepare_input(empty_line_string, prepare) assert shapely.shortest_line(g1_empty, line_string) is None assert shapely.shortest_line(g1_empty, empty_line_string) is None shapely-2.0.3/shapely/tests/test_measurement.py000066400000000000000000000250251456366510000217400ustar00rootroot00000000000000import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal import shapely from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon from shapely.tests.common import ( empty, geometry_collection, ignore_invalid, line_string, linear_ring, multi_line_string, multi_point, multi_polygon, point, point_polygon_testdata, polygon, polygon_with_hole, ) @pytest.mark.parametrize( "geom", [ point, line_string, linear_ring, multi_point, multi_line_string, geometry_collection, ], ) def test_area_non_polygon(geom): assert shapely.area(geom) == 0.0 def test_area(): actual = shapely.area([polygon, polygon_with_hole, multi_polygon]) assert actual.tolist() == [4.0, 96.0, 1.01] def test_distance(): actual = shapely.distance(*point_polygon_testdata) expected = [2 * 2**0.5, 2**0.5, 0, 0, 0, 2**0.5] np.testing.assert_allclose(actual, expected) def test_distance_missing(): actual = shapely.distance(point, None) assert np.isnan(actual) def test_distance_duplicated(): a = Point(1, 2) b = LineString([(0, 0), (0, 0), (1, 1)]) with ignore_invalid(shapely.geos_version < (3, 12, 0)): # https://github.com/shapely/shapely/issues/1552 # GEOS < 3.12 raises "invalid" floating point errors actual = shapely.distance(a, b) assert actual == 1.0 @pytest.mark.parametrize( "geom,expected", [ (point, [2, 3, 2, 3]), ([point, multi_point], [[2, 3, 2, 3], [0, 0, 1, 2]]), (shapely.linestrings([[0, 0], [0, 1]]), [0, 0, 0, 1]), (shapely.linestrings([[0, 0], [1, 0]]), [0, 0, 1, 0]), (multi_point, [0, 0, 1, 2]), (multi_polygon, [0, 0, 2.2, 2.2]), (geometry_collection, [49, -1, 52, 2]), (empty, [np.nan, np.nan, np.nan, np.nan]), (None, [np.nan, np.nan, np.nan, np.nan]), ], ) def test_bounds(geom, expected): assert_array_equal(shapely.bounds(geom), expected) @pytest.mark.parametrize( "geom,shape", [ (point, (4,)), (None, (4,)), ([point, multi_point], (2, 4)), ([[point, multi_point], [polygon, point]], (2, 2, 4)), ([[[point, multi_point]], [[polygon, point]]], (2, 1, 2, 4)), ], ) def test_bounds_dimensions(geom, shape): assert shapely.bounds(geom).shape == shape @pytest.mark.parametrize( "geom,expected", [ (point, [2, 3, 2, 3]), (shapely.linestrings([[0, 0], [0, 1]]), [0, 0, 0, 1]), (shapely.linestrings([[0, 0], [1, 0]]), [0, 0, 1, 0]), (multi_point, [0, 0, 1, 2]), (multi_polygon, [0, 0, 2.2, 2.2]), (geometry_collection, [49, -1, 52, 2]), (empty, [np.nan, np.nan, np.nan, np.nan]), (None, [np.nan, np.nan, np.nan, np.nan]), ([empty, empty, None], [np.nan, np.nan, np.nan, np.nan]), # mixed missing and non-missing coordinates ([point, None], [2, 3, 2, 3]), ([point, empty], [2, 3, 2, 3]), ([point, empty, None], [2, 3, 2, 3]), ([point, empty, None, multi_point], [0, 0, 2, 3]), ], ) def test_total_bounds(geom, expected): assert_array_equal(shapely.total_bounds(geom), expected) @pytest.mark.parametrize( "geom", [ point, None, [point, multi_point], [[point, multi_point], [polygon, point]], [[[point, multi_point]], [[polygon, point]]], ], ) def test_total_bounds_dimensions(geom): assert shapely.total_bounds(geom).shape == (4,) def test_length(): actual = shapely.length( [ point, line_string, linear_ring, polygon, polygon_with_hole, multi_point, multi_polygon, ] ) assert actual.tolist() == [0.0, 2.0, 4.0, 8.0, 48.0, 0.0, 4.4] def test_length_missing(): actual = shapely.length(None) assert np.isnan(actual) def test_hausdorff_distance(): # example from GEOS docs a = shapely.linestrings([[0, 0], [100, 0], [10, 100], [10, 100]]) b = shapely.linestrings([[0, 100], [0, 10], [80, 10]]) with ignore_invalid(shapely.geos_version < (3, 12, 0)): # Hausdorff distance emits "invalid value encountered" # (see https://github.com/libgeos/geos/issues/515) actual = shapely.hausdorff_distance(a, b) assert actual == pytest.approx(22.360679775, abs=1e-7) def test_hausdorff_distance_densify(): # example from GEOS docs a = shapely.linestrings([[0, 0], [100, 0], [10, 100], [10, 100]]) b = shapely.linestrings([[0, 100], [0, 10], [80, 10]]) with ignore_invalid(shapely.geos_version < (3, 12, 0)): # Hausdorff distance emits "invalid value encountered" # (see https://github.com/libgeos/geos/issues/515) actual = shapely.hausdorff_distance(a, b, densify=0.001) assert actual == pytest.approx(47.8, abs=0.1) def test_hausdorff_distance_missing(): actual = shapely.hausdorff_distance(point, None) assert np.isnan(actual) actual = shapely.hausdorff_distance(point, None, densify=0.001) assert np.isnan(actual) def test_hausdorff_densify_nan(): actual = shapely.hausdorff_distance(point, point, densify=np.nan) assert np.isnan(actual) def test_distance_empty(): actual = shapely.distance(point, empty) assert np.isnan(actual) def test_hausdorff_distance_empty(): actual = shapely.hausdorff_distance(point, empty) assert np.isnan(actual) def test_hausdorff_distance_densify_empty(): actual = shapely.hausdorff_distance(point, empty, densify=0.2) assert np.isnan(actual) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize( "geom1, geom2, expected", [ # identical geometries should have 0 distance ( shapely.linestrings([[0, 0], [100, 0]]), shapely.linestrings([[0, 0], [100, 0]]), 0, ), # example from GEOS docs ( shapely.linestrings([[0, 0], [50, 200], [100, 0], [150, 200], [200, 0]]), shapely.linestrings([[0, 200], [200, 150], [0, 100], [200, 50], [0, 0]]), 200, ), # same geometries but different curve direction results in maximum # distance between vertices on the lines. ( shapely.linestrings([[0, 0], [50, 200], [100, 0], [150, 200], [200, 0]]), shapely.linestrings([[200, 0], [150, 200], [100, 0], [50, 200], [0, 0]]), 200, ), # another example from GEOS docs ( shapely.linestrings([[0, 0], [50, 200], [100, 0], [150, 200], [200, 0]]), shapely.linestrings([[0, 0], [200, 50], [0, 100], [200, 150], [0, 200]]), 282.842712474619, ), # example from GEOS tests ( shapely.linestrings([[0, 0], [100, 0]]), shapely.linestrings([[0, 0], [50, 50], [100, 0]]), 70.7106781186548, ), ], ) def test_frechet_distance(geom1, geom2, expected): actual = shapely.frechet_distance(geom1, geom2) assert actual == pytest.approx(expected, abs=1e-12) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize( "geom1, geom2, densify, expected", [ # example from GEOS tests ( shapely.linestrings([[0, 0], [100, 0]]), shapely.linestrings([[0, 0], [50, 50], [100, 0]]), 0.001, 50, ) ], ) def test_frechet_distance_densify(geom1, geom2, densify, expected): actual = shapely.frechet_distance(geom1, geom2, densify=densify) assert actual == pytest.approx(expected, abs=1e-12) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize( "geom1, geom2", [ (line_string, None), (None, line_string), (None, None), (line_string, empty), (empty, line_string), (empty, empty), ], ) def test_frechet_distance_nan_for_invalid_geometry_inputs(geom1, geom2): actual = shapely.frechet_distance(geom1, geom2) assert np.isnan(actual) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_frechet_densify_ndarray(): actual = shapely.frechet_distance( shapely.linestrings([[0, 0], [100, 0]]), shapely.linestrings([[0, 0], [50, 50], [100, 0]]), densify=[0.1, 0.2, 1], ) expected = np.array([50, 50.99019514, 70.7106781186548]) np.testing.assert_array_almost_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_frechet_densify_nan(): actual = shapely.frechet_distance(line_string, line_string, densify=np.nan) assert np.isnan(actual) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize("densify", [0, -1, 2]) def test_frechet_densify_invalid_values(densify): with pytest.raises(shapely.GEOSException, match="Fraction is not in range"): shapely.frechet_distance(line_string, line_string, densify=densify) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") def test_frechet_distance_densify_empty(): actual = shapely.frechet_distance(line_string, empty, densify=0.2) assert np.isnan(actual) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_minimum_clearance(): actual = shapely.minimum_clearance([polygon, polygon_with_hole, multi_polygon]) assert_allclose(actual, [2.0, 2.0, 0.1]) @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_minimum_clearance_nonexistent(): actual = shapely.minimum_clearance([point, empty]) assert np.isinf(actual).all() @pytest.mark.skipif(shapely.geos_version < (3, 6, 0), reason="GEOS < 3.6") def test_minimum_clearance_missing(): actual = shapely.minimum_clearance(None) assert np.isnan(actual) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geometry, expected", [ ( Polygon([(0, 5), (5, 10), (10, 5), (5, 0), (0, 5)]), 5, ), ( LineString([(1, 0), (1, 10)]), 5, ), ( MultiPoint([(2, 2), (4, 2)]), 1, ), ( Point(2, 2), 0, ), ( GeometryCollection(), 0, ), ], ) def test_minimum_bounding_radius(geometry, expected): actual = shapely.minimum_bounding_radius(geometry) assert actual == pytest.approx(expected, abs=1e-12) shapely-2.0.3/shapely/tests/test_misc.py000066400000000000000000000127421456366510000203500ustar00rootroot00000000000000import os import sys from inspect import cleandoc from itertools import chain from string import ascii_letters, digits from unittest import mock import numpy as np import pytest import shapely from shapely.decorators import multithreading_enabled, requires_geos @pytest.fixture def mocked_geos_version(): with mock.patch.object(shapely.lib, "geos_version", new=(3, 7, 1)): yield "3.7.1" @pytest.fixture def sphinx_doc_build(): os.environ["SPHINX_DOC_BUILD"] = "1" yield del os.environ["SPHINX_DOC_BUILD"] def test_version(): assert isinstance(shapely.__version__, str) def test_geos_version(): expected = "{}.{}.{}".format(*shapely.geos_version) actual = shapely.geos_version_string # strip any beta / dev qualifiers if any(c.isalpha() for c in actual): if actual[-1].isnumeric(): actual = actual.rstrip(digits) actual = actual.rstrip(ascii_letters) assert actual == expected @pytest.mark.skipif( sys.platform.startswith("win") and (shapely.geos_version == (3, 6, 6) or shapely.geos_version[:2] == (3, 7)), reason="GEOS_C_API_VERSION broken for GEOS 3.6.6 and 3.7.x on Windows", ) def test_geos_capi_version(): expected = "{}.{}.{}-CAPI-{}.{}.{}".format( *(shapely.geos_version + shapely.geos_capi_version) ) # split into component parts and strip any beta / dev qualifiers ( actual_geos_version, actual_geos_api_version, ) = shapely.geos_capi_version_string.split("-CAPI-") if any(c.isalpha() for c in actual_geos_version): if actual_geos_version[-1].isnumeric(): actual_geos_version = actual_geos_version.rstrip(digits) actual_geos_version = actual_geos_version.rstrip(ascii_letters) actual_geos_version = actual_geos_version.rstrip(ascii_letters) assert f"{actual_geos_version}-CAPI-{actual_geos_api_version}" == expected def func(): """Docstring that will be mocked. A multiline. Some description. """ class SomeClass: def func(self): """Docstring that will be mocked. A multiline. Some description. """ def expected_docstring(**kwds): doc = """Docstring that will be mocked. {indent}A multiline. {indent}.. note:: 'func' requires at least GEOS {version}. {indent}Some description. {indent}""".format( **kwds ) if sys.version_info[:2] >= (3, 13): # There are subtle differences between inspect.cleandoc() and # _PyCompile_CleanDoc(). Most significantly, the latter does not remove # leading or trailing blank lines. return cleandoc(doc) + "\n" return doc @pytest.mark.parametrize("version", ["3.7.0", "3.7.1", "3.6.2"]) def test_requires_geos_ok(version, mocked_geos_version): wrapped = requires_geos(version)(func) assert wrapped is func @pytest.mark.parametrize("version", ["3.7.2", "3.8.0", "3.8.1"]) def test_requires_geos_not_ok(version, mocked_geos_version): wrapped = requires_geos(version)(func) with pytest.raises(shapely.errors.UnsupportedGEOSVersionError): wrapped() assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4) @pytest.mark.parametrize("version", ["3.6.0", "3.8.0"]) def test_requires_geos_doc_build(version, mocked_geos_version, sphinx_doc_build): """The requires_geos decorator always adapts the docstring.""" wrapped = requires_geos(version)(func) assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 4) @pytest.mark.parametrize("version", ["3.6.0", "3.8.0"]) def test_requires_geos_method(version, mocked_geos_version, sphinx_doc_build): """The requires_geos decorator adjusts methods docstrings correctly""" wrapped = requires_geos(version)(SomeClass.func) assert wrapped.__doc__ == expected_docstring(version=version, indent=" " * 8) @multithreading_enabled def set_first_element(value, *args, **kwargs): for arg in chain(args, kwargs.values()): if hasattr(arg, "__setitem__"): arg[0] = value return arg def test_multithreading_enabled_raises_arg(): arr = np.empty((1,), dtype=object) # set_first_element cannot change the input array with pytest.raises(ValueError): set_first_element(42, arr) # afterwards, we can arr[0] = 42 assert arr[0] == 42 def test_multithreading_enabled_raises_kwarg(): arr = np.empty((1,), dtype=object) # set_first_element cannot change the input array with pytest.raises(ValueError): set_first_element(42, arr=arr) # writable flag goes to original state assert arr.flags.writeable def test_multithreading_enabled_preserves_flag(): arr = np.empty((1,), dtype=object) arr.flags.writeable = False # set_first_element cannot change the input array with pytest.raises(ValueError): set_first_element(42, arr) # writable flag goes to original state assert not arr.flags.writeable @pytest.mark.parametrize( "args,kwargs", [ ((np.empty((1,), dtype=float),), {}), # float-dtype ndarray is untouched ((), {"a": np.empty((1,), dtype=float)}), (([1],), {}), # non-ndarray is untouched ((), {"a": [1]}), ((), {"out": np.empty((1,), dtype=object)}), # ufunc kwarg 'out' is untouched ( (), {"where": np.empty((1,), dtype=object)}, ), # ufunc kwarg 'where' is untouched ], ) def test_multithreading_enabled_ok(args, kwargs): result = set_first_element(42, *args, **kwargs) assert result[0] == 42 shapely-2.0.3/shapely/tests/test_plotting.py000066400000000000000000000070371456366510000212560ustar00rootroot00000000000000import pytest from numpy.testing import assert_allclose from shapely import box, get_coordinates, LineString, MultiLineString, Point from shapely.plotting import patch_from_polygon, plot_line, plot_points, plot_polygon pytest.importorskip("matplotlib") def test_patch_from_polygon(): poly = box(0, 0, 1, 1) artist = patch_from_polygon(poly, facecolor="red", edgecolor="blue", linewidth=3) assert equal_color(artist.get_facecolor(), "red") assert equal_color(artist.get_edgecolor(), "blue") assert artist.get_linewidth() == 3 def test_patch_from_polygon_with_interior(): poly = box(0, 0, 1, 1).difference(box(0.2, 0.2, 0.5, 0.5)) artist = patch_from_polygon(poly, facecolor="red", edgecolor="blue", linewidth=3) assert equal_color(artist.get_facecolor(), "red") assert equal_color(artist.get_edgecolor(), "blue") assert artist.get_linewidth() == 3 def test_patch_from_multipolygon(): poly = box(0, 0, 1, 1).union(box(2, 2, 3, 3)) artist = patch_from_polygon(poly, facecolor="red", edgecolor="blue", linewidth=3) assert equal_color(artist.get_facecolor(), "red") assert equal_color(artist.get_edgecolor(), "blue") assert artist.get_linewidth() == 3 def test_plot_polygon(): poly = box(0, 0, 1, 1) artist, _ = plot_polygon(poly) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(poly)) # overriding default styling artist = plot_polygon(poly, add_points=False, color="red", linewidth=3) assert equal_color(artist.get_facecolor(), "red", alpha=0.3) assert equal_color(artist.get_edgecolor(), "red", alpha=1.0) assert artist.get_linewidth() == 3 def test_plot_polygon_with_interior(): poly = box(0, 0, 1, 1).difference(box(0.2, 0.2, 0.5, 0.5)) artist, _ = plot_polygon(poly) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(poly)) def test_plot_multipolygon(): poly = box(0, 0, 1, 1).union(box(2, 2, 3, 3)) artist, _ = plot_polygon(poly) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(poly)) def test_plot_line(): line = LineString([(0, 0), (1, 0), (1, 1)]) artist, _ = plot_line(line) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(line)) # overriding default styling artist = plot_line(line, add_points=False, color="red", linewidth=3) assert equal_color(artist.get_edgecolor(), "red") assert equal_color(artist.get_facecolor(), "none") assert artist.get_linewidth() == 3 def test_plot_multilinestring(): line = MultiLineString( [LineString([(0, 0), (1, 0), (1, 1)]), LineString([(2, 2), (3, 3)])] ) artist, _ = plot_line(line) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(line)) def test_plot_points(): for geom in [Point(0, 0), LineString([(0, 0), (1, 0), (1, 1)]), box(0, 0, 1, 1)]: artist = plot_points(geom) plot_coords = artist.get_path().vertices assert_allclose(plot_coords, get_coordinates(geom)) assert artist.get_linestyle() == "None" # overriding default styling geom = Point(0, 0) artist = plot_points(geom, color="red", marker="+", fillstyle="top") assert artist.get_color() == "red" assert artist.get_marker() == "+" assert artist.get_fillstyle() == "top" def equal_color(actual, expected, alpha=None): import matplotlib.colors as colors conv = colors.colorConverter return actual == conv.to_rgba(expected, alpha=alpha) shapely-2.0.3/shapely/tests/test_predicates.py000066400000000000000000000253441456366510000215420ustar00rootroot00000000000000from functools import partial import numpy as np import pytest import shapely from shapely import LinearRing, LineString, Point from shapely.tests.common import ( all_types, empty, geometry_collection, ignore_invalid, line_string, linear_ring, point, polygon, ) UNARY_PREDICATES = ( shapely.is_empty, shapely.is_simple, shapely.is_ring, shapely.is_closed, shapely.is_valid, shapely.is_missing, shapely.is_geometry, shapely.is_valid_input, shapely.is_prepared, pytest.param( shapely.is_ccw, marks=pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7"), ), ) BINARY_PREDICATES = ( shapely.disjoint, shapely.touches, shapely.intersects, shapely.crosses, shapely.within, shapely.contains, shapely.contains_properly, shapely.overlaps, shapely.covers, shapely.covered_by, pytest.param( partial(shapely.dwithin, distance=1.0), marks=pytest.mark.skipif( shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10" ), ), shapely.equals, shapely.equals_exact, ) BINARY_PREPARED_PREDICATES = BINARY_PREDICATES[:-2] XY_PREDICATES = ( (shapely.contains_xy, shapely.contains), (shapely.intersects_xy, shapely.intersects), ) @pytest.mark.parametrize("geometry", all_types) @pytest.mark.parametrize("func", UNARY_PREDICATES) def test_unary_array(geometry, func): actual = func([geometry, geometry]) assert actual.shape == (2,) assert actual.dtype == np.bool_ @pytest.mark.parametrize("func", UNARY_PREDICATES) def test_unary_with_kwargs(func): out = np.empty((), dtype=np.uint8) actual = func(point, out=out) assert actual is out assert actual.dtype == np.uint8 @pytest.mark.parametrize("func", UNARY_PREDICATES) def test_unary_missing(func): if func in (shapely.is_valid_input, shapely.is_missing): assert func(None) else: assert not func(None) @pytest.mark.parametrize("a", all_types) @pytest.mark.parametrize("func", BINARY_PREDICATES) def test_binary_array(a, func): with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)): # Empty geometries give 'invalid value encountered' in all predicates # (see https://github.com/libgeos/geos/issues/515) actual = func([a, a], point) assert actual.shape == (2,) assert actual.dtype == np.bool_ @pytest.mark.parametrize("func", BINARY_PREDICATES) def test_binary_with_kwargs(func): out = np.empty((), dtype=np.uint8) actual = func(point, point, out=out) assert actual is out assert actual.dtype == np.uint8 @pytest.mark.parametrize("func", BINARY_PREDICATES) def test_binary_missing(func): actual = func(np.array([point, None, None]), np.array([None, point, None])) assert (~actual).all() def test_binary_empty_result(): a = LineString([(0, 0), (3, 0), (3, 3), (0, 3)]) b = LineString([(5, 1), (6, 1)]) with ignore_invalid(shapely.geos_version < (3, 12, 0)): # Intersection resulting in empty geometries give 'invalid value encountered' # (https://github.com/shapely/shapely/issues/1345) assert shapely.intersection(a, b).is_empty @pytest.mark.parametrize("a", all_types) @pytest.mark.parametrize("func, func_bin", XY_PREDICATES) def test_xy_array(a, func, func_bin): with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)): # Empty geometries give 'invalid value encountered' in all predicates # (see https://github.com/libgeos/geos/issues/515) actual = func([a, a], 2, 3) expected = func_bin([a, a], Point(2, 3)) assert actual.shape == (2,) assert actual.dtype == np.bool_ np.testing.assert_allclose(actual, expected) @pytest.mark.parametrize("a", all_types) @pytest.mark.parametrize("func, func_bin", XY_PREDICATES) def test_xy_array_broadcast(a, func, func_bin): with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)): # Empty geometries give 'invalid value encountered' in all predicates # (see https://github.com/libgeos/geos/issues/515) actual = func(a, [0, 1, 2], [1, 2, 3]) expected = func_bin(a, [Point(0, 1), Point(1, 2), Point(2, 3)]) np.testing.assert_allclose(actual, expected) @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES]) def test_xy_array_2D(func): actual = func(polygon, [0, 1, 2], [1, 2, 3]) expected = func(polygon, [[0, 1], [1, 2], [2, 3]]) np.testing.assert_allclose(actual, expected) @pytest.mark.parametrize("func, func_bin", XY_PREDICATES) def test_xy_prepared(func, func_bin): actual = func(_prepare_with_copy([polygon, line_string]), 2, 3) expected = func_bin([polygon, line_string], Point(2, 3)) np.testing.assert_allclose(actual, expected) @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES]) def test_xy_with_kwargs(func): out = np.empty((), dtype=np.uint8) actual = func(point, point.x, point.y, out=out) assert actual is out assert actual.dtype == np.uint8 @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES]) def test_xy_missing(func): actual = func( np.array([point, point, point, None]), np.array([point.x, np.nan, point.x, point.x]), np.array([point.y, point.y, np.nan, point.y]), ) np.testing.assert_allclose(actual, [True, False, False, False]) def test_equals_exact_tolerance(): # specifying tolerance p1 = shapely.points(50, 4) p2 = shapely.points(50.1, 4.1) actual = shapely.equals_exact([p1, p2, None], p1, tolerance=0.05) np.testing.assert_allclose(actual, [True, False, False]) assert actual.dtype == np.bool_ actual = shapely.equals_exact([p1, p2, None], p1, tolerance=0.2) np.testing.assert_allclose(actual, [True, True, False]) assert actual.dtype == np.bool_ # default value for tolerance assert shapely.equals_exact(p1, p1).item() is True assert shapely.equals_exact(p1, p2).item() is False # an array of tolerances actual = shapely.equals_exact(p1, p2, tolerance=[0.05, 0.2, np.nan]) np.testing.assert_allclose(actual, [False, True, False]) @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10") def test_dwithin(): p1 = shapely.points(50, 4) p2 = shapely.points(50.1, 4.1) actual = shapely.dwithin([p1, p2, None], p1, distance=0.05) np.testing.assert_equal(actual, [True, False, False]) assert actual.dtype == np.bool_ actual = shapely.dwithin([p1, p2, None], p1, distance=0.2) np.testing.assert_allclose(actual, [True, True, False]) assert actual.dtype == np.bool_ # an array of distances actual = shapely.dwithin(p1, p2, distance=[0.05, 0.2, np.nan]) np.testing.assert_allclose(actual, [False, True, False]) @pytest.mark.parametrize( "geometry,expected", [ (point, False), (line_string, False), (linear_ring, True), (empty, False), ], ) def test_is_closed(geometry, expected): assert shapely.is_closed(geometry) == expected def test_relate(): p1 = shapely.points(0, 0) p2 = shapely.points(1, 1) actual = shapely.relate(p1, p2) assert isinstance(actual, str) assert actual == "FF0FFF0F2" @pytest.mark.parametrize("g1, g2", [(point, None), (None, point), (None, None)]) def test_relate_none(g1, g2): assert shapely.relate(g1, g2) is None def test_relate_pattern(): g = shapely.linestrings([(0, 0), (1, 0), (1, 1)]) polygon = shapely.box(0, 0, 2, 2) assert shapely.relate(g, polygon) == "11F00F212" assert shapely.relate_pattern(g, polygon, "11F00F212") assert shapely.relate_pattern(g, polygon, "*********") assert not shapely.relate_pattern(g, polygon, "F********") def test_relate_pattern_empty(): with ignore_invalid(shapely.geos_version < (3, 12, 0)): # Empty geometries give 'invalid value encountered' in all predicates # (see https://github.com/libgeos/geos/issues/515) assert shapely.relate_pattern(empty, empty, "*" * 9).item() is True @pytest.mark.parametrize("g1, g2", [(point, None), (None, point), (None, None)]) def test_relate_pattern_none(g1, g2): assert shapely.relate_pattern(g1, g2, "*" * 9).item() is False def test_relate_pattern_incorrect_length(): with pytest.raises(shapely.GEOSException, match="Should be length 9"): shapely.relate_pattern(point, polygon, "**") with pytest.raises(shapely.GEOSException, match="Should be length 9"): shapely.relate_pattern(point, polygon, "**********") @pytest.mark.parametrize("pattern", [b"*********", 10, None]) def test_relate_pattern_non_string(pattern): with pytest.raises(TypeError, match="expected string"): shapely.relate_pattern(point, polygon, pattern) def test_relate_pattern_non_scalar(): with pytest.raises(ValueError, match="only supports scalar"): shapely.relate_pattern([point] * 2, polygon, ["*********"] * 2) @pytest.mark.skipif(shapely.geos_version < (3, 7, 0), reason="GEOS < 3.7") @pytest.mark.parametrize( "geom, expected", [ (LinearRing([(0, 0), (0, 1), (1, 1), (0, 0)]), False), (LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]), True), (LineString([(0, 0), (0, 1), (1, 1), (0, 0)]), False), (LineString([(0, 0), (1, 1), (0, 1), (0, 0)]), True), (LineString([(0, 0), (1, 1), (0, 1)]), False), (LineString([(0, 0), (0, 1), (1, 1)]), False), (point, False), (polygon, False), (geometry_collection, False), (None, False), ], ) def test_is_ccw(geom, expected): assert shapely.is_ccw(geom) == expected def _prepare_with_copy(geometry): """Prepare without modifying inplace""" geometry = shapely.transform(geometry, lambda x: x) # makes a copy shapely.prepare(geometry) return geometry @pytest.mark.parametrize("a", all_types) @pytest.mark.parametrize("func", BINARY_PREPARED_PREDICATES) def test_binary_prepared(a, func): with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)): # Empty geometries give 'invalid value encountered' in all predicates # (see https://github.com/libgeos/geos/issues/515) actual = func(a, point) result = func(_prepare_with_copy(a), point) assert actual == result @pytest.mark.parametrize("geometry", all_types + (empty,)) def test_is_prepared_true(geometry): assert shapely.is_prepared(_prepare_with_copy(geometry)) @pytest.mark.parametrize("geometry", all_types + (empty, None)) def test_is_prepared_false(geometry): assert not shapely.is_prepared(geometry) def test_contains_properly(): # polygon contains itself, but does not properly contains itself assert shapely.contains(polygon, polygon).item() is True assert shapely.contains_properly(polygon, polygon).item() is False shapely-2.0.3/shapely/tests/test_ragged_array.py000066400000000000000000000254101456366510000220400ustar00rootroot00000000000000import numpy as np import pytest from numpy.testing import assert_allclose import shapely from shapely import MultiLineString, MultiPoint, MultiPolygon from shapely.testing import assert_geometries_equal from shapely.tests.common import ( empty_line_string, empty_line_string_z, geometry_collection, line_string, line_string_z, linear_ring, multi_line_string, multi_line_string_z, multi_point, multi_point_z, multi_polygon, multi_polygon_z, point, point_z, polygon, polygon_z, ) all_types = ( point, line_string, polygon, multi_point, multi_line_string, multi_polygon, ) all_types_3d = ( point_z, line_string_z, polygon_z, multi_point_z, multi_line_string_z, multi_polygon_z, ) all_types_not_supported = ( linear_ring, geometry_collection, ) @pytest.mark.parametrize("geom", all_types + all_types_3d) def test_roundtrip(geom): actual = shapely.from_ragged_array(*shapely.to_ragged_array([geom, geom])) assert_geometries_equal(actual, [geom, geom]) @pytest.mark.parametrize("geom", all_types) def test_include_z(geom): _, coords, _ = shapely.to_ragged_array([geom, geom], include_z=True) # For 2D geoms, z coords are filled in with NaN assert np.isnan(coords[:, 2]).all() @pytest.mark.parametrize("geom", all_types_3d) def test_include_z_false(geom): _, coords, _ = shapely.to_ragged_array([geom, geom], include_z=False) # For 3D geoms, z coords are dropped assert coords.shape[1] == 2 def test_include_z_default(): # corner cases for inferring dimensionality # mixed 2D and 3D -> 3D _, coords, _ = shapely.to_ragged_array([line_string, line_string_z]) assert coords.shape[1] == 3 # only empties -> always 2D _, coords, _ = shapely.to_ragged_array([empty_line_string]) assert coords.shape[1] == 2 _, coords, _ = shapely.to_ragged_array([empty_line_string_z]) assert coords.shape[1] == 2 # empty collection -> GEOS indicates 2D _, coords, _ = shapely.to_ragged_array(shapely.from_wkt(["MULTIPOLYGON Z EMPTY"])) assert coords.shape[1] == 2 @pytest.mark.parametrize("geom", all_types) def test_read_only_arrays(geom): # https://github.com/shapely/shapely/pull/1744 typ, coords, offsets = shapely.to_ragged_array([geom, geom]) coords.flags.writeable = False for arr in offsets: arr.flags.writeable = False result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, [geom, geom]) @pytest.mark.parametrize("geom", all_types_not_supported) def test_raise_geometry_type(geom): with pytest.raises(ValueError): shapely.to_ragged_array([geom, geom]) def test_points(): arr = shapely.from_wkt( [ "POINT (0 0)", "POINT (1 1)", "POINT EMPTY", "POINT EMPTY", "POINT (4 4)", "POINT EMPTY", ] ) typ, result, offsets = shapely.to_ragged_array(arr) expected = np.array( [[0, 0], [1, 1], [np.nan, np.nan], [np.nan, np.nan], [4, 4], [np.nan, np.nan]] ) assert typ == shapely.GeometryType.POINT assert_allclose(result, expected) assert len(offsets) == 0 geoms = shapely.from_ragged_array(typ, result) assert_geometries_equal(geoms, arr) def test_linestrings(): arr = shapely.from_wkt( [ "LINESTRING (30 10, 10 30, 40 40)", "LINESTRING (40 40, 30 30, 40 20, 30 10)", "LINESTRING EMPTY", "LINESTRING EMPTY", "LINESTRING (10 10, 20 20, 10 40)", "LINESTRING EMPTY", ] ) typ, coords, offsets = shapely.to_ragged_array(arr) expected = np.array( [ [30.0, 10.0], [10.0, 30.0], [40.0, 40.0], [40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0], [10.0, 10.0], [20.0, 20.0], [10.0, 40.0], ] ) expected_offsets = np.array([0, 3, 7, 7, 7, 10, 10]) assert typ == shapely.GeometryType.LINESTRING assert_allclose(coords, expected) assert len(offsets) == 1 assert_allclose(offsets[0], expected_offsets) result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, arr) def test_polygons(): arr = shapely.from_wkt( [ "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))", "POLYGON EMPTY", "POLYGON EMPTY", "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))", "POLYGON EMPTY", ] ) typ, coords, offsets = shapely.to_ragged_array(arr) expected = np.array( [ [30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0], [35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [35.0, 10.0], [20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0], [30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0], ] ) expected_offsets1 = np.array([0, 5, 10, 14, 19]) expected_offsets2 = np.array([0, 1, 3, 3, 3, 4, 4]) assert typ == shapely.GeometryType.POLYGON assert_allclose(coords, expected) assert len(offsets) == 2 assert_allclose(offsets[0], expected_offsets1) assert_allclose(offsets[1], expected_offsets2) result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, arr) def test_multipoints(): arr = shapely.from_wkt( [ "MULTIPOINT (10 40, 40 30, 20 20, 30 10)", "MULTIPOINT (30 10)", "MULTIPOINT EMPTY", "MULTIPOINT EMPTY", "MULTIPOINT (30 10, 10 30, 40 40)", "MULTIPOINT EMPTY", ] ) typ, coords, offsets = shapely.to_ragged_array(arr) expected = np.array( [ [10.0, 40.0], [40.0, 30.0], [20.0, 20.0], [30.0, 10.0], [30.0, 10.0], [30.0, 10.0], [10.0, 30.0], [40.0, 40.0], ] ) expected_offsets = np.array([0, 4, 5, 5, 5, 8, 8]) assert typ == shapely.GeometryType.MULTIPOINT assert_allclose(coords, expected) assert len(offsets) == 1 assert_allclose(offsets[0], expected_offsets) result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, arr) def test_multilinestrings(): arr = shapely.from_wkt( [ "MULTILINESTRING ((30 10, 10 30, 40 40))", "MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))", "MULTILINESTRING EMPTY", "MULTILINESTRING EMPTY", "MULTILINESTRING ((35 10, 45 45), (15 40, 10 20), (30 10, 10 30, 40 40))", "MULTILINESTRING EMPTY", ] ) typ, coords, offsets = shapely.to_ragged_array(arr) expected = np.array( [ [30.0, 10.0], [10.0, 30.0], [40.0, 40.0], [10.0, 10.0], [20.0, 20.0], [10.0, 40.0], [40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0], [35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [30.0, 10.0], [10.0, 30.0], [40.0, 40.0], ] ) expected_offsets1 = np.array([0, 3, 6, 10, 12, 14, 17]) expected_offsets2 = np.array([0, 1, 3, 3, 3, 6, 6]) assert typ == shapely.GeometryType.MULTILINESTRING assert_allclose(coords, expected) assert len(offsets) == 2 assert_allclose(offsets[0], expected_offsets1) assert_allclose(offsets[1], expected_offsets2) result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, arr) def test_multipolygons(): arr = shapely.from_wkt( [ "MULTIPOLYGON (((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30)))", "MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20)))", "MULTIPOLYGON EMPTY", "MULTIPOLYGON EMPTY", "MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)))", "MULTIPOLYGON EMPTY", ] ) typ, coords, offsets = shapely.to_ragged_array(arr) expected = np.array( [ [35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [35.0, 10.0], [20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0], [40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0], [20.0, 35.0], [10.0, 30.0], [10.0, 10.0], [30.0, 5.0], [45.0, 20.0], [20.0, 35.0], [30.0, 20.0], [20.0, 15.0], [20.0, 25.0], [30.0, 20.0], [40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0], ] ) expected_offsets1 = np.array([0, 5, 9, 13, 19, 23, 27]) expected_offsets2 = np.array([0, 2, 3, 5, 6]) expected_offsets3 = np.array([0, 1, 3, 3, 3, 4, 4]) assert typ == shapely.GeometryType.MULTIPOLYGON assert_allclose(coords, expected) assert len(offsets) == 3 assert_allclose(offsets[0], expected_offsets1) assert_allclose(offsets[1], expected_offsets2) assert_allclose(offsets[2], expected_offsets3) result = shapely.from_ragged_array(typ, coords, offsets) assert_geometries_equal(result, arr) def test_mixture_point_multipoint(): typ, coords, offsets = shapely.to_ragged_array([point, multi_point]) assert typ == shapely.GeometryType.MULTIPOINT result = shapely.from_ragged_array(typ, coords, offsets) expected = np.array([MultiPoint([point]), multi_point]) assert_geometries_equal(result, expected) def test_mixture_linestring_multilinestring(): typ, coords, offsets = shapely.to_ragged_array([line_string, multi_line_string]) assert typ == shapely.GeometryType.MULTILINESTRING result = shapely.from_ragged_array(typ, coords, offsets) expected = np.array([MultiLineString([line_string]), multi_line_string]) assert_geometries_equal(result, expected) def test_mixture_polygon_multipolygon(): typ, coords, offsets = shapely.to_ragged_array([polygon, multi_polygon]) assert typ == shapely.GeometryType.MULTIPOLYGON result = shapely.from_ragged_array(typ, coords, offsets) expected = np.array([MultiPolygon([polygon]), multi_polygon]) assert_geometries_equal(result, expected) shapely-2.0.3/shapely/tests/test_set_operations.py000066400000000000000000000410431456366510000224470ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely import Geometry, GeometryCollection, Polygon from shapely.errors import UnsupportedGEOSVersionError from shapely.testing import assert_geometries_equal from shapely.tests.common import ( all_types, empty, ignore_invalid, multi_polygon, point, polygon, ) # fixed-precision operations raise GEOS exceptions on mixed dimension geometry collections all_single_types = [g for g in all_types if not shapely.get_type_id(g) == 7] SET_OPERATIONS = ( shapely.difference, shapely.intersection, shapely.symmetric_difference, shapely.union, # shapely.coverage_union is tested seperately ) REDUCE_SET_OPERATIONS = ( (shapely.intersection_all, shapely.intersection), (shapely.symmetric_difference_all, shapely.symmetric_difference), (shapely.union_all, shapely.union), # shapely.coverage_union_all, shapely.coverage_union) is tested seperately ) # operations that support fixed precision REDUCE_SET_OPERATIONS_PREC = ((shapely.union_all, shapely.union),) reduce_test_data = [ shapely.box(0, 0, 5, 5), shapely.box(2, 2, 7, 7), shapely.box(4, 4, 9, 9), shapely.box(5, 5, 10, 10), ] non_polygon_types = [ geom for geom in all_types if (not shapely.is_empty(geom) and geom not in (polygon, multi_polygon)) ] @pytest.mark.parametrize("a", all_types) @pytest.mark.parametrize("func", SET_OPERATIONS) def test_set_operation_array(a, func): if ( func is shapely.difference and a.geom_type == "GeometryCollection" and shapely.get_num_geometries(a) == 2 and shapely.geos_version == (3, 9, 5) ): pytest.xfail("GEOS 3.9.5 crashes with mixed collection") actual = func(a, point) assert isinstance(actual, Geometry) actual = func([a, a], point) assert actual.shape == (2,) assert isinstance(actual[0], Geometry) @pytest.mark.skipif(shapely.geos_version >= (3, 9, 0), reason="GEOS >= 3.9") @pytest.mark.parametrize("func", SET_OPERATIONS) @pytest.mark.parametrize("grid_size", [0, 1]) def test_set_operations_prec_not_supported(func, grid_size): with pytest.raises( UnsupportedGEOSVersionError, match="grid_size parameter requires GEOS >= 3.9.0" ): func(point, point, grid_size) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("func", SET_OPERATIONS) def test_set_operation_prec_nonscalar_grid_size(func): with pytest.raises( ValueError, match="grid_size parameter only accepts scalar values" ): func(point, point, grid_size=[1]) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("a", all_single_types) @pytest.mark.parametrize("func", SET_OPERATIONS) @pytest.mark.parametrize("grid_size", [0, 1, 2]) def test_set_operation_prec_array(a, func, grid_size): actual = func([a, a], point, grid_size=grid_size) assert actual.shape == (2,) assert isinstance(actual[0], Geometry) # results should match the operation when the precision is previously set # to same grid_size b = shapely.set_precision(a, grid_size=grid_size) point2 = shapely.set_precision(point, grid_size=grid_size) expected = func([b, b], point2) assert shapely.equals(shapely.normalize(actual), shapely.normalize(expected)).all() @pytest.mark.parametrize("n", range(1, 5)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_1dim(n, func, related_func): actual = func(reduce_test_data[:n]) # perform the reduction in a python loop and compare expected = reduce_test_data[0] for i in range(1, n): expected = related_func(expected, reduce_test_data[i]) assert shapely.equals(actual, expected) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_single_geom(func, related_func): geom = shapely.Point(1, 1) actual = func([geom, None, None]) assert shapely.equals(actual, geom) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_axis(func, related_func): data = [[point] * 2] * 3 # shape = (3, 2) actual = func(data, axis=None) # default assert isinstance(actual, Geometry) # scalar output actual = func(data, axis=0) assert actual.shape == (2,) actual = func(data, axis=1) assert actual.shape == (3,) actual = func(data, axis=-1) assert actual.shape == (3,) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_empty(func, related_func): assert func(np.empty((0,), dtype=object)) == empty arr_empty_2D = np.empty((0, 2), dtype=object) assert func(arr_empty_2D) == empty assert func(arr_empty_2D, axis=0).tolist() == [empty] * 2 assert func(arr_empty_2D, axis=1).tolist() == [] @pytest.mark.parametrize("none_position", range(3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_one_none(func, related_func, none_position): # API change: before, intersection_all and symmetric_difference_all returned # None if any input geometry was None. # The new behaviour is to ignore None values. test_data = reduce_test_data[:2] test_data.insert(none_position, None) actual = func(test_data) expected = related_func(reduce_test_data[0], reduce_test_data[1]) assert_geometries_equal(actual, expected) @pytest.mark.parametrize("none_position", range(3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_two_none(func, related_func, none_position): test_data = reduce_test_data[:2] test_data.insert(none_position, None) test_data.insert(none_position, None) actual = func(test_data) expected = related_func(reduce_test_data[0], reduce_test_data[1]) assert_geometries_equal(actual, expected) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_some_none_len2(func, related_func): # in a previous implementation, this would take a different code path # and return wrong result assert func([empty, None]) == empty @pytest.mark.parametrize("n", range(1, 3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_all_none(n, func, related_func): assert_geometries_equal(func([None] * n), GeometryCollection([])) @pytest.mark.parametrize("n", range(1, 3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS) def test_set_operation_reduce_all_none_arr(n, func, related_func): assert func([[None] * n] * 2, axis=1).tolist() == [empty, empty] assert func([[None] * 2] * n, axis=0).tolist() == [empty, empty] @pytest.mark.skipif(shapely.geos_version >= (3, 9, 0), reason="GEOS >= 3.9") @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) @pytest.mark.parametrize("grid_size", [0, 1]) def test_set_operation_prec_reduce_not_supported(func, related_func, grid_size): with pytest.raises( UnsupportedGEOSVersionError, match="grid_size parameter requires GEOS >= 3.9.0" ): func([point, point], grid_size) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_nonscalar_grid_size(func, related_func): with pytest.raises( ValueError, match="grid_size parameter only accepts scalar values" ): func([point, point], grid_size=[1]) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_grid_size_nan(func, related_func): actual = func([point, point], grid_size=np.nan) assert actual is None @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("n", range(1, 5)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) @pytest.mark.parametrize("grid_size", [0, 1]) def test_set_operation_prec_reduce_1dim(n, func, related_func, grid_size): actual = func(reduce_test_data[:n], grid_size=grid_size) # perform the reduction in a python loop and compare expected = reduce_test_data[0] for i in range(1, n): expected = related_func(expected, reduce_test_data[i], grid_size=grid_size) assert shapely.equals(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_axis(func, related_func): data = [[point] * 2] * 3 # shape = (3, 2) actual = func(data, grid_size=1, axis=None) # default assert isinstance(actual, Geometry) # scalar output actual = func(data, grid_size=1, axis=0) assert actual.shape == (2,) actual = func(data, grid_size=1, axis=1) assert actual.shape == (3,) actual = func(data, grid_size=1, axis=-1) assert actual.shape == (3,) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("none_position", range(3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_one_none(func, related_func, none_position): test_data = reduce_test_data[:2] test_data.insert(none_position, None) actual = func(test_data, grid_size=1) expected = related_func(reduce_test_data[0], reduce_test_data[1], grid_size=1) assert_geometries_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("none_position", range(3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_two_none(func, related_func, none_position): test_data = reduce_test_data[:2] test_data.insert(none_position, None) test_data.insert(none_position, None) actual = func(test_data, grid_size=1) expected = related_func(reduce_test_data[0], reduce_test_data[1], grid_size=1) assert_geometries_equal(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize("n", range(1, 3)) @pytest.mark.parametrize("func, related_func", REDUCE_SET_OPERATIONS_PREC) def test_set_operation_prec_reduce_all_none(n, func, related_func): assert_geometries_equal(func([None] * n, grid_size=1), GeometryCollection([])) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize("n", range(1, 4)) def test_coverage_union_reduce_1dim(n): """ This is tested seperately from other set operations as it differs in two ways: 1. It expects only non-overlapping polygons 2. It expects GEOS 3.8.0+ """ test_data = [ shapely.box(0, 0, 1, 1), shapely.box(1, 0, 2, 1), shapely.box(2, 0, 3, 1), ] actual = shapely.coverage_union_all(test_data[:n]) # perform the reduction in a python loop and compare expected = test_data[0] for i in range(1, n): expected = shapely.coverage_union(expected, test_data[i]) assert_geometries_equal(actual, expected, normalize=True) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") def test_coverage_union_reduce_axis(): # shape = (3, 2), all polygons - none of them overlapping data = [[shapely.box(i, j, i + 1, j + 1) for i in range(2)] for j in range(3)] actual = shapely.coverage_union_all(data, axis=None) # default assert isinstance(actual, Geometry) actual = shapely.coverage_union_all(data, axis=0) assert actual.shape == (2,) actual = shapely.coverage_union_all(data, axis=1) assert actual.shape == (3,) actual = shapely.coverage_union_all(data, axis=-1) assert actual.shape == (3,) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") def test_coverage_union_overlapping_inputs(): polygon = Polygon([(1, 1), (1, 0), (0, 0), (0, 1), (1, 1)]) other = Polygon([(1, 0), (0.9, 1), (2, 1), (2, 0), (1, 0)]) if shapely.geos_version >= (3, 12, 0): # Return mostly unchaged output result = shapely.coverage_union(polygon, other) expected = shapely.multipolygons([polygon, other]) assert_geometries_equal(result, expected, normalize=True) else: # Overlapping polygons raise an error with pytest.raises( shapely.GEOSException, match="CoverageUnion cannot process incorrectly noded inputs.", ): shapely.coverage_union(polygon, other) @pytest.mark.skipif(shapely.geos_version < (3, 8, 0), reason="GEOS < 3.8") @pytest.mark.parametrize( "geom_1, geom_2", # All possible polygon, non_polygon combinations [[polygon, non_polygon] for non_polygon in non_polygon_types] # All possible non_polygon, non_polygon combinations + [ [non_polygon_1, non_polygon_2] for non_polygon_1 in non_polygon_types for non_polygon_2 in non_polygon_types ], ) def test_coverage_union_non_polygon_inputs(geom_1, geom_2): if shapely.geos_version >= (3, 12, 0): def effective_geom_types(geom): if hasattr(geom, "geoms") and not geom.is_empty: gts = set() for geom in geom.geoms: gts |= effective_geom_types(geom) return gts return {geom.geom_type.lstrip("Multi").replace("LinearRing", "LineString")} geom_types_1 = effective_geom_types(geom_1) geom_types_2 = effective_geom_types(geom_2) if len(geom_types_1) == 1 and geom_types_1 == geom_types_2: with ignore_invalid(): # these show "invalid value encountered in coverage_union" result = shapely.coverage_union(geom_1, geom_2) assert geom_types_1 == effective_geom_types(result) else: with pytest.raises( shapely.GEOSException, match="Overlay input is mixed-dimension" ): shapely.coverage_union(geom_1, geom_2) else: # Non polygon geometries raise an error with pytest.raises( shapely.GEOSException, match="Unhandled geometry type in CoverageUnion." ): shapely.coverage_union(geom_1, geom_2) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") @pytest.mark.parametrize( "geom,grid_size,expected", [ # floating point precision, expect no change ( [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)], 0, Polygon( ( (0, 0.2), (0, 10), (5.1, 10), (5.1, 0.2), (5, 0.2), (5, 0.1), (0.1, 0.1), (0.1, 0.2), (0, 0.2), ) ), ), # grid_size is at effective precision, expect no change ( [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)], 0.1, Polygon( ( (0, 0.2), (0, 10), (5.1, 10), (5.1, 0.2), (5, 0.2), (5, 0.1), (0.1, 0.1), (0.1, 0.2), (0, 0.2), ) ), ), # grid_size forces rounding to nearest integer ( [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)], 1, Polygon([(0, 5), (0, 10), (5, 10), (5, 5), (5, 0), (0, 0), (0, 5)]), ), # grid_size much larger than effective precision causes rounding to nearest # multiple of 10 ( [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)], 10, Polygon([(0, 10), (10, 10), (10, 0), (0, 0), (0, 10)]), ), # grid_size is so large that polygons collapse to empty ( [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)], 100, Polygon(), ), ], ) def test_union_all_prec(geom, grid_size, expected): actual = shapely.union_all(geom, grid_size=grid_size) assert shapely.equals(actual, expected) @pytest.mark.skipif(shapely.geos_version < (3, 9, 0), reason="GEOS < 3.9") def test_uary_union_alias(): geoms = [shapely.box(0.1, 0.1, 5, 5), shapely.box(0, 0.2, 5.1, 10)] actual = shapely.unary_union(geoms, grid_size=1) expected = shapely.union_all(geoms, grid_size=1) assert shapely.equals(actual, expected) shapely-2.0.3/shapely/tests/test_strtree.py000066400000000000000000002205221456366510000211020ustar00rootroot00000000000000import itertools import math import pickle import subprocess import sys from concurrent.futures import ThreadPoolExecutor import numpy as np import pytest from numpy.testing import assert_array_equal import shapely from shapely import box, geos_version, LineString, MultiPoint, Point, STRtree from shapely.errors import UnsupportedGEOSVersionError from shapely.testing import assert_geometries_equal from shapely.tests.common import ( empty, empty_line_string, empty_point, ignore_invalid, point, ) # the distance between 2 points spaced at whole numbers along a diagonal HALF_UNIT_DIAG = math.sqrt(2) / 2 EPS = 1e-9 @pytest.fixture(scope="session") def tree(): geoms = shapely.points(np.arange(10), np.arange(10)) yield STRtree(geoms) @pytest.fixture(scope="session") def line_tree(): x = np.arange(10) y = np.arange(10) offset = 1 geoms = shapely.linestrings(np.array([[x, x + offset], [y, y + offset]]).T) yield STRtree(geoms) @pytest.fixture(scope="session") def poly_tree(): # create buffers so that midpoint between two buffers intersects # each buffer. NOTE: add EPS to help mitigate rounding errors at midpoint. geoms = shapely.buffer( shapely.points(np.arange(10), np.arange(10)), HALF_UNIT_DIAG + EPS, quad_segs=32 ) yield STRtree(geoms) @pytest.mark.parametrize( "geometry,count, hits", [ # Empty array produces empty tree ([], 0, 0), ([point], 1, 1), # None geometries are ignored when creating tree ([None], 0, 0), ([point, None], 1, 1), # empty geometries are ignored when creating tree ([empty, empty_point, empty_line_string], 0, 0), # only the valid geometry should have a hit ([empty, point, empty_point, empty_line_string], 1, 1), ], ) def test_init(geometry, count, hits): tree = STRtree(geometry) assert len(tree) == count assert tree.query(box(0, 0, 100, 100)).size == hits def test_init_with_invalid_geometry(): with pytest.raises(TypeError): STRtree(["Not a geometry"]) def test_references(): point1 = Point() point2 = Point(0, 1) geoms = [point1, point2] tree = STRtree(geoms) point1 = None point2 = None import gc gc.collect() # query after freeing geometries does not lead to segfault assert tree.query(box(0, 0, 1, 1)).tolist() == [1] def test_flush_geometries(): arr = shapely.points(np.arange(10), np.arange(10)) tree = STRtree(arr) # Dereference geometries arr[:] = None import gc gc.collect() # Still it does not lead to a segfault tree.query(point) def test_geometries_property(): arr = np.array([point]) tree = STRtree(arr) assert_geometries_equal(arr, tree.geometries) # modifying elements of input should not modify tree.geometries arr[0] = shapely.Point(0, 0) assert_geometries_equal(point, tree.geometries[0]) # TODO(shapely-2.0) this fails on Appveyor, see # https://github.com/shapely/shapely/pull/983#issuecomment-718557666 @pytest.mark.skipif(sys.platform.startswith("win32"), reason="does not run on Appveyor") def test_pickle_persistence(tmp_path): # write the pickeled tree to another process; the process should not crash tree = STRtree([Point(i, i).buffer(0.1) for i in range(3)]) pickled_strtree = pickle.dumps(tree) unpickle_script = """ import pickle import sys from shapely import Point, geos_version pickled_strtree = sys.stdin.buffer.read() print("received pickled strtree:", repr(pickled_strtree)) tree = pickle.loads(pickled_strtree) tree.query(Point(0, 0)) if geos_version >= (3, 6, 0): tree.nearest(Point(0, 0)) print("done") """ filename = tmp_path / "unpickle-strtree.py" with open(filename, "w") as out: out.write(unpickle_script) proc = subprocess.Popen( [sys.executable, str(filename)], stdin=subprocess.PIPE, ) proc.communicate(input=pickled_strtree) proc.wait() assert proc.returncode == 0 @pytest.mark.parametrize( "geometry", [ "I am not a geometry", ["I am not a geometry"], [Point(0, 0), "still not a geometry"], [[], "in a mixed array", 1], ], ) @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences:") def test_query_invalid_geometry(tree, geometry): with pytest.raises((TypeError, ValueError)): tree.query(geometry) def test_query_invalid_dimension(tree): with pytest.raises(TypeError, match="Array should be one dimensional"): tree.query([[Point(0.5, 0.5)]]) @pytest.mark.parametrize( "tree_geometry, geometry,expected", [ # Empty tree returns no results ([], point, []), ([], [point], [[], []]), ([], None, []), ([], [None], [[], []]), # Tree with only None returns no results ([None], point, []), ([None], [point], [[], []]), ([None], None, []), ([None], [None], [[], []]), # querying with None returns no results ([point], None, []), ([point], [None], [[], []]), # Empty is included in the tree, but ignored when querying the tree ([empty], empty, []), ([empty], [empty], [[], []]), ([empty], point, []), ([empty], [point], [[], []]), ([point, empty], empty, []), ([point, empty], [empty], [[], []]), # None and empty are ignored in the tree, but the index of the valid # geometry should be retained. ([None, point], box(0, 0, 10, 10), [1]), ([None, point], [box(0, 0, 10, 10)], [[0], [1]]), ([None, empty, point], box(0, 0, 10, 10), [2]), ([point, None, point], box(0, 0, 10, 10), [0, 2]), ([point, None, point], [box(0, 0, 10, 10)], [[0, 0], [0, 2]]), # Only the non-empty query geometry gets hits ([empty, point], [empty, point], [[1], [1]]), ( [empty, empty_point, empty_line_string, point], [empty, empty_point, empty_line_string, point], [[3], [3]], ), ], ) def test_query_with_none_and_empty(tree_geometry, geometry, expected): tree = STRtree(tree_geometry) assert_array_equal(tree.query(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # first and last points intersect ( [Point(1, 1), Point(-1, -1), Point(2, 2)], [[0, 2], [1, 2]], ), # box contains points (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # bigger box contains more points (box(5, 5, 15, 15), [5, 6, 7, 8, 9]), ([box(5, 5, 15, 15)], [[0, 0, 0, 0, 0], [5, 6, 7, 8, 9]]), # first and last boxes contains points ( [box(0, 0, 1, 1), box(100, 100, 110, 110), box(5, 5, 15, 15)], [[0, 0, 2, 2, 2, 2, 2], [0, 1, 5, 6, 7, 8, 9]], ), # envelope of buffer contains points (shapely.buffer(Point(3, 3), 1), [2, 3, 4]), ([shapely.buffer(Point(3, 3), 1)], [[0, 0, 0], [2, 3, 4]]), # envelope of points contains points (MultiPoint([[5, 7], [7, 5]]), [5, 6, 7]), ([MultiPoint([[5, 7], [7, 5]])], [[0, 0, 0], [5, 6, 7]]), ], ) def test_query_points(tree, geometry, expected): assert_array_equal(tree.query(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects first line (Point(0, 0), [0]), ([Point(0, 0)], [[0], [0]]), (Point(0.5, 0.5), [0]), ([Point(0.5, 0.5)], [[0], [0]]), # point within envelope of first line (Point(0, 0.5), [0]), ([Point(0, 0.5)], [[0], [0]]), # point at shared vertex between 2 lines (Point(1, 1), [0, 1]), ([Point(1, 1)], [[0, 0], [0, 1]]), # box overlaps envelope of first 2 lines (touches edge of 1) (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # envelope of buffer overlaps envelope of 2 lines (shapely.buffer(Point(3, 3), 0.5), [2, 3]), ([shapely.buffer(Point(3, 3), 0.5)], [[0, 0], [2, 3]]), # envelope of points overlaps 5 lines (touches edge of 2 envelopes) (MultiPoint([[5, 7], [7, 5]]), [4, 5, 6, 7]), ([MultiPoint([[5, 7], [7, 5]])], [[0, 0, 0, 0], [4, 5, 6, 7]]), ], ) def test_query_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects edge of envelopes of 2 polygons (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), # point intersects single polygon (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box overlaps envelope of 2 polygons (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # larger box overlaps envelope of 3 polygons (box(0, 0, 1.5, 1.5), [0, 1, 2]), ([box(0, 0, 1.5, 1.5)], [[0, 0, 0], [0, 1, 2]]), # first and last boxes overlap envelope of 2 polyons ( [box(0, 0, 1, 1), box(100, 100, 110, 110), box(2, 2, 3, 3)], [[0, 0, 2, 2], [0, 1, 2, 3]], ), # envelope of buffer overlaps envelope of 3 polygons (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 3, 4]), ( [shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]], ), # envelope of larger buffer overlaps envelope of 6 polygons (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [1, 2, 3, 4, 5]), ( [shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0, 0, 0], [1, 2, 3, 4, 5]], ), # envelope of points overlaps 3 polygons (MultiPoint([[5, 7], [7, 5]]), [5, 6, 7]), ([MultiPoint([[5, 7], [7, 5]])], [[0, 0, 0], [5, 6, 7]]), ], ) def test_query_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry), expected) @pytest.mark.parametrize( "predicate", [ "bad_predicate", # disjoint is a valid GEOS binary predicate, but not supported for query "disjoint", ], ) def test_query_invalid_predicate(tree, predicate): with pytest.raises(ValueError, match="is not a valid option"): tree.query(Point(1, 1), predicate=predicate) @pytest.mark.parametrize( "predicate,expected", [ ("intersects", [0, 1, 2]), ("within", []), ("contains", [1]), ("overlaps", []), ("crosses", []), ("covers", [0, 1, 2]), ("covered_by", []), ("contains_properly", [1]), ], ) def test_query_prepared_inputs(tree, predicate, expected): geom = box(0, 0, 2, 2) shapely.prepare(geom) assert_array_equal(tree.query(geom, predicate=predicate), expected) def test_query_with_partially_prepared_inputs(tree): geom = np.array([box(0, 0, 1, 1), box(3, 3, 5, 5)]) expected = tree.query(geom, predicate="intersects") # test with array of partially prepared geometries shapely.prepare(geom[0]) assert_array_equal(expected, tree.query(geom, predicate="intersects")) @pytest.mark.parametrize( "predicate", [ # intersects is intentionally omitted; it does not raise an exception pytest.param( "within", marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"), ), pytest.param( "contains", marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"), ), "overlaps", "crosses", "touches", pytest.param( "covers", marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"), ), pytest.param( "covered_by", marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"), ), pytest.param( "contains_properly", marks=pytest.mark.xfail(geos_version < (3, 8, 0), reason="GEOS < 3.8"), ), ], ) def test_query_predicate_errors(tree, predicate): with ignore_invalid(): line_nan = shapely.linestrings([1, 1], [1, float("nan")]) with pytest.raises(shapely.GEOSException): tree.query(line_nan, predicate=predicate) ### predicate == 'intersects' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box contains points (box(3, 3, 6, 6), [3, 4, 5, 6]), ([box(3, 3, 6, 6)], [[0, 0, 0, 0], [3, 4, 5, 6]]), # first and last boxes contain points ( [box(0, 0, 1, 1), box(100, 100, 110, 110), box(3, 3, 6, 6)], [[0, 0, 2, 2, 2, 2], [0, 1, 3, 4, 5, 6]], ), # envelope of buffer contains more points than intersect buffer # due to diagonal distance (shapely.buffer(Point(3, 3), 1), [3]), ([shapely.buffer(Point(3, 3), 1)], [[0], [3]]), # envelope of buffer with 1/2 distance between points should intersect # same points as envelope (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [2, 3, 4]), ( [shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]], ), # multipoints intersect ( MultiPoint([[5, 5], [7, 7]]), [5, 7], ), ( [MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]], ), # envelope of points contains points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects ( MultiPoint([[5, 7], [7, 7]]), [7], ), ( [MultiPoint([[5, 7], [7, 7]])], [[0], [7]], ), ], ) def test_query_intersects_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="intersects"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects first line (Point(0, 0), [0]), ([Point(0, 0)], [[0], [0]]), (Point(0.5, 0.5), [0]), ([Point(0.5, 0.5)], [[0], [0]]), # point within envelope of first line but does not intersect (Point(0, 0.5), []), ([Point(0, 0.5)], [[], []]), # point at shared vertex between 2 lines (Point(1, 1), [0, 1]), ([Point(1, 1)], [[0, 0], [0, 1]]), # box overlaps envelope of first 2 lines (touches edge of 1) (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # first and last boxes overlap multiple lines each ( [box(0, 0, 1, 1), box(100, 100, 110, 110), box(2, 2, 3, 3)], [[0, 0, 2, 2, 2], [0, 1, 1, 2, 3]], ), # buffer intersects 2 lines (shapely.buffer(Point(3, 3), 0.5), [2, 3]), ([shapely.buffer(Point(3, 3), 0.5)], [[0, 0], [2, 3]]), # buffer intersects midpoint of line at tangent (shapely.buffer(Point(2, 1), HALF_UNIT_DIAG), [1]), ([shapely.buffer(Point(2, 1), HALF_UNIT_DIAG)], [[0], [1]]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), [6, 7]), ([MultiPoint([[5, 7], [7, 7]])], [[0, 0], [6, 7]]), ], ) def test_query_intersects_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="intersects"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point within first polygon (Point(0, 0.5), [0]), ([Point(0, 0.5)], [[0], [0]]), (Point(0.5, 0), [0]), ([Point(0.5, 0)], [[0], [0]]), # midpoint between two polygons intersects both (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), # point intersects single polygon (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box overlaps envelope of 2 polygons (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # larger box intersects 3 polygons (box(0, 0, 1.5, 1.5), [0, 1, 2]), ([box(0, 0, 1.5, 1.5)], [[0, 0, 0], [0, 1, 2]]), # first and last boxes overlap ( [box(0, 0, 1, 1), box(100, 100, 110, 110), box(2, 2, 3, 3)], [[0, 0, 2, 2], [0, 1, 2, 3]], ), # buffer overlaps 3 polygons (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 3, 4]), ( [shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]], ), # larger buffer overlaps 6 polygons (touches midpoints) (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [1, 2, 3, 4, 5]), ( [shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0, 0, 0], [1, 2, 3, 4, 5]], ), # envelope of points overlaps polygons, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint within polygon (MultiPoint([[5, 7], [7, 7]]), [7]), ([MultiPoint([[5, 7], [7, 7]])], [[0], [7]]), ], ) def test_query_intersects_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="intersects"), expected) ### predicate == 'within' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box not within points (box(3, 3, 6, 6), []), ([box(3, 3, 6, 6)], [[], []]), # envelope of buffer not within points (shapely.buffer(Point(3, 3), 1), []), ([shapely.buffer(Point(3, 3), 1)], [[], []]), # multipoints intersect but are not within points in tree (MultiPoint([[5, 5], [7, 7]]), []), ([MultiPoint([[5, 5], [7, 7]])], [[], []]), # only one point of multipoint intersects, but multipoints are not # within any points in tree (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # envelope of points contains points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), ], ) def test_query_within_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="within"), expected) @pytest.mark.parametrize( "geometry,expected", [ # endpoint not within first line (Point(0, 0), []), ([Point(0, 0)], [[], []]), # point within first line (Point(0.5, 0.5), [0]), ([Point(0.5, 0.5)], [[0], [0]]), # point within envelope of first line but does not intersect (Point(0, 0.5), []), ([Point(0, 0.5)], [[], []]), # point at shared vertex between 2 lines (but within neither) (Point(1, 1), []), ([Point(1, 1)], [[], []]), # box not within line (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # buffer intersects 2 lines but not within either (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects, but both are not within line (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), (MultiPoint([[6.5, 6.5], [7, 7]]), [6]), ([MultiPoint([[6.5, 6.5], [7, 7]])], [[0], [6]]), ], ) def test_query_within_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="within"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point within first polygon (Point(0, 0.5), [0]), ([Point(0, 0.5)], [[0], [0]]), (Point(0.5, 0), [0]), ([Point(0.5, 0)], [[0], [0]]), # midpoint between two polygons intersects both (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), # point intersects single polygon (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box overlaps envelope of 2 polygons but within neither (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # box within polygon (box(0, 0, 0.5, 0.5), [0]), ([box(0, 0, 0.5, 0.5)], [[0], [0]]), # larger box intersects 3 polygons but within none (box(0, 0, 1.5, 1.5), []), ([box(0, 0, 1.5, 1.5)], [[], []]), # buffer intersects 3 polygons but only within one (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [3]), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0], [3]]), # larger buffer overlaps 6 polygons (touches midpoints) but within none (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[], []]), # envelope of points overlaps polygons, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint within polygon (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points in multipoint within polygon (MultiPoint([[5.25, 5.5], [5.25, 5.0]]), [5]), ([MultiPoint([[5.25, 5.5], [5.25, 5.0]])], [[0], [5]]), ], ) def test_query_within_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="within"), expected) ### predicate == 'contains' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box contains points (2 are at edges and not contained) (box(3, 3, 6, 6), [4, 5]), ([box(3, 3, 6, 6)], [[0, 0], [4, 5]]), # envelope of buffer contains more points than within buffer # due to diagonal distance (shapely.buffer(Point(3, 3), 1), [3]), ([shapely.buffer(Point(3, 3), 1)], [[0], [3]]), # envelope of buffer with 1/2 distance between points should intersect # same points as envelope (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [2, 3, 4]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]]), # multipoints intersect (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]]), # envelope of points contains points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), [7]), ([MultiPoint([[5, 7], [7, 7]])], [[0], [7]]), ], ) def test_query_contains_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="contains"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point does not contain any lines (not valid relation) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box contains first line (touches edge of 1 but does not contain it) (box(0, 0, 1, 1), [0]), ([box(0, 0, 1, 1)], [[0], [0]]), # buffer intersects 2 lines but contains neither (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points intersect but do not contain any lines (not valid relation) (MultiPoint([[5, 5], [6, 6]]), []), ([MultiPoint([[5, 5], [6, 6]])], [[], []]), ], ) def test_query_contains_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="contains"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point does not contain any polygons (not valid relation) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box overlaps envelope of 2 polygons but contains neither (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # larger box intersects 3 polygons but contains only one (box(0, 0, 2, 2), [1]), ([box(0, 0, 2, 2)], [[0], [1]]), # buffer overlaps 3 polygons but contains none (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[], []]), # larger buffer overlaps 6 polygons (touches midpoints) but contains one (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [3]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0], [3]]), # envelope of points overlaps polygons, but points do not intersect # (not valid relation) (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), ], ) def test_query_contains_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="contains"), expected) ### predicate == 'overlaps' # Overlaps only returns results where geometries are of same dimensions # and do not completely contain each other. # See: https://postgis.net/docs/ST_Overlaps.html @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect but do not overlap (Point(1, 1), []), ([Point(1, 1)], [[], []]), # box overlaps points including those at edge but does not overlap # (completely contains all points) (box(3, 3, 6, 6), []), ([box(3, 3, 6, 6)], [[], []]), # envelope of buffer contains points, but does not overlap (shapely.buffer(Point(3, 3), 1), []), ([shapely.buffer(Point(3, 3), 1)], [[], []]), # multipoints intersect but do not overlap (both completely contain each other) (MultiPoint([[5, 5], [7, 7]]), []), ([MultiPoint([[5, 5], [7, 7]])], [[], []]), # envelope of points contains points in tree, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects but does not overlap # the intersecting point from multipoint completely contains point in tree (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), ], ) def test_query_overlaps_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="overlaps"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects line but is completely contained by it (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box overlaps second line (contains first line) # but of different dimensions so does not overlap (box(0, 0, 1.5, 1.5), []), ([box(0, 0, 1.5, 1.5)], [[], []]), # buffer intersects 2 lines but of different dimensions so does not overlap (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points intersect but different dimensions (MultiPoint([[5, 5], [6, 6]]), []), ([MultiPoint([[5, 5], [6, 6]])], [[], []]), ], ) def test_query_overlaps_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="overlaps"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point does not overlap any polygons (different dimensions) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box overlaps 2 polygons (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # larger box intersects 3 polygons and contains one (box(0, 0, 2, 2), [0, 2]), ([box(0, 0, 2, 2)], [[0, 0], [0, 2]]), # buffer overlaps 3 polygons and contains 1 (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 4]), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0, 0], [2, 4]]), # larger buffer overlaps 6 polygons (touches midpoints) but contains one (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [1, 2, 4, 5]), ( [shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0, 0], [1, 2, 4, 5]], ), # one of two points intersects but different dimensions (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), ], ) def test_query_overlaps_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="overlaps"), expected) ### predicate == 'crosses' # Only valid for certain geometry combinations # See: https://postgis.net/docs/ST_Crosses.html @pytest.mark.parametrize( "geometry,expected", [ # points intersect but not valid relation (Point(1, 1), []), # all points of result from tree are in common with box (box(3, 3, 6, 6), []), # all points of result from tree are in common with buffer (shapely.buffer(Point(3, 3), 1), []), # only one point of multipoint intersects but not valid relation (MultiPoint([[5, 7], [7, 7]]), []), ], ) def test_query_crosses_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="crosses"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects first line but is completely in common with line (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box overlaps envelope of first 2 lines, contains first and crosses second (box(0, 0, 1.5, 1.5), [1]), ([box(0, 0, 1.5, 1.5)], [[0], [1]]), # buffer intersects 2 lines (shapely.buffer(Point(3, 3), 0.5), [2, 3]), ([shapely.buffer(Point(3, 3), 0.5)], [[0, 0], [2, 3]]), # line crosses line (shapely.linestrings([(1, 0), (0, 1)]), [0]), ([shapely.linestrings([(1, 0), (0, 1)])], [[0], [0]]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7], [7, 8]]), []), ([MultiPoint([[5, 7], [7, 7], [7, 8]])], [[], []]), ], ) def test_query_crosses_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="crosses"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point within first polygon but not valid relation (Point(0, 0.5), []), ([Point(0, 0.5)], [[], []]), # box overlaps 2 polygons but not valid relation (box(0, 0, 1.5, 1.5), []), ([box(0, 0, 1.5, 1.5)], [[], []]), # buffer overlaps 3 polygons but not valid relation (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[], []]), # only one point of multipoint within (MultiPoint([[5, 7], [7, 7], [7, 8]]), [7]), ([MultiPoint([[5, 7], [7, 7], [7, 8]])], [[0], [7]]), ], ) def test_query_crosses_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="crosses"), expected) ### predicate == 'touches' # See: https://postgis.net/docs/ST_Touches.html @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect but not valid relation (Point(1, 1), []), ([Point(1, 1)], [[], []]), # box contains points but touches only those at edges (box(3, 3, 6, 6), [3, 6]), ([box(3, 3, 6, 6)], [[0, 0], [3, 6]]), # polygon completely contains point in tree (shapely.buffer(Point(3, 3), 1), []), ([shapely.buffer(Point(3, 3), 1)], [[], []]), # linestring intersects 2 points but touches only one (LineString([(-1, -1), (1, 1)]), [1]), ([LineString([(-1, -1), (1, 1)])], [[0], [1]]), # multipoints intersect but not valid relation (MultiPoint([[5, 5], [7, 7]]), []), ([MultiPoint([[5, 5], [7, 7]])], [[], []]), ], ) def test_query_touches_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="touches"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point intersects first line (Point(0, 0), [0]), ([Point(0, 0)], [[0], [0]]), # point is within line (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # point at shared vertex between 2 lines (Point(1, 1), [0, 1]), ([Point(1, 1)], [[0, 0], [0, 1]]), # box overlaps envelope of first 2 lines (touches edge of 1) (box(0, 0, 1, 1), [1]), ([box(0, 0, 1, 1)], [[0], [1]]), # buffer intersects 2 lines but does not touch edges of either (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # buffer intersects midpoint of line at tangent but there is a little overlap # due to precision issues (shapely.buffer(Point(2, 1), HALF_UNIT_DIAG + 1e-7), []), ([shapely.buffer(Point(2, 1), HALF_UNIT_DIAG + 1e-7)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects at vertex between lines (MultiPoint([[5, 7], [7, 7], [7, 8]]), [6, 7]), ([MultiPoint([[5, 7], [7, 7], [7, 8]])], [[0, 0], [6, 7]]), ], ) def test_query_touches_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="touches"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point within first polygon (Point(0, 0.5), []), ([Point(0, 0.5)], [[], []]), # point is at edge of first polygon (Point(HALF_UNIT_DIAG + EPS, 0), [0]), ([Point(HALF_UNIT_DIAG + EPS, 0)], [[0], [0]]), # box overlaps envelope of 2 polygons does not touch any at edge (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # box overlaps 2 polygons and touches edge of first (box(HALF_UNIT_DIAG + EPS, 0, 2, 2), [0]), ([box(HALF_UNIT_DIAG + EPS, 0, 2, 2)], [[0], [0]]), # buffer overlaps 3 polygons but does not touch any at edge (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG + EPS), []), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG + EPS)], [[], []]), # only one point of multipoint within polygon but does not touch (MultiPoint([[0, 0], [7, 7], [7, 8]]), []), ([MultiPoint([[0, 0], [7, 7], [7, 8]])], [[], []]), ], ) def test_query_touches_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="touches"), expected) ### predicate == 'covers' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect and thus no point is outside the other (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box covers any points that intersect or are within (box(3, 3, 6, 6), [3, 4, 5, 6]), ([box(3, 3, 6, 6)], [[0, 0, 0, 0], [3, 4, 5, 6]]), # envelope of buffer covers more points than are covered by buffer # due to diagonal distance (shapely.buffer(Point(3, 3), 1), [3]), ([shapely.buffer(Point(3, 3), 1)], [[0], [3]]), # envelope of buffer with 1/2 distance between points should intersect # same points as envelope (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [2, 3, 4]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]]), # multipoints intersect and thus no point is outside the other (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]]), # envelope of points contains points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), [7]), ([MultiPoint([[5, 7], [7, 7]])], [[0], [7]]), ], ) def test_query_covers_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="covers"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point does not cover any lines (not valid relation) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box covers first line (intersects another does not contain it) (box(0, 0, 1.5, 1.5), [0]), ([box(0, 0, 1.5, 1.5)], [[0], [0]]), # box completely covers 2 lines (touches edges of 2 others) (box(1, 1, 3, 3), [1, 2]), ([box(1, 1, 3, 3)], [[0, 0], [1, 2]]), # buffer intersects 2 lines but does not completely cover either (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects a line, but does not completely cover it (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points intersect but do not cover any lines (not valid relation) (MultiPoint([[5, 5], [6, 6]]), []), ([MultiPoint([[5, 5], [6, 6]])], [[], []]), ], ) def test_query_covers_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="covers"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point does not cover any polygons (not valid relation) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # box overlaps envelope of 2 polygons but does not completely cover either (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # larger box intersects 3 polygons but covers only one (box(0, 0, 2, 2), [1]), ([box(0, 0, 2, 2)], [[0], [1]]), # buffer overlaps 3 polygons but does not completely cover any (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[], []]), # larger buffer overlaps 6 polygons (touches midpoints) but covers only one (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [3]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0], [3]]), # envelope of points overlaps polygons, but points do not intersect # (not valid relation) (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), ], ) def test_query_covers_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="covers"), expected) ### predicate == 'covered_by' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # box not covered by points (box(3, 3, 6, 6), []), ([box(3, 3, 6, 6)], [[], []]), # envelope of buffer not covered by points (shapely.buffer(Point(3, 3), 1), []), ([shapely.buffer(Point(3, 3), 1)], [[], []]), # multipoints intersect but are not covered by points in tree (MultiPoint([[5, 5], [7, 7]]), []), ([MultiPoint([[5, 5], [7, 7]])], [[], []]), # only one point of multipoint intersects, but multipoints are not # covered by any points in tree (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # envelope of points overlaps points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), ], ) def test_query_covered_by_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="covered_by"), expected) @pytest.mark.parametrize( "geometry,expected", [ # endpoint is covered by first line (Point(0, 0), [0]), ([Point(0, 0)], [[0], [0]]), # point covered by first line (Point(0.5, 0.5), [0]), ([Point(0.5, 0.5)], [[0], [0]]), # point within envelope of first line but does not intersect (Point(0, 0.5), []), ([Point(0, 0.5)], [[], []]), # point at shared vertex between 2 lines and is covered by both (Point(1, 1), [0, 1]), ([Point(1, 1)], [[0, 0], [0, 1]]), # line intersects 3 lines, but is covered by only one (shapely.linestrings([[1, 1], [2, 2]]), [1]), ([shapely.linestrings([[1, 1], [2, 2]])], [[0], [1]]), # line intersects 2 lines, but is covered by neither (shapely.linestrings([[1.5, 1.5], [2.5, 2.5]]), []), ([shapely.linestrings([[1.5, 1.5], [2.5, 2.5]])], [[], []]), # box not covered by line (not valid geometric relation) (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # buffer intersects 2 lines but not within either (not valid geometric relation) (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), # envelope of points overlaps lines but intersects none (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects, but both are not covered by line (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points are covered by a line (MultiPoint([[6.5, 6.5], [7, 7]]), [6]), ([MultiPoint([[6.5, 6.5], [7, 7]])], [[0], [6]]), ], ) def test_query_covered_by_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query(geometry, predicate="covered_by"), expected) @pytest.mark.parametrize( "geometry,expected", [ # point covered by polygon (Point(0, 0.5), [0]), ([Point(0, 0.5)], [[0], [0]]), (Point(0.5, 0), [0]), ([Point(0.5, 0)], [[0], [0]]), (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # midpoint between two polygons is covered by both (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), # line intersects multiple polygons but is not covered by any (shapely.linestrings([[0, 0], [2, 2]]), []), ([shapely.linestrings([[0, 0], [2, 2]])], [[], []]), # line intersects multiple polygons but is covered by only one (shapely.linestrings([[1.5, 1.5], [2.5, 2.5]]), [2]), ([shapely.linestrings([[1.5, 1.5], [2.5, 2.5]])], [[0], [2]]), # box overlaps envelope of 2 polygons but not covered by either (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # box covered by polygon (box(0, 0, 0.5, 0.5), [0]), ([box(0, 0, 0.5, 0.5)], [[0], [0]]), # larger box intersects 3 polygons but not covered by any (box(0, 0, 1.5, 1.5), []), ([box(0, 0, 1.5, 1.5)], [[], []]), # buffer intersects 3 polygons but only within one (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [3]), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0], [3]]), # larger buffer overlaps 6 polygons (touches midpoints) but within none (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[], []]), # envelope of points overlaps polygons, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint within polygon (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), # both points in multipoint within polygon (MultiPoint([[5.25, 5.5], [5.25, 5.0]]), [5]), ([MultiPoint([[5.25, 5.5], [5.25, 5.0]])], [[0], [5]]), ], ) def test_query_covered_by_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query(geometry, predicate="covered_by"), expected) ### predicate == 'contains_properly' @pytest.mark.parametrize( "geometry,expected", [ # points do not intersect (Point(0.5, 0.5), []), ([Point(0.5, 0.5)], [[], []]), # points intersect (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # line contains every point that is not on its first or last coordinate # these are on the "exterior" of the line (shapely.linestrings([[0, 0], [2, 2]]), [1]), ([shapely.linestrings([[0, 0], [2, 2]])], [[0], [1]]), # slightly longer line contains multiple points (shapely.linestrings([[0.5, 0.5], [2.5, 2.5]]), [1, 2]), ([shapely.linestrings([[0.5, 0.5], [2.5, 2.5]])], [[0, 0], [1, 2]]), # line intersects and contains one point (shapely.linestrings([[0, 2], [2, 0]]), [1]), ([shapely.linestrings([[0, 2], [2, 0]])], [[0], [1]]), # box contains points (2 are at edges and not contained) (box(3, 3, 6, 6), [4, 5]), ([box(3, 3, 6, 6)], [[0, 0], [4, 5]]), # envelope of buffer contains more points than within buffer # due to diagonal distance (shapely.buffer(Point(3, 3), 1), [3]), ([shapely.buffer(Point(3, 3), 1)], [[0], [3]]), # envelope of buffer with 1/2 distance between points should intersect # same points as envelope (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [2, 3, 4]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]]), # multipoints intersect (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]]), # envelope of points contains points, but points do not intersect (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), # only one point of multipoint intersects (MultiPoint([[5, 7], [7, 7]]), [7]), ([MultiPoint([[5, 7], [7, 7]])], [[0], [7]]), ], ) def test_query_contains_properly_points(tree, geometry, expected): assert_array_equal(tree.query(geometry, predicate="contains_properly"), expected) @pytest.mark.parametrize( "geometry,expected", [ # None of the following conditions satisfy the relation for linestrings # because they have no interior: # "a contains b if no points of b lie in the exterior of a, and at least one # point of the interior of b lies in the interior of a" (Point(0, 0), []), ([Point(0, 0)], [[], []]), (shapely.linestrings([[0, 0], [1, 1]]), []), ([shapely.linestrings([[0, 0], [1, 1]])], [[], []]), (shapely.linestrings([[0, 0], [2, 2]]), []), ([shapely.linestrings([[0, 0], [2, 2]])], [[], []]), (shapely.linestrings([[0, 2], [2, 0]]), []), ([shapely.linestrings([[0, 2], [2, 0]])], [[], []]), (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), (MultiPoint([[5, 7], [7, 7]]), []), ([MultiPoint([[5, 7], [7, 7]])], [[], []]), (MultiPoint([[5, 5], [6, 6]]), []), ([MultiPoint([[5, 5], [6, 6]])], [[], []]), (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), (box(0, 0, 2, 2), []), ([box(0, 0, 2, 2)], [[], []]), (shapely.buffer(Point(3, 3), 0.5), []), ([shapely.buffer(Point(3, 3), 0.5)], [[], []]), ], ) def test_query_contains_properly_lines(line_tree, geometry, expected): assert_array_equal( line_tree.query(geometry, predicate="contains_properly"), expected ) @pytest.mark.parametrize( "geometry,expected", [ # point does not contain any polygons (not valid relation) (Point(0, 0), []), ([Point(0, 0)], [[], []]), # line intersects multiple polygons but does not contain any (not valid relation) (shapely.linestrings([[0, 0], [2, 2]]), []), ([shapely.linestrings([[0, 0], [2, 2]])], [[], []]), # box overlaps envelope of 2 polygons but contains neither (box(0, 0, 1, 1), []), ([box(0, 0, 1, 1)], [[], []]), # larger box intersects 3 polygons but contains only one (box(0, 0, 2, 2), [1]), ([box(0, 0, 2, 2)], [[0], [1]]), # buffer overlaps 3 polygons but contains none (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), []), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[], []]), # larger buffer overlaps 6 polygons (touches midpoints) but contains one (shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG), [3]), ([shapely.buffer(Point(3, 3), 3 * HALF_UNIT_DIAG)], [[0], [3]]), # envelope of points overlaps polygons, but points do not intersect # (not valid relation) (MultiPoint([[5, 7], [7, 5]]), []), ([MultiPoint([[5, 7], [7, 5]])], [[], []]), ], ) def test_query_contains_properly_polygons(poly_tree, geometry, expected): assert_array_equal( poly_tree.query(geometry, predicate="contains_properly"), expected ) ### predicate = 'dwithin' @pytest.mark.skipif(geos_version >= (3, 10, 0), reason="GEOS >= 3.10") @pytest.mark.parametrize( "geometry", [Point(0, 0), [Point(0, 0)], None, [None], empty, [empty]] ) def test_query_dwithin_geos_version(tree, geometry): with pytest.raises(UnsupportedGEOSVersionError, match="requires GEOS >= 3.10"): tree.query(geometry, predicate="dwithin", distance=1) @pytest.mark.skipif(geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry,distance,match", [ (Point(0, 0), None, "distance parameter must be provided"), ([Point(0, 0)], None, "distance parameter must be provided"), (Point(0, 0), "foo", "could not convert string to float"), ([Point(0, 0)], "foo", "could not convert string to float"), ([Point(0, 0)], ["foo"], "could not convert string to float"), (Point(0, 0), [0, 1], "Could not broadcast distance to match geometry"), ([Point(0, 0)], [0, 1], "Could not broadcast distance to match geometry"), (Point(0, 0), [[1.0]], "should be one dimensional"), ([Point(0, 0)], [[1.0]], "should be one dimensional"), ], ) def test_query_dwithin_invalid_distance(tree, geometry, distance, match): with pytest.raises(ValueError, match=match): tree.query(geometry, predicate="dwithin", distance=distance) @pytest.mark.skipif(geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry,distance,expected", [ (None, 1.0, []), ([None], 1.0, [[], []]), (Point(0.25, 0.25), 0, []), ([Point(0.25, 0.25)], 0, [[], []]), (Point(0.25, 0.25), -1, []), ([Point(0.25, 0.25)], -1, [[], []]), (Point(0.25, 0.25), np.nan, []), ([Point(0.25, 0.25)], np.nan, [[], []]), (Point(), 1, []), ([Point()], 1, [[], []]), (Point(0.25, 0.25), 0.5, [0]), ([Point(0.25, 0.25)], 0.5, [[0], [0]]), (Point(0.25, 0.25), 2.5, [0, 1, 2]), ([Point(0.25, 0.25)], 2.5, [[0, 0, 0], [0, 1, 2]]), (Point(3, 3), 1.5, [2, 3, 4]), ([Point(3, 3)], 1.5, [[0, 0, 0], [2, 3, 4]]), # 2 equidistant points in tree (Point(0.5, 0.5), 0.75, [0, 1]), ([Point(0.5, 0.5)], 0.75, [[0, 0], [0, 1]]), ( [None, Point(0.5, 0.5)], 0.75, [ [ 1, 1, ], [0, 1], ], ), ( [Point(0.5, 0.5), Point(0.25, 0.25)], 0.75, [[0, 0, 1], [0, 1, 0]], ), ( [Point(0, 0.2), Point(1.75, 1.75)], [0.25, 2], [[0, 1, 1, 1], [0, 1, 2, 3]], ), # all points intersect box (box(0, 0, 3, 3), 0, [0, 1, 2, 3]), ([box(0, 0, 3, 3)], 0, [[0, 0, 0, 0], [0, 1, 2, 3]]), (box(0, 0, 3, 3), 0.25, [0, 1, 2, 3]), ([box(0, 0, 3, 3)], 0.25, [[0, 0, 0, 0], [0, 1, 2, 3]]), # intersecting and nearby points (box(1, 1, 2, 2), 1.5, [0, 1, 2, 3]), ([box(1, 1, 2, 2)], 1.5, [[0, 0, 0, 0], [0, 1, 2, 3]]), # # return nearest point in tree for each point in multipoint (MultiPoint([[0.25, 0.25], [1.5, 1.5]]), 0.75, [0, 1, 2]), ([MultiPoint([[0.25, 0.25], [1.5, 1.5]])], 0.75, [[0, 0, 0], [0, 1, 2]]), # 2 equidistant points per point in multipoint ( MultiPoint([[0.5, 0.5], [3.5, 3.5]]), 0.75, [0, 1, 3, 4], ), ( [MultiPoint([[0.5, 0.5], [3.5, 3.5]])], 0.75, [[0, 0, 0, 0], [0, 1, 3, 4]], ), ], ) def test_query_dwithin_points(tree, geometry, distance, expected): assert_array_equal( tree.query(geometry, predicate="dwithin", distance=distance), expected ) @pytest.mark.skipif(geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry,distance,expected", [ (None, 1.0, []), ([None], 1.0, [[], []]), (Point(0.5, 0.5), 0, [0]), ([Point(0.5, 0.5)], 0, [[0], [0]]), (Point(0.5, 0.5), 1.0, [0, 1]), ([Point(0.5, 0.5)], 1.0, [[0, 0], [0, 1]]), (Point(2, 2), 0.5, [1, 2]), ([Point(2, 2)], 0.5, [[0, 0], [1, 2]]), (box(0, 0, 1, 1), 0.5, [0, 1]), ([box(0, 0, 1, 1)], 0.5, [[0, 0], [0, 1]]), (box(0.5, 0.5, 1.5, 1.5), 0.5, [0, 1]), ([box(0.5, 0.5, 1.5, 1.5)], 0.5, [[0, 0], [0, 1]]), # multipoints at endpoints of 2 lines each (MultiPoint([[5, 5], [7, 7]]), 0.5, [4, 5, 6, 7]), ([MultiPoint([[5, 5], [7, 7]])], 0.5, [[0, 0, 0, 0], [4, 5, 6, 7]]), # multipoints are equidistant from 2 lines (MultiPoint([[5, 7], [7, 5]]), 1.5, [5, 6]), ([MultiPoint([[5, 7], [7, 5]])], 1.5, [[0, 0], [5, 6]]), ], ) def test_query_dwithin_lines(line_tree, geometry, distance, expected): assert_array_equal( line_tree.query(geometry, predicate="dwithin", distance=distance), expected, ) @pytest.mark.skipif(geos_version < (3, 10, 0), reason="GEOS < 3.10") @pytest.mark.parametrize( "geometry,distance,expected", [ (Point(0, 0), 0, [0]), ([Point(0, 0)], 0, [[0], [0]]), (Point(0, 0), 0.5, [0]), ([Point(0, 0)], 0.5, [[0], [0]]), (Point(0, 0), 1.5, [0, 1]), ([Point(0, 0)], 1.5, [[0, 0], [0, 1]]), (Point(0.5, 0.5), 1, [0, 1]), ([Point(0.5, 0.5)], 1, [[0, 0], [0, 1]]), (Point(0.5, 0.5), 0.5, [0, 1]), ([Point(0.5, 0.5)], 0.5, [[0, 0], [0, 1]]), (box(0, 0, 1, 1), 0, [0, 1]), ([box(0, 0, 1, 1)], 0, [[0, 0], [0, 1]]), (box(0, 0, 1, 1), 2, [0, 1, 2]), ([box(0, 0, 1, 1)], 2, [[0, 0, 0], [0, 1, 2]]), (MultiPoint([[5, 5], [7, 7]]), 0.5, [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], 0.5, [[0, 0], [5, 7]]), ( MultiPoint([[5, 5], [7, 7]]), 2.5, [3, 4, 5, 6, 7, 8, 9], ), ( [MultiPoint([[5, 5], [7, 7]])], 2.5, [[0, 0, 0, 0, 0, 0, 0], [3, 4, 5, 6, 7, 8, 9]], ), ], ) def test_query_dwithin_polygons(poly_tree, geometry, distance, expected): assert_array_equal( poly_tree.query(geometry, predicate="dwithin", distance=distance), expected, ) ### STRtree nearest def test_nearest_empty_tree(): tree = STRtree([]) assert tree.nearest(point) is None @pytest.mark.parametrize("geometry", ["I am not a geometry"]) def test_nearest_invalid_geom(tree, geometry): with pytest.raises(TypeError): tree.nearest(geometry) @pytest.mark.parametrize("geometry", [None, [None], [Point(1, 1), None]]) def test_nearest_none(tree, geometry): with pytest.raises(ValueError): tree.nearest(geometry) @pytest.mark.parametrize( "geometry", [empty_point, [empty_point], [Point(1, 1), empty_point]] ) def test_nearest_empty(tree, geometry): with pytest.raises(ValueError): tree.nearest(geometry) @pytest.mark.parametrize( "geometry,expected", [ (Point(0.25, 0.25), 0), (Point(0.75, 0.75), 1), (Point(1, 1), 1), ([Point(1, 1), Point(0, 0)], [1, 0]), ([Point(1, 1), Point(0.25, 1)], [1, 1]), ([Point(-10, -10), Point(100, 100)], [0, 9]), (box(0.5, 0.5, 0.75, 0.75), 1), (shapely.buffer(Point(2.5, 2.5), HALF_UNIT_DIAG), 2), (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), 3), (MultiPoint([[5.5, 5], [7, 7]]), 7), (MultiPoint([[5, 7], [7, 5]]), 6), ], ) def test_nearest_points(tree, geometry, expected): assert_array_equal(tree.nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # 2 equidistant points in tree (Point(0.5, 0.5), [0, 1]), # multiple points in box (box(0, 0, 3, 3), [0, 1, 2, 3]), # return nearest point in tree for each point in multipoint (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ], ) def test_nearest_points_equidistant(tree, geometry, expected): # results are returned in order they are traversed when searching the tree, # which can vary between GEOS versions, so we test that one of the valid # results is present result = tree.nearest(geometry) assert result in expected @pytest.mark.parametrize( "geometry,expected", [ (Point(0.5, 0.5), 0), (Point(1.5, 0.5), 0), (shapely.box(0.5, 1.5, 1, 2), 1), (shapely.linestrings([[0, 0.5], [1, 2.5]]), 0), ], ) def test_nearest_lines(line_tree, geometry, expected): assert_array_equal(line_tree.nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # at junction between 2 lines (Point(2, 2), [1, 2]), # contains one line, intersects with another (box(0, 0, 1, 1), [0, 1]), # overlaps 2 lines (box(0.5, 0.5, 1.5, 1.5), [0, 1]), # box overlaps 2 lines and intersects endpoints of 2 more (box(3, 3, 5, 5), [2, 3, 4, 5]), (shapely.buffer(Point(2.5, 2.5), HALF_UNIT_DIAG), [1, 2]), (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 3]), # multipoints at endpoints of 2 lines each (MultiPoint([[5, 5], [7, 7]]), [4, 5, 6, 7]), # second point in multipoint at endpoints of 2 lines (MultiPoint([[5.5, 5], [7, 7]]), [6, 7]), # multipoints are equidistant from 2 lines (MultiPoint([[5, 7], [7, 5]]), [5, 6]), ], ) def test_nearest_lines_equidistant(line_tree, geometry, expected): # results are returned in order they are traversed when searching the tree, # which can vary between GEOS versions, so we test that one of the valid # results is present result = line_tree.nearest(geometry) assert result in expected @pytest.mark.parametrize( "geometry,expected", [ (Point(0, 0), 0), (Point(2, 2), 2), (shapely.box(0, 5, 1, 6), 3), (MultiPoint([[5, 7], [7, 5]]), 6), ], ) def test_nearest_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ # 2 polygons in tree overlap point (Point(0.5, 0.5), [0, 1]), # box overlaps multiple polygons (box(0, 0, 1, 1), [0, 1]), (box(0.5, 0.5, 1.5, 1.5), [0, 1, 2]), (box(3, 3, 5, 5), [3, 4, 5]), (shapely.buffer(Point(2.5, 2.5), HALF_UNIT_DIAG), [2, 3]), # completely overlaps one polygon, touches 2 others (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 3, 4]), # each point in multi point intersects a polygon in tree (MultiPoint([[5, 5], [7, 7]]), [5, 7]), (MultiPoint([[5.5, 5], [7, 7]]), [5, 7]), ], ) def test_nearest_polygons_equidistant(poly_tree, geometry, expected): # results are returned in order they are traversed when searching the tree, # which can vary between GEOS versions, so we test that one of the valid # results is present result = poly_tree.nearest(geometry) assert result in expected def test_query_nearest_empty_tree(): tree = STRtree([]) assert_array_equal(tree.query_nearest(point), []) assert_array_equal(tree.query_nearest([point]), [[], []]) @pytest.mark.parametrize("geometry", ["I am not a geometry", ["still not a geometry"]]) def test_query_nearest_invalid_geom(tree, geometry): with pytest.raises(TypeError): tree.query_nearest(geometry) @pytest.mark.parametrize( "geometry,return_distance,expected", [ (None, False, []), ([None], False, [[], []]), (None, True, ([], [])), ([None], True, ([[], []], [])), ], ) def test_query_nearest_none(tree, geometry, return_distance, expected): if return_distance: index, distance = tree.query_nearest(geometry, return_distance=True) assert_array_equal(index, expected[0]) assert_array_equal(distance, expected[1]) else: assert_array_equal(tree.query_nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [(empty, []), ([empty], [[], []]), ([empty, point], [[1, 1], [2, 3]])], ) def test_query_nearest_empty_geom(tree, geometry, expected): assert_array_equal(tree.query_nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ (Point(0.25, 0.25), [0]), ([Point(0.25, 0.25)], [[0], [0]]), (Point(0.75, 0.75), [1]), ([Point(0.75, 0.75)], [[0], [1]]), (Point(1, 1), [1]), ([Point(1, 1)], [[0], [1]]), # 2 equidistant points in tree (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), ([Point(1, 1), Point(0, 0)], [[0, 1], [1, 0]]), ([Point(1, 1), Point(0.25, 1)], [[0, 1], [1, 1]]), ([Point(-10, -10), Point(100, 100)], [[0, 1], [0, 9]]), (box(0.5, 0.5, 0.75, 0.75), [1]), ([box(0.5, 0.5, 0.75, 0.75)], [[0], [1]]), # multiple points in box (box(0, 0, 3, 3), [0, 1, 2, 3]), ([box(0, 0, 3, 3)], [[0, 0, 0, 0], [0, 1, 2, 3]]), (shapely.buffer(Point(2.5, 2.5), 1), [2, 3]), ([shapely.buffer(Point(2.5, 2.5), 1)], [[0, 0], [2, 3]]), (shapely.buffer(Point(3, 3), 0.5), [3]), ([shapely.buffer(Point(3, 3), 0.5)], [[0], [3]]), (MultiPoint([[5.5, 5], [7, 7]]), [7]), ([MultiPoint([[5.5, 5], [7, 7]])], [[0], [7]]), (MultiPoint([[5, 7], [7, 5]]), [6]), ([MultiPoint([[5, 7], [7, 5]])], [[0], [6]]), # return nearest point in tree for each point in multipoint (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]]), # 2 equidistant points per point in multipoint (MultiPoint([[0.5, 0.5], [3.5, 3.5]]), [0, 1, 3, 4]), ([MultiPoint([[0.5, 0.5], [3.5, 3.5]])], [[0, 0, 0, 0], [0, 1, 3, 4]]), ], ) def test_query_nearest_points(tree, geometry, expected): assert_array_equal(tree.query_nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ (Point(0.5, 0.5), [0]), ([Point(0.5, 0.5)], [[0], [0]]), # at junction between 2 lines, will return both (Point(2, 2), [1, 2]), ([Point(2, 2)], [[0, 0], [1, 2]]), # contains one line, intersects with another (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), # overlaps 2 lines (box(0.5, 0.5, 1.5, 1.5), [0, 1]), ([box(0.5, 0.5, 1.5, 1.5)], [[0, 0], [0, 1]]), # second box overlaps 2 lines and intersects endpoints of 2 more ([box(0, 0, 0.5, 0.5), box(3, 3, 5, 5)], [[0, 1, 1, 1, 1], [0, 2, 3, 4, 5]]), (shapely.buffer(Point(2.5, 2.5), 1), [1, 2, 3]), ([shapely.buffer(Point(2.5, 2.5), 1)], [[0, 0, 0], [1, 2, 3]]), (shapely.buffer(Point(3, 3), 0.5), [2, 3]), ([shapely.buffer(Point(3, 3), 0.5)], [[0, 0], [2, 3]]), # multipoints at endpoints of 2 lines each (MultiPoint([[5, 5], [7, 7]]), [4, 5, 6, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0, 0, 0], [4, 5, 6, 7]]), # second point in multipoint at endpoints of 2 lines (MultiPoint([[5.5, 5], [7, 7]]), [6, 7]), ([MultiPoint([[5.5, 5], [7, 7]])], [[0, 0], [6, 7]]), # multipoints are equidistant from 2 lines (MultiPoint([[5, 7], [7, 5]]), [5, 6]), ([MultiPoint([[5, 7], [7, 5]])], [[0, 0], [5, 6]]), ], ) def test_query_nearest_lines(line_tree, geometry, expected): assert_array_equal(line_tree.query_nearest(geometry), expected) @pytest.mark.parametrize( "geometry,expected", [ (Point(0, 0), [0]), ([Point(0, 0)], [[0], [0]]), (Point(2, 2), [2]), ([Point(2, 2)], [[0], [2]]), # 2 polygons in tree overlap point (Point(0.5, 0.5), [0, 1]), ([Point(0.5, 0.5)], [[0, 0], [0, 1]]), # box overlaps multiple polygons (box(0, 0, 1, 1), [0, 1]), ([box(0, 0, 1, 1)], [[0, 0], [0, 1]]), (box(0.5, 0.5, 1.5, 1.5), [0, 1, 2]), ([box(0.5, 0.5, 1.5, 1.5)], [[0, 0, 0], [0, 1, 2]]), ([box(0, 0, 1, 1), box(3, 3, 5, 5)], [[0, 0, 1, 1, 1], [0, 1, 3, 4, 5]]), (shapely.buffer(Point(2.5, 2.5), HALF_UNIT_DIAG), [2, 3]), ([shapely.buffer(Point(2.5, 2.5), HALF_UNIT_DIAG)], [[0, 0], [2, 3]]), # completely overlaps one polygon, touches 2 others (shapely.buffer(Point(3, 3), HALF_UNIT_DIAG), [2, 3, 4]), ([shapely.buffer(Point(3, 3), HALF_UNIT_DIAG)], [[0, 0, 0], [2, 3, 4]]), # each point in multi point intersects a polygon in tree (MultiPoint([[5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5, 5], [7, 7]])], [[0, 0], [5, 7]]), (MultiPoint([[5.5, 5], [7, 7]]), [5, 7]), ([MultiPoint([[5.5, 5], [7, 7]])], [[0, 0], [5, 7]]), (MultiPoint([[5, 7], [7, 5]]), [6]), ([MultiPoint([[5, 7], [7, 5]])], [[0], [6]]), ], ) def test_query_nearest_polygons(poly_tree, geometry, expected): assert_array_equal(poly_tree.query_nearest(geometry), expected) @pytest.mark.parametrize( "geometry,max_distance,expected", [ # using unset max_distance should return all nearest (Point(0.5, 0.5), None, [0, 1]), ([Point(0.5, 0.5)], None, [[0, 0], [0, 1]]), # using large max_distance should return all nearest (Point(0.5, 0.5), 10, [0, 1]), ([Point(0.5, 0.5)], 10, [[0, 0], [0, 1]]), # using small max_distance should return no results (Point(0.5, 0.5), 0.1, []), ([Point(0.5, 0.5)], 0.1, [[], []]), # using small max_distance should only return results in that distance ([Point(0.5, 0.5), Point(0, 0)], 0.1, [[1], [0]]), ], ) def test_query_nearest_max_distance(tree, geometry, max_distance, expected): assert_array_equal( tree.query_nearest(geometry, max_distance=max_distance), expected ) @pytest.mark.parametrize( "geometry,max_distance", [ (Point(0.5, 0.5), 0), ([Point(0.5, 0.5)], 0), (Point(0.5, 0.5), -1), ([Point(0.5, 0.5)], -1), ], ) def test_query_nearest_invalid_max_distance(tree, geometry, max_distance): with pytest.raises(ValueError, match="max_distance must be greater than 0"): tree.query_nearest(geometry, max_distance=max_distance) def test_query_nearest_nonscalar_max_distance(tree): with pytest.raises(ValueError, match="parameter only accepts scalar values"): tree.query_nearest(Point(0.5, 0.5), max_distance=[1]) @pytest.mark.parametrize( "geometry,expected", [ (Point(0, 0), ([0], [0.0])), ([Point(0, 0)], ([[0], [0]], [0.0])), (Point(0.5, 0.5), ([0, 1], [0.7071, 0.7071])), ([Point(0.5, 0.5)], ([[0, 0], [0, 1]], [0.7071, 0.7071])), (box(0, 0, 1, 1), ([0, 1], [0.0, 0.0])), ([box(0, 0, 1, 1)], ([[0, 0], [0, 1]], [0.0, 0.0])), ], ) def test_query_nearest_return_distance(tree, geometry, expected): expected_indices, expected_dist = expected actual_indices, actual_dist = tree.query_nearest(geometry, return_distance=True) assert_array_equal(actual_indices, expected_indices) assert_array_equal(np.round(actual_dist, 4), expected_dist) @pytest.mark.parametrize( "geometry,exclusive,expected", [ (Point(1, 1), False, [1]), ([Point(1, 1)], False, [[0], [1]]), (Point(1, 1), True, [0, 2]), ([Point(1, 1)], True, [[0, 0], [0, 2]]), ([Point(1, 1), Point(2, 2)], True, [[0, 0, 1, 1], [0, 2, 1, 3]]), ], ) def test_query_nearest_exclusive(tree, geometry, exclusive, expected): assert_array_equal(tree.query_nearest(geometry, exclusive=exclusive), expected) @pytest.mark.parametrize( "geometry,expected", [ (Point(1, 1), []), ([Point(1, 1)], [[], []]), ], ) def test_query_nearest_exclusive_no_results(tree, geometry, expected): tree = STRtree([Point(1, 1)]) assert_array_equal(tree.query_nearest(geometry, exclusive=True), expected) @pytest.mark.parametrize( "geometry,exclusive", [ (Point(1, 1), "invalid"), # non-scalar exclusive parameter not allowed (Point(1, 1), ["also invalid"]), ([Point(1, 1)], []), ([Point(1, 1)], [False]), ], ) def test_query_nearest_invalid_exclusive(tree, geometry, exclusive): with pytest.raises(ValueError): tree.query_nearest(geometry, exclusive=exclusive) @pytest.mark.parametrize( "geometry,all_matches", [ (Point(1, 1), "invalid"), # non-scalar all_matches parameter not allowed (Point(1, 1), ["also invalid"]), ([Point(1, 1)], []), ([Point(1, 1)], [False]), ], ) def test_query_nearest_invalid_all_matches(tree, geometry, all_matches): with pytest.raises(ValueError): tree.query_nearest(geometry, all_matches=all_matches) def test_query_nearest_all_matches(tree): point = Point(0.5, 0.5) assert_array_equal(tree.query_nearest(point, all_matches=True), [0, 1]) indices = tree.query_nearest(point, all_matches=False) # result is dependent on tree traversal order; may vary across test runs assert np.array_equal(indices, [0]) or np.array_equal(indices, [1]) def test_strtree_threaded_query(): ## Create data polygons = shapely.polygons(np.random.randn(1000, 3, 2)) # needs to be big enough to trigger the segfault N = 100_000 points = shapely.points(4 * np.random.random(N) - 2, 4 * np.random.random(N) - 2) ## Slice parts of the arrays -> 4x4 => 16 combinations n = int(len(polygons) / 4) polygons_parts = [ polygons[:n], polygons[n : 2 * n], polygons[2 * n : 3 * n], polygons[3 * n :], ] n = int(len(points) / 4) points_parts = [ points[:n], points[n : 2 * n], points[2 * n : 3 * n], points[3 * n :], ] ## Creating the trees in advance trees = [] for i in range(4): left = points_parts[i] tree = STRtree(left) trees.append(tree) ## The function querying the trees in parallel def thread_func(idxs): i, j = idxs tree = trees[i] right = polygons_parts[j] return tree.query(right, predicate="contains") with ThreadPoolExecutor() as pool: list(pool.map(thread_func, itertools.product(range(4), range(4)))) shapely-2.0.3/shapely/tests/test_testing.py000066400000000000000000000060001456366510000210600ustar00rootroot00000000000000import numpy as np import pytest import shapely from shapely.testing import assert_geometries_equal from shapely.tests.common import ( all_types, empty, empty_line_string, empty_line_string_z, empty_point, empty_point_z, empty_polygon, line_string, line_string_nan, line_string_z, point, ) EMPTY_GEOMS = ( empty_point, empty_point_z, empty_line_string, empty_line_string_z, empty_polygon, empty, ) line_string_reversed = shapely.linestrings([(0, 0), (1, 0), (1, 1)][::-1]) PRE_GEOS_390 = pytest.mark.skipif( shapely.geos_version < (3, 9, 0), reason="2D and 3D empty geometries did not have dimensionality before GEOS 3.9", ) def make_array(left, right, use_array): if use_array in ("left", "both"): left = np.array([left] * 3, dtype=object) if use_array in ("right", "both"): right = np.array([right] * 3, dtype=object) return left, right @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) @pytest.mark.parametrize("geom", all_types + EMPTY_GEOMS) def test_assert_geometries_equal(geom, use_array): assert_geometries_equal(*make_array(geom, geom, use_array)) @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) @pytest.mark.parametrize( "geom1,geom2", [ (point, line_string), (line_string, line_string_z), (empty_point, empty_polygon), pytest.param(empty_point, empty_point_z, marks=PRE_GEOS_390), pytest.param(empty_line_string, empty_line_string_z, marks=PRE_GEOS_390), ], ) def test_assert_geometries_not_equal(geom1, geom2, use_array): with pytest.raises(AssertionError): assert_geometries_equal(*make_array(geom1, geom2, use_array)) @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) def test_assert_none_equal(use_array): assert_geometries_equal(*make_array(None, None, use_array)) @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) def test_assert_none_not_equal(use_array): with pytest.raises(AssertionError): assert_geometries_equal(*make_array(None, None, use_array), equal_none=False) @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) def test_assert_nan_equal(use_array): assert_geometries_equal(*make_array(line_string_nan, line_string_nan, use_array)) @pytest.mark.parametrize("use_array", ["none", "left", "right", "both"]) def test_assert_nan_not_equal(use_array): with pytest.raises(AssertionError): assert_geometries_equal( *make_array(line_string_nan, line_string_nan, use_array), equal_nan=False ) def test_normalize_true(): assert_geometries_equal(line_string_reversed, line_string, normalize=True) def test_normalize_default(): with pytest.raises(AssertionError): assert_geometries_equal(line_string_reversed, line_string) def test_normalize_false(): with pytest.raises(AssertionError): assert_geometries_equal(line_string_reversed, line_string, normalize=False) shapely-2.0.3/shapely/validation.py000066400000000000000000000026311456366510000173420ustar00rootroot00000000000000# TODO: allow for implementations using other than GEOS import shapely __all__ = ["explain_validity", "make_valid"] def explain_validity(ob): """ Explain the validity of the input geometry, if it is invalid. This will describe why the geometry is invalid, and might include a location if there is a self-intersection or a ring self-intersection. Parameters ---------- ob: Geometry A shapely geometry object Returns ------- str A string describing the reason the geometry is invalid. """ return shapely.is_valid_reason(ob) def make_valid(ob): """ Make the input geometry valid according to the GEOS MakeValid algorithm. If the input geometry is already valid, then it will be returned. If the geometry must be split into multiple parts of the same type to be made valid, then a multi-part geometry will be returned. If the geometry must be split into multiple parts of different types to be made valid, then a GeometryCollection will be returned. Parameters ---------- ob : Geometry A shapely geometry object which should be made valid. If the object is already valid, it will be returned as-is. Returns ------- Geometry The input geometry, made valid according to the GEOS MakeValid algorithm. """ if ob.is_valid: return ob return shapely.make_valid(ob) shapely-2.0.3/shapely/vectorized/000077500000000000000000000000001456366510000170125ustar00rootroot00000000000000shapely-2.0.3/shapely/vectorized/__init__.py000066400000000000000000000043111456366510000211220ustar00rootroot00000000000000"""Provides multi-point element-wise operations such as ``contains``.""" import numpy as np import shapely from shapely.prepared import PreparedGeometry def _construct_points(x, y): x, y = np.asanyarray(x), np.asanyarray(y) if x.shape != y.shape: raise ValueError("X and Y shapes must be equivalent.") if x.dtype != np.float64: x = x.astype(np.float64) if y.dtype != np.float64: y = y.astype(np.float64) return shapely.points(x, y) def contains(geometry, x, y): """ Vectorized (element-wise) version of `contains` which checks whether multiple points are contained by a single geometry. Parameters ---------- geometry : PreparedGeometry or subclass of BaseGeometry The geometry which is to be checked to see whether each point is contained within. The geometry will be "prepared" if it is not already a PreparedGeometry instance. x : array The x coordinates of the points to check. y : array The y coordinates of the points to check. Returns ------- Mask of points contained by the given `geometry`. """ if isinstance(geometry, PreparedGeometry): geometry = geometry.context shapely.prepare(geometry) return shapely.contains_xy(geometry, x, y) def touches(geometry, x, y): """ Vectorized (element-wise) version of `touches` which checks whether multiple points touch the exterior of a single geometry. Parameters ---------- geometry : PreparedGeometry or subclass of BaseGeometry The geometry which is to be checked to see whether each point is contained within. The geometry will be "prepared" if it is not already a PreparedGeometry instance. x : array The x coordinates of the points to check. y : array The y coordinates of the points to check. Returns ------- Mask of points which touch the exterior of the given `geometry`. """ if isinstance(geometry, PreparedGeometry): geometry = geometry.context # Touches(geom, point) == Intersects(Boundary(geom), point) boundary = geometry.boundary shapely.prepare(boundary) return shapely.intersects_xy(boundary, x, y) shapely-2.0.3/shapely/wkb.py000066400000000000000000000035701456366510000157760ustar00rootroot00000000000000"""Load/dump geometries using the well-known binary (WKB) format. Also provides pickle-like convenience functions. """ import shapely def loads(data, hex=False): """Load a geometry from a WKB byte string, or hex-encoded string if ``hex=True``. Raises ------ GEOSException, UnicodeDecodeError If ``data`` contains an invalid geometry. """ return shapely.from_wkb(data) def load(fp, hex=False): """Load a geometry from an open file. Raises ------ GEOSException, UnicodeDecodeError If the given file contains an invalid geometry. """ data = fp.read() return loads(data, hex=hex) def dumps(ob, hex=False, srid=None, **kw): """Dump a WKB representation of a geometry to a byte string, or a hex-encoded string if ``hex=True``. Parameters ---------- ob : geometry The geometry to export to well-known binary (WKB) representation hex : bool If true, export the WKB as a hexadecimal string. The default is to return a binary string/bytes object. srid : int Spatial reference system ID to include in the output. The default value means no SRID is included. **kw : kwargs, optional Keyword output options passed to :func:`~shapely.to_wkb`. """ if srid is not None: # clone the object and set the SRID before dumping ob = shapely.set_srid(ob, srid) kw["include_srid"] = True if "big_endian" in kw: # translate big_endian=True/False into byte_order=0/1 # but if not specified, keep the default of byte_order=-1 (native) big_endian = kw.pop("big_endian") byte_order = 0 if big_endian else 1 kw.update(byte_order=byte_order) return shapely.to_wkb(ob, hex=hex, **kw) def dump(ob, fp, hex=False, **kw): """Dump a geometry to an open file.""" fp.write(dumps(ob, hex=hex, **kw)) shapely-2.0.3/shapely/wkt.py000066400000000000000000000036331456366510000160200ustar00rootroot00000000000000"""Load/dump geometries using the well-known text (WKT) format Also provides pickle-like convenience functions. """ import shapely def loads(data): """ Load a geometry from a WKT string. Parameters ---------- data : str A WKT string Returns ------- Shapely geometry object """ return shapely.from_wkt(data) def load(fp): """ Load a geometry from an open file. Parameters ---------- fp : A file-like object which implements a `read` method. Returns ------- Shapely geometry object """ data = fp.read() return loads(data) def dumps(ob, trim=False, rounding_precision=-1, **kw): """ Dump a WKT representation of a geometry to a string. Parameters ---------- ob : A geometry object of any type to be dumped to WKT. trim : bool, default False Remove excess decimals from the WKT. rounding_precision : int Round output to the specified number of digits. Default behavior returns full precision. output_dimension : int, default 3 Force removal of dimensions above the one specified. Returns ------- input geometry as WKT string """ return shapely.to_wkt(ob, trim=trim, rounding_precision=rounding_precision, **kw) def dump(ob, fp, **settings): """ Dump a geometry to an open file. Parameters ---------- ob : A geometry object of any type to be dumped to WKT. fp : A file-like object which implements a `write` method. trim : bool, default False Remove excess decimals from the WKT. rounding_precision : int Round output to the specified number of digits. Default behavior returns full precision. output_dimension : int, default 3 Force removal of dimensions above the one specified. Returns ------- None """ fp.write(dumps(ob, **settings)) shapely-2.0.3/src/000077500000000000000000000000001456366510000137565ustar00rootroot00000000000000shapely-2.0.3/src/c_api.c000066400000000000000000000021201456366510000151700ustar00rootroot00000000000000/************************************************************************ * PyGEOS C API * * This file wraps internal PyGEOS C extension functions for use in other * extensions. These are specifically wrapped to enable dynamic loading * after Python initialization (see c_api.h and lib.c). * ***********************************************************************/ #define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #define PyGEOS_API_Module #include "c_api.h" #include "geos.h" #include "pygeom.h" extern PyObject* PyGEOS_CreateGeometry(GEOSGeometry *ptr, GEOSContextHandle_t ctx) { return GeometryObject_FromGEOS(ptr, ctx); } extern char PyGEOS_GetGEOSGeometry(PyObject *obj, GEOSGeometry **out) { return get_geom((GeometryObject*)obj, out); } extern GEOSCoordSequence* PyGEOS_CoordSeq_FromBuffer(GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure) { return coordseq_from_buffer(ctx, buf, size, dims, ring_closure, dims * 8, 8); } shapely-2.0.3/src/c_api.h000066400000000000000000000064121456366510000152050ustar00rootroot00000000000000/************************************************************************ * PyGEOS C API * * This file wraps internal PyGEOS C extension functions for use in other * extensions. These are specifically wrapped to enable dynamic loading * after Python initialization. * * Each function must provide 3 defines for use in the dynamic loading process: * NUM: the index in function pointer array * RETURN: the return type * PROTO: function prototype * * IMPORTANT: each function must provide 2 sets of defines below and * provide an entry into PyGEOS_API in lib.c module declaration block. * ***********************************************************************/ #ifndef _PYGEOS_API_H #define _PYGEOS_API_H #include #include "geos.h" #include "pygeom.h" /* PyObject* PyGEOS_CreateGeometry(GEOSGeometry *ptr, GEOSContextHandle_t ctx) */ #define PyGEOS_CreateGeometry_NUM 0 #define PyGEOS_CreateGeometry_RETURN PyObject * #define PyGEOS_CreateGeometry_PROTO (GEOSGeometry * ptr, GEOSContextHandle_t ctx) /* char PyGEOS_GetGEOSGeometry(GeometryObject *obj, GEOSGeometry **out) */ #define PyGEOS_GetGEOSGeometry_NUM 1 #define PyGEOS_GetGEOSGeometry_RETURN char #define PyGEOS_GetGEOSGeometry_PROTO (PyObject * obj, GEOSGeometry * *out) /* GEOSCoordSequence* PyGEOS_CoordSeq_FromBuffer(GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure)*/ #define PyGEOS_CoordSeq_FromBuffer_NUM 2 #define PyGEOS_CoordSeq_FromBuffer_RETURN GEOSCoordSequence* #define PyGEOS_CoordSeq_FromBuffer_PROTO (GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure) /* Total number of C API pointers */ #define PyGEOS_API_num_pointers 3 #ifdef PyGEOS_API_Module /* This section is used when compiling shapely.lib C extension. * Each API function needs to provide a corresponding *_PROTO here. */ extern PyGEOS_CreateGeometry_RETURN PyGEOS_CreateGeometry PyGEOS_CreateGeometry_PROTO; extern PyGEOS_GetGEOSGeometry_RETURN PyGEOS_GetGEOSGeometry PyGEOS_GetGEOSGeometry_PROTO; extern PyGEOS_CoordSeq_FromBuffer_RETURN PyGEOS_CoordSeq_FromBuffer PyGEOS_CoordSeq_FromBuffer_PROTO; #else /* This section is used in modules that use the PyGEOS C API * Each API function needs to provide the lookup into PyGEOS_API as a * define statement. */ static void **PyGEOS_API; #define PyGEOS_CreateGeometry \ (*(PyGEOS_CreateGeometry_RETURN(*) PyGEOS_CreateGeometry_PROTO)PyGEOS_API[PyGEOS_CreateGeometry_NUM]) #define PyGEOS_GetGEOSGeometry \ (*(PyGEOS_GetGEOSGeometry_RETURN(*) PyGEOS_GetGEOSGeometry_PROTO)PyGEOS_API[PyGEOS_GetGEOSGeometry_NUM]) #define PyGEOS_CoordSeq_FromBuffer \ (*(PyGEOS_CoordSeq_FromBuffer_RETURN(*) PyGEOS_CoordSeq_FromBuffer_PROTO)PyGEOS_API[PyGEOS_CoordSeq_FromBuffer_NUM]) /* Dynamically load C API from PyCapsule. * This MUST be called prior to using C API functions in other modules; otherwise * segfaults will occur when the PyGEOS C API functions are called. * * Returns 0 on success, -1 if error. * PyCapsule_Import will set an exception on error. */ static int import_shapely_c_api(void) { PyGEOS_API = (void **)PyCapsule_Import("shapely.lib._C_API", 0); return (PyGEOS_API == NULL) ? -1 : 0; } #endif #endif /* !defined(_PYGEOS_API_H) */ shapely-2.0.3/src/coords.c000066400000000000000000000451671456366510000154300ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #define NO_IMPORT_ARRAY #define PY_ARRAY_UNIQUE_SYMBOL shapely_ARRAY_API #include #include #include #include "geos.h" #include "pygeom.h" /* These function prototypes enables that these functions can call themselves */ static char get_coordinates(GEOSContextHandle_t, GEOSGeometry*, PyArrayObject*, npy_intp*, int); static void* set_coordinates(GEOSContextHandle_t, GEOSGeometry*, PyArrayObject*, npy_intp*, int); /* Get coordinates from a point, linestring or linearring and puts them at position `cursor` in the array `out`. Increases the cursor correspondingly. Returns 0 on error, 1 on success */ static char get_coordinates_simple(GEOSContextHandle_t ctx, GEOSGeometry* geom, int type, PyArrayObject* out, npy_intp* cursor, int include_z) { unsigned int n, dims; double *buf; const GEOSCoordSequence* seq; char is_empty; /* For points, directly check if they are empty. This is because empty points * internally have a coordinate sequence of length 1, but we did not count them in * the allocation of the coordinate array */ if (type == GEOS_POINT) { is_empty = GEOSisEmpty_r(ctx, geom); if (is_empty == 2) { return 0; } else if (is_empty == 1) { return 1; } } seq = GEOSGeom_getCoordSeq_r(ctx, geom); if (seq == NULL) { return 0; } if (GEOSCoordSeq_getSize_r(ctx, seq, &n) == 0) { return 0; } dims = (include_z) ? 3 : 2; buf = PyArray_GETPTR2(out, *cursor, 0); if (!coordseq_to_buffer(ctx, seq, buf, n, dims)) { return 0; } *cursor += n; return 1; } /* Get coordinates from a polygon by calling `get_coordinates_simple` on each ring (exterior ring, interior ring 1, ..., interior ring N). Returns 0 on error, 1 on success */ static char get_coordinates_polygon(GEOSContextHandle_t ctx, GEOSGeometry* geom, PyArrayObject* out, npy_intp* cursor, int include_z) { int n, i; GEOSGeometry* ring; ring = (GEOSGeometry*)GEOSGetExteriorRing_r(ctx, geom); if (ring == NULL) { return 0; } if (!get_coordinates_simple(ctx, ring, GEOS_LINEARRING, out, cursor, include_z)) { return 0; } n = GEOSGetNumInteriorRings_r(ctx, geom); if (n == -1) { return 0; } for (i = 0; i < n; i++) { ring = (GEOSGeometry*)GEOSGetInteriorRingN_r(ctx, geom, i); if (ring == NULL) { return 0; } if (!get_coordinates_simple(ctx, ring, GEOS_LINEARRING, out, cursor, include_z)) { return 0; } } return 1; } /* Get coordinates from a collection by calling `get_coordinates` on each subgeometry. The call to `get_coordinates` is a recursive call so that nested collections are allowed. Returns 0 on error, 1 on success */ static char get_coordinates_collection(GEOSContextHandle_t ctx, GEOSGeometry* geom, PyArrayObject* out, npy_intp* cursor, int include_z) { int n, i; GEOSGeometry* sub_geom; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return 0; } for (i = 0; i < n; i++) { sub_geom = (GEOSGeometry*)GEOSGetGeometryN_r(ctx, geom, i); if (sub_geom == NULL) { return 0; } if (!get_coordinates(ctx, sub_geom, out, cursor, include_z)) { return 0; } } return 1; } /* Gets coordinates from a geometry and puts them at position `cursor` in the array `out`. The value of the cursor is increased correspondingly. Returns 0 on error, 1 on success*/ static char get_coordinates(GEOSContextHandle_t ctx, GEOSGeometry* geom, PyArrayObject* out, npy_intp* cursor, int include_z) { int type = GEOSGeomTypeId_r(ctx, geom); if ((type == 0) || (type == 1) || (type == 2)) { return get_coordinates_simple(ctx, geom, type, out, cursor, include_z); } else if (type == 3) { return get_coordinates_polygon(ctx, geom, out, cursor, include_z); } else if ((type >= 4) && (type <= 7)) { return get_coordinates_collection(ctx, geom, out, cursor, include_z); } else { return 0; } } /* Returns a copy of the input geometry (point, linestring or linearring) with new coordinates set from position `cursor` in the array `out`. The value of the cursor is increased correspondingly. Returns NULL on error,*/ static void* set_coordinates_simple(GEOSContextHandle_t ctx, GEOSGeometry* geom, int type, PyArrayObject* coords, npy_intp* cursor, int include_z) { unsigned int n, i, dims; double *x, *y, *z; GEOSGeometry* ret; /* Special case for POINT EMPTY (Point coordinate list cannot be 0-length) */ if ((type == 0) && (GEOSisEmpty_r(ctx, geom) == 1)) { if (include_z) { // 2D or 3D, depending on the input return GEOSGeom_clone_r(ctx, geom); } else { // Always 2D return GEOSGeom_createEmptyPoint_r(ctx); } } /* Investigate the current (const) CoordSequence */ const GEOSCoordSequence* seq = GEOSGeom_getCoordSeq_r(ctx, geom); if (seq == NULL) { return NULL; } if (GEOSCoordSeq_getSize_r(ctx, seq, &n) == 0) { return NULL; } if (GEOSCoordSeq_getDimensions_r(ctx, seq, &dims) == 0) { return NULL; } /* If we have CoordSeq with z dim, but new coordinates only are 2D, * create new CoordSeq that is also only 2D */ if ((dims == 3) && !include_z) { dims = 2; } /* Create a new one to fill with the new coordinates */ GEOSCoordSequence* seq_new = GEOSCoordSeq_create_r(ctx, n, dims); if (seq_new == NULL) { return NULL; } for (i = 0; i < n; i++, *cursor += 1) { x = PyArray_GETPTR2(coords, *cursor, 0); y = PyArray_GETPTR2(coords, *cursor, 1); if (GEOSCoordSeq_setX_r(ctx, seq_new, i, *x) == 0) { goto fail; } if (GEOSCoordSeq_setY_r(ctx, seq_new, i, *y) == 0) { goto fail; } if (dims == 3) { z = PyArray_GETPTR2(coords, *cursor, 2); if (GEOSCoordSeq_setZ_r(ctx, seq_new, i, *z) == 0) { goto fail; } } } /* Construct a new geometry */ if (type == 0) { ret = GEOSGeom_createPoint_r(ctx, seq_new); } else if (type == 1) { ret = GEOSGeom_createLineString_r(ctx, seq_new); } else if (type == 2) { ret = GEOSGeom_createLinearRing_r(ctx, seq_new); } else { goto fail; } /* Do not destroy the seq_new if ret is NULL; will lead to segfaults */ return ret; fail: GEOSCoordSeq_destroy_r(ctx, seq_new); return NULL; } /* Returns a copy of the input polygon with new coordinates set by calling `set_coordinates_simple` on the linearrings that make the polygon. Returns NULL on error,*/ static void* set_coordinates_polygon(GEOSContextHandle_t ctx, GEOSGeometry* geom, PyArrayObject* coords, npy_intp* cursor, int include_z) { int i, n; const GEOSGeometry *shell, *hole; GEOSGeometry *new_shell, *new_hole, *result = NULL; GEOSGeometry** new_holes; n = GEOSGetNumInteriorRings_r(ctx, geom); if (n == -1) { return NULL; } /* create the exterior ring */ shell = GEOSGetExteriorRing_r(ctx, geom); if (shell == NULL) { return NULL; } new_shell = set_coordinates_simple(ctx, (GEOSGeometry*)shell, 2, coords, cursor, include_z); if (new_shell == NULL) { return NULL; } new_holes = malloc(sizeof(void*) * n); if (new_holes == NULL) { GEOSGeom_destroy_r(ctx, new_shell); return NULL; } for (i = 0; i < n; i++) { hole = GEOSGetInteriorRingN_r(ctx, geom, i); if (hole == NULL) { GEOSGeom_destroy_r(ctx, new_shell); destroy_geom_arr(ctx, new_holes, i - 1); goto finish; } new_hole = set_coordinates_simple(ctx, (GEOSGeometry*)hole, 2, coords, cursor, include_z); if (new_hole == NULL) { GEOSGeom_destroy_r(ctx, new_shell); destroy_geom_arr(ctx, new_holes, i - 1); goto finish; } new_holes[i] = new_hole; } result = GEOSGeom_createPolygon_r(ctx, new_shell, new_holes, n); finish: if (new_holes != NULL) { free(new_holes); } return result; } /* Returns a copy of the input collection with new coordinates set by calling `set_coordinates` on the constituent subgeometries. Returns NULL on error,*/ static void* set_coordinates_collection(GEOSContextHandle_t ctx, GEOSGeometry* geom, int type, PyArrayObject* coords, npy_intp* cursor, int include_z) { int i, n; const GEOSGeometry* sub_geom; GEOSGeometry *new_sub_geom, *result = NULL; GEOSGeometry** geoms; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return NULL; } geoms = malloc(sizeof(void*) * n); if (geoms == NULL) { return NULL; } for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); if (sub_geom == NULL) { destroy_geom_arr(ctx, geoms, i - i); goto finish; } new_sub_geom = set_coordinates(ctx, (GEOSGeometry*)sub_geom, coords, cursor, include_z); if (new_sub_geom == NULL) { destroy_geom_arr(ctx, geoms, i - i); goto finish; } geoms[i] = new_sub_geom; } result = GEOSGeom_createCollection_r(ctx, type, geoms, n); finish: if (geoms != NULL) { free(geoms); } return result; } /* Returns a copy of the input geometry with new coordinates set from position `cursor` in the array `out`. The value of the cursor is increased correspondingly. Returns NULL on error,*/ static void* set_coordinates(GEOSContextHandle_t ctx, GEOSGeometry* geom, PyArrayObject* coords, npy_intp* cursor, int include_z) { int type = GEOSGeomTypeId_r(ctx, geom); if ((type == 0) || (type == 1) || (type == 2)) { return set_coordinates_simple(ctx, geom, type, coords, cursor, include_z); } else if (type == 3) { return set_coordinates_polygon(ctx, geom, coords, cursor, include_z); } else if ((type >= 4) && (type <= 7)) { return set_coordinates_collection(ctx, geom, type, coords, cursor, include_z); } else { return NULL; } } /* Count the total number of coordinate pairs in an array of Geometry objects */ npy_intp CountCoords(PyArrayObject* arr) { NpyIter* iter; NpyIter_IterNextFunc* iternext; char** dataptr; int ret; npy_intp result = 0; GeometryObject* obj; GEOSGeometry* geom; /* Handle zero-sized arrays specially */ if (PyArray_SIZE(arr) == 0) { return 0; } /* We use the Numpy iterator C-API here. The iterator exposes an "iternext" function which updates a "dataptr" see also: https://docs.scipy.org/doc/numpy/reference/c-api.iterator.html */ iter = NpyIter_New(arr, NPY_ITER_READONLY | NPY_ITER_REFS_OK, NPY_KEEPORDER, NPY_NO_CASTING, NULL); if (iter == NULL) { return -1; } iternext = NpyIter_GetIterNext(iter, NULL); if (iternext == NULL) { NpyIter_Deallocate(iter); return -1; } dataptr = NpyIter_GetDataPtrArray(iter); GEOS_INIT; do { /* get the geometry */ obj = *(GeometryObject**)dataptr[0]; if (!get_geom(obj, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; result = -1; goto finish; } /* skip incase obj was None */ if (geom == NULL) { continue; } /* count coordinates */ ret = GEOSGetNumCoordinates_r(ctx, geom); if (ret < 0) { errstate = PGERR_GEOS_EXCEPTION; result = -1; goto finish; } result += ret; } while (iternext(iter)); finish: GEOS_FINISH; NpyIter_Deallocate(iter); return result; } PyObject* GetCoords(PyArrayObject* arr, int include_z, int return_index) { npy_intp coord_dim; NpyIter* iter; NpyIter_IterNextFunc* iternext; char** dataptr; npy_intp cursor, i, geom_i; GeometryObject* obj; GEOSGeometry* geom; PyArrayObject* index = NULL; /* create a coordinate array with the appropriate dimensions */ npy_intp size = CountCoords(arr); if (size == -1) { return NULL; } if (include_z) { coord_dim = 3; } else { coord_dim = 2; } npy_intp dims[2] = {size, coord_dim}; PyArrayObject* result = (PyArrayObject*)PyArray_SimpleNew(2, dims, NPY_DOUBLE); if (result == NULL) { return NULL; } if (return_index) { npy_intp dims_ind[1] = {size}; index = (PyArrayObject*)PyArray_SimpleNew(1, dims_ind, NPY_INTP); if (index == NULL) { Py_DECREF(result); return NULL; } } /* Handle zero-sized arrays specially */ if (size == 0) { if (return_index) { PyObject* result_tpl = PyTuple_New(2); PyTuple_SET_ITEM(result_tpl, 0, (PyObject*)result); PyTuple_SET_ITEM(result_tpl, 1, (PyObject*)index); return result_tpl; } else { return (PyObject*)result; } } /* We use the Numpy iterator C-API here. The iterator exposes an "iternext" function which updates a "dataptr" see also: https://docs.scipy.org/doc/numpy/reference/c-api.iterator.html */ iter = NpyIter_New(arr, NPY_ITER_READONLY | NPY_ITER_REFS_OK, NPY_CORDER, NPY_NO_CASTING, NULL); if (iter == NULL) { Py_DECREF(result); Py_XDECREF(index); return NULL; } iternext = NpyIter_GetIterNext(iter, NULL); if (iternext == NULL) { NpyIter_Deallocate(iter); Py_DECREF(result); Py_XDECREF(index); return NULL; } dataptr = NpyIter_GetDataPtrArray(iter); GEOS_INIT; /* We work with a "cursor" that tells the get_coordinates function where to write the coordinate data into the output array "result" */ cursor = 0; geom_i = -1; do { /* get the geometry */ obj = *(GeometryObject**)dataptr[0]; geom_i++; if (!get_geom(obj, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } /* skip None values */ if (geom == NULL) { continue; } /* keep track of the current cursor in "i" to be able to loop from the first to the last coordinate belonging to this geometry later */ i = cursor; /* get the coordinates (updates "cursor") */ if (!get_coordinates(ctx, geom, result, &cursor, include_z)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (return_index) { /* loop from "i" to "cursor" */ for (; i < cursor; i++) { *(npy_intp*)PyArray_GETPTR1(index, i) = geom_i; } } } while (iternext(iter)); finish: GEOS_FINISH; NpyIter_Deallocate(iter); if (errstate != PGERR_SUCCESS) { Py_DECREF(result); Py_XDECREF(index); return NULL; } else if (return_index) { PyObject* result_tpl = PyTuple_New(2); PyTuple_SET_ITEM(result_tpl, 0, (PyObject*)result); PyTuple_SET_ITEM(result_tpl, 1, (PyObject*)index); return result_tpl; } else { return (PyObject*)result; } } PyObject* SetCoords(PyArrayObject* geoms, PyArrayObject* coords) { NpyIter* iter; NpyIter_IterNextFunc* iternext; char** dataptr; npy_intp cursor; npy_intp* coords_shape; int include_z; GeometryObject* obj; PyObject* new_obj; GEOSGeometry *geom, *new_geom; /* SetCoords acts implace: if the array is zero-sized, just return the same object */ if (PyArray_SIZE(geoms) == 0) { Py_INCREF((PyObject*)geoms); return (PyObject*)geoms; } coords_shape = PyArray_SHAPE(coords); include_z = (coords_shape[1] == 3); /* We use the Numpy iterator C-API here. The iterator exposes an "iternext" function which updates a "dataptr" see also: https://docs.scipy.org/doc/numpy/reference/c-api.iterator.html */ iter = NpyIter_New(geoms, NPY_ITER_READWRITE | NPY_ITER_REFS_OK, NPY_CORDER, NPY_NO_CASTING, NULL); if (iter == NULL) { return NULL; } iternext = NpyIter_GetIterNext(iter, NULL); if (iternext == NULL) { NpyIter_Deallocate(iter); return NULL; } dataptr = NpyIter_GetDataPtrArray(iter); GEOS_INIT; /* We work with a "cursor" that tells the set_coordinates function where to read the coordinate data from the coordinate array "coords" */ cursor = 0; do { /* get the geometry */ obj = *(GeometryObject**)dataptr[0]; if (!get_geom(obj, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } /* skip None values */ if (geom == NULL) { continue; } /* create a new geometry with coordinates from "coords" array */ new_geom = set_coordinates(ctx, geom, coords, &cursor, include_z); if (new_geom == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } /* pack into a GeometryObject and set it to the geometry array */ new_obj = GeometryObject_FromGEOS(new_geom, ctx); Py_XDECREF(obj); *(PyObject**)dataptr[0] = new_obj; } while (iternext(iter)); finish: GEOS_FINISH; NpyIter_Deallocate(iter); if (errstate == PGERR_SUCCESS) { Py_INCREF((PyObject*)geoms); return (PyObject*)geoms; } else { return NULL; } } PyObject* PyCountCoords(PyObject* self, PyObject* args) { PyObject* arr; npy_intp ret; if (!PyArg_ParseTuple(args, "O", &arr)) { return NULL; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } if (!PyArray_ISOBJECT((PyArrayObject*)arr)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } ret = CountCoords((PyArrayObject*)arr); if (ret == -1) { return NULL; } return PyLong_FromSsize_t(ret); } PyObject* PyGetCoords(PyObject* self, PyObject* args) { PyObject* arr; int include_z; int return_index; if (!PyArg_ParseTuple(args, "Opp", &arr, &include_z, &return_index)) { return NULL; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } if (!PyArray_ISOBJECT((PyArrayObject*)arr)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } return GetCoords((PyArrayObject*)arr, include_z, return_index); } PyObject* PySetCoords(PyObject* self, PyObject* args) { PyObject* geoms; PyObject* coords; if (!PyArg_ParseTuple(args, "OO", &geoms, &coords)) { return NULL; } if ((!PyArray_Check(geoms)) || (!PyArray_Check(coords))) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } if (!PyArray_ISOBJECT((PyArrayObject*)geoms)) { PyErr_SetString(PyExc_TypeError, "Geometry array should be of object dtype"); return NULL; } if ((PyArray_TYPE((PyArrayObject*)coords)) != NPY_DOUBLE) { PyErr_SetString(PyExc_TypeError, "Coordinate array should be of float64 dtype"); return NULL; } if ((PyArray_NDIM((PyArrayObject*)coords)) != 2) { PyErr_SetString(PyExc_ValueError, "Coordinate array should be 2-dimensional"); return NULL; } geoms = SetCoords((PyArrayObject*)geoms, (PyArrayObject*)coords); if (geoms == Py_None) { return NULL; } return geoms; } shapely-2.0.3/src/coords.h000066400000000000000000000004361456366510000154230ustar00rootroot00000000000000#ifndef _PYGEOSCOORDS_H #define _PYGEOSCOORDS_H #include #include "geos.h" extern PyObject* PyCountCoords(PyObject* self, PyObject* args); extern PyObject* PyGetCoords(PyObject* self, PyObject* args); extern PyObject* PySetCoords(PyObject* self, PyObject* args); #endif shapely-2.0.3/src/fast_loop_macros.h000066400000000000000000000113571456366510000174700ustar00rootroot00000000000000/** * Copied from numpy/src/comp/umath/fast_loop_macros.h * * Macros to help build fast ufunc inner loops. * * These expect to have access to the arguments of a typical ufunc loop, * * char **args * npy_intp *dimensions * npy_intp *steps */ /** (ip1) -> () */ #define NO_OUTPUT_LOOP\ char *ip1 = args[0];\ npy_intp is1 = steps[0];\ npy_intp n = dimensions[0];\ npy_intp i;\ for(i = 0; i < n; i++, ip1 += is1) /** (ip1) -> (op1) */ #define UNARY_LOOP \ char *ip1 = args[0], *op1 = args[1]; \ npy_intp is1 = steps[0], os1 = steps[1]; \ npy_intp n = dimensions[0]; \ npy_intp i; \ for (i = 0; i < n; i++, ip1 += is1, op1 += os1) /** (ip1, ip2) -> (op1) */ #define BINARY_LOOP \ char *ip1 = args[0], *ip2 = args[1], *op1 = args[2]; \ npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2]; \ npy_intp n = dimensions[0]; \ npy_intp i; \ for (i = 0; i < n; i++, ip1 += is1, ip2 += is2, op1 += os1) /** (ip1, ip2, ip3) -> (op1) */ #define TERNARY_LOOP \ char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *op1 = args[3]; \ npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], os1 = steps[3]; \ npy_intp n = dimensions[0]; \ npy_intp i; \ for (i = 0; i < n; i++, ip1 += is1, ip2 += is2, ip3 += is3, op1 += os1) /** (ip1, ip2, ip3, ip4) -> (op1) */ #define QUATERNARY_LOOP \ char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *op1 = args[4]; \ npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], \ os1 = steps[4]; \ npy_intp n = dimensions[0]; \ npy_intp i; \ for (i = 0; i < n; i++, ip1 += is1, ip2 += is2, ip3 += is3, ip4 += is4, op1 += os1) /** (ip1, cp1) -> (op1) */ #define SINGLE_COREDIM_LOOP_OUTER \ char *ip1 = args[0], *op1 = args[1], *cp1; \ npy_intp is1 = steps[0], os1 = steps[1], cs1 = steps[2]; \ npy_intp n = dimensions[0], n_c1 = dimensions[1]; \ npy_intp i, i_c1; \ for (i = 0; i < n; i++, ip1 += is1, op1 += os1) #define SINGLE_COREDIM_LOOP_INNER \ cp1 = ip1; \ for (i_c1 = 0; i_c1 < n_c1; i_c1++, cp1 += cs1) /** (ip1, cp1) -> (op1, op2, op3, op4) */ #define SINGLE_COREDIM_LOOP_OUTER_NOUT4 \ char *ip1 = args[0], *op1 = args[1], *op2 = args[2], *op3 = args[3], *op4 = args[4], \ *cp1; \ npy_intp is1 = steps[0], os1 = steps[1], os2 = steps[2], os3 = steps[3], \ os4 = steps[4], cs1 = steps[5]; \ npy_intp n = dimensions[0], n_c1 = dimensions[1]; \ npy_intp i, i_c1; \ for (i = 0; i < n; i++, ip1 += is1, op1 += os1, op2 += os2, op3 += os3, op4 += os4) /** (ip1, cp1, cp2) -> (op1) */ #define DOUBLE_COREDIM_LOOP_OUTER \ char *ip1 = args[0], *op1 = args[1], *cp1, *cp2; \ npy_intp is1 = steps[0], os1 = steps[1], cs1 = steps[2], cs2 = steps[3]; \ npy_intp n = dimensions[0], n_c1 = dimensions[1], n_c2 = dimensions[2]; \ npy_intp i, i_c1, i_c2; \ for (i = 0; i < n; i++, ip1 += is1, op1 += os1) #define DOUBLE_COREDIM_LOOP_INNER_1 \ cp1 = ip1; \ for (i_c1 = 0; i_c1 < n_c1; i_c1++, cp1 += cs1) #define DOUBLE_COREDIM_LOOP_INNER_2 \ cp2 = cp1; \ for (i_c2 = 0; i_c2 < n_c2; i_c2++, cp2 += cs2) /** (ip1, ip2, cp1) -> (op1) */ #define BINARY_SINGLE_COREDIM_LOOP_OUTER \ char *ip1 = args[0], *ip2 = args[1], *op1 = args[2], *cp1; \ npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2], cs1 = steps[3]; \ npy_intp n = dimensions[0], n_c1 = dimensions[1]; \ npy_intp i, i_c1; \ for (i = 0; i < n; i++, ip1 += is1, ip2 += is2, op1 += os1) #define BINARY_SINGLE_COREDIM_LOOP_INNER for (i_c1 = 0; i_c1 < n_c1; i_c1++, cp1 += cs1) shapely-2.0.3/src/geos.c000066400000000000000000000667361456366510000151010ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include "geos.h" #include #include #include #include /* This initializes a globally accessible GEOS Context, only to be used when holding the GIL */ void* geos_context[1] = {NULL}; /* This initializes a globally accessible GEOSException object */ PyObject* geos_exception[1] = {NULL}; int init_geos(PyObject* m) { PyObject* base_class = PyErr_NewException("shapely.errors.ShapelyError", NULL, NULL); PyModule_AddObject(m, "ShapelyError", base_class); geos_exception[0] = PyErr_NewException("shapely.errors.GEOSException", base_class, NULL); PyModule_AddObject(m, "GEOSException", geos_exception[0]); void* context_handle = GEOS_init_r(); // TODO: the error handling is not yet set up for the global context (it is right now // only used where error handling is not used) // GEOSContext_setErrorMessageHandler_r(context_handle, geos_error_handler, last_error); geos_context[0] = context_handle; return 0; } void destroy_geom_arr(void* context, GEOSGeometry** array, int length) { int i; for (i = 0; i < length; i++) { if (array[i] != NULL) { GEOSGeom_destroy_r(context, array[i]); } } } /* These functions are used to workaround two GEOS issues (in WKB writer for * GEOS < 3.9, in WKT writer for GEOS < 3.9 and in GeoJSON writer for GEOS 3.10.0): * - POINT EMPTY was not handled correctly (we do it ourselves) * - MULTIPOINT (EMPTY) resulted in segfault (we check for it and raise) */ /* Returns 1 if a multipoint has an empty point, 0 otherwise, 2 on error. */ char multipoint_has_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int n, i; char is_empty; const GEOSGeometry* sub_geom; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return 2; } for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); if (sub_geom == NULL) { return 2; } is_empty = GEOSisEmpty_r(ctx, sub_geom); if (is_empty != 0) { // If empty encountered, or on exception, return: return is_empty; } } return 0; } /* Returns 1 if geometry is an empty point, 0 otherwise, 2 on error. */ char is_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int geom_type; geom_type = GEOSGeomTypeId_r(ctx, geom); if (geom_type == GEOS_POINT) { return GEOSisEmpty_r(ctx, geom); } else if (geom_type == -1) { return 2; // GEOS exception } else { return 0; // No empty point } } /* Returns 1 if a geometrycollection has an empty point, 0 otherwise, 2 on error. Checks recursively (geometrycollections may contain multipoints / geometrycollections) */ char geometrycollection_has_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int n, i; char has_empty; const GEOSGeometry* sub_geom; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return 2; } for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); if (sub_geom == NULL) { return 2; } has_empty = has_point_empty(ctx, (GEOSGeometry*)sub_geom); if (has_empty != 0) { // If empty encountered, or on exception, return: return has_empty; } } return 0; } /* Returns 1 if geometry is / has an empty point, 0 otherwise, 2 on error. */ char has_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int geom_type; geom_type = GEOSGeomTypeId_r(ctx, geom); if (geom_type == GEOS_POINT) { return GEOSisEmpty_r(ctx, geom); } else if (geom_type == GEOS_MULTIPOINT) { return multipoint_has_point_empty(ctx, geom); } else if (geom_type == GEOS_GEOMETRYCOLLECTION) { return geometrycollection_has_point_empty(ctx, geom); } else if (geom_type == -1) { return 2; // GEOS exception } else { return 0; // No empty point } } /* Creates a POINT (nan, nan[, nan)] from a POINT EMPTY template Returns NULL on error */ GEOSGeometry* point_empty_to_nan(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int j, ndim; GEOSCoordSequence* coord_seq; GEOSGeometry* result; ndim = GEOSGeom_getCoordinateDimension_r(ctx, geom); if (ndim == 0) { return NULL; } coord_seq = GEOSCoordSeq_create_r(ctx, 1, ndim); if (coord_seq == NULL) { return NULL; } for (j = 0; j < ndim; j++) { if (!GEOSCoordSeq_setOrdinate_r(ctx, coord_seq, 0, j, Py_NAN)) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } } result = GEOSGeom_createPoint_r(ctx, coord_seq); if (result == NULL) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } GEOSSetSRID_r(ctx, result, GEOSGetSRID_r(ctx, geom)); return result; } /* Creates a new multipoint, replacing empty points with POINT (nan, nan[, nan)] Returns NULL on error */ GEOSGeometry* multipoint_empty_to_nan(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int n, i; GEOSGeometry* result; const GEOSGeometry* sub_geom; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return NULL; } GEOSGeometry** geoms = malloc(sizeof(void*) * n); for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); if (GEOSisEmpty_r(ctx, sub_geom)) { geoms[i] = point_empty_to_nan(ctx, (GEOSGeometry*)sub_geom); } else { geoms[i] = GEOSGeom_clone_r(ctx, (GEOSGeometry*)sub_geom); } // If the function errored: cleanup and return if (geoms[i] == NULL) { destroy_geom_arr(ctx, geoms, i); free(geoms); return NULL; } } result = GEOSGeom_createCollection_r(ctx, GEOS_MULTIPOINT, geoms, n); // If the function errored: cleanup and return if (result == NULL) { destroy_geom_arr(ctx, geoms, i); free(geoms); return NULL; } free(geoms); GEOSSetSRID_r(ctx, result, GEOSGetSRID_r(ctx, geom)); return result; } /* Creates a new geometrycollection, replacing all empty points with POINT (nan, nan[, nan)] Returns NULL on error */ GEOSGeometry* geometrycollection_empty_to_nan(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int n, i; GEOSGeometry* result = NULL; const GEOSGeometry* sub_geom; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return NULL; } GEOSGeometry** geoms = malloc(sizeof(void*) * n); for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); geoms[i] = point_empty_to_nan_all_geoms(ctx, (GEOSGeometry*)sub_geom); // If the function errored: cleanup and return if (geoms[i] == NULL) { goto finish; } } result = GEOSGeom_createCollection_r(ctx, GEOS_GEOMETRYCOLLECTION, geoms, n); finish: // If the function errored: cleanup, else set SRID if (result == NULL) { destroy_geom_arr(ctx, geoms, i); } else { GEOSSetSRID_r(ctx, result, GEOSGetSRID_r(ctx, geom)); } free(geoms); return result; } /* Creates a new geometry, replacing empty points with POINT (nan, nan[, nan)] Returns NULL on error. */ GEOSGeometry* point_empty_to_nan_all_geoms(GEOSContextHandle_t ctx, GEOSGeometry* geom) { int geom_type; GEOSGeometry* result; geom_type = GEOSGeomTypeId_r(ctx, geom); if (geom_type == -1) { result = NULL; } else if (is_point_empty(ctx, geom)) { result = point_empty_to_nan(ctx, geom); } else if (geom_type == GEOS_MULTIPOINT) { result = multipoint_empty_to_nan(ctx, geom); } else if (geom_type == GEOS_GEOMETRYCOLLECTION) { result = geometrycollection_empty_to_nan(ctx, geom); } else { result = GEOSGeom_clone_r(ctx, geom); } GEOSSetSRID_r(ctx, result, GEOSGetSRID_r(ctx, geom)); return result; } /* Checks whether the geometry is a multipoint with an empty point in it * * According to https://github.com/libgeos/geos/issues/305, this check is not * necessary for GEOS 3.7.3, 3.8.2, or 3.9. When these versions are out, we * should add version conditionals and test. * * The return value is one of: * - PGERR_SUCCESS * - PGERR_MULTIPOINT_WITH_POINT_EMPTY * - PGERR_GEOS_EXCEPTION */ char check_to_wkt_compatible(GEOSContextHandle_t ctx, GEOSGeometry* geom) { char geom_type, is_empty; geom_type = GEOSGeomTypeId_r(ctx, geom); if (geom_type == -1) { return PGERR_GEOS_EXCEPTION; } if (geom_type != GEOS_MULTIPOINT) { return PGERR_SUCCESS; } is_empty = multipoint_has_point_empty(ctx, geom); if (is_empty == 0) { return PGERR_SUCCESS; } else if (is_empty == 1) { return PGERR_MULTIPOINT_WITH_POINT_EMPTY; } else { return PGERR_GEOS_EXCEPTION; } } #if GEOS_SINCE_3_9_0 /* Checks whether the geometry is a 3D empty geometry and, if so, create the WKT string * * GEOS 3.9.* is able to distiguish 2D and 3D simple geometries (non-collections). But the * but the WKT serialization never writes a 3D empty geometry. This function fixes that. * It only makes sense to use this for GEOS versions >= 3.9. * * Pending GEOS ticket: https://trac.osgeo.org/geos/ticket/1129 * * If the geometry is a 3D empty, then the (char**) wkt argument is filled with the * correct WKT string. Else, wkt becomes NULL and the GEOS WKT writer should be used. * * The return value is one of: * - PGERR_SUCCESS * - PGERR_GEOS_EXCEPTION */ char wkt_empty_3d_geometry(GEOSContextHandle_t ctx, GEOSGeometry* geom, char** wkt) { char is_empty; int geom_type; is_empty = GEOSisEmpty_r(ctx, geom); if (is_empty == 2) { return PGERR_GEOS_EXCEPTION; } else if (is_empty == 0) { *wkt = NULL; return PGERR_SUCCESS; } if (GEOSGeom_getCoordinateDimension_r(ctx, geom) == 2) { *wkt = NULL; return PGERR_SUCCESS; } geom_type = GEOSGeomTypeId_r(ctx, geom); switch (geom_type) { case GEOS_POINT: *wkt = "POINT Z EMPTY"; return PGERR_SUCCESS; case GEOS_LINESTRING: *wkt = "LINESTRING Z EMPTY"; break; case GEOS_LINEARRING: *wkt = "LINEARRING Z EMPTY"; break; case GEOS_POLYGON: *wkt = "POLYGON Z EMPTY"; break; // Note: Empty collections cannot be 3D in GEOS. // We do include the options in case of future support. case GEOS_MULTIPOINT: *wkt = "MULTIPOINT Z EMPTY"; break; case GEOS_MULTILINESTRING: *wkt = "MULTILINESTRING Z EMPTY"; break; case GEOS_MULTIPOLYGON: *wkt = "MULTIPOLYGON Z EMPTY"; break; case GEOS_GEOMETRYCOLLECTION: *wkt = "GEOMETRYCOLLECTION Z EMPTY"; break; default: return PGERR_GEOS_EXCEPTION; } return PGERR_SUCCESS; } #endif // GEOS_SINCE_3_9_0 /* GEOSInterpolate_r and GEOSInterpolateNormalized_r segfault on empty * geometries and also on collections with the first geometry empty. * * This function returns: * - PGERR_GEOMETRY_TYPE on non-linear geometries * - PGERR_EMPTY_GEOMETRY on empty linear geometries * - PGERR_EXCEPTIONS on GEOS exceptions * - PGERR_SUCCESS on a non-empty and linear geometry * * Note that GEOS 3.8 fixed this situation for empty LINESTRING/LINEARRING, * but it still segfaults on other empty geometries. */ char geos_interpolate_checker(GEOSContextHandle_t ctx, GEOSGeometry* geom) { char type; char is_empty; const GEOSGeometry* sub_geom; // Check if the geometry is linear type = GEOSGeomTypeId_r(ctx, geom); if (type == -1) { return PGERR_GEOS_EXCEPTION; } else if ((type == GEOS_POINT) || (type == GEOS_POLYGON) || (type == GEOS_MULTIPOINT) || (type == GEOS_MULTIPOLYGON)) { return PGERR_GEOMETRY_TYPE; } // Check if the geometry is empty is_empty = GEOSisEmpty_r(ctx, geom); if (is_empty == 1) { return PGERR_EMPTY_GEOMETRY; } else if (is_empty == 2) { return PGERR_GEOS_EXCEPTION; } // For collections: also check the type and emptyness of the first geometry if ((type == GEOS_MULTILINESTRING) || (type == GEOS_GEOMETRYCOLLECTION)) { sub_geom = GEOSGetGeometryN_r(ctx, geom, 0); if (sub_geom == NULL) { return PGERR_GEOS_EXCEPTION; // GEOSException } type = GEOSGeomTypeId_r(ctx, sub_geom); if (type == -1) { return PGERR_GEOS_EXCEPTION; } else if ((type != GEOS_LINESTRING) && (type != GEOS_LINEARRING)) { return PGERR_GEOMETRY_TYPE; } is_empty = GEOSisEmpty_r(ctx, sub_geom); if (is_empty == 1) { return PGERR_EMPTY_GEOMETRY; } else if (is_empty == 2) { return PGERR_GEOS_EXCEPTION; } } return PGERR_SUCCESS; } /* Define the GEOS error handler. See GEOS_INIT / GEOS_FINISH macros in geos.h*/ void geos_error_handler(const char* message, void* userdata) { snprintf(userdata, 1024, "%s", message); } /* Extract bounds from geometry. * * Bounds coordinates will be set to NPY_NAN if geom is NULL, empty, or does not have an * envelope. * * Parameters * ---------- * ctx: GEOS context handle * geom: pointer to GEOSGeometry; can be NULL * xmin: pointer to xmin value * ymin: pointer to ymin value * xmax: pointer to xmax value * ymax: pointer to ymax value * * Must be called from within a GEOS_INIT_THREADS / GEOS_FINISH_THREADS * or GEOS_INIT / GEOS_FINISH block. * * Returns * ------- * 1 on success; 0 on error */ int get_bounds(GEOSContextHandle_t ctx, GEOSGeometry* geom, double* xmin, double* ymin, double* xmax, double* ymax) { int retval = 1; if (geom == NULL || GEOSisEmpty_r(ctx, geom)) { *xmin = *ymin = *xmax = *ymax = NPY_NAN; return 1; } #if GEOS_SINCE_3_7_0 // use min / max coordinates if (!(GEOSGeom_getXMin_r(ctx, geom, xmin) && GEOSGeom_getYMin_r(ctx, geom, ymin) && GEOSGeom_getXMax_r(ctx, geom, xmax) && GEOSGeom_getYMax_r(ctx, geom, ymax))) { return 0; } #else // extract coordinates from envelope GEOSGeometry* envelope = NULL; const GEOSGeometry* ring = NULL; const GEOSCoordSequence* coord_seq = NULL; int size; /* construct the envelope */ envelope = GEOSEnvelope_r(ctx, geom); if (envelope == NULL) { return 0; } size = GEOSGetNumCoordinates_r(ctx, envelope); /* get the bbox depending on the number of coordinates in the envelope */ if (size == 0) { /* Envelope is empty */ *xmin = *ymin = *xmax = *ymax = NPY_NAN; } else if (size == 1) { /* Envelope is a point */ if (!GEOSGeomGetX_r(ctx, envelope, xmin)) { retval = 0; goto finish; } if (!GEOSGeomGetY_r(ctx, envelope, ymin)) { retval = 0; goto finish; } *xmax = *xmin; *ymax = *ymin; } else if (size == 5) { /* Envelope is a box */ ring = GEOSGetExteriorRing_r(ctx, envelope); if (ring == NULL) { retval = 0; goto finish; } coord_seq = GEOSGeom_getCoordSeq_r(ctx, ring); if (coord_seq == NULL) { retval = 0; goto finish; } if (!GEOSCoordSeq_getX_r(ctx, coord_seq, 0, xmin)) { retval = 0; goto finish; } if (!GEOSCoordSeq_getY_r(ctx, coord_seq, 0, ymin)) { retval = 0; goto finish; } if (!GEOSCoordSeq_getX_r(ctx, coord_seq, 2, xmax)) { retval = 0; goto finish; } if (!GEOSCoordSeq_getY_r(ctx, coord_seq, 2, ymax)) { retval = 0; goto finish; } } finish: if (envelope != NULL) { GEOSGeom_destroy_r(ctx, envelope); } #endif return retval; } /* Create a Polygon from bounding coordinates. * * Must be called from within a GEOS_INIT_THREADS / GEOS_FINISH_THREADS * or GEOS_INIT / GEOS_FINISH block. * * Parameters * ---------- * ctx: GEOS context handle * xmin: minimum X value * ymin: minimum Y value * xmax: maximum X value * ymax: maximum Y value * ccw: if 1, box will be created in counterclockwise direction from bottom right; * otherwise will be created in clockwise direction from bottom left. * * Returns * ------- * GEOSGeometry* on success (owned by caller) or * NULL on failure or NPY_NAN coordinates */ GEOSGeometry* create_box(GEOSContextHandle_t ctx, double xmin, double ymin, double xmax, double ymax, char ccw) { if (npy_isnan(xmin) || npy_isnan(ymin) || npy_isnan(xmax) || npy_isnan(ymax)) { return NULL; } GEOSCoordSequence* coords = NULL; GEOSGeometry* geom = NULL; GEOSGeometry* ring = NULL; // Construct coordinate sequence and set vertices coords = GEOSCoordSeq_create_r(ctx, 5, 2); if (coords == NULL) { return NULL; } if (ccw) { // Start from bottom right (xmax, ymin) to match shapely if (!(GEOSCoordSeq_setX_r(ctx, coords, 0, xmax) && GEOSCoordSeq_setY_r(ctx, coords, 0, ymin) && GEOSCoordSeq_setX_r(ctx, coords, 1, xmax) && GEOSCoordSeq_setY_r(ctx, coords, 1, ymax) && GEOSCoordSeq_setX_r(ctx, coords, 2, xmin) && GEOSCoordSeq_setY_r(ctx, coords, 2, ymax) && GEOSCoordSeq_setX_r(ctx, coords, 3, xmin) && GEOSCoordSeq_setY_r(ctx, coords, 3, ymin) && GEOSCoordSeq_setX_r(ctx, coords, 4, xmax) && GEOSCoordSeq_setY_r(ctx, coords, 4, ymin))) { if (coords != NULL) { GEOSCoordSeq_destroy_r(ctx, coords); } return NULL; } } else { // Start from bottom left (min, ymin) to match shapely if (!(GEOSCoordSeq_setX_r(ctx, coords, 0, xmin) && GEOSCoordSeq_setY_r(ctx, coords, 0, ymin) && GEOSCoordSeq_setX_r(ctx, coords, 1, xmin) && GEOSCoordSeq_setY_r(ctx, coords, 1, ymax) && GEOSCoordSeq_setX_r(ctx, coords, 2, xmax) && GEOSCoordSeq_setY_r(ctx, coords, 2, ymax) && GEOSCoordSeq_setX_r(ctx, coords, 3, xmax) && GEOSCoordSeq_setY_r(ctx, coords, 3, ymin) && GEOSCoordSeq_setX_r(ctx, coords, 4, xmin) && GEOSCoordSeq_setY_r(ctx, coords, 4, ymin))) { if (coords != NULL) { GEOSCoordSeq_destroy_r(ctx, coords); } return NULL; } } // Construct linear ring then use to construct polygon // Note: coords are owned by ring; if ring fails to construct, it will // automatically clean up the coords ring = GEOSGeom_createLinearRing_r(ctx, coords); if (ring == NULL) { return NULL; } // Note: ring is owned by polygon; if polygon fails to construct, it will // automatically clean up the ring geom = GEOSGeom_createPolygon_r(ctx, ring, NULL, 0); if (geom == NULL) { return NULL; } return geom; } /* Create a Point from x and y coordinates. * * Must be called from within a GEOS_INIT_THREADS / GEOS_FINISH_THREADS * or GEOS_INIT / GEOS_FINISH block. * * Helper function for quickly creating a Point for older GEOS versions. * * Parameters * ---------- * ctx: GEOS context handle * x: X value * y: Y value * * Returns * ------- * GEOSGeometry* on success (owned by caller) or NULL on failure */ GEOSGeometry* create_point(GEOSContextHandle_t ctx, double x, double y) { #if GEOS_SINCE_3_8_0 return GEOSGeom_createPointFromXY_r(ctx, x, y); #else GEOSCoordSequence* coord_seq = NULL; GEOSGeometry* geom = NULL; coord_seq = GEOSCoordSeq_create_r(ctx, 1, 2); if (coord_seq == NULL) { return NULL; } if (!GEOSCoordSeq_setX_r(ctx, coord_seq, 0, x)) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } if (!GEOSCoordSeq_setY_r(ctx, coord_seq, 0, y)) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } geom = GEOSGeom_createPoint_r(ctx, coord_seq); if (geom == NULL) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } return geom; #endif } /* Create a 3D empty Point * * Works around a limitation of the GEOS C API by constructing the point * from its WKT representation (POINT Z EMPTY). * * Returns * ------- * GEOSGeometry* on success (owned by caller) or NULL on failure */ GEOSGeometry* PyGEOS_create3DEmptyPoint(GEOSContextHandle_t ctx) { const char* wkt = "POINT Z EMPTY"; GEOSGeometry* geom; GEOSWKTReader* reader; reader = GEOSWKTReader_create_r(ctx); if (reader == NULL) { return NULL; } geom = GEOSWKTReader_read_r(ctx, reader, wkt); GEOSWKTReader_destroy_r(ctx, reader); return geom; } /* Force the coordinate dimensionality (2D / 3D) of any geometry * * Parameters * ---------- * ctx: GEOS context handle * geom: geometry * dims: dimensions to force (2 or 3) * z: Z coordinate (ignored if dims==2) * * Returns * ------- * GEOSGeometry* on success (owned by caller) or NULL on failure */ GEOSGeometry* force_dims(GEOSContextHandle_t, GEOSGeometry*, unsigned int, double); GEOSGeometry* force_dims_simple(GEOSContextHandle_t ctx, GEOSGeometry* geom, int type, unsigned int dims, double z) { unsigned int actual_dims, n, i, j; double coord; const GEOSCoordSequence* seq = GEOSGeom_getCoordSeq_r(ctx, geom); /* Special case for POINT EMPTY (Point coordinate list cannot be 0-length) */ if ((type == 0) && (GEOSisEmpty_r(ctx, geom) == 1)) { if (dims == 2) { return GEOSGeom_createEmptyPoint_r(ctx); } else if (dims == 3) { return PyGEOS_create3DEmptyPoint(ctx); } else { return NULL; } } /* Investigate the coordinate sequence, return when already of correct dimensionality */ if (GEOSCoordSeq_getDimensions_r(ctx, seq, &actual_dims) == 0) { return NULL; } if (actual_dims == dims) { return GEOSGeom_clone_r(ctx, geom); } if (GEOSCoordSeq_getSize_r(ctx, seq, &n) == 0) { return NULL; } /* Create a new one to fill with the new coordinates */ GEOSCoordSequence* seq_new = GEOSCoordSeq_create_r(ctx, n, dims); if (seq_new == NULL) { return NULL; } for (i = 0; i < n; i++) { for (j = 0; j < 2; j++) { if (!GEOSCoordSeq_getOrdinate_r(ctx, seq, i, j, &coord)) { GEOSCoordSeq_destroy_r(ctx, seq_new); return NULL; } if (!GEOSCoordSeq_setOrdinate_r(ctx, seq_new, i, j, coord)) { GEOSCoordSeq_destroy_r(ctx, seq_new); return NULL; } } if (dims == 3) { if (!GEOSCoordSeq_setZ_r(ctx, seq_new, i, z)) { GEOSCoordSeq_destroy_r(ctx, seq_new); return NULL; } } } /* Construct a new geometry */ if (type == 0) { return GEOSGeom_createPoint_r(ctx, seq_new); } else if (type == 1) { return GEOSGeom_createLineString_r(ctx, seq_new); } else if (type == 2) { return GEOSGeom_createLinearRing_r(ctx, seq_new); } else { return NULL; } } GEOSGeometry* force_dims_polygon(GEOSContextHandle_t ctx, GEOSGeometry* geom, unsigned int dims, double z) { int i, n; const GEOSGeometry *shell, *hole; GEOSGeometry *new_shell, *new_hole, *result = NULL; GEOSGeometry** new_holes; n = GEOSGetNumInteriorRings_r(ctx, geom); if (n == -1) { return NULL; } /* create the exterior ring */ shell = GEOSGetExteriorRing_r(ctx, geom); if (shell == NULL) { return NULL; } new_shell = force_dims_simple(ctx, (GEOSGeometry*)shell, 2, dims, z); if (new_shell == NULL) { return NULL; } new_holes = malloc(sizeof(void*) * n); if (new_holes == NULL) { GEOSGeom_destroy_r(ctx, new_shell); return NULL; } for (i = 0; i < n; i++) { hole = GEOSGetInteriorRingN_r(ctx, geom, i); if (hole == NULL) { GEOSGeom_destroy_r(ctx, new_shell); destroy_geom_arr(ctx, new_holes, i - 1); goto finish; } new_hole = force_dims_simple(ctx, (GEOSGeometry*)hole, 2, dims, z); if (hole == NULL) { GEOSGeom_destroy_r(ctx, new_shell); destroy_geom_arr(ctx, new_holes, i - 1); goto finish; } new_holes[i] = new_hole; } result = GEOSGeom_createPolygon_r(ctx, new_shell, new_holes, n); finish: if (new_holes != NULL) { free(new_holes); } return result; } GEOSGeometry* force_dims_collection(GEOSContextHandle_t ctx, GEOSGeometry* geom, int type, unsigned int dims, double z) { int i, n; const GEOSGeometry* sub_geom; GEOSGeometry *new_sub_geom, *result = NULL; GEOSGeometry** geoms; n = GEOSGetNumGeometries_r(ctx, geom); if (n == -1) { return NULL; } geoms = malloc(sizeof(void*) * n); if (geoms == NULL) { return NULL; } for (i = 0; i < n; i++) { sub_geom = GEOSGetGeometryN_r(ctx, geom, i); if (sub_geom == NULL) { destroy_geom_arr(ctx, geoms, i - i); goto finish; } new_sub_geom = force_dims(ctx, (GEOSGeometry*)sub_geom, dims, z); if (new_sub_geom == NULL) { destroy_geom_arr(ctx, geoms, i - i); goto finish; } geoms[i] = new_sub_geom; } result = GEOSGeom_createCollection_r(ctx, type, geoms, n); finish: if (geoms != NULL) { free(geoms); } return result; } GEOSGeometry* force_dims(GEOSContextHandle_t ctx, GEOSGeometry* geom, unsigned int dims, double z) { int type = GEOSGeomTypeId_r(ctx, geom); if ((type == 0) || (type == 1) || (type == 2)) { return force_dims_simple(ctx, geom, type, dims, z); } else if (type == 3) { return force_dims_polygon(ctx, geom, dims, z); } else if ((type >= 4) && (type <= 7)) { return force_dims_collection(ctx, geom, type, dims, z); } else { return NULL; } } GEOSGeometry* PyGEOSForce2D(GEOSContextHandle_t ctx, GEOSGeometry* geom) { return force_dims(ctx, geom, 2, 0.0); } GEOSGeometry* PyGEOSForce3D(GEOSContextHandle_t ctx, GEOSGeometry* geom, double z) { return force_dims(ctx, geom, 3, z); } /* Create a GEOSCoordSequence from an array * * Note: this function assumes that the dimension of the buffer is already * checked before calling this function, so the buffer and the dims argument * is only 2D or 3D. */ GEOSCoordSequence* coordseq_from_buffer(GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure, npy_intp cs1, npy_intp cs2) { GEOSCoordSequence* coord_seq; char *cp1, *cp2; unsigned int i, j; double first_coord; #if GEOS_SINCE_3_10_0 if (!ring_closure) { if ((cs1 == dims * 8) && (cs2 == 8)) { /* C-contiguous memory */ int hasZ = dims == 3; coord_seq = GEOSCoordSeq_copyFromBuffer_r(ctx, buf, size, hasZ, 0); return coord_seq; } else if ((cs1 == 8) && (cs2 == size * 8)) { /* F-contiguous memory (note: this for the subset, so we don't necessarily end up here if the full array is F-contiguous) */ const double* x = buf; const double* y = (double*)((char*)buf + cs2); const double* z = (dims == 3) ? (double*)((char*)buf + 2 * cs2) : NULL; coord_seq = GEOSCoordSeq_copyFromArrays_r(ctx, x, y, z, NULL, size); return coord_seq; } } #endif coord_seq = GEOSCoordSeq_create_r(ctx, size + ring_closure, dims); if (coord_seq == NULL) { return NULL; } cp1 = (char*)buf; for (i = 0; i < size; i++, cp1 += cs1) { cp2 = cp1; for (j = 0; j < dims; j++, cp2 += cs2) { if (!GEOSCoordSeq_setOrdinate_r(ctx, coord_seq, i, j, *(double*)cp2)) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } } } /* add the closing coordinate if necessary */ if (ring_closure) { for (j = 0; j < dims; j++){ first_coord = *(double*)((char*)buf + j * cs2); if (!GEOSCoordSeq_setOrdinate_r(ctx, coord_seq, size, j, first_coord)) { GEOSCoordSeq_destroy_r(ctx, coord_seq); return NULL; } } } return coord_seq; } /* Copy coordinates of a GEOSCoordSequence to an array * * Note: this function assumes that the buffer is from a C-contiguous array, * and that the dimension of the buffer is only 2D or 3D. * * Returns 0 on error, 1 on success. */ int coordseq_to_buffer(GEOSContextHandle_t ctx, const GEOSCoordSequence* coord_seq, double* buf, unsigned int size, unsigned int dims) { #if GEOS_SINCE_3_10_0 int hasZ = dims == 3; return GEOSCoordSeq_copyToBuffer_r(ctx, coord_seq, buf, hasZ, 0); #else char *cp1, *cp2; unsigned int i, j; cp1 = (char*)buf; for (i = 0; i < size; i++, cp1 += 8 * dims) { cp2 = cp1; for (j = 0; j < dims; j++, cp2 += 8) { if (!GEOSCoordSeq_getOrdinate_r(ctx, coord_seq, i, j, (double*)cp2)) { return 0; } } } return 1; #endif } shapely-2.0.3/src/geos.h000066400000000000000000000232211456366510000150640ustar00rootroot00000000000000#ifndef _GEOS_H #define _GEOS_H #include #include /* To avoid accidental use of non reentrant GEOS API. */ #ifndef GEOS_USE_ONLY_R_API #define GEOS_USE_ONLY_R_API #endif // wrap geos.h import to silence geos gcc warnings #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstrict-prototypes" #include #pragma GCC diagnostic pop /* Macros to setup GEOS Context and error handlers Typical PyGEOS pattern in a function that uses GEOS: // GEOS_INIT will do three things: // 1. Make the GEOS context available in the variable ``ctx`` // 2. Initialize a variable ``errstate`` to PGERR_SUCCESS. // 3. Set up GEOS error and warning buffers, respectively ``last_error`` and ``last_warning`` GEOS_INIT; // or GEOS_INIT_THREADS if you use no CPython calls // call a GEOS function using the context 'ctx' result = SomeGEOSFunc(ctx, ...); // handle an error state if (result == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } // GEOS_FINISH will remove the GEOS context and set python errors in case // errstate != PGERR_SUCCESS. finish: GEOS_FINISH; // or GEOS_FINISH_THREADS if you use no CPython calls */ // Define the error states enum { PGERR_SUCCESS, PGERR_NOT_A_GEOMETRY, PGERR_GEOS_EXCEPTION, PGERR_NO_MALLOC, PGERR_GEOMETRY_TYPE, PGERR_MULTIPOINT_WITH_POINT_EMPTY, PGERR_EMPTY_GEOMETRY, PGERR_GEOJSON_EMPTY_POINT, PGERR_LINEARRING_NCOORDS, PGWARN_INVALID_WKB, // raise the GEOS WKB error as a warning instead of exception PGWARN_INVALID_WKT, // raise the GEOS WKT error as a warning instead of exception PGWARN_INVALID_GEOJSON, PGERR_PYSIGNAL }; // Define how the states are handled by CPython #define GEOS_HANDLE_ERR \ if (last_warning[0] != 0) { \ PyErr_WarnEx(PyExc_Warning, last_warning, 0); \ } \ switch (errstate) { \ case PGERR_SUCCESS: \ break; \ case PGERR_NOT_A_GEOMETRY: \ PyErr_SetString(PyExc_TypeError, \ "One of the arguments is of incorrect type. Please provide only " \ "Geometry objects."); \ break; \ case PGERR_GEOS_EXCEPTION: \ PyErr_SetString(geos_exception[0], last_error); \ break; \ case PGERR_NO_MALLOC: \ PyErr_SetString(PyExc_MemoryError, "Could not allocate memory"); \ break; \ case PGERR_GEOMETRY_TYPE: \ PyErr_SetString(PyExc_TypeError, \ "One of the Geometry inputs is of incorrect geometry type."); \ break; \ case PGERR_MULTIPOINT_WITH_POINT_EMPTY: \ PyErr_SetString(PyExc_ValueError, \ "WKT output of multipoints with an empty point is unsupported on " \ "this version of GEOS."); \ break; \ case PGERR_EMPTY_GEOMETRY: \ PyErr_SetString(PyExc_ValueError, "One of the Geometry inputs is empty."); \ break; \ case PGERR_GEOJSON_EMPTY_POINT: \ PyErr_SetString(PyExc_ValueError, \ "GeoJSON output of empty points is currently unsupported."); \ break; \ case PGERR_LINEARRING_NCOORDS: \ PyErr_SetString(PyExc_ValueError, \ "A linearring requires at least 4 coordinates."); \ break; \ case PGWARN_INVALID_WKB: \ PyErr_WarnFormat(PyExc_Warning, 0, \ "Invalid WKB: geometry is returned as None. %s", last_error); \ break; \ case PGWARN_INVALID_WKT: \ PyErr_WarnFormat(PyExc_Warning, 0, \ "Invalid WKT: geometry is returned as None. %s", last_error); \ break; \ case PGWARN_INVALID_GEOJSON: \ PyErr_WarnFormat(PyExc_Warning, 0, \ "Invalid GeoJSON: geometry is returned as None. %s", last_error); \ break; \ case PGERR_PYSIGNAL: \ break; \ default: \ PyErr_Format(PyExc_RuntimeError, \ "Pygeos ufunc returned with unknown error state code %d.", errstate); \ break; \ } // Define initialization / finalization macros #define _GEOS_INIT_DEF \ char errstate = PGERR_SUCCESS; \ char last_error[1024] = ""; \ char last_warning[1024] = ""; \ GEOSContextHandle_t ctx #define _GEOS_INIT \ ctx = GEOS_init_r(); \ GEOSContext_setErrorMessageHandler_r(ctx, geos_error_handler, last_error) #define GEOS_INIT \ _GEOS_INIT_DEF; \ _GEOS_INIT #define GEOS_INIT_THREADS \ _GEOS_INIT_DEF; \ Py_BEGIN_ALLOW_THREADS _GEOS_INIT #define GEOS_FINISH \ GEOS_finish_r(ctx); \ GEOS_HANDLE_ERR #define GEOS_FINISH_THREADS \ GEOS_finish_r(ctx); \ Py_END_ALLOW_THREADS GEOS_HANDLE_ERR #define GEOS_SINCE_3_5_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 5)) #define GEOS_SINCE_3_6_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 6)) #define GEOS_SINCE_3_7_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 7)) #define GEOS_SINCE_3_8_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 8)) #define GEOS_SINCE_3_9_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 9)) #define GEOS_SINCE_3_10_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 10)) #define GEOS_SINCE_3_11_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 11)) #define GEOS_SINCE_3_12_0 ((GEOS_VERSION_MAJOR >= 3) && (GEOS_VERSION_MINOR >= 12)) extern void* geos_context[1]; extern PyObject* geos_exception[1]; extern void geos_error_handler(const char* message, void* userdata); extern void destroy_geom_arr(void* context, GEOSGeometry** array, int length); extern char has_point_empty(GEOSContextHandle_t ctx, GEOSGeometry* geom); extern GEOSGeometry* point_empty_to_nan_all_geoms(GEOSContextHandle_t ctx, GEOSGeometry* geom); extern char check_to_wkt_compatible(GEOSContextHandle_t ctx, GEOSGeometry* geom); #if GEOS_SINCE_3_9_0 extern char wkt_empty_3d_geometry(GEOSContextHandle_t ctx, GEOSGeometry* geom, char** wkt); #endif // GEOS_SINCE_3_9_0 extern char geos_interpolate_checker(GEOSContextHandle_t ctx, GEOSGeometry* geom); extern int init_geos(PyObject* m); int get_bounds(GEOSContextHandle_t ctx, GEOSGeometry* geom, double* xmin, double* ymin, double* xmax, double* ymax); GEOSGeometry* create_box(GEOSContextHandle_t ctx, double xmin, double ymin, double xmax, double ymax, char ccw); GEOSGeometry* create_point(GEOSContextHandle_t ctx, double x, double y); GEOSGeometry* PyGEOSForce2D(GEOSContextHandle_t ctx, GEOSGeometry* geom); GEOSGeometry* PyGEOSForce3D(GEOSContextHandle_t ctx, GEOSGeometry* geom, double z); GEOSCoordSequence* coordseq_from_buffer(GEOSContextHandle_t ctx, const double* buf, unsigned int size, unsigned int dims, char ring_closure, npy_intp cs1, npy_intp cs2); extern int coordseq_to_buffer(GEOSContextHandle_t ctx, const GEOSCoordSequence* coord_seq, double* buf, unsigned int size, unsigned int dims); #endif // _GEOS_H shapely-2.0.3/src/kvec.h000066400000000000000000000056061456366510000150660ustar00rootroot00000000000000/* This contains a standalone resizable array implementation in C */ /* The MIT License Copyright (c) 2008, by Attractive Chaos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* An example: #include "kvec.h" int main() { kvec_t(int) array; kv_init(array); kv_push(int, array, 10); // append kv_a(int, array, 20) = 5; // dynamic kv_A(array, 20) = 4; // static kv_destroy(array); return 0; } */ /* 2008-09-22 (0.1.0): * The initial version. */ #ifndef AC_KVEC_H #define AC_KVEC_H #include #define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x)) #define kvec_t(type) struct { size_t n, m; type *a; } #define kv_init(v) ((v).n = (v).m = 0, (v).a = 0) #define kv_destroy(v) free((v).a) #define kv_A(v, i) ((v).a[(i)]) #define kv_pop(v) ((v).a[--(v).n]) #define kv_size(v) ((v).n) #define kv_max(v) ((v).m) #define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m)) #define kv_copy(type, v1, v0) do { \ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \ (v1).n = (v0).n; \ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \ } while (0) \ #define kv_push(type, v, x) do { \ if ((v).n == (v).m) { \ (v).m = (v).m? (v).m<<1 : 2; \ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \ } \ (v).a[(v).n++] = (x); \ } while (0) #define kv_pushp(type, v) (((v).n == (v).m)? \ ((v).m = ((v).m? (v).m<<1 : 2), \ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ : 0), ((v).a + ((v).n++)) #define kv_a(type, v, i) (((v).m <= (size_t)(i)? \ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \ : 0), (v).a[(i)]) #endif shapely-2.0.3/src/lib.c000066400000000000000000000064041456366510000146740ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #define PyGEOS_API_Module #define PY_ARRAY_UNIQUE_SYMBOL shapely_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL shapely_UFUNC_API #include #include #include #include "c_api.h" #include "coords.h" #include "geos.h" #include "pygeom.h" #include "strtree.h" #include "ufuncs.h" /* This tells Python what methods this module has. */ static PyMethodDef GeosModule[] = { {"count_coordinates", PyCountCoords, METH_VARARGS, "Counts the total amount of coordinates in a array with geometry objects"}, {"get_coordinates", PyGetCoords, METH_VARARGS, "Gets the coordinates as an (N, 2) shaped ndarray of floats"}, {"set_coordinates", PySetCoords, METH_VARARGS, "Sets coordinates to a geometry array"}, {"_setup_signal_checks", PySetupSignalChecks, METH_VARARGS, "Sets the thread id and interval for signal checks"}, {NULL, NULL, 0, NULL}}; static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "lib", NULL, -1, GeosModule, NULL, NULL, NULL, NULL}; PyMODINIT_FUNC PyInit_lib(void) { PyObject *m, *d; static void* PyGEOS_API[PyGEOS_API_num_pointers]; PyObject* c_api_object; m = PyModule_Create(&moduledef); if (!m) { return NULL; } if (init_geos(m) < 0) { return NULL; }; if (init_geom_type(m) < 0) { return NULL; }; if (init_strtree_type(m) < 0) { return NULL; }; d = PyModule_GetDict(m); import_array(); import_umath(); /* GEOS_VERSION_PATCH may contain non-integer characters, e.g., 0beta1 add quotes using https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html then take the first digit */ #define Q(x) #x #define QUOTE(x) Q(x) #define GEOS_VERSION_PATCH_STR QUOTE(GEOS_VERSION_PATCH) int geos_version_patch_int = GEOS_VERSION_PATCH_STR[0] - '0'; /* export the GEOS versions as python tuple and string */ PyModule_AddObject(m, "geos_version", PyTuple_Pack(3, PyLong_FromLong((long)GEOS_VERSION_MAJOR), PyLong_FromLong((long)GEOS_VERSION_MINOR), PyLong_FromLong((long)geos_version_patch_int))); PyModule_AddObject(m, "geos_capi_version", PyTuple_Pack(3, PyLong_FromLong((long)GEOS_CAPI_VERSION_MAJOR), PyLong_FromLong((long)GEOS_CAPI_VERSION_MINOR), PyLong_FromLong((long)GEOS_CAPI_VERSION_PATCH))); PyModule_AddObject(m, "geos_version_string", PyUnicode_FromString(GEOS_VERSION)); PyModule_AddObject(m, "geos_capi_version_string", PyUnicode_FromString(GEOS_CAPI_VERSION)); if (init_ufuncs(m, d) < 0) { return NULL; }; /* Initialize the C API pointer array */ PyGEOS_API[PyGEOS_CreateGeometry_NUM] = (void*)PyGEOS_CreateGeometry; PyGEOS_API[PyGEOS_GetGEOSGeometry_NUM] = (void*)PyGEOS_GetGEOSGeometry; PyGEOS_API[PyGEOS_CoordSeq_FromBuffer_NUM] = (void*)PyGEOS_CoordSeq_FromBuffer; /* Create a Capsule containing the API pointer array's address */ c_api_object = PyCapsule_New((void*)PyGEOS_API, "shapely.lib._C_API", NULL); if (c_api_object != NULL) { PyModule_AddObject(m, "_C_API", c_api_object); } return m; } shapely-2.0.3/src/pygeom.c000066400000000000000000000273031456366510000154270ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include "pygeom.h" #include #include #include "geos.h" /* This initializes a global geometry type registry */ PyObject* geom_registry[1] = {NULL}; /* Initializes a new geometry object */ PyObject* GeometryObject_FromGEOS(GEOSGeometry* ptr, GEOSContextHandle_t ctx) { if (ptr == NULL) { Py_INCREF(Py_None); return Py_None; } int type_id = GEOSGeomTypeId_r(ctx, ptr); if (type_id == -1) { return NULL; } PyObject* type_obj = PyList_GET_ITEM(geom_registry[0], type_id); if (type_obj == NULL) { return NULL; } if (!PyType_Check(type_obj)) { PyErr_Format(PyExc_RuntimeError, "Invalid registry value"); return NULL; } PyTypeObject* type = (PyTypeObject*)type_obj; GeometryObject* self = (GeometryObject*)type->tp_alloc(type, 0); if (self == NULL) { return NULL; } else { self->ptr = ptr; self->ptr_prepared = NULL; self->weakreflist = (PyObject*)NULL; return (PyObject*)self; } } static void GeometryObject_dealloc(GeometryObject* self) { if (self->weakreflist != NULL) { PyObject_ClearWeakRefs((PyObject*)self); } if (self->ptr != NULL) { // not using GEOS_INIT, but using global context instead GEOSContextHandle_t ctx = geos_context[0]; GEOSGeom_destroy_r(ctx, self->ptr); if (self->ptr_prepared != NULL) { GEOSPreparedGeom_destroy_r(ctx, self->ptr_prepared); } } Py_TYPE(self)->tp_free((PyObject*)self); } static PyMemberDef GeometryObject_members[] = { {"_geom", T_PYSSIZET, offsetof(GeometryObject, ptr), READONLY, "pointer to GEOSGeometry"}, {"_geom_prepared", T_PYSSIZET, offsetof(GeometryObject, ptr_prepared), READONLY, "pointer to PreparedGEOSGeometry"}, {NULL} /* Sentinel */ }; static PyObject* GeometryObject_ToWKT(GeometryObject* obj) { char* wkt; PyObject* result; GEOSGeometry* geom = obj->ptr; if (geom == NULL) { Py_INCREF(Py_None); return Py_None; } GEOS_INIT; #if GEOS_SINCE_3_9_0 errstate = wkt_empty_3d_geometry(ctx, geom, &wkt); if (errstate != PGERR_SUCCESS) { goto finish; } if (wkt != NULL) { result = PyUnicode_FromString(wkt); goto finish; } #else // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY) errstate = check_to_wkt_compatible(ctx, geom); if (errstate != PGERR_SUCCESS) { goto finish; } #endif GEOSWKTWriter* writer = GEOSWKTWriter_create_r(ctx); if (writer == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } char trim = 1; int precision = 3; int dimension = 3; int use_old_3d = 0; GEOSWKTWriter_setRoundingPrecision_r(ctx, writer, precision); GEOSWKTWriter_setTrim_r(ctx, writer, trim); GEOSWKTWriter_setOutputDimension_r(ctx, writer, dimension); GEOSWKTWriter_setOld3D_r(ctx, writer, use_old_3d); // Check if the above functions caused a GEOS exception if (last_error[0] != 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } wkt = GEOSWKTWriter_write_r(ctx, writer, geom); result = PyUnicode_FromString(wkt); GEOSFree_r(ctx, wkt); GEOSWKTWriter_destroy_r(ctx, writer); finish: GEOS_FINISH; if (errstate == PGERR_SUCCESS) { return result; } else { return NULL; } } static PyObject* GeometryObject_ToWKB(GeometryObject* obj) { unsigned char* wkb = NULL; char has_empty = 0; size_t size; PyObject* result = NULL; GEOSGeometry* geom = NULL; GEOSWKBWriter* writer = NULL; if (obj->ptr == NULL) { Py_INCREF(Py_None); return Py_None; } GEOS_INIT; #if !GEOS_SINCE_3_9_0 // WKB Does not allow empty points in GEOS < 3.9. // We check for that and patch the POINT EMPTY if necessary has_empty = has_point_empty(ctx, obj->ptr); if (has_empty == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (has_empty == 1) { geom = point_empty_to_nan_all_geoms(ctx, obj->ptr); } else { geom = obj->ptr; } #else geom = obj->ptr; #endif // !GEOS_SINCE_3_9_0 /* Create the WKB writer */ writer = GEOSWKBWriter_create_r(ctx); if (writer == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } // Allow 3D output and include SRID GEOSWKBWriter_setOutputDimension_r(ctx, writer, 3); GEOSWKBWriter_setIncludeSRID_r(ctx, writer, 1); // Check if the above functions caused a GEOS exception if (last_error[0] != 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } wkb = GEOSWKBWriter_write_r(ctx, writer, geom, &size); if (wkb == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } result = PyBytes_FromStringAndSize((char*)wkb, size); finish: // Destroy the geom if it was patched (POINT EMPTY patch) if (has_empty && (geom != NULL)) { GEOSGeom_destroy_r(ctx, geom); } if (writer != NULL) { GEOSWKBWriter_destroy_r(ctx, writer); } if (wkb != NULL) { GEOSFree_r(ctx, wkb); } GEOS_FINISH; return result; } static PyObject* GeometryObject_repr(GeometryObject* self) { PyObject *result, *wkt, *truncated; wkt = GeometryObject_ToWKT(self); // we never want a repr() to fail; that can be very confusing if (wkt == NULL) { PyErr_Clear(); return PyUnicode_FromString(""); } // the total length is limited to 80 characters if (PyUnicode_GET_LENGTH(wkt) > 62) { truncated = PyUnicode_Substring(wkt, 0, 59); result = PyUnicode_FromFormat("", truncated); Py_XDECREF(truncated); } else { result = PyUnicode_FromFormat("", wkt); } Py_XDECREF(wkt); return result; } static PyObject* GeometryObject_str(GeometryObject* self) { return GeometryObject_ToWKT(self); } /* For lookups in sets / dicts. * Python should be told how to generate a hash from the Geometry object. */ static Py_hash_t GeometryObject_hash(GeometryObject* self) { PyObject* wkb = NULL; Py_hash_t x; if (self->ptr == NULL) { return -1; } // Transform to a WKB (PyBytes object) wkb = GeometryObject_ToWKB(self); if (wkb == NULL) { return -1; } // Use the python built-in method to hash the PyBytes object x = wkb->ob_type->tp_hash(wkb); if (x == -1) { x = -2; } else { x ^= 374761393UL; // to make the result distinct from the actual WKB hash // } Py_DECREF(wkb); return x; } static PyObject* GeometryObject_richcompare(GeometryObject* self, PyObject* other, int op) { PyObject* result = NULL; GEOS_INIT; if (Py_TYPE(self)->tp_richcompare != Py_TYPE(other)->tp_richcompare) { result = Py_NotImplemented; } else { GeometryObject* other_geom = (GeometryObject*)other; switch (op) { case Py_LT: result = Py_NotImplemented; break; case Py_LE: result = Py_NotImplemented; break; case Py_EQ: result = GEOSEqualsExact_r(ctx, self->ptr, other_geom->ptr, 0) ? Py_True : Py_False; break; case Py_NE: result = GEOSEqualsExact_r(ctx, self->ptr, other_geom->ptr, 0) ? Py_False : Py_True; break; case Py_GT: result = Py_NotImplemented; break; case Py_GE: result = Py_NotImplemented; break; } } GEOS_FINISH; Py_XINCREF(result); return result; } static PyObject* GeometryObject_SetState(PyObject* self, PyObject* value) { unsigned char* wkb = NULL; Py_ssize_t size; GEOSGeometry* geom = NULL; GEOSWKBReader* reader = NULL; PyErr_WarnFormat(PyExc_UserWarning, 0, "Unpickling a shapely <2.0 geometry object. Please save the pickle " "again; shapely 2.1 will not have this compatibility."); /* Cast the PyObject bytes to char */ if (!PyBytes_Check(value)) { PyErr_Format(PyExc_TypeError, "Expected bytes, found %s", value->ob_type->tp_name); return NULL; } size = PyBytes_Size(value); wkb = (unsigned char*)PyBytes_AsString(value); if (wkb == NULL) { return NULL; } PyObject* linearring_type_obj = PyList_GET_ITEM(geom_registry[0], 2); if (linearring_type_obj == NULL) { return NULL; } if (!PyType_Check(linearring_type_obj)) { PyErr_Format(PyExc_RuntimeError, "Invalid registry value"); return NULL; } PyTypeObject* linearring_type = (PyTypeObject*)linearring_type_obj; GEOS_INIT; reader = GEOSWKBReader_create_r(ctx); if (reader == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } geom = GEOSWKBReader_read_r(ctx, reader, wkb, size); if (geom == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (Py_TYPE(self) == linearring_type) { const GEOSCoordSequence* coord_seq = GEOSGeom_getCoordSeq_r(ctx, geom); if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } geom = GEOSGeom_createLinearRing_r(ctx, (GEOSCoordSequence*)coord_seq); if (geom == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } if (((GeometryObject*)self)->ptr != NULL) { GEOSGeom_destroy_r(ctx, ((GeometryObject*)self)->ptr); } ((GeometryObject*)self)->ptr = geom; finish: if (reader != NULL) { GEOSWKBReader_destroy_r(ctx, reader); } GEOS_FINISH; if (errstate == PGERR_SUCCESS) { Py_INCREF(Py_None); return Py_None; } return NULL; } static PyMethodDef GeometryObject_methods[] = { {"__setstate__", (PyCFunction)GeometryObject_SetState, METH_O, "For unpickling pre-shapely 2.0 pickles"}, {NULL} /* Sentinel */ }; PyTypeObject GeometryType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "shapely.lib.Geometry", .tp_doc = "Geometry type", .tp_basicsize = sizeof(GeometryObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_dealloc = (destructor)GeometryObject_dealloc, .tp_members = GeometryObject_members, .tp_methods = GeometryObject_methods, .tp_repr = (reprfunc)GeometryObject_repr, .tp_hash = (hashfunc)GeometryObject_hash, .tp_richcompare = (richcmpfunc)GeometryObject_richcompare, .tp_weaklistoffset = offsetof(GeometryObject, weakreflist), .tp_str = (reprfunc)GeometryObject_str, }; /* Check if type `a` is a subclass of type `b` (copied from cython generated code) */ int __Pyx_InBases(PyTypeObject* a, PyTypeObject* b) { while (a) { a = a->tp_base; if (a == b) return 1; } return b == &PyBaseObject_Type; } /* Get a GEOSGeometry pointer from a GeometryObject, or NULL if the input is Py_None. Returns 0 on error, 1 on success. */ char get_geom(GeometryObject* obj, GEOSGeometry** out) { // Numpy treats NULL the same as Py_None if ((obj == NULL) || ((PyObject*)obj == Py_None)) { *out = NULL; return 1; } PyTypeObject* type = ((PyObject*)obj)->ob_type; if ((type != &GeometryType) && !(__Pyx_InBases(type, &GeometryType))) { return 0; } else { *out = obj->ptr; return 1; } } /* Get a GEOSGeometry AND GEOSPreparedGeometry pointer from a GeometryObject, or NULL if the input is Py_None. Returns 0 on error, 1 on success. */ char get_geom_with_prepared(GeometryObject* obj, GEOSGeometry** out, GEOSPreparedGeometry** prep) { if (!get_geom(obj, out)) { // It is not a GeometryObject / None: Error return 0; } if (*out != NULL) { // Only if it is not None, fill the prepared geometry *prep = obj->ptr_prepared; } else { *prep = NULL; } return 1; } int init_geom_type(PyObject* m) { Py_ssize_t i; PyObject* type; if (PyType_Ready(&GeometryType) < 0) { return -1; } type = (PyObject*)&GeometryType; Py_INCREF(type); PyModule_AddObject(m, "Geometry", type); geom_registry[0] = PyList_New(8); for (i = 0; i < 8; i++) { Py_INCREF(type); PyList_SET_ITEM(geom_registry[0], i, type); } PyModule_AddObject(m, "registry", geom_registry[0]); return 0; } shapely-2.0.3/src/pygeom.h000066400000000000000000000012461456366510000154320ustar00rootroot00000000000000#ifndef _PYGEOM_H #define _PYGEOM_H #include #include "geos.h" typedef struct { PyObject_HEAD void* ptr; void* ptr_prepared; /* For weak references */ PyObject *weakreflist; } GeometryObject; extern PyTypeObject GeometryType; /* Initializes a new geometry object */ extern PyObject* GeometryObject_FromGEOS(GEOSGeometry* ptr, GEOSContextHandle_t ctx); /* Get a GEOSGeometry from a GeometryObject */ extern char get_geom(GeometryObject* obj, GEOSGeometry** out); extern char get_geom_with_prepared(GeometryObject* obj, GEOSGeometry** out, GEOSPreparedGeometry** prep); extern int init_geom_type(PyObject* m); #endif shapely-2.0.3/src/strtree.c000066400000000000000000001124761456366510000156250ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #include #include #define NO_IMPORT_ARRAY #define PY_ARRAY_UNIQUE_SYMBOL shapely_ARRAY_API #include #include #include #include "coords.h" #include "geos.h" #include "kvec.h" #include "pygeom.h" #include "strtree.h" #include "vector.h" /* GEOS function that takes a prepared geometry and a regular geometry * and returns bool value */ typedef char FuncGEOS_YpY_b(void* context, const GEOSPreparedGeometry* a, const GEOSGeometry* b); /* get predicate function based on ID. See strtree.py::BinaryPredicate for * lookup table of id to function name */ FuncGEOS_YpY_b* get_predicate_func(int predicate_id) { switch (predicate_id) { case 1: { // intersects return (FuncGEOS_YpY_b*)GEOSPreparedIntersects_r; } case 2: { // within return (FuncGEOS_YpY_b*)GEOSPreparedWithin_r; } case 3: { // contains return (FuncGEOS_YpY_b*)GEOSPreparedContains_r; } case 4: { // overlaps return (FuncGEOS_YpY_b*)GEOSPreparedOverlaps_r; } case 5: { // crosses return (FuncGEOS_YpY_b*)GEOSPreparedCrosses_r; } case 6: { // touches return (FuncGEOS_YpY_b*)GEOSPreparedTouches_r; } case 7: { // covers return (FuncGEOS_YpY_b*)GEOSPreparedCovers_r; } case 8: { // covered_by return (FuncGEOS_YpY_b*)GEOSPreparedCoveredBy_r; } case 9: { // contains_properly return (FuncGEOS_YpY_b*)GEOSPreparedContainsProperly_r; } default: { // unknown predicate PyErr_SetString(PyExc_ValueError, "Invalid query predicate"); return NULL; } } } static void STRtree_dealloc(STRtreeObject* self) { size_t i; // free the tree if (self->ptr != NULL) { GEOS_INIT; GEOSSTRtree_destroy_r(ctx, self->ptr); GEOS_FINISH; } // free the geometries for (i = 0; i < self->_geoms_size; i++) { Py_XDECREF(self->_geoms[i]); } free(self->_geoms); // free the PyObject Py_TYPE(self)->tp_free((PyObject*)self); } void dummy_query_callback(void* item, void* user_data) {} static PyObject* STRtree_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { int node_capacity; PyObject* arr; void *tree, *ptr; npy_intp n, i, counter = 0, count_indexed = 0; GEOSGeometry* geom; GeometryObject* obj; GeometryObject** _geoms; if (!PyArg_ParseTuple(args, "Oi", &arr, &node_capacity)) { return NULL; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } if (!PyArray_ISOBJECT((PyArrayObject*)arr)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } if (PyArray_NDIM((PyArrayObject*)arr) != 1) { PyErr_SetString(PyExc_TypeError, "Array should be one dimensional"); return NULL; } GEOS_INIT; tree = GEOSSTRtree_create_r(ctx, (size_t)node_capacity); if (tree == NULL) { errstate = PGERR_GEOS_EXCEPTION; return NULL; GEOS_FINISH; } n = PyArray_SIZE((PyArrayObject*)arr); _geoms = (GeometryObject**)malloc(n * sizeof(GeometryObject*)); for (i = 0; i < n; i++) { /* get the geometry */ ptr = PyArray_GETPTR1((PyArrayObject*)arr, i); obj = *(GeometryObject**)ptr; /* fail and cleanup incase obj was no geometry */ if (!get_geom(obj, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; GEOSSTRtree_destroy_r(ctx, tree); // free the geometries for (i = 0; i < counter; i++) { Py_XDECREF(_geoms[i]); } free(_geoms); GEOS_FINISH; return NULL; } /* If geometry is None or empty, do not add it to the tree or count. * Set it as NULL for the internal geometries used for predicate tests. */ if (geom == NULL || GEOSisEmpty_r(ctx, geom)) { _geoms[i] = NULL; } else { // NOTE: we must keep a reference to the GeometryObject added to the tree in order // to avoid segfaults later. See: https://github.com/pygeos/pygeos/pull/100. Py_INCREF(obj); _geoms[i] = obj; count_indexed++; // Store the address of this geometry within _geoms array as the item data in the // tree. This address is used to calculate the original index of the geometry in // the input array. // NOTE: the type of item data we store is GeometryObject**. GEOSSTRtree_insert_r(ctx, tree, geom, &(_geoms[i])); } counter++; } // A dummy query to trigger the build of the tree (only if the tree is not empty) if (count_indexed > 0) { GEOSGeometry* dummy = create_point(ctx, 0.0, 0.0); if (dummy == NULL) { GEOSSTRtree_destroy_r(ctx, tree); GEOS_FINISH; return NULL; } GEOSSTRtree_query_r(ctx, tree, dummy, dummy_query_callback, NULL); GEOSGeom_destroy_r(ctx, dummy); } STRtreeObject* self = (STRtreeObject*)type->tp_alloc(type, 0); if (self == NULL) { GEOSSTRtree_destroy_r(ctx, tree); GEOS_FINISH; return NULL; } GEOS_FINISH; self->ptr = tree; self->count = count_indexed; self->_geoms_size = n; self->_geoms = _geoms; return (PyObject*)self; } /* Callback called by strtree_query with item data of each intersecting geometry * and a dynamic vector to push that item onto. * * Item data is the address of that geometry within the tree geometries (_geoms) array. * * Parameters * ---------- * item: index of intersected geometry in the tree * * user_data: pointer to dynamic vector * */ void query_callback(void* item, void* user_data) { kv_push(GeometryObject**, *(tree_geom_vec_t*)user_data, item); } /* Evaluate the predicate function against a prepared version of geom * for each geometry in the tree specified by indexes in out_indexes. * out_indexes is updated in place with the indexes of the geometries in the * tree that meet the predicate. * * Parameters * ---------- * predicate_func: pointer to a prepared predicate function, e.g., * GEOSPreparedIntersects_r * * geom: input geometry to prepare and test against each geometry in the tree specified by * in_indexes. * * prepared_geom: input prepared geometry, only if previously created. If NULL, geom * will be prepared instead. * * in_geoms: pointer to dynamic vector of addresses in tree geometries (_geoms) that have * overlapping envelopes with envelope of input geometry. * * out_geoms: pointer to dynamic vector of addresses in tree geometries (_geoms) that meet * predicate function. * * count: pointer to an integer where the number of geometries that met the predicate will * be written. * * Returns PGERR_GEOS_EXCEPTION if an error was encountered or PGERR_SUCCESS otherwise * */ static char evaluate_predicate(void* context, FuncGEOS_YpY_b* predicate_func, GEOSGeometry* geom, GEOSPreparedGeometry* prepared_geom, tree_geom_vec_t* in_geoms, tree_geom_vec_t* out_geoms, npy_intp* count) { char errstate = PGERR_SUCCESS; GeometryObject* pg_geom; GeometryObject** pg_geom_loc; // address of geometry in tree geometries (_geoms) GEOSGeometry* target_geom; const GEOSPreparedGeometry* prepared_geom_tmp; npy_intp i, size; char predicate_result; if (prepared_geom == NULL) { // geom was not previously prepared, prepare it now prepared_geom_tmp = GEOSPrepare_r(context, geom); if (prepared_geom_tmp == NULL) { return PGERR_GEOS_EXCEPTION; } } else { // cast to const only needed until larger refactor of all geom pointers to const prepared_geom_tmp = (const GEOSPreparedGeometry*)prepared_geom; } size = kv_size(*in_geoms); *count = 0; for (i = 0; i < size; i++) { // get address of geometry in tree geometries, then use that to get associated // GEOS geometry pg_geom_loc = kv_A(*in_geoms, i); pg_geom = *pg_geom_loc; if (pg_geom == NULL) { continue; } get_geom(pg_geom, &target_geom); // keep the geometry if it passes the predicate predicate_result = predicate_func(context, prepared_geom_tmp, target_geom); if (predicate_result == 2) { // error evaluating predicate; break and cleanup prepared geometry below errstate = PGERR_GEOS_EXCEPTION; break; } else if (predicate_result == 1) { kv_push(GeometryObject**, *out_geoms, pg_geom_loc); (*count)++; } } if (prepared_geom == NULL) { // only if we created prepared_geom_tmp here, destroy it GEOSPreparedGeom_destroy_r(context, prepared_geom_tmp); prepared_geom_tmp = NULL; } return errstate; } /* Query the tree based on input geometries and predicate function. * The index of each geometry in the tree whose envelope intersects the * envelope of the input geometry is returned by default. * If predicate function is provided, only the index of those geometries that * satisfy the predicate function are returned. * Returns two arrays of equal length: first is indexes of the source geometries * and second is indexes of tree geometries that meet the above conditions. * * args must be: * - ndarray of shapely geometries * - predicate id (see strtree.py for list of ids) * * */ static PyObject* STRtree_query(STRtreeObject* self, PyObject* args) { PyObject* arr; PyArrayObject* pg_geoms; GeometryObject* pg_geom = NULL; int predicate_id = 0; // default no predicate GEOSGeometry* geom = NULL; GEOSPreparedGeometry* prepared_geom = NULL; index_vec_t src_indexes; // Indices of input geometries npy_intp i, j, n, size, geom_index; FuncGEOS_YpY_b* predicate_func = NULL; char* head_ptr = (char*)self->_geoms; PyArrayObject* result; // Addresses in tree geometries (_geoms) that match tree tree_geom_vec_t query_geoms; // Aggregated addresses in tree geometries (_geoms) that also meet predicate (if // present) tree_geom_vec_t target_geoms; if (self->ptr == NULL) { PyErr_SetString(PyExc_RuntimeError, "Tree is uninitialized"); return NULL; } if (!PyArg_ParseTuple(args, "Oi", &arr, &predicate_id)) { return NULL; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } pg_geoms = (PyArrayObject*)arr; if (!PyArray_ISOBJECT(pg_geoms)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } if (PyArray_NDIM(pg_geoms) != 1) { PyErr_SetString(PyExc_TypeError, "Array should be one dimensional"); return NULL; } if (predicate_id != 0) { predicate_func = get_predicate_func(predicate_id); if (predicate_func == NULL) { return NULL; } } n = PyArray_SIZE(pg_geoms); if (self->count == 0 || n == 0) { npy_intp dims[2] = {2, 0}; return PyArray_SimpleNew(2, dims, NPY_INTP); } kv_init(src_indexes); kv_init(target_geoms); GEOS_INIT_THREADS; for (i = 0; i < n; i++) { // get shapely geometry from input geometry array pg_geom = *(GeometryObject**)PyArray_GETPTR1(pg_geoms, i); if (!get_geom_with_prepared(pg_geom, &geom, &prepared_geom)) { errstate = PGERR_NOT_A_GEOMETRY; break; } if (geom == NULL || GEOSisEmpty_r(ctx, geom)) { continue; } kv_init(query_geoms); GEOSSTRtree_query_r(ctx, self->ptr, geom, query_callback, &query_geoms); if (kv_size(query_geoms) == 0) { // no target geoms in query window, skip this source geom kv_destroy(query_geoms); continue; } if (predicate_id == 0) { // no predicate, push results directly onto target_geoms size = kv_size(query_geoms); for (j = 0; j < size; j++) { // push index of source geometry onto src_indexes kv_push(npy_intp, src_indexes, i); // push geometry that matched tree onto target_geoms kv_push(GeometryObject**, target_geoms, kv_A(query_geoms, j)); } } else { // Tree geometries that meet the predicate are pushed onto target_geoms errstate = evaluate_predicate(ctx, predicate_func, geom, prepared_geom, &query_geoms, &target_geoms, &size); if (errstate != PGERR_SUCCESS) { kv_destroy(query_geoms); break; } for (j = 0; j < size; j++) { kv_push(npy_intp, src_indexes, i); } } kv_destroy(query_geoms); } GEOS_FINISH_THREADS; if (errstate != PGERR_SUCCESS) { kv_destroy(src_indexes); kv_destroy(target_geoms); return NULL; } size = kv_size(src_indexes); npy_intp dims[2] = {2, size}; // the following raises a compiler warning based on how the macro is defined // in numpy. There doesn't appear to be anything we can do to avoid it. result = (PyArrayObject*)PyArray_SimpleNew(2, dims, NPY_INTP); if (result == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); kv_destroy(src_indexes); kv_destroy(target_geoms); return NULL; } for (i = 0; i < size; i++) { // assign value into numpy arrays *(npy_intp*)PyArray_GETPTR2(result, 0, i) = kv_A(src_indexes, i); // Calculate index using offset of its address compared to head of _geoms geom_index = (npy_intp)(((char*)kv_A(target_geoms, i) - head_ptr) / sizeof(GeometryObject*)); *(npy_intp*)PyArray_GETPTR2(result, 1, i) = geom_index; } kv_destroy(src_indexes); kv_destroy(target_geoms); return (PyObject*)result; } /* Callback called by strtree_query with item data of each intersecting geometry * and a counter to increment each time this function is called. Used to prescreen * geometries for intersections with the tree. * * Parameters * ---------- * item: address of intersected geometry in the tree geometries (_geoms) array. * * user_data: pointer to size_t counter incremented on every call to this function * */ void prescreen_query_callback(void* item, void* user_data) { (*(size_t*)user_data)++; } /* Calculate the distance between items in the tree and the src_geom. * Note: this is only called by the tree after first evaluating the overlap between * the the envelope of a tree node and the query geometry. It may not be called for * all equidistant results. * * Parameters * ---------- * item1: address of geometry in tree geometries (_geoms) * * item2: pointer to GEOSGeometry* of query geometry * * distance: pointer to distance that gets updated in this function * * userdata: GEOS context handle. * * Returns * ------- * 0 on error (caller immediately exits and returns NULL); 1 on success * */ int nearest_distance_callback(const void* item1, const void* item2, double* distance, void* userdata) { GEOSGeometry* tree_geom = NULL; const GEOSContextHandle_t* ctx = (GEOSContextHandle_t*)userdata; GeometryObject* tree_pg_geom = *((GeometryObject**)item1); // Note: this is guarded for NULL during construction of tree; no need to check here. get_geom(tree_pg_geom, &tree_geom); // distance returns 1 on success, 0 on error return GEOSDistance_r(*ctx, (GEOSGeometry*)item2, tree_geom, distance); } /* Calculate the distance between items in the tree and the src_geom. * Note: this is only called by the tree after first evaluating the overlap between * the the envelope of a tree node and the query geometry. It may not be called for * all equidistant results. * * In order to force GEOS to check neighbors in adjacent tree nodes, a slight adjustment * is added to the distance returned to the tree. Otherwise, the tree nearest neighbor * algorithm terminates if the envelopes of adjacent tree nodes are not further than * this distance, and not all equidistant or intersected neighbors are checked. The * accurate distances are stored into the distance pairs in userdata. * * Parameters * ---------- * item1: address of geometry in tree geometries (_geoms) * * item2: pointer to GEOSGeometry* of query geometry * * distance: pointer to distance that gets updated in this function * * userdata: instance of tree_nearest_userdata_t, includes vector to cache nearest * distance pairs visited by this function, GEOS context handle, and minimum observed * distance. * * Returns * ------- * 0 on error (caller immediately exits and returns NULL); 1 on success * */ int query_nearest_distance_callback(const void* item1, const void* item2, double* distance, void* userdata) { GEOSGeometry* tree_geom = NULL; size_t pairs_size; double calc_distance; GeometryObject* tree_pg_geom = *((GeometryObject**)item1); // Note: this is guarded for NULL during construction of tree; no need to check here. get_geom(tree_pg_geom, &tree_geom); tree_nearest_userdata_t* params = (tree_nearest_userdata_t*)userdata; // ignore geometries that are equal to the input if (params->exclusive && GEOSEquals_r(params->ctx, (GEOSGeometry*)item2, tree_geom)) { *distance = DBL_MAX; // set large distance to force searching for other matches return 1; } // distance returns 1 on success, 0 on error if (GEOSDistance_r(params->ctx, (GEOSGeometry*)item2, tree_geom, &calc_distance) == 0) { return 0; } // store any pairs that are smaller than the minimum distance observed so far // and update min_distance if (calc_distance <= params->min_distance) { params->min_distance = calc_distance; // if smaller than last item in vector, remove that item first pairs_size = kv_size(*(params->dist_pairs)); if (pairs_size > 0 && calc_distance < (kv_A(*(params->dist_pairs), pairs_size - 1)).distance) { kv_pop(*(params->dist_pairs)); } tree_geom_dist_vec_item_t dist_pair = (tree_geom_dist_vec_item_t){(GeometryObject**)item1, calc_distance}; kv_push(tree_geom_dist_vec_item_t, *(params->dist_pairs), dist_pair); if (params->all_matches == 1) { // Set distance for callback with a slight adjustment to force checking of adjacent // tree nodes; otherwise they are skipped by the GEOS nearest neighbor algorithm // check against bounds of adjacent nodes. calc_distance += 1e-6; } } *distance = calc_distance; return 1; } #if GEOS_SINCE_3_6_0 /* Find the nearest singular item in the tree to each input geometry. * Returns indices of source array and tree items. * * If there are multiple equidistant or intersected items, only one is returned, * based on whichever nearest tree item is visited first by GEOS. * * Returns a tuple of empty arrays of shape (2,0) if tree is empty. * * * Returns * ------- * ndarray of shape (2, n) with input indexes, tree indexes * */ static PyObject* STRtree_nearest(STRtreeObject* self, PyObject* arr) { PyArrayObject* pg_geoms; GeometryObject* pg_geom = NULL; GEOSGeometry* geom = NULL; GeometryObject** nearest_result = NULL; npy_intp i, n, size, geom_index; char* head_ptr = (char*)self->_geoms; PyArrayObject* result; // Indices of input and tree geometries index_vec_t src_indexes; index_vec_t nearest_indexes; if (self->ptr == NULL) { PyErr_SetString(PyExc_RuntimeError, "Tree is uninitialized"); return NULL; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } pg_geoms = (PyArrayObject*)arr; if (!PyArray_ISOBJECT(pg_geoms)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } if (PyArray_NDIM(pg_geoms) != 1) { PyErr_SetString(PyExc_TypeError, "Array should be one dimensional"); return NULL; } // If tree is empty, return empty arrays if (self->count == 0) { npy_intp index_dims[2] = {2, 0}; result = (PyArrayObject*)PyArray_SimpleNew(2, index_dims, NPY_INTP); return (PyObject*)result; } n = PyArray_SIZE(pg_geoms); // preallocate arrays to size of input array kv_init(src_indexes); kv_resize(npy_intp, src_indexes, n); kv_init(nearest_indexes); kv_resize(npy_intp, nearest_indexes, n); GEOS_INIT_THREADS; for (i = 0; i < n; i++) { // get shapely geometry from input geometry array pg_geom = *(GeometryObject**)PyArray_GETPTR1(pg_geoms, i); if (!get_geom(pg_geom, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; break; } if (geom == NULL || GEOSisEmpty_r(ctx, geom)) { continue; } // pass in ctx as userdata because we need it for distance calculation in // the callback nearest_result = (GeometryObject**)GEOSSTRtree_nearest_generic_r( ctx, self->ptr, geom, geom, nearest_distance_callback, &ctx); // GEOSSTRtree_nearest_r returns NULL on error if (nearest_result == NULL) { errstate = PGERR_GEOS_EXCEPTION; break; } kv_push(npy_intp, src_indexes, i); // Calculate index using offset of its address compared to head of _geoms geom_index = (npy_intp)(((char*)nearest_result - head_ptr) / sizeof(GeometryObject*)); kv_push(npy_intp, nearest_indexes, geom_index); } GEOS_FINISH_THREADS; if (errstate != PGERR_SUCCESS) { kv_destroy(src_indexes); kv_destroy(nearest_indexes); return NULL; } size = kv_size(src_indexes); // Create array of indexes npy_intp index_dims[2] = {2, size}; result = (PyArrayObject*)PyArray_SimpleNew(2, index_dims, NPY_INTP); if (result == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); kv_destroy(src_indexes); kv_destroy(nearest_indexes); return NULL; } for (i = 0; i < size; i++) { // assign value into numpy arrays *(npy_intp*)PyArray_GETPTR2(result, 0, i) = kv_A(src_indexes, i); *(npy_intp*)PyArray_GETPTR2(result, 1, i) = kv_A(nearest_indexes, i); } kv_destroy(src_indexes); kv_destroy(nearest_indexes); return (PyObject*)result; } /* Find the nearest item(s) in the tree to each input geometry. * Returns indices of source array, tree items, and distance between them. * * If there are multiple equidistant or intersected items, all should be returned. * Tree indexes are returned in the order they are visited, not necessarily in ascending * order. * * Returns a tuple of empty arrays (shape (2,0), shape (0,)) if tree is empty. * * * Returns * ------- * tuple of ([arr indexes (shape n), tree indexes (shape n)], distances (shape n)) * */ static PyObject* STRtree_query_nearest(STRtreeObject* self, PyObject* args) { PyObject* arr; double max_distance = 0; // default of 0 indicates max_distance not set int use_max_distance = 0; // flag for the above PyArrayObject* pg_geoms; GeometryObject* pg_geom = NULL; GEOSGeometry* geom = NULL; GEOSGeometry* envelope = NULL; GeometryObject** nearest_result = NULL; npy_intp i, n, size, geom_index; size_t j, query_counter; double xmin, ymin, xmax, ymax; char* head_ptr = (char*)self->_geoms; tree_nearest_userdata_t userdata; double distance; int exclusive = 0; // if 1, only non-equal tree geometries will be returned int all_matches = 1; // if 0, only first matching nearest geometry will be returned int has_match = 0; PyArrayObject* result_indexes; // array of [source index, tree index] PyArrayObject* result_distances; // array of distances PyObject* result; // tuple of (indexes array, distance array) // Indices of input geometries index_vec_t src_indexes; // Pairs of addresses in tree geometries and distances; used in userdata tree_geom_dist_vec_t dist_pairs; // Addresses in tree geometries (_geoms) that match tree tree_geom_vec_t nearest_geoms; // Distances of nearest items tree_dist_vec_t nearest_dist; if (self->ptr == NULL) { PyErr_SetString(PyExc_RuntimeError, "Tree is uninitialized"); return NULL; } if (!PyArg_ParseTuple(args, "Odii", &arr, &max_distance, &exclusive, &all_matches)) { return NULL; } if (max_distance > 0) { use_max_distance = 1; } if (!PyArray_Check(arr)) { PyErr_SetString(PyExc_TypeError, "Not an ndarray"); return NULL; } pg_geoms = (PyArrayObject*)arr; if (!PyArray_ISOBJECT(pg_geoms)) { PyErr_SetString(PyExc_TypeError, "Array should be of object dtype"); return NULL; } if (PyArray_NDIM(pg_geoms) != 1) { PyErr_SetString(PyExc_TypeError, "Array should be one dimensional"); return NULL; } // If tree is empty, return empty arrays if (self->count == 0) { npy_intp index_dims[2] = {2, 0}; result_indexes = (PyArrayObject*)PyArray_SimpleNew(2, index_dims, NPY_INTP); npy_intp distance_dims[1] = {0}; result_distances = (PyArrayObject*)PyArray_SimpleNew(1, distance_dims, NPY_DOUBLE); result = PyTuple_New(2); PyTuple_SET_ITEM(result, 0, (PyObject*)result_indexes); PyTuple_SET_ITEM(result, 1, (PyObject*)result_distances); return (PyObject*)result; } n = PyArray_SIZE(pg_geoms); // preallocate arrays to size of input array; for a non-empty tree, there should be // at least 1 nearest item per input geometry kv_init(src_indexes); kv_resize(npy_intp, src_indexes, n); kv_init(nearest_geoms); kv_resize(GeometryObject**, nearest_geoms, n); kv_init(nearest_dist); kv_resize(double, nearest_dist, n); GEOS_INIT_THREADS; // initialize userdata context and dist_pairs vector userdata.ctx = ctx; userdata.dist_pairs = &dist_pairs; userdata.exclusive = exclusive; userdata.all_matches = all_matches; for (i = 0; i < n; i++) { // get shapely geometry from input geometry array pg_geom = *(GeometryObject**)PyArray_GETPTR1(pg_geoms, i); if (!get_geom(pg_geom, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; break; } if (geom == NULL || GEOSisEmpty_r(ctx, geom)) { continue; } if (use_max_distance) { // if max_distance is defined, prescreen geometries using simple bbox expansion; // this only helps to eliminate input geometries that have no tree geometries // within_max distance, and adds overhead when there is a large number // of hits within this distance if (get_bounds(ctx, geom, &xmin, &ymin, &xmax, &ymax) == 0) { errstate = PGERR_GEOS_EXCEPTION; break; } envelope = create_box(ctx, xmin - max_distance, ymin - max_distance, xmax + max_distance, ymax + max_distance, 1); if (envelope == NULL) { errstate = PGERR_GEOS_EXCEPTION; break; } query_counter = 0; GEOSSTRtree_query_r(ctx, self->ptr, envelope, prescreen_query_callback, &query_counter); GEOSGeom_destroy_r(ctx, envelope); if (query_counter == 0) { // no features are within max_distance, skip distance calculations continue; } } if (errstate == PGERR_GEOS_EXCEPTION) { // break outer loop break; } // reset loop-dependent values of userdata kv_init(dist_pairs); userdata.min_distance = DBL_MAX; nearest_result = (GeometryObject**)GEOSSTRtree_nearest_generic_r( ctx, self->ptr, geom, geom, query_nearest_distance_callback, &userdata); // GEOSSTRtree_nearest_generic_r returns NULL on error if (nearest_result == NULL) { errstate = PGERR_GEOS_EXCEPTION; kv_destroy(dist_pairs); break; } has_match = 0; for (j = 0; j < kv_size(dist_pairs); j++) { distance = kv_A(dist_pairs, j).distance; // only keep entries from the smallest distances for this input geometry // only keep entries within max_distance, if nonzero // Note: there may be multiple equidistant or intersected tree items; // only 1 is returned if all_matches == 0 if (distance <= userdata.min_distance && (!use_max_distance || distance <= max_distance) && (all_matches || !has_match)) { kv_push(npy_intp, src_indexes, i); kv_push(GeometryObject**, nearest_geoms, kv_A(dist_pairs, j).geom); kv_push(double, nearest_dist, distance); has_match = 1; } } kv_destroy(dist_pairs); } GEOS_FINISH_THREADS; if (errstate != PGERR_SUCCESS) { kv_destroy(src_indexes); kv_destroy(nearest_geoms); kv_destroy(nearest_dist); return NULL; } size = kv_size(src_indexes); // Create array of indexes npy_intp index_dims[2] = {2, size}; result_indexes = (PyArrayObject*)PyArray_SimpleNew(2, index_dims, NPY_INTP); if (result_indexes == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); kv_destroy(src_indexes); kv_destroy(nearest_geoms); kv_destroy(nearest_dist); return NULL; } // Create array of distances npy_intp distance_dims[1] = {size}; result_distances = (PyArrayObject*)PyArray_SimpleNew(1, distance_dims, NPY_DOUBLE); if (result_distances == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); kv_destroy(src_indexes); kv_destroy(nearest_geoms); kv_destroy(nearest_dist); return NULL; } for (i = 0; i < size; i++) { // assign value into numpy arrays *(npy_intp*)PyArray_GETPTR2(result_indexes, 0, i) = kv_A(src_indexes, i); // Calculate index using offset of its address compared to head of _geoms geom_index = (npy_intp)(((char*)kv_A(nearest_geoms, i) - head_ptr) / sizeof(GeometryObject*)); *(npy_intp*)PyArray_GETPTR2(result_indexes, 1, i) = geom_index; *(double*)PyArray_GETPTR1(result_distances, i) = kv_A(nearest_dist, i); } kv_destroy(src_indexes); kv_destroy(nearest_geoms); kv_destroy(nearest_dist); // return a tuple of (indexes array, distances array) result = PyTuple_New(2); PyTuple_SET_ITEM(result, 0, (PyObject*)result_indexes); PyTuple_SET_ITEM(result, 1, (PyObject*)result_distances); return (PyObject*)result; } #endif // GEOS_SINCE_3_6_0 #if GEOS_SINCE_3_10_0 static PyObject* STRtree_dwithin(STRtreeObject* self, PyObject* args) { char ret; PyObject* geom_arr; PyObject* dist_arr; PyArrayObject* pg_geoms; PyArrayObject* distances; GeometryObject* pg_geom = NULL; GeometryObject* target_pg_geom = NULL; GeometryObject** target_geom_loc; // address of geometry in tree geometries (_geoms) GEOSGeometry* geom = NULL; GEOSGeometry* target_geom = NULL; GEOSPreparedGeometry* prepared_geom = NULL; const GEOSPreparedGeometry* prepared_geom_tmp = NULL; GEOSGeometry* envelope = NULL; npy_intp i, j, n, size, geom_index; double xmin, ymin, xmax, ymax, distance; char* head_ptr = (char*)self->_geoms; PyArrayObject* result; // array of [source index, tree index] // Indices of input geometries index_vec_t src_indexes; // Addresses in tree geometries (_geoms) that overlap with expanded bboxes around // intput geometries tree_geom_vec_t query_geoms; // Addresses in tree geometries (_geoms) that meet DistanceWithin predicate tree_geom_vec_t target_geoms; if (self->ptr == NULL) { PyErr_SetString(PyExc_RuntimeError, "Tree is uninitialized"); return NULL; } if (!PyArg_ParseTuple(args, "OO", &geom_arr, &dist_arr)) { return NULL; } if (!PyArray_Check(geom_arr)) { PyErr_SetString(PyExc_TypeError, "Geometries not an ndarray"); return NULL; } if (!PyArray_Check(dist_arr)) { PyErr_SetString(PyExc_TypeError, "Distances not an ndarray"); return NULL; } pg_geoms = (PyArrayObject*)geom_arr; if (!PyArray_ISOBJECT(pg_geoms)) { PyErr_SetString(PyExc_TypeError, "Geometry array should be of object dtype"); return NULL; } if (PyArray_NDIM(pg_geoms) != 1) { PyErr_SetString(PyExc_ValueError, "Geometry array should be one dimensional"); return NULL; } distances = (PyArrayObject*)dist_arr; if (!PyArray_ISFLOAT(distances)) { PyErr_SetString(PyExc_TypeError, "Distance array should be floating point dtype"); return NULL; } if (PyArray_NDIM(distances) != 1) { PyErr_SetString(PyExc_ValueError, "Distance array should be one dimensional"); return NULL; } n = PyArray_SIZE(pg_geoms); if (n != PyArray_SIZE(distances)) { PyErr_SetString(PyExc_ValueError, "Geometries and distances must be same length"); return NULL; } // If tree is empty, return empty arrays if (self->count == 0 || n == 0) { npy_intp index_dims[2] = {2, 0}; return PyArray_SimpleNew(2, index_dims, NPY_INTP); } kv_init(src_indexes); kv_init(target_geoms); GEOS_INIT_THREADS; for (i = 0; i < n; i++) { // get shapely geometry from input geometry array pg_geom = *(GeometryObject**)PyArray_GETPTR1(pg_geoms, i); if (!get_geom_with_prepared(pg_geom, &geom, &prepared_geom)) { errstate = PGERR_NOT_A_GEOMETRY; break; } distance = *(double*)PyArray_GETPTR1(distances, i); if (geom == NULL || GEOSisEmpty_r(ctx, geom) || npy_isnan(distance)) { continue; } // prescreen geometries using simple bbox expansion if (get_bounds(ctx, geom, &xmin, &ymin, &xmax, &ymax) == 0) { errstate = PGERR_GEOS_EXCEPTION; break; } envelope = create_box(ctx, xmin - distance, ymin - distance, xmax + distance, ymax + distance, 1); if (envelope == NULL) { errstate = PGERR_GEOS_EXCEPTION; break; } kv_init(query_geoms); GEOSSTRtree_query_r(ctx, self->ptr, envelope, query_callback, &query_geoms); GEOSGeom_destroy_r(ctx, envelope); size = kv_size(query_geoms); if (size == 0) { // no target geoms in query window, skip this source geom kv_destroy(query_geoms); continue; } // prepare the query geometry if not already prepared if (prepared_geom == NULL) { // geom was not previously prepared, prepare it now prepared_geom_tmp = GEOSPrepare_r(ctx, (const GEOSGeometry*)geom); if (prepared_geom_tmp == NULL) { errstate = PGERR_GEOS_EXCEPTION; kv_destroy(query_geoms); break; } } else { // cast to const only needed until larger refactor of all geom pointers to const prepared_geom_tmp = (const GEOSPreparedGeometry*)prepared_geom; } for (j = 0; j < size; j++) { // get address of geometry in tree geometries, then use that to get associated // GEOS geometry target_geom_loc = kv_A(query_geoms, j); target_pg_geom = *target_geom_loc; if (target_pg_geom == NULL) { continue; } get_geom(target_pg_geom, &target_geom); ret = GEOSPreparedDistanceWithin_r(ctx, prepared_geom_tmp, target_geom, distance); if (ret == 2) { // exception: checked below to break out of outer loop errstate = PGERR_GEOS_EXCEPTION; break; } if (ret == 1) { // success: add to outputs kv_push(npy_intp, src_indexes, i); kv_push(GeometryObject**, target_geoms, target_geom_loc); } } kv_destroy(query_geoms); // only if we created prepared_geom_tmp here, destroy it if (prepared_geom == NULL) { GEOSPreparedGeom_destroy_r(ctx, prepared_geom_tmp); prepared_geom_tmp = NULL; } if (errstate != PGERR_SUCCESS) { // break outer loop break; } } GEOS_FINISH_THREADS; if (errstate != PGERR_SUCCESS) { kv_destroy(src_indexes); kv_destroy(target_geoms); return NULL; } size = kv_size(src_indexes); npy_intp dims[2] = {2, size}; // the following raises a compiler warning based on how the macro is defined // in numpy. There doesn't appear to be anything we can do to avoid it. result = (PyArrayObject*)PyArray_SimpleNew(2, dims, NPY_INTP); if (result == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); kv_destroy(src_indexes); kv_destroy(target_geoms); return NULL; } for (i = 0; i < size; i++) { // assign value into numpy arrays *(npy_intp*)PyArray_GETPTR2(result, 0, i) = kv_A(src_indexes, i); // Calculate index using offset of its address compared to head of _geoms geom_index = (npy_intp)(((char*)kv_A(target_geoms, i) - head_ptr) / sizeof(GeometryObject*)); *(npy_intp*)PyArray_GETPTR2(result, 1, i) = geom_index; } kv_destroy(src_indexes); kv_destroy(target_geoms); return (PyObject*)result; } #endif // GEOS_SINCE_3_10_0 static PyMemberDef STRtree_members[] = { {"_ptr", T_PYSSIZET, offsetof(STRtreeObject, ptr), READONLY, "Pointer to GEOSSTRtree"}, {"count", T_LONG, offsetof(STRtreeObject, count), READONLY, "The number of geometries inside the tree"}, {NULL} /* Sentinel */ }; static PyMethodDef STRtree_methods[] = { {"query", (PyCFunction)STRtree_query, METH_VARARGS, "Queries the index for all items whose extents intersect the given search " "geometries, and optionally tests them " "against predicate function if provided. "}, #if GEOS_SINCE_3_6_0 {"nearest", (PyCFunction)STRtree_nearest, METH_O, "Queries the index for the nearest item to each of the given search geometries"}, {"query_nearest", (PyCFunction)STRtree_query_nearest, METH_VARARGS, "Queries the index for all nearest item(s) to each of the given search geometries"}, #endif // GEOS_SINCE_3_6_0 #if GEOS_SINCE_3_10_0 {"dwithin", (PyCFunction)STRtree_dwithin, METH_VARARGS, "Queries the index for all item(s) in the tree within given distance of search " "geometries"}, #endif // GEOS_SINCE_3_10_0 {NULL} /* Sentinel */ }; PyTypeObject STRtreeType = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "shapely.lib.STRtree", .tp_doc = "A query-only R-tree created using the Sort-Tile-Recursive (STR) algorithm.", .tp_basicsize = sizeof(STRtreeObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = STRtree_new, .tp_dealloc = (destructor)STRtree_dealloc, .tp_members = STRtree_members, .tp_methods = STRtree_methods}; int init_strtree_type(PyObject* m) { if (PyType_Ready(&STRtreeType) < 0) { return -1; } Py_INCREF(&STRtreeType); PyModule_AddObject(m, "STRtree", (PyObject*)&STRtreeType); return 0; } shapely-2.0.3/src/strtree.h000066400000000000000000000027701456366510000156250ustar00rootroot00000000000000#ifndef _RTREE_H #define _RTREE_H #include #include "geos.h" #include "pygeom.h" /* A resizable vector with addresses of geometries within tree geometries array */ typedef struct { size_t n, m; GeometryObject*** a; } tree_geom_vec_t; /* A struct to hold pairs of GeometryObject** and distance for use in * STRtree::query_nearest */ typedef struct { GeometryObject** geom; double distance; } tree_geom_dist_vec_item_t; /* A resizeable vector with pairs of GeometryObject** and distance for use in * STRtree::query_nearest */ typedef struct { size_t n, m; tree_geom_dist_vec_item_t* a; } tree_geom_dist_vec_t; /* A resizeable vector with distances to nearest tree items */ typedef struct { size_t n, m; double* a; } tree_dist_vec_t; /* A struct to hold userdata argument data for distance_callback used by * GEOSSTRtree_nearest_generic_r */ typedef struct { GEOSContextHandle_t ctx; tree_geom_dist_vec_t* dist_pairs; double min_distance; int exclusive; // if 1, only non-equal tree geometries will be returned int all_matches; // if 0, only first nearest tree geometry will be returned } tree_nearest_userdata_t; typedef struct { PyObject_HEAD void* ptr; npy_intp count; // count of geometries added to the tree size_t _geoms_size; // size of _geoms array (same as original size of input array) GeometryObject** _geoms; // array of input geometries } STRtreeObject; extern PyTypeObject STRtreeType; extern int init_strtree_type(PyObject* m); #endif shapely-2.0.3/src/ufuncs.c000066400000000000000000003525211456366510000154350ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #define NO_IMPORT_ARRAY #define NO_IMPORT_UFUNC #define PY_ARRAY_UNIQUE_SYMBOL shapely_ARRAY_API #define PY_UFUNC_UNIQUE_SYMBOL shapely_UFUNC_API #include #include #include #include "fast_loop_macros.h" #include "geos.h" #include "pygeom.h" /* This initializes a global value for interrupt checking */ int check_signals_interval[1] = {10000}; unsigned long main_thread_id[1] = {0}; PyObject* PySetupSignalChecks(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, "ik", check_signals_interval, main_thread_id)) { return NULL; } Py_INCREF(Py_None); return Py_None; } #define OUTPUT_Y \ PyObject* ret = GeometryObject_FromGEOS(ret_ptr, ctx); \ PyObject** out = (PyObject**)op1; \ Py_XDECREF(*out); \ *out = ret #define OUTPUT_Y_I(I, RET_PTR) \ PyObject* ret##I = GeometryObject_FromGEOS(RET_PTR, ctx); \ PyObject** out##I = (PyObject**)op##I; \ Py_XDECREF(*out##I); \ *out##I = ret##I // Fail if inputs output multiple times on the same place in memory. That would // lead to segfaults as the same GEOSGeometry would be 'owned' by multiple PyObjects. #define CHECK_NO_INPLACE_OUTPUT(N) \ if ((steps[N] == 0) && (dimensions[0] > 1)) { \ PyErr_Format(PyExc_NotImplementedError, \ "Zero-strided output detected. Ufunc mode with args[0]=%p, " \ "args[N]=%p, steps[0]=%ld, steps[N]=%ld, dimensions[0]=%ld.", \ args[0], args[N], steps[0], steps[N], dimensions[0]); \ return; \ } #define CHECK_ALLOC(ARR) \ if (ARR == NULL) { \ PyErr_SetString(PyExc_MemoryError, "Could not allocate memory"); \ return; \ } /* PyErr_CheckSignals calls python signal handler at iteration 10000, 20000, and * so forth. If a signal handler raises an exception (by default, SIGINT raises * a KeyboardIterrupt), it returns -1. * The caller needs to check 'errstate' and cleanup & exit if it equals PGERR_PYSIGNAL. */ #define CHECK_SIGNALS(I) \ if (((I + 1) % check_signals_interval[0]) == 0) { \ if (PyErr_CheckSignals() == -1) { \ errstate = PGERR_PYSIGNAL; \ }; \ } /* This version of CHECK_SIGNALS is to be used in a context without GIL * the GIL is only acquired if the current thread is the main thread (else, * signals won't be set anyway) */ #define CHECK_SIGNALS_THREADS(I) \ if (((I + 1) % check_signals_interval[0]) == 0) { \ if (PyThread_get_thread_ident() == main_thread_id[0]) { \ Py_BLOCK_THREADS; \ if (PyErr_CheckSignals() == -1) { \ errstate = PGERR_PYSIGNAL; \ } \ Py_UNBLOCK_THREADS; \ } \ } static void geom_arr_to_npy(GEOSGeometry** array, char* ptr, npy_intp stride, npy_intp count) { npy_intp i; PyObject* ret; PyObject** out; GEOS_INIT; for (i = 0; i < count; i++, ptr += stride) { ret = GeometryObject_FromGEOS(array[i], ctx); out = (PyObject**)ptr; Py_XDECREF(*out); *out = ret; } GEOS_FINISH; } /* Define the geom -> bool functions (Y_b) */ static void* is_empty_data[1] = {GEOSisEmpty_r}; /* the GEOSisSimple_r function fails on geometrycollections */ static char GEOSisSimpleAllTypes_r(void* context, void* geom) { int type = GEOSGeomTypeId_r(context, geom); if (type == -1) { return 2; // Predicates use a return value of 2 for errors } else if (type == 7) { return 0; } else { return GEOSisSimple_r(context, geom); } } static void* is_simple_data[1] = {GEOSisSimpleAllTypes_r}; static void* is_ring_data[1] = {GEOSisRing_r}; static void* has_z_data[1] = {GEOSHasZ_r}; /* the GEOSisClosed_r function fails on non-linestrings */ static char GEOSisClosedAllTypes_r(void* context, void* geom) { int type = GEOSGeomTypeId_r(context, geom); if (type == -1) { return 2; // Predicates use a return value of 2 for errors } else if ((type == 1) || (type == 2) || (type == 5)) { return GEOSisClosed_r(context, geom); } else { return 0; } } static void* is_closed_data[1] = {GEOSisClosedAllTypes_r}; static void* is_valid_data[1] = {GEOSisValid_r}; #if GEOS_SINCE_3_7_0 static char GEOSGeom_isCCW_r(void* context, void* geom) { const GEOSCoordSequence* coord_seq; char is_ccw = 2; // return value of 2 means GEOSException int i; // Return False for non-linear geometries i = GEOSGeomTypeId_r(context, geom); if (i == -1) { return 2; } if ((i != GEOS_LINEARRING) && (i != GEOS_LINESTRING)) { return 0; } // Return False for lines with fewer than 4 points i = GEOSGeomGetNumPoints_r(context, geom); if (i == -1) { return 2; } if (i < 4) { return 0; } // Get the coordinatesequence and call isCCW() coord_seq = GEOSGeom_getCoordSeq_r(context, geom); if (coord_seq == NULL) { return 2; } if (!GEOSCoordSeq_isCCW_r(context, coord_seq, &is_ccw)) { return 2; } return is_ccw; } static void* is_ccw_data[1] = {GEOSGeom_isCCW_r}; #endif typedef char FuncGEOS_Y_b(void* context, void* a); static char Y_b_dtypes[2] = {NPY_OBJECT, NPY_BOOL}; static void Y_b_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Y_b* func = (FuncGEOS_Y_b*)data; GEOSGeometry* in1 = NULL; char ret; GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometry; return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (in1 == NULL) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { /* call the GEOS function */ ret = func(ctx, in1); /* finish for illegal values */ if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction Y_b_funcs[1] = {&Y_b_func}; /* Define the object -> bool functions (O_b) which do not raise on non-geom objects*/ static char IsMissing(void* context, PyObject* obj) { GEOSGeometry* g = NULL; if (!get_geom((GeometryObject*)obj, &g)) { return 0; }; return g == NULL; // get_geom sets g to NULL for None input } static void* is_missing_data[1] = {IsMissing}; static char IsGeometry(void* context, PyObject* obj) { GEOSGeometry* g = NULL; if (!get_geom((GeometryObject*)obj, &g)) { return 0; } return g != NULL; } static void* is_geometry_data[1] = {IsGeometry}; static char IsValidInput(void* context, PyObject* obj) { GEOSGeometry* g = NULL; return get_geom((GeometryObject*)obj, &g); } static void* is_valid_input_data[1] = {IsValidInput}; typedef char FuncGEOS_O_b(void* context, PyObject* obj); static char O_b_dtypes[2] = {NPY_OBJECT, NPY_BOOL}; static void O_b_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_O_b* func = (FuncGEOS_O_b*)data; GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { break; } *(npy_bool*)op1 = func(ctx, *(PyObject**)ip1); } GEOS_FINISH_THREADS; } static PyUFuncGenericFunction O_b_funcs[1] = {&O_b_func}; /* Define the geom, geom -> bool functions (YY_b) */ static void* equals_data[1] = {GEOSEquals_r}; typedef char FuncGEOS_YY_b(void* context, void* a, void* b); static char YY_b_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_BOOL}; static void YY_b_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YY_b* func = (FuncGEOS_YY_b*)data; GEOSGeometry *in1 = NULL, *in2 = NULL; char ret; GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if ((in1 == NULL) || (in2 == NULL)) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { /* call the GEOS function */ ret = func(ctx, in1, in2); /* return for illegal values */ if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction YY_b_funcs[1] = {&YY_b_func}; /* Define the geom, geom -> bool functions (YY_b) prepared */ static void* contains_func_tuple[2] = {GEOSContains_r, GEOSPreparedContains_r}; static void* contains_data[1] = {contains_func_tuple}; static char GEOSContainsProperly(void* context, void* g1, void* g2) { const GEOSPreparedGeometry* prepared_geom_tmp = NULL; char ret; prepared_geom_tmp = GEOSPrepare_r(context, g1); if (prepared_geom_tmp == NULL) { return 2; } ret = GEOSPreparedContainsProperly_r(context, prepared_geom_tmp, g2); GEOSPreparedGeom_destroy_r(context, prepared_geom_tmp); return ret; } static void* contains_properly_func_tuple[2] = {GEOSContainsProperly, GEOSPreparedContainsProperly_r}; static void* contains_properly_data[1] = {contains_properly_func_tuple}; static void* covered_by_func_tuple[2] = {GEOSCoveredBy_r, GEOSPreparedCoveredBy_r}; static void* covered_by_data[1] = {covered_by_func_tuple}; static void* covers_func_tuple[2] = {GEOSCovers_r, GEOSPreparedCovers_r}; static void* covers_data[1] = {covers_func_tuple}; static void* crosses_func_tuple[2] = {GEOSCrosses_r, GEOSPreparedCrosses_r}; static void* crosses_data[1] = {crosses_func_tuple}; static void* disjoint_func_tuple[2] = {GEOSDisjoint_r, GEOSPreparedDisjoint_r}; static void* disjoint_data[1] = {disjoint_func_tuple}; static void* intersects_func_tuple[2] = {GEOSIntersects_r, GEOSPreparedIntersects_r}; static void* intersects_data[1] = {intersects_func_tuple}; static void* overlaps_func_tuple[2] = {GEOSOverlaps_r, GEOSPreparedOverlaps_r}; static void* overlaps_data[1] = {overlaps_func_tuple}; static void* touches_func_tuple[2] = {GEOSTouches_r, GEOSPreparedTouches_r}; static void* touches_data[1] = {touches_func_tuple}; static void* within_func_tuple[2] = {GEOSWithin_r, GEOSPreparedWithin_r}; static void* within_data[1] = {within_func_tuple}; static char YY_b_p_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_BOOL}; static void YY_b_p_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YY_b* func = ((FuncGEOS_YY_b**)data)[0]; FuncGEOS_YY_b* func_prepared = ((FuncGEOS_YY_b**)data)[1]; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOSPreparedGeometry* in1_prepared = NULL; char ret; GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom_with_prepared(*(GeometryObject**)ip1, &in1, &in1_prepared)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if ((in1 == NULL) || (in2 == NULL)) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { if (in1_prepared == NULL) { /* call the GEOS function */ ret = func(ctx, in1, in2); } else { /* call the prepared GEOS function */ ret = func_prepared(ctx, in1_prepared, in2); } /* return for illegal values */ if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction YY_b_p_funcs[1] = {&YY_b_p_func}; /* Define the geom, X, Y -> bool functions (Ydd_b) prepared */ #if GEOS_SINCE_3_12_0 static void* contains_xy_data[1] = {GEOSPreparedContainsXY_r}; static void* intersects_xy_data[1] = {GEOSPreparedIntersectsXY_r}; #else static void* contains_xy_data[1] = {GEOSPreparedContains_r}; static void* intersects_xy_data[1] = {GEOSPreparedIntersects_r}; #endif typedef char FuncGEOS_Ydd_b(void* context, void* pg, double x, double y); static char Ydd_b_p_dtypes[4] = {NPY_OBJECT, NPY_DOUBLE, NPY_DOUBLE, NPY_BOOL}; static void Ydd_b_p_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { #if GEOS_SINCE_3_12_0 FuncGEOS_Ydd_b* func = (FuncGEOS_Ydd_b*)data; #else FuncGEOS_YY_b* func = (FuncGEOS_YY_b*)data; #endif GEOSGeometry* in1 = NULL; GEOSPreparedGeometry* in1_prepared = NULL; GEOSGeometry *geom = NULL; const GEOSPreparedGeometry* prepared_geom_tmp = NULL; char ret; GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom_with_prepared(*(GeometryObject**)ip1, &in1, &in1_prepared)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } double in2 = *(double*)ip2; double in3 = *(double*)ip3; if ((in1 == NULL) || npy_isnan(in2) || npy_isnan(in3)) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { /* if input geometry is not yet prepared, prepare (and destroy) on the fly*/ char destroy_prepared = 0; if (in1_prepared == NULL) { prepared_geom_tmp = GEOSPrepare_r(ctx, in1); if (prepared_geom_tmp == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } destroy_prepared = 1; } else { prepared_geom_tmp = in1_prepared; } #if GEOS_SINCE_3_12_0 ret = func(ctx, prepared_geom_tmp, in2, in3); #else geom = create_point(ctx, in2, in3); if (geom == NULL) { if (destroy_prepared) { GEOSPreparedGeom_destroy_r(ctx, prepared_geom_tmp); } errstate = PGERR_GEOS_EXCEPTION; goto finish; } ret = func(ctx, prepared_geom_tmp, geom); GEOSGeom_destroy_r(ctx, geom); #endif if (destroy_prepared) { GEOSPreparedGeom_destroy_r(ctx, prepared_geom_tmp); } /* return for illegal values */ if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction Ydd_b_p_funcs[1] = {&Ydd_b_p_func}; static char is_prepared_dtypes[2] = {NPY_OBJECT, NPY_BOOL}; static void is_prepared_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* in1 = NULL; GEOSPreparedGeometry* in1_prepared = NULL; GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { break; } /* get the geometry: return on error */ if (!get_geom_with_prepared(*(GeometryObject**)ip1, &in1, &in1_prepared)) { errstate = PGERR_NOT_A_GEOMETRY; break; } *(npy_bool*)op1 = (in1_prepared != NULL); } GEOS_FINISH_THREADS; } static PyUFuncGenericFunction is_prepared_funcs[1] = {&is_prepared_func}; /* Define the geom -> geom functions (Y_Y) */ static void* envelope_data[1] = {GEOSEnvelope_r}; static void* convex_hull_data[1] = {GEOSConvexHull_r}; static void* GEOSBoundaryAllTypes_r(void* context, void* geom) { char typ = GEOSGeomTypeId_r(context, geom); if (typ == 7) { /* return None for geometrycollections */ return NULL; } else { return GEOSBoundary_r(context, geom); } } static void* boundary_data[1] = {GEOSBoundaryAllTypes_r}; static void* unary_union_data[1] = {GEOSUnaryUnion_r}; static void* point_on_surface_data[1] = {GEOSPointOnSurface_r}; static void* centroid_data[1] = {GEOSGetCentroid_r}; static void* line_merge_data[1] = {GEOSLineMerge_r}; static void* node_data[1] = {GEOSNode_r}; static void* extract_unique_points_data[1] = {GEOSGeom_extractUniquePoints_r}; static void* GetExteriorRing(void* context, void* geom) { char typ = GEOSGeomTypeId_r(context, geom); if (typ != 3) { return NULL; } void* ret = (void*)GEOSGetExteriorRing_r(context, geom); /* Create a copy of the obtained geometry */ if (ret != NULL) { ret = GEOSGeom_clone_r(context, ret); } return ret; } static void* get_exterior_ring_data[1] = {GetExteriorRing}; /* the normalize funcion acts inplace */ static void* GEOSNormalize_r_with_clone(void* context, void* geom) { int ret; void* new_geom = GEOSGeom_clone_r(context, geom); if (new_geom == NULL) { return NULL; } ret = GEOSNormalize_r(context, new_geom); if (ret == -1) { GEOSGeom_destroy_r(context, new_geom); return NULL; } return new_geom; } static void* normalize_data[1] = {GEOSNormalize_r_with_clone}; static void* force_2d_data[1] = {PyGEOSForce2D}; #if GEOS_SINCE_3_8_0 static void* build_area_data[1] = {GEOSBuildArea_r}; static void* make_valid_data[1] = {GEOSMakeValid_r}; static void* coverage_union_data[1] = {GEOSCoverageUnion_r}; static void* GEOSMinimumBoundingCircleWithReturn(void* context, void* geom) { GEOSGeometry* center = NULL; double radius; GEOSGeometry* ret = GEOSMinimumBoundingCircle_r(context, geom, &radius, ¢er); if (ret == NULL) { return NULL; } GEOSGeom_destroy_r(context, center); return ret; } static void* minimum_bounding_circle_data[1] = {GEOSMinimumBoundingCircleWithReturn}; #endif #if GEOS_SINCE_3_7_0 static void* reverse_data[1] = {GEOSReverse_r}; #endif #if GEOS_SINCE_3_6_0 static void* oriented_envelope_data[1] = {GEOSMinimumRotatedRectangle_r}; #endif #if GEOS_SINCE_3_11_0 static void* line_merge_directed_data[1] = {GEOSLineMergeDirected_r}; #endif typedef void* FuncGEOS_Y_Y(void* context, void* a); static char Y_Y_dtypes[2] = {NPY_OBJECT, NPY_OBJECT}; static void Y_Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Y_Y* func = (FuncGEOS_Y_Y*)data; GEOSGeometry* in1 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(1); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (in1 == NULL) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = func(ctx, in1); // NULL means: exception, but for some functions it may also indicate a // "missing value" (None) (GetExteriorRing, GEOSBoundaryAllTypes_r) // So: check the last_error before setting error state if ((geom_arr[i] == NULL) && (last_error[0] != 0)) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[1], steps[1], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction Y_Y_funcs[1] = {&Y_Y_func}; /* Define the geom -> no return value functions (Y) */ static char PrepareGeometryObject(void* ctx, GeometryObject* geom) { if (geom->ptr_prepared == NULL) { geom->ptr_prepared = (GEOSPreparedGeometry*)GEOSPrepare_r(ctx, geom->ptr); if (geom->ptr_prepared == NULL) { return PGERR_GEOS_EXCEPTION; } } return PGERR_SUCCESS; } static char DestroyPreparedGeometryObject(void* ctx, GeometryObject* geom) { if (geom->ptr_prepared != NULL) { GEOSPreparedGeom_destroy_r(ctx, geom->ptr_prepared); geom->ptr_prepared = NULL; } return PGERR_SUCCESS; } static void* prepare_data[1] = {PrepareGeometryObject}; static void* destroy_prepared_data[1] = {DestroyPreparedGeometryObject}; typedef char FuncPyGEOS_Y(void* ctx, GeometryObject* geom); static char Y_dtypes[1] = {NPY_OBJECT}; static void Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncPyGEOS_Y* func = (FuncPyGEOS_Y*)data; GEOSGeometry* in1 = NULL; GeometryObject* geom_obj = NULL; GEOS_INIT; NO_OUTPUT_LOOP { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } geom_obj = *(GeometryObject**)ip1; if (!get_geom(geom_obj, &in1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (in1 != NULL) { errstate = func(ctx, geom_obj); if (errstate != PGERR_SUCCESS) { goto finish; } } } finish: GEOS_FINISH; } static PyUFuncGenericFunction Y_funcs[1] = {&Y_func}; /* Define the geom, double -> geom functions (Yd_Y) */ static void* GEOSInterpolateProtectEmpty_r(void* context, void* geom, double d) { char errstate = geos_interpolate_checker(context, geom); if (errstate == PGERR_SUCCESS) { return GEOSInterpolate_r(context, geom, d); } else if (errstate == PGERR_EMPTY_GEOMETRY) { return GEOSGeom_createEmptyPoint_r(context); } else { return NULL; } } static void* line_interpolate_point_data[1] = {GEOSInterpolateProtectEmpty_r}; static void* GEOSInterpolateNormalizedProtectEmpty_r(void* context, void* geom, double d) { char errstate = geos_interpolate_checker(context, geom); if (errstate == PGERR_SUCCESS) { return GEOSInterpolateNormalized_r(context, geom, d); } else if (errstate == PGERR_EMPTY_GEOMETRY) { return GEOSGeom_createEmptyPoint_r(context); } else { return NULL; } } static void* line_interpolate_point_normalized_data[1] = { GEOSInterpolateNormalizedProtectEmpty_r}; static void* simplify_data[1] = {GEOSSimplify_r}; static void* simplify_preserve_topology_data[1] = {GEOSTopologyPreserveSimplify_r}; static void* force_3d_data[1] = {PyGEOSForce3D}; #if GEOS_SINCE_3_9_0 static void* unary_union_prec_data[1] = {GEOSUnaryUnionPrec_r}; #endif #if GEOS_SINCE_3_10_0 static void* segmentize_data[1] = {GEOSDensify_r}; #endif #if GEOS_SINCE_3_11_0 static void* remove_repeated_points_data[1] = {GEOSRemoveRepeatedPoints_r}; #endif typedef void* FuncGEOS_Yd_Y(void* context, void* a, double b); static char Yd_Y_dtypes[3] = {NPY_OBJECT, NPY_DOUBLE, NPY_OBJECT}; static void Yd_Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Yd_Y* func = (FuncGEOS_Yd_Y*)data; GEOSGeometry* in1 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(2); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } double in2 = *(double*)ip2; if ((in1 == NULL) || (npy_isnan(in2))) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = func(ctx, in1, in2); if (geom_arr[i] == NULL) { // Interpolate functions return NULL on PGERR_GEOMETRY_TYPE and on // PGERR_GEOS_EXCEPTION. Distinguish these by the state of last_error. errstate = last_error[0] == 0 ? PGERR_GEOMETRY_TYPE : PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction Yd_Y_funcs[1] = {&Yd_Y_func}; /* Define the geom, int -> geom functions (Yi_Y) */ /* We add bound and type checking to the various indexing functions */ static void* GetPointN(void* context, void* geom, int n) { char typ = GEOSGeomTypeId_r(context, geom); int size, i; if ((typ != 1) && (typ != 2)) { return NULL; } size = GEOSGeomGetNumPoints_r(context, geom); if (size == -1) { return NULL; } if (n < 0) { /* Negative indexing: we get it for free */ i = size + n; } else { i = n; } if ((i < 0) || (i >= size)) { /* Important, could give segfaults else */ return NULL; } return GEOSGeomGetPointN_r(context, geom, i); } static void* get_point_data[1] = {GetPointN}; static void* GetInteriorRingN(void* context, void* geom, int n) { char typ = GEOSGeomTypeId_r(context, geom); int size, i; if (typ != 3) { return NULL; } size = GEOSGetNumInteriorRings_r(context, geom); if (size == -1) { return NULL; } if (n < 0) { /* Negative indexing: we get it for free */ i = size + n; } else { i = n; } if ((i < 0) || (i >= size)) { /* Important, could give segfaults else */ return NULL; } void* ret = (void*)GEOSGetInteriorRingN_r(context, geom, i); /* Create a copy of the obtained geometry */ if (ret != NULL) { ret = GEOSGeom_clone_r(context, ret); } return ret; } static void* get_interior_ring_data[1] = {GetInteriorRingN}; static void* GetGeometryN(void* context, void* geom, int n) { int size, i; size = GEOSGetNumGeometries_r(context, geom); if (size == -1) { return NULL; } if (n < 0) { /* Negative indexing: we get it for free */ i = size + n; } else { i = n; } if ((i < 0) || (i >= size)) { /* Important, could give segfaults else */ return NULL; } void* ret = (void*)GEOSGetGeometryN_r(context, geom, i); /* Create a copy of the obtained geometry */ if (ret != NULL) { ret = GEOSGeom_clone_r(context, ret); } return ret; } static void* get_geometry_data[1] = {GetGeometryN}; /* the set srid funcion acts inplace */ static void* GEOSSetSRID_r_with_clone(void* context, void* geom, int srid) { void* ret = GEOSGeom_clone_r(context, geom); if (ret == NULL) { return NULL; } GEOSSetSRID_r(context, ret, srid); return ret; } static void* set_srid_data[1] = {GEOSSetSRID_r_with_clone}; typedef void* FuncGEOS_Yi_Y(void* context, void* a, int b); static char Yi_Y_dtypes[3] = {NPY_OBJECT, NPY_INT, NPY_OBJECT}; static void Yi_Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Yi_Y* func = (FuncGEOS_Yi_Y*)data; GEOSGeometry* in1 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(2); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } int in2 = *(int*)ip2; if (in1 == NULL) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = func(ctx, in1, in2); // NULL means: exception, but for some functions it may also indicate a // "missing value" (None) (GetPointN, GetInteriorRingN, GetGeometryN) // So: check the last_error before setting error state if ((geom_arr[i] == NULL) && (last_error[0] != 0)) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction Yi_Y_funcs[1] = {&Yi_Y_func}; /* Define the geom, geom -> geom functions (YY_Y) */ static void* intersection_data[1] = {GEOSIntersection_r}; static void* difference_data[1] = {GEOSDifference_r}; static void* symmetric_difference_data[1] = {GEOSSymDifference_r}; static void* union_data[1] = {GEOSUnion_r}; static void* shared_paths_data[1] = {GEOSSharedPaths_r}; typedef void* FuncGEOS_YY_Y(void* context, void* a, void* b); static char YY_Y_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT}; static void YY_Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YY_Y* func = (FuncGEOS_YY_Y*)data; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(2); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometries: return on error if (!get_geom(*(GeometryObject**)ip1, &in1) || !get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if ((in1 == NULL) || (in2 == NULL)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = func(ctx, in1, in2); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction YY_Y_funcs[1] = {&YY_Y_func}; /* Define the reducing geoms -> geom functions (Y_Y_reduce) */ static void* intersection_all_data[1] = {GEOSIntersection_r}; static void* symmetric_difference_all_data[1] = {GEOSSymDifference_r}; static char Y_Y_reduce_dtypes[2] = {NPY_OBJECT, NPY_OBJECT}; static void Y_Y_reduce_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YY_Y* func = (FuncGEOS_YY_Y*)data; GEOSGeometry* geom = NULL; GEOSGeometry* temp = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(1); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; SINGLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } GEOSGeometry* ret_ptr = NULL; SINGLE_COREDIM_LOOP_INNER { if (!get_geom(*(GeometryObject**)cp1, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } if (geom == NULL) { continue; } if (ret_ptr == NULL) { // clone first geometry we encounter (in case this gets returned) ret_ptr = GEOSGeom_clone_r(ctx, geom); } else { // subsequenct geometries temp = func(ctx, ret_ptr, geom); GEOSGeom_destroy_r(ctx, ret_ptr); ret_ptr = temp; if (ret_ptr == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } } } if (ret_ptr == NULL) { // dimension didn't have geometries (empty or all-None) ret_ptr = GEOSGeom_createEmptyCollection_r(ctx, 7); } geom_arr[i] = ret_ptr; } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[1], steps[1], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction Y_Y_reduce_funcs[1] = {&Y_Y_reduce_func}; /* Define the geom -> double functions (Y_d) */ static int GetX(void* context, void* a, double* b) { char typ = GEOSGeomTypeId_r(context, a); if (typ != 0) { *(double*)b = NPY_NAN; return 1; } else { return GEOSGeomGetX_r(context, a, b); } } static void* get_x_data[1] = {GetX}; static int GetY(void* context, void* a, double* b) { char typ = GEOSGeomTypeId_r(context, a); if (typ != 0) { *(double*)b = NPY_NAN; return 1; } else { return GEOSGeomGetY_r(context, a, b); } } static void* get_y_data[1] = {GetY}; #if GEOS_SINCE_3_7_0 static int GetZ(void* context, void* a, double* b) { char typ = GEOSGeomTypeId_r(context, a); if (typ != 0) { *(double*)b = NPY_NAN; return 1; } else { return GEOSGeomGetZ_r(context, a, b); } } static void* get_z_data[1] = {GetZ}; #endif static void* area_data[1] = {GEOSArea_r}; static void* length_data[1] = {GEOSLength_r}; #if GEOS_SINCE_3_6_0 static int GetPrecision(void* context, void* a, double* b) { // GEOS returns -1 on error; 0 indicates double precision; > 0 indicates a precision // grid size was set for this geometry. double out = GEOSGeom_getPrecision_r(context, a); if (out == -1) { return 0; } *(double*)b = out; return 1; } static void* get_precision_data[1] = {GetPrecision}; static int MinimumClearance(void* context, void* a, double* b) { // GEOSMinimumClearance deviates from the pattern of returning 0 on exception and 1 on // success for functions that return an int (it follows pattern for boolean functions // returning char 0/1 and 2 on exception) int retcode = GEOSMinimumClearance_r(context, a, b); if (retcode == 2) { return 0; } else { return 1; } } static void* minimum_clearance_data[1] = {MinimumClearance}; #endif #if GEOS_SINCE_3_8_0 static int GEOSMinimumBoundingRadius(void* context, GEOSGeometry* geom, double* radius) { GEOSGeometry* center = NULL; GEOSGeometry* ret = GEOSMinimumBoundingCircle_r(context, geom, radius, ¢er); if (ret == NULL) { return 0; // exception code } GEOSGeom_destroy_r(context, center); GEOSGeom_destroy_r(context, ret); return 1; // success code } static void* minimum_bounding_radius_data[1] = {GEOSMinimumBoundingRadius}; #endif typedef int FuncGEOS_Y_d(void* context, void* a, double* b); static char Y_d_dtypes[2] = {NPY_OBJECT, NPY_DOUBLE}; static void Y_d_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Y_d* func = (FuncGEOS_Y_d*)data; GEOSGeometry* in1 = NULL; GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (in1 == NULL) { *(double*)op1 = NPY_NAN; } else { /* let the GEOS function set op1; return on error */ if (func(ctx, in1, (npy_double*)op1) == 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction Y_d_funcs[1] = {&Y_d_func}; /* Define the geom -> int functions (Y_i) */ /* data values are GEOS func, GEOS error code, return value when input is None */ static void* get_type_id_func_tuple[3] = {GEOSGeomTypeId_r, (void*)-1, (void*)-1}; static void* get_type_id_data[1] = {get_type_id_func_tuple}; static void* get_dimensions_func_tuple[3] = {GEOSGeom_getDimensions_r, (void*)0, (void*)-1}; static void* get_dimensions_data[1] = {get_dimensions_func_tuple}; static void* get_coordinate_dimension_func_tuple[3] = {GEOSGeom_getCoordinateDimension_r, (void*)-1, (void*)-1}; static void* get_coordinate_dimension_data[1] = {get_coordinate_dimension_func_tuple}; static void* get_srid_func_tuple[3] = {GEOSGetSRID_r, (void*)0, (void*)-1}; static void* get_srid_data[1] = {get_srid_func_tuple}; static int GetNumPoints(void* context, void* geom, int n) { char typ = GEOSGeomTypeId_r(context, geom); if ((typ == 1) || (typ == 2)) { /* Linestring & Linearring */ return GEOSGeomGetNumPoints_r(context, geom); } else { return 0; } } static void* get_num_points_func_tuple[3] = {GetNumPoints, (void*)-1, (void*)0}; static void* get_num_points_data[1] = {get_num_points_func_tuple}; static int GetNumInteriorRings(void* context, void* geom) { char typ = GEOSGeomTypeId_r(context, geom); if (typ == 3) { /* Polygon */ return GEOSGetNumInteriorRings_r(context, geom); } else { return 0; } } static void* get_num_interior_rings_func_tuple[3] = {GetNumInteriorRings, (void*)-1, (void*)0}; static void* get_num_interior_rings_data[1] = {get_num_interior_rings_func_tuple}; static void* get_num_geometries_func_tuple[3] = {GEOSGetNumGeometries_r, (void*)-1, (void*)0}; static void* get_num_geometries_data[1] = {get_num_geometries_func_tuple}; static void* get_num_coordinates_func_tuple[3] = {GEOSGetNumCoordinates_r, (void*)-1, (void*)0}; static void* get_num_coordinates_data[1] = {get_num_coordinates_func_tuple}; typedef int FuncGEOS_Y_i(void* context, void* a); static char Y_i_dtypes[2] = {NPY_OBJECT, NPY_INT}; static void Y_i_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_Y_i* func = ((FuncGEOS_Y_i**)data)[0]; int errcode = (int)((int**)data)[1]; int none_value = (int)((int**)data)[2]; GEOSGeometry* in1 = NULL; int result; GEOS_INIT_THREADS; UNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (in1 == NULL) { /* None results in 0 for counting functions, -1 otherwise */ *(npy_int*)op1 = none_value; } else { result = func(ctx, in1); // Check last_error if the result equals errcode. // Otherwise we can't be sure if it is an exception if ((result == errcode) && (last_error[0] != 0)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } *(npy_int*)op1 = result; } } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction Y_i_funcs[1] = {&Y_i_func}; /* Define the geom, geom -> double functions (YY_d) */ static void* distance_data[1] = {GEOSDistance_r}; static void* hausdorff_distance_data[1] = {GEOSHausdorffDistance_r}; #if GEOS_SINCE_3_7_0 static int GEOSFrechetDistanceWrapped_r(void* context, void* a, void* b, double* c) { /* Handle empty geometries (they give segfaults) */ if (GEOSisEmpty_r(context, a) || GEOSisEmpty_r(context, b)) { *c = NPY_NAN; return 1; } return GEOSFrechetDistance_r(context, a, b, c); } static void* frechet_distance_data[1] = {GEOSFrechetDistanceWrapped_r}; #endif /* Project and ProjectNormalize don't return error codes. wrap them. */ static int GEOSProjectWrapped_r(void* context, void* a, void* b, double* c) { /* Handle empty points (they give segfaults (for b) or give exception (for a)) */ if (GEOSisEmpty_r(context, a) || GEOSisEmpty_r(context, b)) { *c = NPY_NAN; } else { *c = GEOSProject_r(context, a, b); } if (*c == -1.0) { return 0; } else { return 1; } } static void* line_locate_point_data[1] = {GEOSProjectWrapped_r}; static int GEOSProjectNormalizedWrapped_r(void* context, void* a, void* b, double* c) { double length; double distance; /* Handle empty points (they give segfaults (for b) or give exception (for a)) */ if (GEOSisEmpty_r(context, a) || GEOSisEmpty_r(context, b)) { *c = NPY_NAN; } else { /* Use custom implementation of GEOSProjectNormalized to overcome bug in older GEOS versions (https://trac.osgeo.org/geos/ticket/1058) */ if (GEOSLength_r(context, a, &length) != 1) { return 0; }; distance = GEOSProject_r(context, a, b); if (distance == -1.0) { return 0; } else { *c = distance / length; } } return 1; } static void* line_locate_point_normalized_data[1] = {GEOSProjectNormalizedWrapped_r}; typedef int FuncGEOS_YY_d(void* context, void* a, void* b, double* c); static char YY_d_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE}; static void YY_d_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YY_d* func = (FuncGEOS_YY_d*)data; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if ((in1 == NULL) || (in2 == NULL)) { /* in case of a missing value: return NaN */ *(double*)op1 = NPY_NAN; } else { /* let the GEOS function set op1; return on error */ if (func(ctx, in1, in2, (double*)op1) == 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } /* incase the outcome is 0.0, check the inputs for emptyness */ if (*op1 == 0.0) { if (GEOSisEmpty_r(ctx, in1) || GEOSisEmpty_r(ctx, in2)) { *(double*)op1 = NPY_NAN; } } } } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction YY_d_funcs[1] = {&YY_d_func}; /* Define the geom, geom, double -> double functions (YYd_d) */ static void* hausdorff_distance_densify_data[1] = {GEOSHausdorffDistanceDensify_r}; #if GEOS_SINCE_3_7_0 static void* frechet_distance_densify_data[1] = {GEOSFrechetDistanceDensify_r}; #endif typedef int FuncGEOS_YYd_d(void* context, void* a, void* b, double c, double* d); static char YYd_d_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE, NPY_DOUBLE}; static void YYd_d_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YYd_d* func = (FuncGEOS_YYd_d*)data; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } double in3 = *(double*)ip3; if ((in1 == NULL) || (in2 == NULL) || npy_isnan(in3) || GEOSisEmpty_r(ctx, in1) || GEOSisEmpty_r(ctx, in2)) { *(double*)op1 = NPY_NAN; } else { /* let the GEOS function set op1; return on error */ if (func(ctx, in1, in2, in3, (double*)op1) == 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction YYd_d_funcs[1] = {&YYd_d_func}; #if GEOS_SINCE_3_9_0 /* Define the geom, geom, double -> geom functions (YYd_Y) */ static void* intersection_prec_data[1] = {GEOSIntersectionPrec_r}; static void* difference_prec_data[1] = {GEOSDifferencePrec_r}; static void* symmetric_difference_prec_data[1] = {GEOSSymDifferencePrec_r}; static void* union_prec_data[1] = {GEOSUnionPrec_r}; typedef void* FuncGEOS_YYd_Y(void* context, void* a, void* b, double c); static char YYd_Y_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE, NPY_OBJECT}; static void YYd_Y_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { FuncGEOS_YYd_Y* func = (FuncGEOS_YYd_Y*)data; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOSGeometry** geom_arr; // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometries: return on error if (!get_geom(*(GeometryObject**)ip1, &in1) || !get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } double in3 = *(double*)ip3; if ((in1 == NULL) || (in2 == NULL) || npy_isnan(in3)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = func(ctx, in1, in2, in3); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction YYd_Y_funcs[1] = {&YYd_Y_func}; #endif /* Define functions with unique call signatures */ static char box_dtypes[6] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_BOOL, NPY_OBJECT}; static void box_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(5); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * n); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; for (i = 0; i < n; i++, ip1 += is1, ip2 += is2, ip3 += is3, ip4 += is4, ip5 += is5) { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } geom_arr[i] = create_box(ctx, *(double*)ip1, *(double*)ip2, *(double*)ip3, *(double*)ip4, *(char*)ip5); if (geom_arr[i] == NULL) { // result will be NULL for any nan coordinates, which is OK; // otherwise raise an error if (!(npy_isnan(*(double*)ip1) || npy_isnan(*(double*)ip2) || npy_isnan(*(double*)ip3) || npy_isnan(*(double*)ip4))) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[5], steps[5], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction box_funcs[1] = {&box_func}; static void* null_data[1] = {NULL}; static char buffer_inner(void* ctx, GEOSBufferParams* params, void* ip1, void* ip2, GEOSGeometry** geom_arr, npy_intp i) { GEOSGeometry* in1 = NULL; /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { return PGERR_NOT_A_GEOMETRY; } double in2 = *(double*)ip2; /* handle NULL geometries or NaN buffer width */ if ((in1 == NULL) || npy_isnan(in2)) { geom_arr[i] = NULL; } else { geom_arr[i] = GEOSBufferWithParams_r(ctx, in1, params, in2); if (geom_arr[i] == NULL) { return PGERR_GEOS_EXCEPTION; } } return PGERR_SUCCESS; } static char buffer_dtypes[8] = {NPY_OBJECT, NPY_DOUBLE, NPY_INT, NPY_INT, NPY_INT, NPY_DOUBLE, NPY_BOOL, NPY_OBJECT}; static void buffer_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4], *ip6 = args[5], *ip7 = args[6]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4], is6 = steps[5], is7 = steps[6]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(7); if ((is3 != 0) || (is4 != 0) || (is5 != 0) || (is6 != 0) || (is7 != 0)) { PyErr_Format(PyExc_ValueError, "Buffer function called with non-scalar parameters"); return; } // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * n); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; GEOSBufferParams* params = GEOSBufferParams_create_r(ctx); if (params != 0) { if (!GEOSBufferParams_setQuadrantSegments_r(ctx, params, *(int*)ip3)) { errstate = PGERR_GEOS_EXCEPTION; } if (!GEOSBufferParams_setEndCapStyle_r(ctx, params, *(int*)ip4)) { errstate = PGERR_GEOS_EXCEPTION; } if (!GEOSBufferParams_setJoinStyle_r(ctx, params, *(int*)ip5)) { errstate = PGERR_GEOS_EXCEPTION; } if (!GEOSBufferParams_setMitreLimit_r(ctx, params, *(double*)ip6)) { errstate = PGERR_GEOS_EXCEPTION; } if (!GEOSBufferParams_setSingleSided_r(ctx, params, *(npy_bool*)ip7)) { errstate = PGERR_GEOS_EXCEPTION; } } else { errstate = PGERR_GEOS_EXCEPTION; } if (errstate == PGERR_SUCCESS) { for (i = 0; i < n; i++, ip1 += is1, ip2 += is2) { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } errstate = buffer_inner(ctx, params, ip1, ip2, geom_arr, i); if (errstate != PGERR_SUCCESS) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } if (params != 0) { GEOSBufferParams_destroy_r(ctx, params); } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[7], steps[7], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction buffer_funcs[1] = {&buffer_func}; static char offset_curve_dtypes[6] = {NPY_OBJECT, NPY_DOUBLE, NPY_INT, NPY_INT, NPY_DOUBLE, NPY_OBJECT}; static void offset_curve_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry** geom_arr; GEOSGeometry* in1 = NULL; CHECK_NO_INPLACE_OUTPUT(5); if ((is3 != 0) || (is4 != 0) || (is5 != 0)) { PyErr_Format(PyExc_ValueError, "Offset curve function called with non-scalar parameters"); return; } double width; int quadsegs = *(int*)ip3; int joinStyle = *(int*)ip4; double mitreLimit = *(double*)ip5; // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * n); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; for (i = 0; i < n; i++, ip1 += is1, ip2 += is2) { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } width = *(double*)ip2; if ((in1 == NULL) || npy_isnan(width)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSOffsetCurve_r(ctx, in1, width, quadsegs, joinStyle, mitreLimit); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[5], steps[5], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction offset_curve_funcs[1] = {&offset_curve_func}; static char snap_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE, NPY_OBJECT}; static void snap_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *in1 = NULL, *in2 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(3); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1) || !get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } double in3 = *(double*)ip3; if ((in1 == NULL) || (in2 == NULL) || npy_isnan(in3)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSSnap_r(ctx, in1, in2, in3); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction snap_funcs[1] = {&snap_func}; #if GEOS_SINCE_3_11_0 static char concave_hull_dtypes[4] = {NPY_OBJECT, NPY_DOUBLE, NPY_BOOL, NPY_OBJECT}; static void concave_hull_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry** geom_arr; GEOSGeometry* in1 = NULL; CHECK_NO_INPLACE_OUTPUT(3); if ((is2 != 0) || (is3 != 0)) { PyErr_Format(PyExc_ValueError, "concave_hull function called with non-scalar parameters"); return; } double ratio = *(double*)ip2; unsigned int allowHoles = (unsigned int)(*(npy_bool*)ip3); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * n); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; for (i = 0; i < n; i++, ip1 += is1) { /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (in1 == NULL) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSConcaveHull_r(ctx, in1, ratio, allowHoles); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction concave_hull_funcs[1] = {&concave_hull_func}; #endif // GEOS_SINCE_3_11_0 static char clip_by_rect_dtypes[6] = {NPY_OBJECT, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_OBJECT}; static void clip_by_rect_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry** geom_arr; GEOSGeometry* in1 = NULL; CHECK_NO_INPLACE_OUTPUT(5); if ((is2 != 0) || (is3 != 0) || (is4 != 0) || (is5 != 0)) { PyErr_Format(PyExc_ValueError, "clip_by_rect function called with non-scalar parameters"); return; } double xmin = *(double*)ip2; double ymin = *(double*)ip3; double xmax = *(double*)ip4; double ymax = *(double*)ip5; // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * n); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; for (i = 0; i < n; i++, ip1 += is1) { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } /* get the geometry: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (in1 == NULL) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSClipByRect_r(ctx, in1, xmin, ymin, xmax, ymax); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[5], steps[5], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction clip_by_rect_funcs[1] = {&clip_by_rect_func}; static char equals_exact_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE, NPY_BOOL}; static void equals_exact_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *in1 = NULL, *in2 = NULL; double in3; npy_bool ret; GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } in3 = *(double*)ip3; if ((in1 == NULL) || (in2 == NULL) || npy_isnan(in3)) { /* return 0 (False) for missing values */ ret = 0; } else { ret = GEOSEqualsExact_r(ctx, in1, in2, in3); if ((ret != 0) && (ret != 1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction equals_exact_funcs[1] = {&equals_exact_func}; #if GEOS_SINCE_3_10_0 static char dwithin_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_DOUBLE, NPY_BOOL}; static void dwithin_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *in1 = NULL, *in2 = NULL; GEOSPreparedGeometry* in1_prepared = NULL; double in3; npy_bool ret; GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom_with_prepared(*(GeometryObject**)ip1, &in1, &in1_prepared)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } in3 = *(double*)ip3; if ((in1 == NULL) || (in2 == NULL) || npy_isnan(in3)) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { if (in1_prepared == NULL) { /* call the GEOS function */ ret = GEOSDistanceWithin_r(ctx, in1, in2, in3); } else { /* call the prepared GEOS function */ ret = GEOSPreparedDistanceWithin_r(ctx, in1_prepared, in2, in3); } /* return for illegal values */ if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction dwithin_funcs[1] = {&dwithin_func}; #endif // GEOS_SINCE_3_10_0 static char delaunay_triangles_dtypes[4] = {NPY_OBJECT, NPY_DOUBLE, NPY_BOOL, NPY_OBJECT}; static void delaunay_triangles_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* in1 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(3); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } double in2 = *(double*)ip2; npy_bool in3 = *(npy_bool*)ip3; if ((in1 == NULL) || npy_isnan(in2)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSDelaunayTriangulation_r(ctx, in1, in2, (int)in3); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction delaunay_triangles_funcs[1] = {&delaunay_triangles_func}; static char voronoi_polygons_dtypes[5] = {NPY_OBJECT, NPY_DOUBLE, NPY_OBJECT, NPY_BOOL, NPY_OBJECT}; static void voronoi_polygons_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *in1 = NULL, *in3 = NULL; GEOSGeometry** geom_arr; CHECK_NO_INPLACE_OUTPUT(4); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; QUATERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1) || !get_geom(*(GeometryObject**)ip3, &in3)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } double in2 = *(double*)ip2; npy_bool in4 = *(npy_bool*)ip4; if ((in1 == NULL) || npy_isnan(in2)) { /* propagate NULL geometries; in3 = NULL is actually supported */ geom_arr[i] = NULL; } else { geom_arr[i] = GEOSVoronoiDiagram_r(ctx, in1, in3, in2, (int)in4); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[4], steps[4], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction voronoi_polygons_funcs[1] = {&voronoi_polygons_func}; static char is_valid_reason_dtypes[2] = {NPY_OBJECT, NPY_OBJECT}; static void is_valid_reason_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char* reason; GEOSGeometry* in1 = NULL; GEOS_INIT; UNARY_LOOP { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } PyObject** out = (PyObject**)op1; /* get the geometry return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (in1 == NULL) { /* Missing geometries give None */ Py_XDECREF(*out); Py_INCREF(Py_None); *out = Py_None; } else { reason = GEOSisValidReason_r(ctx, in1); if (reason == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } /* convert to python string and set to out */ Py_XDECREF(*out); *out = PyUnicode_FromString(reason); GEOSFree_r(ctx, reason); } } finish: GEOS_FINISH; } static PyUFuncGenericFunction is_valid_reason_funcs[1] = {&is_valid_reason_func}; static char relate_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT}; static void relate_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char* pattern; GEOSGeometry *in1 = NULL, *in2 = NULL; GEOS_INIT; BINARY_LOOP { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } PyObject** out = (PyObject**)op1; /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if ((in1 == NULL) || (in2 == NULL)) { /* Missing geometries give None */ Py_XDECREF(*out); Py_INCREF(Py_None); *out = Py_None; } else { pattern = GEOSRelate_r(ctx, in1, in2); if (pattern == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } /* convert to python string and set to out */ Py_XDECREF(*out); *out = PyUnicode_FromString(pattern); GEOSFree_r(ctx, pattern); } } finish: GEOS_FINISH; } static PyUFuncGenericFunction relate_funcs[1] = {&relate_func}; static char relate_pattern_dtypes[4] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT, NPY_BOOL}; static void relate_pattern_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *in1 = NULL, *in2 = NULL; const char* pattern = NULL; npy_bool ret; /* get the pattern argument (only deal with scalar for now) */ char* ip3 = args[2]; npy_intp is3 = steps[2]; if (is3 != 0) { PyErr_Format(PyExc_ValueError, "pattern keyword only supports scalar argument"); return; } PyObject* in3 = *(PyObject**)ip3; if (PyUnicode_Check(in3)) { pattern = PyUnicode_AsUTF8(in3); if (pattern == NULL) { /* error happened in PyUnicode_AsUTF8, error already set by Python */ return; } } else { PyErr_Format(PyExc_TypeError, "pattern keyword expected string, got %s", Py_TYPE(in3)->tp_name); return; } GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* get the geometries: return on error */ if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } /* ip3 is already handled above */ if ((in1 == NULL) || (in2 == NULL)) { /* in case of a missing value: return 0 (False) */ ret = 0; } else { ret = GEOSRelatePattern_r(ctx, in1, in2, pattern); if (ret == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } *(npy_bool*)op1 = ret; } finish: GEOS_FINISH_THREADS; } static PyUFuncGenericFunction relate_pattern_funcs[1] = {&relate_pattern_func}; static char polygonize_dtypes[2] = {NPY_OBJECT, NPY_OBJECT}; static void polygonize_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* geom = NULL; unsigned int n_geoms; GEOS_INIT; GEOSGeometry** geoms = malloc(sizeof(void*) * dimensions[1]); if (geoms == NULL) { errstate = PGERR_NO_MALLOC; goto finish; } SINGLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } n_geoms = 0; SINGLE_COREDIM_LOOP_INNER { if (!get_geom(*(GeometryObject**)cp1, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (geom == NULL) { continue; } geoms[n_geoms] = geom; n_geoms++; } GEOSGeometry* ret_ptr = GEOSPolygonize_r(ctx, geoms, n_geoms); if (ret_ptr == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } OUTPUT_Y; } finish: if (geoms != NULL) { free(geoms); } GEOS_FINISH; } static PyUFuncGenericFunction polygonize_funcs[1] = {&polygonize_func}; static char polygonize_full_dtypes[5] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT, NPY_OBJECT, NPY_OBJECT}; static void polygonize_full_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* geom = NULL; GEOSGeometry* geom_copy = NULL; unsigned int n_geoms; GEOSGeometry* collection = NULL; GEOSGeometry* cuts = NULL; GEOSGeometry* dangles = NULL; GEOSGeometry* invalidRings = NULL; GEOS_INIT; GEOSGeometry** geoms = malloc(sizeof(void*) * dimensions[1]); if (geoms == NULL) { errstate = PGERR_NO_MALLOC; goto finish; } SINGLE_COREDIM_LOOP_OUTER_NOUT4 { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } n_geoms = 0; SINGLE_COREDIM_LOOP_INNER { if (!get_geom(*(GeometryObject**)cp1, &geom)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } if (geom == NULL) { continue; } // need to copy the input geometries, because the Collection takes ownership geom_copy = GEOSGeom_clone_r(ctx, geom); if (geom_copy == NULL) { // if something went wrong before creating the collection, destroy previously // cloned geoms for (i = 0; i < n_geoms; i++) { GEOSGeom_destroy_r(ctx, geoms[i]); } errstate = PGERR_GEOS_EXCEPTION; goto finish; } geoms[n_geoms] = geom_copy; n_geoms++; } collection = GEOSGeom_createCollection_r(ctx, GEOS_GEOMETRYCOLLECTION, geoms, n_geoms); if (collection == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } GEOSGeometry* ret_ptr = GEOSPolygonize_full_r(ctx, collection, &cuts, &dangles, &invalidRings); if (ret_ptr == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } OUTPUT_Y_I(1, ret_ptr); OUTPUT_Y_I(2, cuts); OUTPUT_Y_I(3, dangles); OUTPUT_Y_I(4, invalidRings); GEOSGeom_destroy_r(ctx, collection); collection = NULL; } finish: if (collection != NULL) { GEOSGeom_destroy_r(ctx, collection); } if (geoms != NULL) { free(geoms); } GEOS_FINISH; } static PyUFuncGenericFunction polygonize_full_funcs[1] = {&polygonize_full_func}; static char shortest_line_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT}; static void shortest_line_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* in1 = NULL; GEOSGeometry* in2 = NULL; GEOSPreparedGeometry* in1_prepared = NULL; GEOSGeometry** geom_arr; GEOSCoordSequence* coord_seq = NULL; CHECK_NO_INPLACE_OUTPUT(2); // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; BINARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } /* get the geometries: return on error */ if (!get_geom_with_prepared(*(GeometryObject**)ip1, &in1, &in1_prepared)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (!get_geom(*(GeometryObject**)ip2, &in2)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if ((in1 == NULL) || (in2 == NULL) || GEOSisEmpty_r(ctx, in1) || GEOSisEmpty_r(ctx, in2)) { // in case of a missing value or empty geometry: return NULL (None) // GEOSNearestPoints_r returns NULL for empty geometries // but this is not distinguishable from an actual error, so we handle this ourselves geom_arr[i] = NULL; continue; } #if GEOS_SINCE_3_9_0 if (in1_prepared != NULL) { coord_seq = GEOSPreparedNearestPoints_r(ctx, in1_prepared, in2); } else { coord_seq = GEOSNearestPoints_r(ctx, in1, in2); } #else coord_seq = GEOSNearestPoints_r(ctx, in1, in2); #endif if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } geom_arr[i] = GEOSGeom_createLineString_r(ctx, coord_seq); // Note: coordinate sequence is owned by linestring; if linestring fails to // construct, it will automatically clean up the coordinate sequence if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction shortest_line_funcs[1] = {&shortest_line_func}; #if GEOS_SINCE_3_6_0 static char set_precision_dtypes[4] = {NPY_OBJECT, NPY_DOUBLE, NPY_INT, NPY_OBJECT}; static void set_precision_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry* in1 = NULL; GEOSGeometry** geom_arr; int flags; CHECK_NO_INPLACE_OUTPUT(3); /* preserve topology flag * flags: * - 0: default (from GEOS 3.10 this is named GEOS_PREC_VALID_OUTPUT) * - 1: GEOS_PREC_NO_TOPO * - 2: GEOS_PREC_KEEP_COLLAPSED */ if (steps[2] != 0) { PyErr_Format(PyExc_ValueError, "set_precision function called with non-scalar mode"); return; } flags = *(int*)args[2]; if (!((flags == 0) || (flags == GEOS_PREC_NO_TOPO) || (flags == GEOS_PREC_KEEP_COLLAPSED))) { PyErr_Format(PyExc_ValueError, "set_precision function called with illegal mode"); return; } // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; TERNARY_LOOP { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } // get the geometry: return on error if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } // grid size double in2 = *(double*)ip2; if ((in1 == NULL) || npy_isnan(in2)) { // in case of a missing value: return NULL (None) geom_arr[i] = NULL; } else { geom_arr[i] = GEOSGeom_setPrecision_r(ctx, in1, in2, flags); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } } } GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[3], steps[3], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction set_precision_funcs[1] = {&set_precision_func}; #endif /* define double -> geometry construction functions */ static char points_dtypes[2] = {NPY_DOUBLE, NPY_OBJECT}; static void points_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSCoordSequence* coord_seq = NULL; GEOSGeometry** geom_arr; // check the ordinate dimension before calling GEOSCoordSeq_create_r if (dimensions[1] < 2 || dimensions[1] > 3) { PyErr_Format(PyExc_ValueError, "The ordinate (last) dimension should be 2 or 3, got %ld", dimensions[1]); return; } // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; SINGLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } coord_seq = GEOSCoordSeq_create_r(ctx, 1, n_c1); if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } SINGLE_COREDIM_LOOP_INNER { if (!GEOSCoordSeq_setOrdinate_r(ctx, coord_seq, 0, i_c1, *(double*)cp1)) { errstate = PGERR_GEOS_EXCEPTION; GEOSCoordSeq_destroy_r(ctx, coord_seq); destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } } geom_arr[i] = GEOSGeom_createPoint_r(ctx, coord_seq); // Note: coordinate sequence is owned by point; if point fails to construct, it will // automatically clean up the coordinate sequence if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[1], steps[1], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction points_funcs[1] = {&points_func}; static char linestrings_dtypes[2] = {NPY_DOUBLE, NPY_OBJECT}; static void linestrings_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSCoordSequence* coord_seq = NULL; GEOSGeometry** geom_arr; // check the ordinate dimension before calling coordseq_from_buffer if (dimensions[2] < 2 || dimensions[2] > 3) { PyErr_Format(PyExc_ValueError, "The ordinate (last) dimension should be 2 or 3, got %ld", dimensions[2]); return; } // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; DOUBLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } coord_seq = coordseq_from_buffer(ctx, (double*)ip1, n_c1, n_c2, 0, cs1, cs2); if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } geom_arr[i] = GEOSGeom_createLineString_r(ctx, coord_seq); // Note: coordinate sequence is owned by linestring; if linestring fails to construct, // it will automatically clean up the coordinate sequence if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[1], steps[1], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction linestrings_funcs[1] = {&linestrings_func}; static char linearrings_dtypes[2] = {NPY_DOUBLE, NPY_OBJECT}; static void linearrings_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSCoordSequence* coord_seq = NULL; GEOSGeometry** geom_arr; char ring_closure = 0; double first_coord, last_coord; // check the ordinate dimension before calling coordseq_from_buffer if (dimensions[2] < 2 || dimensions[2] > 3) { PyErr_Format(PyExc_ValueError, "The ordinate (last) dimension should be 2 or 3, got %ld", dimensions[2]); return; } // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr); GEOS_INIT_THREADS; DOUBLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } /* check if first and last coords are equal; duplicate if necessary */ ring_closure = 0; if (n_c1 == 3) { ring_closure = 1; } else { DOUBLE_COREDIM_LOOP_INNER_2 { first_coord = *(double*)(ip1 + i_c2 * cs2); last_coord = *(double*)(ip1 + (n_c1 - 1) * cs1 + i_c2 * cs2); if (first_coord != last_coord) { ring_closure = 1; break; } } } /* the minimum number of coordinates in a linearring is 4 */ if (n_c1 + ring_closure < 4) { errstate = PGERR_LINEARRING_NCOORDS; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } /* fill the coordinate sequence */ coord_seq = coordseq_from_buffer(ctx, (double*)ip1, n_c1, n_c2, ring_closure, cs1, cs2); if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } geom_arr[i] = GEOSGeom_createLinearRing_r(ctx, coord_seq); // Note: coordinate sequence is owned by linearring; if linearring fails to construct, // it will automatically clean up the coordinate sequence if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[1], steps[1], dimensions[0]); } free(geom_arr); } static PyUFuncGenericFunction linearrings_funcs[1] = {&linearrings_func}; static char polygons_dtypes[3] = {NPY_OBJECT, NPY_OBJECT, NPY_OBJECT}; static void polygons_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *hole, *shell, *hole_copy, *shell_copy; GEOSGeometry **holes, **geom_arr; int geom_type; int n_holes; // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr) // allocate a temporary array to store holes holes = malloc(sizeof(void*) * dimensions[1]); CHECK_ALLOC(holes) GEOS_INIT_THREADS; BINARY_SINGLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (!get_geom(*(GeometryObject**)ip1, &shell)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); break; } if (shell == NULL) { // output empty polygon if shell is None (ignoring holes) geom_arr[i] = GEOSGeom_createEmptyPolygon_r(ctx); if (geom_arr[i] == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; }; continue; } geom_type = GEOSGeomTypeId_r(ctx, shell); // Pre-emptively check the geometry type (https://trac.osgeo.org/geos/ticket/1111) if (geom_type == -1) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; } else if (geom_type != 2) { errstate = PGERR_GEOMETRY_TYPE; destroy_geom_arr(ctx, geom_arr, i - 1); break; } n_holes = 0; cp1 = ip2; BINARY_SINGLE_COREDIM_LOOP_INNER { if (!get_geom(*(GeometryObject**)cp1, &hole)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, holes, n_holes - 1); goto finish; } if (hole == NULL) { continue; } // Pre-emptively check the geometry type (https://trac.osgeo.org/geos/ticket/1111) geom_type = GEOSGeomTypeId_r(ctx, hole); if (geom_type == -1) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, holes, n_holes - 1); goto finish; } else if (geom_type != 2) { errstate = PGERR_GEOMETRY_TYPE; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, holes, n_holes - 1); goto finish; } hole_copy = GEOSGeom_clone_r(ctx, hole); if (hole_copy == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, holes, n_holes - 1); goto finish; } holes[n_holes] = hole_copy; n_holes++; } shell_copy = GEOSGeom_clone_r(ctx, shell); if (shell_copy == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, holes, n_holes - 1); break; } geom_arr[i] = GEOSGeom_createPolygon_r(ctx, shell_copy, holes, n_holes); if (geom_arr[i] == NULL) { // We will have a memory leak now (https://trac.osgeo.org/geos/ticket/1111) // but we have covered all known cases that GEOS would error by pre-emptively // checking if all inputs are linearrings. errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; }; } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); if (holes != NULL) { free(holes); } } static PyUFuncGenericFunction polygons_funcs[1] = {&polygons_func}; static char create_collection_dtypes[3] = {NPY_OBJECT, NPY_INT, NPY_OBJECT}; static void create_collection_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *g, *g_copy; int n_geoms, type, actual_type, expected_type, alt_expected_type; GEOSGeometry **temp_geoms, **geom_arr; // allocate a temporary array to store output GEOSGeometry objects geom_arr = malloc(sizeof(void*) * dimensions[0]); CHECK_ALLOC(geom_arr) // allocate a temporary array to store geometries to put in a collection temp_geoms = malloc(sizeof(void*) * dimensions[1]); CHECK_ALLOC(temp_geoms) GEOS_INIT_THREADS; BINARY_SINGLE_COREDIM_LOOP_OUTER { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } type = *(int*)ip2; switch (type) { case GEOS_MULTIPOINT: expected_type = GEOS_POINT; alt_expected_type = -1; break; case GEOS_MULTILINESTRING: expected_type = GEOS_LINESTRING; alt_expected_type = GEOS_LINEARRING; break; case GEOS_MULTIPOLYGON: expected_type = GEOS_POLYGON; alt_expected_type = -1; break; case GEOS_GEOMETRYCOLLECTION: expected_type = -1; alt_expected_type = -1; break; default: errstate = PGERR_GEOMETRY_TYPE; destroy_geom_arr(ctx, geom_arr, i - 1); goto finish; } n_geoms = 0; cp1 = ip1; BINARY_SINGLE_COREDIM_LOOP_INNER { if (!get_geom(*(GeometryObject**)cp1, &g)) { errstate = PGERR_NOT_A_GEOMETRY; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, temp_geoms, n_geoms - 1); goto finish; } if (g == NULL) { continue; } if (expected_type != -1) { actual_type = GEOSGeomTypeId_r(ctx, g); if (actual_type == -1) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, temp_geoms, n_geoms - 1); goto finish; } if ((actual_type != expected_type) && (actual_type != alt_expected_type)) { errstate = PGERR_GEOMETRY_TYPE; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, temp_geoms, n_geoms - 1); goto finish; } } g_copy = GEOSGeom_clone_r(ctx, g); if (g_copy == NULL) { errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); destroy_geom_arr(ctx, temp_geoms, n_geoms - 1); goto finish; } temp_geoms[n_geoms] = g_copy; n_geoms++; } geom_arr[i] = GEOSGeom_createCollection_r(ctx, type, temp_geoms, n_geoms); if (geom_arr[i] == NULL) { // We may have a memory leak now (https://trac.osgeo.org/geos/ticket/1111) // but we have covered all known cases that GEOS would error by pre-emptively // checking if all inputs are the correct geometry types. errstate = PGERR_GEOS_EXCEPTION; destroy_geom_arr(ctx, geom_arr, i - 1); break; }; } finish: GEOS_FINISH_THREADS; // fill the numpy array with PyObjects while holding the GIL if (errstate == PGERR_SUCCESS) { geom_arr_to_npy(geom_arr, args[2], steps[2], dimensions[0]); } free(geom_arr); if (temp_geoms != NULL) { free(temp_geoms); } } static PyUFuncGenericFunction create_collection_funcs[1] = {&create_collection_func}; static char bounds_dtypes[2] = {NPY_OBJECT, NPY_DOUBLE}; static void bounds_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { GEOSGeometry *envelope = NULL, *in1; const GEOSGeometry* ring; const GEOSCoordSequence* coord_seq; int size; char *ip1 = args[0], *op1 = args[1]; double *x1, *y1, *x2, *y2; GEOS_INIT_THREADS; npy_intp is1 = steps[0], os1 = steps[1], cs1 = steps[2]; npy_intp n = dimensions[0], i; for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS_THREADS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } /* get the 4 (pointers to) the bbox values from the "core stride 1" (cs1) */ x1 = (double*)(op1); y1 = (double*)(op1 + cs1); x2 = (double*)(op1 + 2 * cs1); y2 = (double*)(op1 + 3 * cs1); if (in1 == NULL) { /* no geometry => bbox becomes (nan, nan, nan, nan) */ *x1 = *y1 = *x2 = *y2 = NPY_NAN; } else { #if GEOS_SINCE_3_11_0 if (GEOSisEmpty_r(ctx, in1)) { *x1 = *y1 = *x2 = *y2 = NPY_NAN; } else { if (!GEOSGeom_getExtent_r(ctx, in1, x1, y1, x2, y2)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } #elif GEOS_SINCE_3_7_0 if (GEOSisEmpty_r(ctx, in1)) { *x1 = *y1 = *x2 = *y2 = NPY_NAN; } else { if (!GEOSGeom_getXMin_r(ctx, in1, x1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSGeom_getYMin_r(ctx, in1, y1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSGeom_getXMax_r(ctx, in1, x2)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSGeom_getYMax_r(ctx, in1, y2)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } #else /* construct the envelope */ envelope = GEOSEnvelope_r(ctx, in1); if (envelope == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } size = GEOSGetNumCoordinates_r(ctx, envelope); /* get the bbox depending on the number of coordinates in the envelope */ if (size == 0) { /* Envelope is empty */ *x1 = *y1 = *x2 = *y2 = NPY_NAN; } else if (size == 1) { /* Envelope is a point */ if (!GEOSGeomGetX_r(ctx, envelope, x1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSGeomGetY_r(ctx, envelope, y1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } *x2 = *x1; *y2 = *y1; } else if (size == 5) { /* Envelope is a box */ ring = GEOSGetExteriorRing_r(ctx, envelope); if (ring == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } coord_seq = GEOSGeom_getCoordSeq_r(ctx, ring); if (coord_seq == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSCoordSeq_getX_r(ctx, coord_seq, 0, x1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSCoordSeq_getY_r(ctx, coord_seq, 0, y1)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSCoordSeq_getX_r(ctx, coord_seq, 2, x2)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (!GEOSCoordSeq_getY_r(ctx, coord_seq, 2, y2)) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else { errstate = PGERR_GEOMETRY_TYPE; goto finish; } GEOSGeom_destroy_r(ctx, envelope); envelope = NULL; #endif } } finish: if (envelope != NULL) { GEOSGeom_destroy_r(ctx, envelope); } GEOS_FINISH_THREADS; } static PyUFuncGenericFunction bounds_funcs[1] = {&bounds_func}; /* Define the object -> geom functions (O_Y) */ static char from_wkb_dtypes[3] = {NPY_OBJECT, NPY_UINT8, NPY_OBJECT}; static void from_wkb_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *op1 = args[2]; npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2]; PyObject* in1; npy_uint8 on_invalid = *(npy_uint8*)ip2; npy_intp n = dimensions[0]; npy_intp i; GEOSWKBReader* reader; unsigned char* wkb; GEOSGeometry* ret_ptr; Py_ssize_t size; char is_hex; if ((is2 != 0)) { PyErr_Format(PyExc_ValueError, "from_wkb function called with non-scalar parameters"); return; } GEOS_INIT; /* Create the WKB reader */ reader = GEOSWKBReader_create_r(ctx); if (reader == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* ip1 is pointer to array element PyObject* */ in1 = *(PyObject**)ip1; if (in1 == Py_None) { /* None in the input propagates to the output */ ret_ptr = NULL; } else { /* Cast the PyObject (only bytes) to char* */ if (PyBytes_Check(in1)) { size = PyBytes_Size(in1); wkb = (unsigned char*)PyBytes_AsString(in1); if (wkb == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else if (PyUnicode_Check(in1)) { wkb = (unsigned char*)PyUnicode_AsUTF8AndSize(in1, &size); if (wkb == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else { PyErr_Format(PyExc_TypeError, "Expected bytes or string, got %s", Py_TYPE(in1)->tp_name); goto finish; } /* Check if this is a HEX WKB */ if (size != 0) { is_hex = ((wkb[0] == 48) || (wkb[0] == 49)); } else { is_hex = 0; } /* Read the WKB */ if (is_hex) { ret_ptr = GEOSWKBReader_readHEX_r(ctx, reader, wkb, size); } else { ret_ptr = GEOSWKBReader_read_r(ctx, reader, wkb, size); } if (ret_ptr == NULL) { if (on_invalid == 2) { // raise exception errstate = PGERR_GEOS_EXCEPTION; goto finish; } else if (on_invalid == 1) { // raise warning, return None errstate = PGWARN_INVALID_WKB; } // else: return None, no warning } } OUTPUT_Y; } finish: GEOSWKBReader_destroy_r(ctx, reader); GEOS_FINISH; } static PyUFuncGenericFunction from_wkb_funcs[1] = {&from_wkb_func}; static char from_wkt_dtypes[3] = {NPY_OBJECT, NPY_UINT8, NPY_OBJECT}; static void from_wkt_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *op1 = args[2]; npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2]; PyObject* in1; npy_uint8 on_invalid = *(npy_uint8*)ip2; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry* ret_ptr; GEOSWKTReader* reader; const char* wkt; if ((is2 != 0)) { PyErr_Format(PyExc_ValueError, "from_wkt function called with non-scalar parameters"); return; } GEOS_INIT; /* Create the WKT reader */ reader = GEOSWKTReader_create_r(ctx); if (reader == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* ip1 is pointer to array element PyObject* */ in1 = *(PyObject**)ip1; if (in1 == Py_None) { /* None in the input propagates to the output */ ret_ptr = NULL; } else { /* Cast the PyObject (bytes or str) to char* */ if (PyBytes_Check(in1)) { wkt = PyBytes_AsString(in1); if (wkt == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else if (PyUnicode_Check(in1)) { wkt = PyUnicode_AsUTF8(in1); if (wkt == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else { PyErr_Format(PyExc_TypeError, "Expected bytes or string, got %s", Py_TYPE(in1)->tp_name); goto finish; } /* Read the WKT */ ret_ptr = GEOSWKTReader_read_r(ctx, reader, wkt); if (ret_ptr == NULL) { if (on_invalid == 2) { // raise exception errstate = PGERR_GEOS_EXCEPTION; goto finish; } else if (on_invalid == 1) { // raise warning, return None errstate = PGWARN_INVALID_WKT; } // else: return None, no warning } } OUTPUT_Y; } finish: GEOSWKTReader_destroy_r(ctx, reader); GEOS_FINISH; } static PyUFuncGenericFunction from_wkt_funcs[1] = {&from_wkt_func}; static char to_wkb_dtypes[7] = {NPY_OBJECT, NPY_BOOL, NPY_INT, NPY_INT, NPY_BOOL, NPY_INT, NPY_OBJECT}; static void to_wkb_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4], *ip6 = args[5], *op1 = args[6]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4], is6 = steps[5], os1 = steps[6]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry *in1, *temp_geom; GEOSWKBWriter* writer; unsigned char* wkb; size_t size; #if !GEOS_SINCE_3_9_0 char has_empty; #endif // !GEOS_SINCE_3_9_0 if ((is2 != 0) || (is3 != 0) || (is4 != 0) || (is5 != 0) || (is6 != 0)) { PyErr_Format(PyExc_ValueError, "to_wkb function called with non-scalar parameters"); return; } GEOS_INIT; /* Create the WKB writer */ writer = GEOSWKBWriter_create_r(ctx); if (writer == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } char hex = *(npy_bool*)ip2; GEOSWKBWriter_setOutputDimension_r(ctx, writer, *(int*)ip3); int byte_order = *(int*)ip4; if (byte_order != -1) { GEOSWKBWriter_setByteOrder_r(ctx, writer, *(int*)ip4); } GEOSWKBWriter_setIncludeSRID_r(ctx, writer, *(npy_bool*)ip5); #if GEOS_SINCE_3_10_0 GEOSWKBWriter_setFlavor_r(ctx, writer, *(int*)ip6); #endif // Check if the above functions caused a GEOS exception if (last_error[0] != 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } PyObject** out = (PyObject**)op1; if (in1 == NULL) { Py_XDECREF(*out); Py_INCREF(Py_None); *out = Py_None; } else { #if !GEOS_SINCE_3_9_0 // WKB Does not allow empty points in GEOS<3.9. // We check for that and patch the POINT EMPTY if necessary has_empty = has_point_empty(ctx, in1); if (has_empty == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } if (has_empty) { temp_geom = point_empty_to_nan_all_geoms(ctx, in1); } else { temp_geom = in1; } #else temp_geom = in1; #endif // !GEOS_SINCE_3_9_0 if (hex) { wkb = GEOSWKBWriter_writeHEX_r(ctx, writer, temp_geom, &size); } else { wkb = GEOSWKBWriter_write_r(ctx, writer, temp_geom, &size); } #if !GEOS_SINCE_3_9_0 // Destroy the temp_geom if it was patched (POINT EMPTY patch) if (has_empty) { GEOSGeom_destroy_r(ctx, temp_geom); } #endif // !GEOS_SINCE_3_9_0 if (wkb == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } Py_XDECREF(*out); if (hex) { *out = PyUnicode_FromStringAndSize((char*)wkb, size); } else { *out = PyBytes_FromStringAndSize((char*)wkb, size); } GEOSFree_r(ctx, wkb); } } finish: GEOSWKBWriter_destroy_r(ctx, writer); GEOS_FINISH; } static PyUFuncGenericFunction to_wkb_funcs[1] = {&to_wkb_func}; static char to_wkt_dtypes[6] = {NPY_OBJECT, NPY_INT, NPY_BOOL, NPY_INT, NPY_BOOL, NPY_OBJECT}; static void to_wkt_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *ip4 = args[3], *ip5 = args[4], *op1 = args[5]; npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], is4 = steps[3], is5 = steps[4], os1 = steps[5]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry* in1; GEOSWKTWriter* writer; char* wkt; if ((is2 != 0) || (is3 != 0) || (is4 != 0) || (is5 != 0)) { PyErr_Format(PyExc_ValueError, "to_wkt function called with non-scalar parameters"); return; } GEOS_INIT; /* Create the WKT writer */ writer = GEOSWKTWriter_create_r(ctx); if (writer == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } GEOSWKTWriter_setRoundingPrecision_r(ctx, writer, *(int*)ip2); GEOSWKTWriter_setTrim_r(ctx, writer, *(npy_bool*)ip3); GEOSWKTWriter_setOutputDimension_r(ctx, writer, *(int*)ip4); GEOSWKTWriter_setOld3D_r(ctx, writer, *(npy_bool*)ip5); // Check if the above functions caused a GEOS exception if (last_error[0] != 0) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } PyObject** out = (PyObject**)op1; if (in1 == NULL) { Py_XDECREF(*out); Py_INCREF(Py_None); *out = Py_None; } else { #if GEOS_SINCE_3_9_0 errstate = wkt_empty_3d_geometry(ctx, in1, &wkt); if (errstate != PGERR_SUCCESS) { goto finish; } if (wkt != NULL) { *out = PyUnicode_FromString(wkt); goto finish; } #else // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY) errstate = check_to_wkt_compatible(ctx, in1); if (errstate != PGERR_SUCCESS) { goto finish; } #endif wkt = GEOSWKTWriter_write_r(ctx, writer, in1); if (wkt == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } Py_XDECREF(*out); *out = PyUnicode_FromString(wkt); GEOSFree_r(ctx, wkt); } } finish: GEOSWKTWriter_destroy_r(ctx, writer); GEOS_FINISH; } static PyUFuncGenericFunction to_wkt_funcs[1] = {&to_wkt_func}; #if GEOS_SINCE_3_10_0 static char from_geojson_dtypes[3] = {NPY_OBJECT, NPY_UINT8, NPY_OBJECT}; static void from_geojson_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *op1 = args[2]; npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2]; PyObject* in1; npy_uint8 on_invalid = *(npy_uint8*)ip2; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry* ret_ptr; GEOSGeoJSONReader* reader; const char* geojson; if ((is2 != 0)) { PyErr_Format(PyExc_ValueError, "from_geojson function called with non-scalar parameters"); return; } GEOS_INIT; /* Create the WKT reader */ reader = GEOSGeoJSONReader_create_r(ctx); if (reader == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } /* ip1 is pointer to array element PyObject* */ in1 = *(PyObject**)ip1; if (in1 == Py_None) { /* None in the input propagates to the output */ ret_ptr = NULL; } else { /* Cast the PyObject (bytes or str) to char* */ if (PyBytes_Check(in1)) { geojson = PyBytes_AsString(in1); if (geojson == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else if (PyUnicode_Check(in1)) { geojson = PyUnicode_AsUTF8(in1); if (geojson == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } } else { PyErr_Format(PyExc_TypeError, "Expected bytes or string, got %s", Py_TYPE(in1)->tp_name); goto finish; } /* Read the GeoJSON */ ret_ptr = GEOSGeoJSONReader_readGeometry_r(ctx, reader, geojson); if (ret_ptr == NULL) { if (on_invalid == 2) { // raise exception errstate = PGERR_GEOS_EXCEPTION; goto finish; } else if (on_invalid == 1) { // raise warning, return None errstate = PGWARN_INVALID_GEOJSON; } // else: return None, no warning } } OUTPUT_Y; } finish: GEOSGeoJSONReader_destroy_r(ctx, reader); GEOS_FINISH; } static PyUFuncGenericFunction from_geojson_funcs[1] = {&from_geojson_func}; static char to_geojson_dtypes[3] = {NPY_OBJECT, NPY_INT, NPY_OBJECT}; static void to_geojson_func(char** args, const npy_intp* dimensions, const npy_intp* steps, void* data) { char *ip1 = args[0], *ip2 = args[1], *op1 = args[2]; npy_intp is1 = steps[0], is2 = steps[1], os1 = steps[2]; npy_intp n = dimensions[0]; npy_intp i; GEOSGeometry* in1; int indent; GEOSGeoJSONWriter* writer; char* geojson; char point_empty_error; if (is2 != 0) { PyErr_Format(PyExc_ValueError, "to_geojson indent parameter must be a scalar"); return; } indent = *(int*)ip2; GEOS_INIT; /* Create the GeoJSON writer */ writer = GEOSGeoJSONWriter_create_r(ctx); if (writer == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } for (i = 0; i < n; i++, ip1 += is1, op1 += os1) { CHECK_SIGNALS(i); if (errstate == PGERR_PYSIGNAL) { goto finish; } if (!get_geom(*(GeometryObject**)ip1, &in1)) { errstate = PGERR_NOT_A_GEOMETRY; goto finish; } PyObject** out = (PyObject**)op1; if (in1 == NULL) { Py_XDECREF(*out); Py_INCREF(Py_None); *out = Py_None; } else { // Check for empty points (https://trac.osgeo.org/geos/ticket/1139) point_empty_error = has_point_empty(ctx, in1); if (point_empty_error == 2) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } else if (point_empty_error == 1) { errstate = PGERR_GEOJSON_EMPTY_POINT; goto finish; } geojson = GEOSGeoJSONWriter_writeGeometry_r(ctx, writer, in1, indent); if (geojson == NULL) { errstate = PGERR_GEOS_EXCEPTION; goto finish; } Py_XDECREF(*out); *out = PyUnicode_FromString(geojson); GEOSFree_r(ctx, geojson); } } finish: GEOSGeoJSONWriter_destroy_r(ctx, writer); GEOS_FINISH; } static PyUFuncGenericFunction to_geojson_funcs[1] = {&to_geojson_func}; #endif // GEOS_SINCE_3_10_0 /* TODO polygonizer functions TODO prepared geometry predicate functions TODO relate functions */ #define DEFINE_Y_b(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_b_funcs, NAME##_data, Y_b_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, NULL, 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_O_b(NAME) \ ufunc = PyUFunc_FromFuncAndData(O_b_funcs, NAME##_data, O_b_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, NULL, 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YY_b(NAME) \ ufunc = PyUFunc_FromFuncAndData(YY_b_funcs, NAME##_data, YY_b_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YY_b_p(NAME) \ ufunc = PyUFunc_FromFuncAndData(YY_b_p_funcs, NAME##_data, YY_b_p_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Ydd_b_p(NAME) \ ufunc = PyUFunc_FromFuncAndData(Ydd_b_p_funcs, NAME##_data, Ydd_b_p_dtypes, 1, 3, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_Y_funcs, NAME##_data, Y_Y_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_funcs, NAME##_data, Y_dtypes, 1, 1, 0, PyUFunc_None, \ #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Yi_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(Yi_Y_funcs, NAME##_data, Yi_Y_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Yd_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(Yd_Y_funcs, NAME##_data, Yd_Y_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YY_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(YY_Y_funcs, NAME##_data, YY_Y_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y_Y_reduce(NAME) \ ufunc = PyUFunc_FromFuncAndDataAndSignature(Y_Y_reduce_funcs, NAME##_data, \ Y_Y_reduce_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, "", 0, \ "(d)->()"); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y_d(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_d_funcs, NAME##_data, Y_d_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y_B(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_B_funcs, NAME##_data, Y_B_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_Y_i(NAME) \ ufunc = PyUFunc_FromFuncAndData(Y_i_funcs, NAME##_data, Y_i_dtypes, 1, 1, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YY_d(NAME) \ ufunc = PyUFunc_FromFuncAndData(YY_d_funcs, NAME##_data, YY_d_dtypes, 1, 2, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YYd_d(NAME) \ ufunc = PyUFunc_FromFuncAndData(YYd_d_funcs, NAME##_data, YYd_d_dtypes, 1, 3, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_YYd_Y(NAME) \ ufunc = PyUFunc_FromFuncAndData(YYd_Y_funcs, NAME##_data, YYd_Y_dtypes, 1, 3, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_CUSTOM(NAME, N_IN) \ ufunc = PyUFunc_FromFuncAndData(NAME##_funcs, null_data, NAME##_dtypes, 1, N_IN, 1, \ PyUFunc_None, #NAME, "", 0); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_GENERALIZED(NAME, N_IN, SIGNATURE) \ ufunc = PyUFunc_FromFuncAndDataAndSignature(NAME##_funcs, null_data, NAME##_dtypes, 1, \ N_IN, 1, PyUFunc_None, #NAME, "", 0, \ SIGNATURE); \ PyDict_SetItemString(d, #NAME, ufunc) #define DEFINE_GENERALIZED_NOUT4(NAME, N_IN, SIGNATURE) \ ufunc = PyUFunc_FromFuncAndDataAndSignature(NAME##_funcs, null_data, NAME##_dtypes, 1, \ N_IN, 4, PyUFunc_None, #NAME, "", 0, \ SIGNATURE); \ PyDict_SetItemString(d, #NAME, ufunc) int init_ufuncs(PyObject* m, PyObject* d) { PyObject* ufunc; DEFINE_Y_b(is_empty); DEFINE_Y_b(is_simple); DEFINE_Y_b(is_geometry); DEFINE_Y_b(is_ring); DEFINE_Y_b(has_z); DEFINE_Y_b(is_closed); DEFINE_Y_b(is_valid); DEFINE_O_b(is_geometry); DEFINE_O_b(is_missing); DEFINE_O_b(is_valid_input); DEFINE_YY_b_p(disjoint); DEFINE_YY_b_p(touches); DEFINE_YY_b_p(intersects); DEFINE_YY_b_p(crosses); DEFINE_YY_b_p(within); DEFINE_YY_b_p(contains); DEFINE_YY_b_p(contains_properly); DEFINE_YY_b_p(overlaps); DEFINE_YY_b(equals); DEFINE_YY_b_p(covers); DEFINE_YY_b_p(covered_by); DEFINE_Ydd_b_p(contains_xy); DEFINE_Ydd_b_p(intersects_xy); DEFINE_CUSTOM(is_prepared, 1); DEFINE_Y_Y(envelope); DEFINE_Y_Y(convex_hull); DEFINE_Y_Y(boundary); DEFINE_Y_Y(unary_union); DEFINE_Y_Y(point_on_surface); DEFINE_Y_Y(centroid); DEFINE_Y_Y(line_merge); DEFINE_Y_Y(node); DEFINE_Y_Y(extract_unique_points); DEFINE_Y_Y(get_exterior_ring); DEFINE_Y_Y(normalize); DEFINE_Y_Y(force_2d); DEFINE_Y(prepare); DEFINE_Y(destroy_prepared); DEFINE_Yi_Y(get_point); DEFINE_Yi_Y(get_interior_ring); DEFINE_Yi_Y(get_geometry); DEFINE_Yi_Y(set_srid); DEFINE_Yd_Y(line_interpolate_point); DEFINE_Yd_Y(line_interpolate_point_normalized); DEFINE_Yd_Y(simplify); DEFINE_Yd_Y(simplify_preserve_topology); DEFINE_Yd_Y(force_3d); DEFINE_YY_Y(intersection); DEFINE_YY_Y(difference); DEFINE_YY_Y(symmetric_difference); DEFINE_YY_Y(union); DEFINE_YY_Y(shared_paths); DEFINE_Y_Y_reduce(intersection_all); DEFINE_Y_Y_reduce(symmetric_difference_all); DEFINE_Y_d(get_x); DEFINE_Y_d(get_y); DEFINE_Y_d(area); DEFINE_Y_d(length); DEFINE_Y_i(get_type_id); DEFINE_Y_i(get_dimensions); DEFINE_Y_i(get_coordinate_dimension); DEFINE_Y_i(get_srid); DEFINE_Y_i(get_num_points); DEFINE_Y_i(get_num_interior_rings); DEFINE_Y_i(get_num_geometries); DEFINE_Y_i(get_num_coordinates); DEFINE_YY_d(distance); DEFINE_YY_d(hausdorff_distance); DEFINE_YY_d(line_locate_point); DEFINE_YY_d(line_locate_point_normalized); DEFINE_YYd_d(hausdorff_distance_densify); DEFINE_CUSTOM(box, 5); DEFINE_CUSTOM(buffer, 7); DEFINE_CUSTOM(offset_curve, 5); DEFINE_CUSTOM(snap, 3); DEFINE_CUSTOM(clip_by_rect, 5); DEFINE_CUSTOM(equals_exact, 3); DEFINE_CUSTOM(delaunay_triangles, 3); DEFINE_CUSTOM(voronoi_polygons, 4); DEFINE_CUSTOM(is_valid_reason, 1); DEFINE_CUSTOM(relate, 2); DEFINE_CUSTOM(relate_pattern, 3); DEFINE_GENERALIZED(polygonize, 1, "(d)->()"); DEFINE_GENERALIZED_NOUT4(polygonize_full, 1, "(d)->(),(),(),()"); DEFINE_CUSTOM(shortest_line, 2); DEFINE_GENERALIZED(points, 1, "(d)->()"); DEFINE_GENERALIZED(linestrings, 1, "(i, d)->()"); DEFINE_GENERALIZED(linearrings, 1, "(i, d)->()"); DEFINE_GENERALIZED(bounds, 1, "()->(n)"); DEFINE_GENERALIZED(polygons, 2, "(),(i)->()"); DEFINE_GENERALIZED(create_collection, 2, "(i),()->()"); DEFINE_CUSTOM(from_wkb, 2); DEFINE_CUSTOM(from_wkt, 2); DEFINE_CUSTOM(to_wkb, 6); DEFINE_CUSTOM(to_wkt, 5); #if GEOS_SINCE_3_6_0 DEFINE_Y_d(minimum_clearance); DEFINE_Y_d(get_precision); DEFINE_Y_Y(oriented_envelope); DEFINE_CUSTOM(set_precision, 3); #endif #if GEOS_SINCE_3_7_0 DEFINE_Y_b(is_ccw); DEFINE_Y_d(get_z); DEFINE_Y_Y(reverse); DEFINE_YY_d(frechet_distance); DEFINE_YYd_d(frechet_distance_densify); #endif #if GEOS_SINCE_3_8_0 DEFINE_Y_Y(make_valid); DEFINE_Y_Y(build_area); DEFINE_Y_Y(coverage_union); DEFINE_Y_Y(minimum_bounding_circle); DEFINE_Y_d(minimum_bounding_radius); #endif #if GEOS_SINCE_3_9_0 DEFINE_YYd_Y(difference_prec); DEFINE_YYd_Y(intersection_prec); DEFINE_YYd_Y(symmetric_difference_prec); DEFINE_YYd_Y(union_prec); DEFINE_Yd_Y(unary_union_prec); #endif #if GEOS_SINCE_3_10_0 DEFINE_Yd_Y(segmentize); DEFINE_CUSTOM(dwithin, 3); DEFINE_CUSTOM(from_geojson, 2); DEFINE_CUSTOM(to_geojson, 2); #endif #if GEOS_SINCE_3_11_0 DEFINE_Yd_Y(remove_repeated_points); DEFINE_Y_Y(line_merge_directed); DEFINE_CUSTOM(concave_hull, 3); #endif Py_DECREF(ufunc); return 0; } shapely-2.0.3/src/ufuncs.h000066400000000000000000000002721456366510000154330ustar00rootroot00000000000000#ifndef _UFUNCS_H #define _UFUNCS_H #include extern int init_ufuncs(PyObject* m, PyObject* d); extern PyObject* PySetupSignalChecks(PyObject* self, PyObject* args); #endif shapely-2.0.3/src/vector.c000066400000000000000000000021661456366510000154310ustar00rootroot00000000000000#define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include #include #define NO_IMPORT_ARRAY #define PY_ARRAY_UNIQUE_SYMBOL shapely_ARRAY_API #include #include #include #include "kvec.h" #include "vector.h" /* Copy values from arr of indexes to a new numpy integer array. * * Parameters * ---------- * arr: dynamic vector array to convert to ndarray */ PyArrayObject* index_vec_to_npy_arr(index_vec_t* arr) { Py_ssize_t i; npy_intp size = kv_size(*arr); npy_intp dims[1] = {size}; // the following raises a compiler warning based on how the macro is defined // in numpy. There doesn't appear to be anything we can do to avoid it. PyArrayObject* result = (PyArrayObject*)PyArray_SimpleNew(1, dims, NPY_INTP); if (result == NULL) { PyErr_SetString(PyExc_RuntimeError, "could not allocate numpy array"); return NULL; } for (i = 0; i < size; i++) { // assign value into numpy array *(npy_intp*)PyArray_GETPTR1(result, i) = kv_A(*arr, i); } return (PyArrayObject*)result; } shapely-2.0.3/src/vector.h000066400000000000000000000007221456366510000154320ustar00rootroot00000000000000#ifndef _VECTOR_H #define _VECTOR_H #include #include "pygeom.h" /* A resizable vector with numpy indices. * Wraps the vector implementation in kvec.h as a type. */ typedef struct { size_t n, m; npy_intp* a; } index_vec_t; /* Copy values from arr to a new numpy integer array. * * Parameters * ---------- * arr: dynamic vector array to convert to ndarray */ extern PyArrayObject* index_vec_to_npy_arr(index_vec_t* arr); #endif shapely-2.0.3/versioneer.py000066400000000000000000002432271456366510000157340ustar00rootroot00000000000000 # Version: 0.28 """The Versioneer - like a rocketeer, but for versions. The Versioneer ============== * like a rocketeer, but for versions! * https://github.com/python-versioneer/python-versioneer * Brian Warner * License: Public Domain (Unlicense) * Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 * [![Latest Version][pypi-image]][pypi-url] * [![Build Status][travis-image]][travis-url] This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control system, and maybe making new tarballs. ## Quick Install Versioneer provides two installation modes. The "classic" vendored mode installs a copy of versioneer into your repository. The experimental build-time dependency mode is intended to allow you to skip this step and simplify the process of upgrading. ### Vendored mode * `pip install versioneer` to somewhere in your $PATH * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is available, so you can also use `conda install -c conda-forge versioneer` * add a `[tool.versioneer]` section to your `pyproject.toml` or a `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) * Note that you will need to add `tomli; python_version < "3.11"` to your build-time dependencies if you use `pyproject.toml` * run `versioneer install --vendor` in your source tree, commit the results * verify version information with `python setup.py version` ### Build-time dependency mode * `pip install versioneer` to somewhere in your $PATH * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is available, so you can also use `conda install -c conda-forge versioneer` * add a `[tool.versioneer]` section to your `pyproject.toml` or a `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) * add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) to the `requires` key of the `build-system` table in `pyproject.toml`: ```toml [build-system] requires = ["setuptools", "versioneer[toml]"] build-backend = "setuptools.build_meta" ``` * run `versioneer install --no-vendor` in your source tree, commit the results * verify version information with `python setup.py version` ## Version Identifiers Source trees come from a variety of places: * a version-control system checkout (mostly used by developers) * a nightly tarball, produced by build automation * a snapshot tarball, produced by a web-based VCS browser, like github's "tarball from tag" feature * a release tarball, produced by "setup.py sdist", distributed through PyPI Within each source tree, the version identifier (either a string or a number, this tool is format-agnostic) can come from a variety of places: * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows about recent "tags" and an absolute revision-id * the name of the directory into which the tarball was unpacked * an expanded VCS keyword ($Id$, etc) * a `_version.py` created by some earlier build step For released software, the version identifier is closely related to a VCS tag. Some projects use tag names that include more than just the version string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool needs to strip the tag prefix to extract the version identifier. For unreleased software (between tags), the version identifier should provide enough information to help developers recreate the same tree, while also giving them an idea of roughly how old the tree is (after version 1.2, before version 1.3). Many VCS systems can report a description that captures this, for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has uncommitted changes). The version identifier is used for multiple purposes: * to allow the module to self-identify its version: `myproject.__version__` * to choose a name and prefix for a 'setup.py sdist' tarball ## Theory of Operation Versioneer works by adding a special `_version.py` file into your source tree, where your `__init__.py` can import it. This `_version.py` knows how to dynamically ask the VCS tool for version information at import time. `_version.py` also contains `$Revision$` markers, and the installation process marks `_version.py` to have this marker rewritten with a tag name during the `git archive` command. As a result, generated tarballs will contain enough information to get the proper version. To allow `setup.py` to compute a version too, a `versioneer.py` is added to the top level of your source tree, next to `setup.py` and the `setup.cfg` that configures it. This overrides several distutils/setuptools commands to compute the version when invoked, and changes `setup.py build` and `setup.py sdist` to replace `_version.py` with a small static file that contains just the generated version data. ## Installation See [INSTALL.md](./INSTALL.md) for detailed installation instructions. ## Version-String Flavors Code which uses Versioneer can learn about its version string at runtime by importing `_version` from your main `__init__.py` file and running the `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can import the top-level `versioneer.py` and run `get_versions()`. Both functions return a dictionary with different flavors of version information: * `['version']`: A condensed version string, rendered using the selected style. This is the most commonly used value for the project's version string. The default "pep440" style yields strings like `0.11`, `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section below for alternative styles. * `['full-revisionid']`: detailed revision identifier. For Git, this is the full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the commit date in ISO 8601 format. This will be None if the date is not available. * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that this is only accurate if run in a VCS checkout, otherwise it is likely to be False or None * `['error']`: if the version string could not be computed, this will be set to a string describing the problem, otherwise it will be None. It may be useful to throw an exception in setup.py if this is set, to avoid e.g. creating tarballs with a version string of "unknown". Some variants are more useful than others. Including `full-revisionid` in a bug report should allow developers to reconstruct the exact code being tested (or indicate the presence of local changes that should be shared with the developers). `version` is suitable for display in an "about" box or a CLI `--version` output: it can be easily compared against release notes and lists of bugs fixed in various releases. The installer adds the following text to your `__init__.py` to place a basic version in `YOURPROJECT.__version__`: from ._version import get_versions __version__ = get_versions()['version'] del get_versions ## Styles The setup.cfg `style=` configuration controls how the VCS information is rendered into a version string. The default style, "pep440", produces a PEP440-compliant string, equal to the un-prefixed tag name for actual releases, and containing an additional "local version" section with more detail for in-between builds. For Git, this is TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and that this commit is two revisions ("+2") beyond the "0.11" tag. For released software (exactly equal to a known tag), the identifier will only contain the stripped tag, e.g. "0.11". Other styles are available. See [details.md](details.md) in the Versioneer source tree for descriptions. ## Debugging Versioneer tries to avoid fatal errors: if something goes wrong, it will tend to return a version of "0+unknown". To investigate the problem, run `setup.py version`, which will run the version-lookup code in a verbose mode, and will display the full contents of `get_versions()` (including the `error` string, which may help identify what went wrong). ## Known Limitations Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github [issues page](https://github.com/python-versioneer/python-versioneer/issues). ### Subprojects Versioneer has limited support for source trees in which `setup.py` is not in the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are two common reasons why `setup.py` might not be in the root: * Source trees which contain multiple subprojects, such as [Buildbot](https://github.com/buildbot/buildbot), which contains both "master" and "slave" subprojects, each with their own `setup.py`, `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also provide bindings to Python (and perhaps other languages) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs and implementation details which frequently cause `pip install .` from a subproject directory to fail to find a correct version string (so it usually defaults to `0+unknown`). `pip install --editable .` should work correctly. `setup.py install` might work too. Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. [Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking this issue. The discussion in [PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve pip to let Versioneer work correctly. Versioneer-0.16 and earlier only looked for a `.git` directory next to the `setup.cfg`, so subprojects were completely unsupported with those releases. ### Editable installs with setuptools <= 18.5 `setup.py develop` and `pip install --editable .` allow you to install a project into a virtualenv once, then continue editing the source code (and test) without re-installing after every change. "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a convenient way to specify executable scripts that should be installed along with the python package. These both work as expected when using modern setuptools. When using setuptools-18.5 or earlier, however, certain operations will cause `pkg_resources.DistributionNotFound` errors when running the entrypoint script, which must be resolved by re-installing the package. This happens when the install happens with one version, then the egg_info data is regenerated while a different version is checked out. Many setup.py commands cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. [Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) * edit `setup.cfg` and `pyproject.toml`, if necessary, to include any new configuration settings indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. * re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files ## Future Directions This tool is designed to make it easily extended to other version-control systems: all VCS-specific components are in separate directories like src/git/ . The top-level `versioneer.py` script is assembled from these components by running make-versioneer.py . In the future, make-versioneer.py will take a VCS name as an argument, and will construct a version of `versioneer.py` that is specific to the given VCS. It might also take the configuration arguments that are currently provided manually during installation by editing setup.py . Alternatively, it might go the other direction and include code from all supported VCS systems, reducing the number of intermediate scripts. ## Similar projects * [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time dependency * [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of versioneer * [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools plugin ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. Specifically, both are released under the "Unlicense", as described in https://unlicense.org/. [pypi-image]: https://img.shields.io/pypi/v/versioneer.svg [pypi-url]: https://pypi.python.org/pypi/versioneer/ [travis-image]: https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer """ # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error # pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with # pylint:disable=attribute-defined-outside-init,too-many-arguments import configparser import errno import json import os import re import subprocess import sys from pathlib import Path from typing import Callable, Dict import functools have_tomllib = True if sys.version_info >= (3, 11): import tomllib else: try: import tomli as tomllib except ImportError: have_tomllib = False class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_root(): """Get the project root directory. We require that all commands are run from the project root, i.e. the directory that contains setup.py, setup.cfg, and versioneer.py . """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): err = ("Versioneer was unable to run the project root directory. " "Versioneer requires setup.py to be executed from " "its immediate directory (like 'python setup.py COMMAND'), " "or in a way that lets it use sys.argv[0] to find the root " "(like 'python path/to/setup.py COMMAND').") raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools # tree) execute all dependencies in a single python process, so # "versioneer" may be imported multiple times, and python's shared # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. my_path = os.path.realpath(os.path.abspath(__file__)) me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(my_path), versioneer_py)) except NameError: pass return root def get_config_from_root(root): """Read the project setup.cfg file to determine Versioneer config.""" # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . root = Path(root) pyproject_toml = root / "pyproject.toml" setup_cfg = root / "setup.cfg" section = None if pyproject_toml.exists() and have_tomllib: try: with open(pyproject_toml, 'rb') as fobj: pp = tomllib.load(fobj) section = pp['tool']['versioneer'] except (tomllib.TOMLDecodeError, KeyError): pass if not section: parser = configparser.ConfigParser() with open(setup_cfg) as cfg_file: parser.read_file(cfg_file) parser.get("versioneer", "VCS") # raise error if missing section = parser["versioneer"] cfg = VersioneerConfig() cfg.VCS = section['VCS'] cfg.style = section.get("style", "") cfg.versionfile_source = section.get("versionfile_source") cfg.versionfile_build = section.get("versionfile_build") cfg.tag_prefix = section.get("tag_prefix") if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" cfg.parentdir_prefix = section.get("parentdir_prefix") cfg.verbose = section.get("verbose") return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" # these dictionaries contain VCS-specific tools LONG_VERSION_PY: Dict[str, str] = {} HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" HANDLERS.setdefault(vcs, {})[method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) process = None popen_kwargs = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW popen_kwargs["startupinfo"] = startupinfo for command in commands: try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git process = subprocess.Popen([command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs) break except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, process.returncode return stdout, process.returncode LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. # Generated by versioneer-0.28 # https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" import errno import os import re import subprocess import sys from typing import Callable, Dict import functools def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "%(STYLE)s" cfg.tag_prefix = "%(TAG_PREFIX)s" cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY: Dict[str, str] = {} HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) process = None popen_kwargs = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW popen_kwargs["startupinfo"] = startupinfo for command in commands: try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git process = subprocess.Popen([command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs) break except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %%s" %% dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) return None, process.returncode return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) except OSError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: print("likely tags: %%s" %% ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') if not re.match(r'\d', r): continue if verbose: print("picking %%s" %% r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] # GIT_DIR can interfere with correct operation of Versioneer. # It may be intended to be passed to the Versioneer-versioned project, # but that should not change where we get our version from. env = os.environ.copy() env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = runner(GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*" ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") branch_name = branch_name.strip() if branch_name == "HEAD": # If we aren't exactly on a branch, pick a branch which represents # the current commit. If all else fails, we are on a branchless # commit. branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) # --contains was added in git-1.5.4 if rc != 0 or branches is None: raise NotThisMethod("'git branch --contains' returned error") branches = branches.split("\n") # Remove the first line if we're running detached if "(" in branches[0]: branches.pop(0) # Strip off the leading "* " from the list of branches. branches = [branch[2:] for branch in branches] if "master" in branches: branch_name = "master" elif not branches: branch_name = None else: # Pick the first branch that is returned. Good or bad. branch_name = branches[0] pieces["branch"] = branch_name # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%%s' doesn't start with prefix '%%s'" print(fmt %% (full_tag, tag_prefix)) pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" %% (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_branch(pieces): """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards (a feature branch will appear "older" than the master branch). Exceptions: 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def pep440_split_post(ver): """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the post-release version number (or -1 if no post-release segment is present). """ vc = str.split(ver, ".post") return vc[0], int(vc[1] or 0) if len(vc) == 2 else None def render_pep440_pre(pieces): """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post0.devDISTANCE """ if pieces["closest-tag"]: if pieces["distance"]: # update the post release segment tag_version, post_version = pep440_split_post(pieces["closest-tag"]) rendered = tag_version if post_version is not None: rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) else: rendered += ".post0.dev%%d" %% (pieces["distance"]) else: # no commits, use the tag as the version rendered = pieces["closest-tag"] else: # exception #1 rendered = "0.post0.dev%%d" %% pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%%s" %% pieces["short"] else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%%s" %% pieces["short"] return rendered def render_pep440_post_branch(pieces): """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. Exceptions: 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%%s" %% pieces["short"] if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += "+g%%s" %% pieces["short"] if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-branch": rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-post-branch": rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%%s'" %% style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} ''' @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: with open(versionfile_abs, "r") as fobj: for line in fobj: if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) except OSError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') if not re.match(r'\d', r): continue if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] # GIT_DIR can interfere with correct operation of Versioneer. # It may be intended to be passed to the Versioneer-versioned project, # but that should not change where we get our version from. env = os.environ.copy() env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = runner(GITS, [ "describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*" ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") branch_name = branch_name.strip() if branch_name == "HEAD": # If we aren't exactly on a branch, pick a branch which represents # the current commit. If all else fails, we are on a branchless # commit. branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) # --contains was added in git-1.5.4 if rc != 0 or branches is None: raise NotThisMethod("'git branch --contains' returned error") branches = branches.split("\n") # Remove the first line if we're running detached if "(" in branches[0]: branches.pop(0) # Strip off the leading "* " from the list of branches. branches = [branch[2:] for branch in branches] if "master" in branches: branch_name = "master" elif not branches: branch_name = None else: # Pick the first branch that is returned. Good or bad. branch_name = branches[0] pieces["branch"] = branch_name # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def do_vcs_install(versionfile_source, ipy): """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py for export-subst keyword substitution. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] files = [versionfile_source] if ipy: files.append(ipy) if "VERSIONEER_PEP518" not in globals(): try: my_path = __file__ if my_path.endswith((".pyc", ".pyo")): my_path = os.path.splitext(my_path)[0] + ".py" versioneer_file = os.path.relpath(my_path) except NameError: versioneer_file = "versioneer.py" files.append(versioneer_file) present = False try: with open(".gitattributes", "r") as fobj: for line in fobj: if line.strip().startswith(versionfile_source): if "export-subst" in line.strip().split()[1:]: present = True break except OSError: pass if not present: with open(".gitattributes", "a+") as fobj: fobj.write(f"{versionfile_source} export-subst\n") files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ # This file was generated by 'versioneer.py' (0.28) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. import json version_json = ''' %s ''' # END VERSION_JSON def get_versions(): return json.loads(version_json) """ def versions_from_file(filename): """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) def write_to_version_file(filename, versions): """Write the given version number to the given _version.py file.""" os.unlink(filename) contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_branch(pieces): """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards (a feature branch will appear "older" than the master branch). Exceptions: 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def pep440_split_post(ver): """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the post-release version number (or -1 if no post-release segment is present). """ vc = str.split(ver, ".post") return vc[0], int(vc[1] or 0) if len(vc) == 2 else None def render_pep440_pre(pieces): """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post0.devDISTANCE """ if pieces["closest-tag"]: if pieces["distance"]: # update the post release segment tag_version, post_version = pep440_split_post(pieces["closest-tag"]) rendered = tag_version if post_version is not None: rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) else: rendered += ".post0.dev%d" % (pieces["distance"]) else: # no commits, use the tag as the version rendered = pieces["closest-tag"] else: # exception #1 rendered = "0.post0.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_post_branch(pieces): """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. Exceptions: 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += "+g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-branch": rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-post-branch": rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" def get_versions(verbose=False): """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. """ if "versioneer" in sys.modules: # see the discussion in cmdclass.py:get_cmdclass() del sys.modules["versioneer"] root = get_root() cfg = get_config_from_root(root) assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or cfg.verbose assert cfg.versionfile_source is not None, \ "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) # extract version from first of: _version.py, VCS command (e.g. 'git # describe'), parentdir. This is meant to work for developers using a # source checkout, for users of a tarball created by 'setup.py sdist', # and for users of a tarball/zipball created by 'git archive' or github's # download-from-tag feature or the equivalent in other VCSes. get_keywords_f = handlers.get("get_keywords") from_keywords_f = handlers.get("keywords") if get_keywords_f and from_keywords_f: try: keywords = get_keywords_f(versionfile_abs) ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) if verbose: print("got version from expanded keyword %s" % ver) return ver except NotThisMethod: pass try: ver = versions_from_file(versionfile_abs) if verbose: print("got version from file %s %s" % (versionfile_abs, ver)) return ver except NotThisMethod: pass from_vcs_f = handlers.get("pieces_from_vcs") if from_vcs_f: try: pieces = from_vcs_f(cfg.tag_prefix, root, verbose) ver = render(pieces, cfg.style) if verbose: print("got version from VCS %s" % ver) return ver except NotThisMethod: pass try: if cfg.parentdir_prefix: ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) if verbose: print("got version from parentdir %s" % ver) return ver except NotThisMethod: pass if verbose: print("unable to compute version") return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} def get_version(): """Get the short version string for this project.""" return get_versions()["version"] def get_cmdclass(cmdclass=None): """Get the custom setuptools subclasses used by Versioneer. If the package uses a different cmdclass (e.g. one from numpy), it should be provide as an argument. """ if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and # 'easy_install .'), in which subdependencies of the main project are # built (using setup.py bdist_egg) in the same python process. Assume # a main project A and a dependency B, which use different versions # of Versioneer. A's setup.py imports A's Versioneer, leaving it in # sys.modules by the time B's setup.py is executed, causing B to run # with the wrong versioneer. Setuptools wraps the sub-dep builds in a # sandbox that restores sys.modules to it's pre-build state, so the # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. # Also see https://github.com/python-versioneer/python-versioneer/issues/52 cmds = {} if cmdclass is None else cmdclass.copy() # we add "version" to setuptools from setuptools import Command class cmd_version(Command): description = "report generated version string" user_options = [] boolean_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) print(" dirty: %s" % vers.get("dirty")) print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) cmds["version"] = cmd_version # we override "build_py" in setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py # distutils/install -> distutils/build ->.. # setuptools/bdist_wheel -> distutils/install ->.. # setuptools/bdist_egg -> distutils/install_lib -> build_py # setuptools/install -> bdist_egg ->.. # setuptools/develop -> ? # pip install: # copies source tree to a tempdir before running egg_info/etc # if .git isn't copied too, 'git describe' will fail # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? # pip install -e . and setuptool/editable_wheel will invoke build_py # but the build_py command is not expected to copy any files. # we override different "build_py" commands for both environments if 'build_py' in cmds: _build_py = cmds['build_py'] else: from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) if getattr(self, "editable_mode", False): # During editable installs `.py` and data files are # not copied to build_lib return # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) cmds["build_py"] = cmd_build_py if 'build_ext' in cmds: _build_ext = cmds['build_ext'] else: from setuptools.command.build_ext import build_ext as _build_ext class cmd_build_ext(_build_ext): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_ext.run(self) if self.inplace: # build_ext --inplace will only build extensions in # build/lib<..> dir with no _version.py to write to. # As in place builds will already have a _version.py # in the module dir, we do not need to write one. return # now locate _version.py in the new build/ directory and replace # it with an updated value if not cfg.versionfile_build: return target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) if not os.path.exists(target_versionfile): print(f"Warning: {target_versionfile} does not exist, skipping " "version update. This can happen if you are running build_ext " "without first running build_py.") return print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) cmds["build_ext"] = cmd_build_ext if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION # "product_version": versioneer.get_version(), # ... class cmd_build_exe(_build_exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _build_exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["build_exe"] = cmd_build_exe del cmds["build_py"] if 'py2exe' in sys.modules: # py2exe enabled? try: from py2exe.setuptools_buildexe import py2exe as _py2exe except ImportError: from py2exe.distutils_buildexe import py2exe as _py2exe class cmd_py2exe(_py2exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _py2exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["py2exe"] = cmd_py2exe # sdist farms its file list building out to egg_info if 'egg_info' in cmds: _egg_info = cmds['egg_info'] else: from setuptools.command.egg_info import egg_info as _egg_info class cmd_egg_info(_egg_info): def find_sources(self): # egg_info.find_sources builds the manifest list and writes it # in one shot super().find_sources() # Modify the filelist and normalize it root = get_root() cfg = get_config_from_root(root) self.filelist.append('versioneer.py') if cfg.versionfile_source: # There are rare cases where versionfile_source might not be # included by default, so we must be explicit self.filelist.append(cfg.versionfile_source) self.filelist.sort() self.filelist.remove_duplicates() # The write method is hidden in the manifest_maker instance that # generated the filelist and was thrown away # We will instead replicate their final normalization (to unicode, # and POSIX-style paths) from setuptools import unicode_utils normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') for f in self.filelist.files] manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') with open(manifest_filename, 'w') as fobj: fobj.write('\n'.join(normalized)) cmds['egg_info'] = cmd_egg_info # we override different "sdist" commands for both environments if 'sdist' in cmds: _sdist = cmds['sdist'] else: from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): def run(self): versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old # version self.distribution.metadata.version = versions["version"] return _sdist.run(self) def make_release_tree(self, base_dir, files): root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) # now locate _version.py in the new base_dir directory # (remembering that it may be a hardlink) and replace it with an # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist return cmds CONFIG_ERROR = """ setup.cfg is missing the necessary Versioneer configuration. You need a section like: [versioneer] VCS = git style = pep440 versionfile_source = src/myproject/_version.py versionfile_build = myproject/_version.py tag_prefix = parentdir_prefix = myproject- You will also need to edit your setup.py to use the results: import versioneer setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), ...) Please read the docstring in ./versioneer.py for configuration instructions, edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. """ SAMPLE_CONFIG = """ # See the docstring in versioneer.py for instructions. Note that you must # re-run 'versioneer.py setup' after changing this section, and commit the # resulting files. [versioneer] #VCS = git #style = pep440 #versionfile_source = #versionfile_build = #tag_prefix = #parentdir_prefix = """ OLD_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ INIT_PY_SNIPPET = """ from . import {0} __version__ = {0}.get_versions()['version'] """ def do_setup(): """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (OSError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) return 1 print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() except OSError: old = "" module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] snippet = INIT_PY_SNIPPET.format(module) if OLD_SNIPPET in old: print(" replacing boilerplate in %s" % ipy) with open(ipy, "w") as f: f.write(old.replace(OLD_SNIPPET, snippet)) elif snippet not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: f.write(snippet) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) ipy = None # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. do_vcs_install(cfg.versionfile_source, ipy) return 0 def scan_setup_py(): """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False errors = 0 with open("setup.py", "r") as f: for line in f.readlines(): if "import versioneer" in line: found.add("import") if "versioneer.get_cmdclass()" in line: found.add("cmdclass") if "versioneer.get_version()" in line: found.add("get_version") if "versioneer.VCS" in line: setters = True if "versioneer.versionfile_source" in line: setters = True if len(found) != 3: print("") print("Your setup.py appears to be missing some important items") print("(but I might be wrong). Please make sure it has something") print("roughly like the following:") print("") print(" import versioneer") print(" setup( version=versioneer.get_version(),") print(" cmdclass=versioneer.get_cmdclass(), ...)") print("") errors += 1 if setters: print("You should remove lines like 'versioneer.VCS = ' and") print("'versioneer.versionfile_source = ' . This configuration") print("now lives in setup.cfg, and should be removed from setup.py") print("") errors += 1 return errors def setup_command(): """Set up Versioneer and exit with appropriate error code.""" errors = do_setup() errors += scan_setup_py() sys.exit(1 if errors else 0) if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": setup_command()