├── .clang-format
├── .github
├── codecov.yml
├── dependabot.yml
├── release.yml
└── workflows
│ ├── ci-additional.yml
│ ├── release.yml
│ └── run-tests.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CMakeLists.txt
├── LICENSE
├── README.md
├── ci
├── LICENSE_absl.txt
├── LICENSE_openssl.txt
├── LICENSE_s2geography.md
├── LICENSE_s2geometry.txt
├── environment-dev.yml
├── environment.yml
├── install_3rdparty.cmd
├── install_3rdparty.sh
├── s2geography-add-include-dir.patch
└── s2geography-add-openssl-as-requirement.patch
├── docs
├── Makefile
├── _static
│ ├── favicon.ico
│ ├── spherely_logo.svg
│ ├── spherely_logo_large.png
│ ├── spherely_logo_medium.png
│ ├── spherely_logo_noline.svg
│ ├── spherely_logo_noline_medium.png
│ ├── spherely_logo_noline_notext.svg
│ └── spherely_logo_noline_notext_square.svg
├── api.rst
├── api_hidden.rst
├── conf.py
├── environment.yml
├── index.md
├── install.md
└── make.bat
├── pixi.lock
├── pyproject.toml
├── src
├── accessors-geog.cpp
├── arrow_abi.h
├── boolean-operations.cpp
├── constants.hpp
├── creation.cpp
├── creation.hpp
├── generate_spherely_vfunc_types.py
├── geoarrow.cpp
├── geography.cpp
├── geography.hpp
├── io.cpp
├── predicates.cpp
├── projections.cpp
├── projections.hpp
├── pybind11.hpp
├── spherely.cpp
└── spherely.pyi
└── tests
├── __init__.py
├── test_accessors.py
├── test_boolean_operations.py
├── test_creation.py
├── test_geoarrow.py
├── test_geography.py
├── test_io.py
└── test_predicates.py
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | Language: Cpp
3 | BasedOnStyle: Google
4 | ColumnLimit: 100
5 | TabWidth: 4
6 | IndentWidth: 4
7 | PointerAlignment: Left
8 | ReferenceAlignment: Pointer
9 | IndentAccessModifiers: false
10 | AccessModifierOffset: -4
11 | BinPackArguments: false
12 | BinPackParameters: false
13 | ExperimentalAutoDetectBinPacking: false
14 | AllowAllParametersOfDeclarationOnNextLine: false
15 | AllowShortFunctionsOnASingleLine: Empty
16 | AllowShortLambdasOnASingleLine: Inline
17 | ...
18 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | notify:
3 | after_n_builds: 3
4 | coverage:
5 | status:
6 | project:
7 | default:
8 | target: auto
9 | threshold: 5%
10 | patch:
11 | default:
12 | informational: true
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | groups:
8 | dependencies:
9 | patterns:
10 | - "*"
11 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | authors:
4 | - dependabot
5 | - pre-commit-ci
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci-additional.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [main]
4 | pull_request:
5 | branches: [main]
6 |
7 | name: CI Additional
8 |
9 | jobs:
10 | check-links:
11 | name: Check markdown hyperlinks
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v4
16 |
17 | - name: Markdown link check
18 | uses: gaurav-nelson/github-action-markdown-link-check@v1
19 |
20 | mypy:
21 | name: Mypy
22 | runs-on: ubuntu-latest
23 | defaults:
24 | run:
25 | shell: bash -l {0}
26 |
27 | steps:
28 | - name: Checkout repo
29 | uses: actions/checkout@v4
30 |
31 | - name: Setup micromamba
32 | uses: mamba-org/setup-micromamba@v2
33 | with:
34 | environment-file: ci/environment.yml
35 | environment-name: spherely-dev
36 | create-args: >-
37 | python=3.11
38 |
39 | - name: Build and install spherely
40 | run: |
41 | python -m pip install . -v --no-build-isolation
42 |
43 | - name: Install mypy
44 | run: |
45 | python -m pip install 'mypy'
46 |
47 | - name: Run mypy
48 | run: |
49 | python -m mypy --install-types --non-interactive
50 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish
2 |
3 | on:
4 | # trigger action from GitHub GUI (testing, no publish)
5 | workflow_dispatch:
6 | release:
7 | types:
8 | - published
9 | pull_request: # also build on PRs touching any file below
10 | paths:
11 | - ".github/workflows/release.yml"
12 | - "ci/*"
13 | - "MANIFEST.in"
14 | - "pyproject.toml"
15 |
16 | jobs:
17 | build_sdist:
18 | name: Build sdist
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 |
25 | - name: Build SDist
26 | run: pipx run build --sdist
27 |
28 | - name: Upload artifacts
29 | uses: actions/upload-artifact@v4
30 | with:
31 | name: release-sdist
32 | path: ./dist/*.tar.gz
33 | retention-days: 30
34 |
35 | - name: Check metadata
36 | run: pipx run twine check dist/*
37 |
38 | build_wheels:
39 | name: Build binary wheel on ${{ matrix.os }}
40 | runs-on: ${{ matrix.os }}
41 | env:
42 | ABSL_VERSION: "20240722.0"
43 | S2GEOMETRY_VERSION: "0.11.1"
44 | S2GEOGRAPHY_VERSION: "0.2.0"
45 | CXX_STANDARD: 17
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | include:
50 | - os: ubuntu-latest
51 | arch: x86_64
52 | - os: windows-2019
53 | arch: AMD64
54 | msvc_arch: x64
55 | - os: macos-13
56 | arch: x86_64
57 | cmake_osx_architectures: x86_64
58 | macosx_deployment_target: 13.0
59 | - os: macos-14
60 | arch: arm64
61 | cmake_osx_architectures: arm64
62 | macosx_deployment_target: 14.0
63 |
64 | steps:
65 | - uses: actions/checkout@v4
66 | with:
67 | fetch-depth: 0
68 |
69 | - name: Cache 3rd-party install directory
70 | id: cache-build
71 | uses: actions/cache@v4
72 | with:
73 | path: ${{ runner.temp }}/3rd-party/dist
74 | key: ${{ matrix.os }}-${{ matrix.arch }}-${{ env.ABSL_VERSION }}-${{ env.S2GEOMETRY_VERSION }}-${{ env.S2GEOGRAPHY_VERSION }}-${{ hashFiles('ci/*') }}
75 |
76 | - name: Copy 3rd-party license files
77 | run: |
78 | cp ci/LICENSE_* .
79 | ls -all .
80 | shell: bash
81 |
82 | # for some reason mingw is selected by cmake within cibuildwheel before_all
83 | - name: Prepare compiler environment for Windows
84 | if: runner.os == 'Windows'
85 | uses: ilammy/msvc-dev-cmd@v1
86 | with:
87 | arch: x64
88 |
89 | - name: Cache vcpkg install directory (Windows)
90 | if: runner.os == 'Windows'
91 | uses: actions/cache@v4
92 | with:
93 | path: "c:\\vcpkg\\installed"
94 | key: vcpkg-${{ runner.os }}
95 |
96 | - name: Install abseil openssl and s2geometry (Windows)
97 | if: runner.os == 'Windows'
98 | shell: bash
99 | run: |
100 | vcpkg install s2geometry:x64-windows --x-install-root=$VCPKG_INSTALLATION_ROOT/installed
101 | vcpkg list
102 | ls /c/vcpkg/installed
103 | ls /c/vcpkg/installed/x64-windows
104 | ls /c/vcpkg/installed/x64-windows/bin
105 |
106 | - name: Build wheels
107 | uses: pypa/cibuildwheel@v2.22.0
108 | env:
109 | CIBW_ARCHS: ${{ matrix.arch }}
110 | CIBW_SKIP: cp36-* pp* *musllinux* *-manylinux_i686
111 | CIBW_TEST_SKIP: "cp38-macosx_arm64"
112 | CIBW_ENVIRONMENT_LINUX:
113 | DEPENDENCIES_DIR=/host${{ runner.temp }}/3rd-party
114 | CMAKE_PREFIX_PATH=/host${{ runner.temp }}/3rd-party/dist
115 | ABSL_VERSION=${{ env.ABSL_VERSION }}
116 | S2GEOMETRY_VERSION=${{ env.S2GEOMETRY_VERSION }}
117 | S2GEOGRAPHY_VERSION=${{ env.S2GEOGRAPHY_VERSION }}
118 | CXX_STANDARD=${{ env.CXX_STANDARD }}
119 | CIBW_ENVIRONMENT_MACOS:
120 | PROJECT_DIR=${{ github.workspace }}
121 | DEPENDENCIES_DIR=${{ runner.temp }}/3rd-party
122 | CMAKE_PREFIX_PATH=${{ runner.temp }}/3rd-party/dist
123 | ABSL_VERSION=${{ env.ABSL_VERSION }}
124 | S2GEOMETRY_VERSION=${{ env.S2GEOMETRY_VERSION }}
125 | S2GEOGRAPHY_VERSION=${{ env.S2GEOGRAPHY_VERSION }}
126 | CXX_STANDARD=${{ env.CXX_STANDARD }}
127 | MACOSX_DEPLOYMENT_TARGET=${{ matrix.macosx_deployment_target }}
128 | CMAKE_OSX_ARCHITECTURES='${{ matrix.cmake_osx_architectures }}'
129 | CIBW_ENVIRONMENT_WINDOWS:
130 | DEPENDENCIES_DIR='${{ runner.temp }}\3rd-party'
131 | CMAKE_PREFIX_PATH='c:\vcpkg\installed\x64-windows;${{ runner.temp }}\3rd-party\dist'
132 | ABSL_VERSION=${{ env.ABSL_VERSION }}
133 | S2GEOMETRY_VERSION=${{ env.S2GEOMETRY_VERSION }}
134 | S2GEOGRAPHY_VERSION=${{ env.S2GEOGRAPHY_VERSION }}
135 | CXX_STANDARD=${{ env.CXX_STANDARD }}
136 | PROJECT_DIR='${{ runner.workspace }}\spherely'
137 | CIBW_BEFORE_ALL: ./ci/install_3rdparty.sh
138 | CIBW_BEFORE_ALL_WINDOWS: ci\install_3rdparty.cmd
139 | CIBW_BEFORE_BUILD_WINDOWS: pip install delvewheel
140 | CIBW_REPAIR_WHEEL_COMMAND_LINUX: 'LD_LIBRARY_PATH=/host${{ runner.temp }}/3rd-party/dist/lib64 auditwheel repair -w {dest_dir} {wheel}'
141 | CIBW_REPAIR_WHEEL_COMMAND_MACOS: 'DYLD_LIBRARY_PATH=${{ runner.temp }}/3rd-party/dist/lib delocate-wheel --require-archs=${{ matrix.arch }} -w {dest_dir} -v {wheel}'
142 | CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: 'delvewheel repair --add-path ${{ runner.temp }}\3rd-party\dist\bin --add-path c:\vcpkg\installed\x64-windows\bin -w {dest_dir} {wheel}'
143 | CIBW_TEST_REQUIRES: pytest
144 | CIBW_TEST_COMMAND: pytest {project}/tests
145 |
146 | - name: Upload artifacts
147 | uses: actions/upload-artifact@v4
148 | with:
149 | name: release-${{ matrix.os }}-${{ matrix.arch }}
150 | path: ./wheelhouse/*.whl
151 | retention-days: 5
152 |
153 | upload_all:
154 | needs: [build_sdist, build_wheels]
155 | environment: pypi
156 | permissions:
157 | id-token: write
158 | runs-on: ubuntu-latest
159 | if: github.event_name == 'release' && github.event.action == 'published'
160 | steps:
161 | - name: Get dist files
162 | uses: actions/download-artifact@v4
163 | with:
164 | pattern: release-*
165 | merge-multiple: true
166 | path: dist
167 |
168 | - name: Publish on PyPI
169 | uses: pypa/gh-action-pypi-publish@release/v1
170 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [main]
4 | pull_request:
5 | branches: [main]
6 |
7 | name: Run tests
8 |
9 | jobs:
10 | test:
11 | name: ${{ matrix.os }}, ${{ matrix.python-version }}, ${{ matrix.env }}
12 | runs-on: ${{ matrix.os }}
13 | defaults:
14 | run:
15 | shell: bash -l {0}
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os: ["ubuntu-latest", "macos-latest", "windows-latest"]
20 | python-version: ["3.10", "3.11", "3.12"]
21 | dev: [false]
22 | env:
23 | - ci/environment.yml
24 | include:
25 | - env: ci/environment-dev.yml
26 | os: ubuntu-latest
27 | python-version: "3.12"
28 | dev: true
29 | - env: ci/environment-dev.yml
30 | os: macos-latest
31 | python-version: "3.12"
32 | dev: true
33 | - env: ci/environment-dev.yml
34 | os: windows-latest
35 | python-version: "3.12"
36 | dev: true
37 |
38 | steps:
39 | - name: Checkout repo
40 | uses: actions/checkout@v4
41 |
42 | - name: Get Date
43 | id: get-date
44 | # cache will last one day
45 | run: echo "::set-output name=today::$(/bin/date -u '+%Y%m%d')"
46 |
47 | - name: Setup micromamba
48 | uses: mamba-org/setup-micromamba@v2
49 | with:
50 | environment-file: ${{ matrix.env }}
51 | environment-name: spherely-dev
52 | cache-environment: true
53 | cache-environment-key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ steps.get-date.outputs.today }}-${{ hashFiles( matrix.env) }}"
54 | create-args: >-
55 | python=${{ matrix.python-version }}
56 |
57 | - name: Fetch s2geography
58 | uses: actions/checkout@v4
59 | with:
60 | repository: paleolimbot/s2geography
61 | ref: main
62 | path: deps/s2geography
63 | fetch-depth: 0
64 | if: |
65 | matrix.dev == true
66 |
67 | - name: Configure, build & install s2geography (unix)
68 | run: |
69 | cd deps/s2geography
70 | cmake -S . -B build \
71 | -DCMAKE_CXX_STANDARD=17 \
72 | -DCMAKE_BUILD_TYPE=Release \
73 | -DS2GEOGRAPHY_S2_SOURCE=CONDA \
74 | -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX
75 | cmake --build build
76 | cmake --install build
77 | if: |
78 | matrix.dev == true &&
79 | (runner.os == 'Linux' || runner.os == 'macOS')
80 |
81 | - name: Configure, build & install s2geography (win)
82 | run: |
83 | cd deps/s2geography
84 | cmake -S . -B build \
85 | -DCMAKE_CXX_STANDARD=17 \
86 | -DS2GEOGRAPHY_S2_SOURCE=CONDA \
87 | -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX/Library \
88 | -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE
89 | cmake --build build --config Release
90 | cmake --install build
91 | if: |
92 | matrix.dev == true && runner.os == 'Windows'
93 |
94 | - name: Build and install spherely
95 | run: |
96 | python -m pip install . -v --no-build-isolation --config-settings cmake.define.SPHERELY_CODE_COVERAGE=ON --config-settings build-dir=_skbuild
97 |
98 | - name: Run tests
99 | run: |
100 | pytest . -vv
101 |
102 | - name: Generate and upload coverage report
103 | uses: codecov/codecov-action@v5
104 | with:
105 | gcov: true
106 | gcov_include: src
107 | verbose: true
108 | if: |
109 | runner.os == 'Linux' && matrix.python-version == '3.11' && matrix.dev == false
110 |
111 | test_with_pixi:
112 | name: Tests via pixi
113 | runs-on: ubuntu-latest
114 | steps:
115 | - name: Checkout repo
116 | uses: actions/checkout@v4
117 |
118 | - uses: prefix-dev/setup-pixi@v0.8.1
119 | with:
120 | pixi-version: v0.40.1
121 | cache: true
122 |
123 | - run: pixi run --environment test tests
124 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # IDE
132 | .vscode
133 | .ccls
134 | .ccls-cache/
135 | compile_commands.json
136 | .dir-locals.el
137 |
138 | # docs
139 | docs/build/
140 | docs/_api_generated
141 |
142 | # pixi
143 | .pixi
144 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-yaml
8 | - id: debug-statements
9 | - id: mixed-line-ending
10 | - repo: https://github.com/psf/black
11 | rev: 24.10.0
12 | hooks:
13 | - id: black
14 | args: [--safe, --quiet]
15 | - repo: https://github.com/pre-commit/mirrors-clang-format
16 | rev: v19.1.6
17 | hooks:
18 | - id: clang-format
19 | args: [--style=file]
20 |
21 | ci:
22 | autofix_prs: false
23 | autoupdate_schedule: quarterly
24 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | sphinx:
4 | configuration: docs/conf.py
5 |
6 | build:
7 | os: "ubuntu-20.04"
8 | tools:
9 | python: "mambaforge-4.10"
10 |
11 | conda:
12 | environment: docs/environment.yml
13 |
14 | python:
15 | install:
16 | - method: pip
17 | path: .
18 |
19 | formats: []
20 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.15...3.25)
2 | project(
3 | "${SKBUILD_PROJECT_NAME}"
4 | LANGUAGES CXX
5 | VERSION "${SKBUILD_PROJECT_VERSION}")
6 |
7 | set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to build with")
8 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
9 |
10 | option(SPHERELY_CODE_COVERAGE "Enable coverage reporting" OFF)
11 |
12 | # Dependencies
13 |
14 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/third_party/cmake")
15 |
16 | find_package(
17 | Python
18 | COMPONENTS Interpreter Development.Module
19 | REQUIRED)
20 |
21 | find_package(pybind11 CONFIG REQUIRED)
22 |
23 | find_package(s2 CONFIG REQUIRED)
24 | if(s2_FOUND)
25 | get_target_property(s2_INCLUDE_DIRS s2::s2 INTERFACE_INCLUDE_DIRECTORIES)
26 | message(STATUS "Found s2: ${s2_INCLUDE_DIRS}")
27 | else()
28 | message(FATAL_ERROR "Couldn't find s2")
29 | endif()
30 |
31 | # this is needed so that openssl headers included from s2geometry headers are found.
32 | find_package(OpenSSL REQUIRED)
33 | target_include_directories(s2::s2 INTERFACE ${OPENSSL_INCLUDE_DIR})
34 |
35 | find_package(s2geography CONFIG REQUIRED)
36 | if(${s2geography_FOUND})
37 | get_target_property(s2geography_INCLUDE_DIRS s2geography INTERFACE_INCLUDE_DIRECTORIES)
38 | message(STATUS "Found s2geography v${s2geography_VERSION}: ${s2geography_INCLUDE_DIRS}")
39 | else()
40 | message(FATAL_ERROR "Couldn't find s2geography")
41 | endif()
42 |
43 | if(SPHERELY_CODE_COVERAGE)
44 | message(STATUS "Building spherely with coverage enabled")
45 | add_library(coverage_config INTERFACE)
46 | endif()
47 |
48 | # Compile definitions and flags
49 |
50 | if (MSVC)
51 | # used in s2geometry's CMakeLists.txt but not defined in target
52 | # TODO: move this in FindS2.cmake?
53 | target_compile_definitions(s2::s2 INTERFACE _USE_MATH_DEFINES)
54 | target_compile_definitions(s2::s2 INTERFACE NOMINMAX)
55 | target_compile_options(s2::s2 INTERFACE /J)
56 | endif()
57 |
58 | if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR
59 | CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR
60 | (CMAKE_CXX_COMPILER_ID MATCHES "Intel" AND NOT WIN32))
61 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wextra -Wreorder")
62 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wunused-variable -Wunused-parameter")
63 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wconversion -Wold-style-cast -Wsign-conversion")
64 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
65 | add_definitions(-D_CRT_SECURE_NO_WARNINGS)
66 | add_definitions(-D_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING)
67 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc /MP /bigobj /J")
68 | set(CMAKE_EXE_LINKER_FLAGS /MANIFEST:NO)
69 | endif()
70 |
71 | # Build
72 |
73 | set(CPP_SOURCES
74 | src/accessors-geog.cpp
75 | src/boolean-operations.cpp
76 | src/creation.cpp
77 | src/geography.cpp
78 | src/io.cpp
79 | src/predicates.cpp
80 | src/spherely.cpp)
81 |
82 | if(${s2geography_VERSION} VERSION_GREATER_EQUAL "0.2.0")
83 | set(CPP_SOURCES ${CPP_SOURCES} src/geoarrow.cpp src/projections.cpp)
84 | endif()
85 |
86 | add_library(spherely MODULE ${CPP_SOURCES})
87 |
88 | target_compile_definitions(
89 | spherely
90 | PRIVATE
91 | VERSION_INFO=${PROJECT_VERSION}
92 | S2GEOGRAPHY_VERSION=${s2geography_VERSION}
93 | S2GEOGRAPHY_VERSION_MAJOR=${s2geography_VERSION_MAJOR}
94 | S2GEOGRAPHY_VERSION_MINOR=${s2geography_VERSION_MINOR})
95 |
96 | target_link_libraries(spherely
97 | PRIVATE pybind11::module pybind11::lto pybind11::windows_extras
98 | PUBLIC s2::s2 s2geography
99 | )
100 |
101 | pybind11_extension(spherely)
102 | if(NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo)
103 | # Strip unnecessary sections of the binary on Linux/macOS
104 | pybind11_strip(spherely)
105 | endif()
106 |
107 | set_target_properties(spherely PROPERTIES CXX_VISIBILITY_PRESET "hidden")
108 |
109 | if (SPHERELY_CODE_COVERAGE)
110 | target_compile_options(coverage_config INTERFACE -O0 -g --coverage)
111 | target_link_options(coverage_config INTERFACE --coverage)
112 | target_link_libraries(spherely PUBLIC coverage_config)
113 | endif()
114 |
115 | # Install
116 |
117 | install(TARGETS spherely LIBRARY DESTINATION .)
118 |
119 | # install type annotations
120 | install(FILES src/spherely.pyi DESTINATION .)
121 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2022, Benoit Bovy
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | 
4 | [](https://spherely.readthedocs.io)
5 | [](https://app.codecov.io/gh/benbovy/spherely?branch=main)
6 |
7 | *Python library for manipulation and analysis of geometric objects on the sphere.*
8 |
9 | Spherely is the counterpart of [Shapely](https://github.com/shapely/shapely)
10 | (2.0+) for manipulation and analysis of spherical geometric objects. It is using
11 | the widely deployed open-source geometry library
12 | [s2geometry](https://github.com/google/s2geometry) via the library
13 | [s2geography](https://github.com/paleolimbot/s2geography) which provides a
14 | [GEOS](https://libgeos.org) compatibility layer on top of s2geometry.
15 |
16 | **This library is at an early stage of development.**
17 |
18 | ## Installation
19 |
20 | The easiest way to install Spherely is via its binary packages available for
21 | Linux, MacOS, and Windows platforms on [conda-forge](https://conda-forge.org/)
22 | and [PyPI](https://pypi.org/project/spherely/).
23 |
24 | Install the binary wheel using [pip](https://pip.pypa.io/):
25 |
26 | ``` sh
27 | $ pip install spherely
28 | ```
29 |
30 | Install the conda-forge package using
31 | [conda](https://docs.conda.io/projects/conda/en/stable/):
32 |
33 | ``` sh
34 | $ conda install spherely --channel conda-forge
35 | ```
36 |
37 | To compile and install Spherely from source, see detailed instructions in the
38 | [documentation](https://spherely.readthedocs.io/en/latest/install.html).
39 |
40 | ## Documentation
41 |
42 | https://spherely.readthedocs.io
43 |
44 | ## License
45 |
46 | Spherely is licensed under BSD 3-Clause license. See the LICENSE file for more
47 | details.
48 |
49 | ## Acknowledgment
50 |
51 | The development of this project has been supported by two
52 | [NumFOCUS](https://numfocus.org) Small Development Grants (GeoPandas 2022 round
53 | 1 and GeoPandas 2023 round 3).
54 |
--------------------------------------------------------------------------------
/ci/LICENSE_absl.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | https://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | https://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/ci/LICENSE_openssl.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | https://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/ci/LICENSE_s2geography.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | ==============
3 |
4 | _Version 2.0, January 2004_
5 | _<>_
6 |
7 | ### Terms and Conditions for use, reproduction, and distribution
8 |
9 | #### 1. Definitions
10 |
11 | “License” shall mean the terms and conditions for use, reproduction, and
12 | distribution as defined by Sections 1 through 9 of this document.
13 |
14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright
15 | owner that is granting the License.
16 |
17 | “Legal Entity” shall mean the union of the acting entity and all other entities
18 | that control, are controlled by, or are under common control with that entity.
19 | For the purposes of this definition, “control” means **(i)** the power, direct or
20 | indirect, to cause the direction or management of such entity, whether by
21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
22 | outstanding shares, or **(iii)** beneficial ownership of such entity.
23 |
24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising
25 | permissions granted by this License.
26 |
27 | “Source” form shall mean the preferred form for making modifications, including
28 | but not limited to software source code, documentation source, and configuration
29 | files.
30 |
31 | “Object” form shall mean any form resulting from mechanical transformation or
32 | translation of a Source form, including but not limited to compiled object code,
33 | generated documentation, and conversions to other media types.
34 |
35 | “Work” shall mean the work of authorship, whether in Source or Object form, made
36 | available under the License, as indicated by a copyright notice that is included
37 | in or attached to the work (an example is provided in the Appendix below).
38 |
39 | “Derivative Works” shall mean any work, whether in Source or Object form, that
40 | is based on (or derived from) the Work and for which the editorial revisions,
41 | annotations, elaborations, or other modifications represent, as a whole, an
42 | original work of authorship. For the purposes of this License, Derivative Works
43 | shall not include works that remain separable from, or merely link (or bind by
44 | name) to the interfaces of, the Work and Derivative Works thereof.
45 |
46 | “Contribution” shall mean any work of authorship, including the original version
47 | of the Work and any modifications or additions to that Work or Derivative Works
48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
49 | by the copyright owner or by an individual or Legal Entity authorized to submit
50 | on behalf of the copyright owner. For the purposes of this definition,
51 | “submitted” means any form of electronic, verbal, or written communication sent
52 | to the Licensor or its representatives, including but not limited to
53 | communication on electronic mailing lists, source code control systems, and
54 | issue tracking systems that are managed by, or on behalf of, the Licensor for
55 | the purpose of discussing and improving the Work, but excluding communication
56 | that is conspicuously marked or otherwise designated in writing by the copyright
57 | owner as “Not a Contribution.”
58 |
59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf
60 | of whom a Contribution has been received by Licensor and subsequently
61 | incorporated within the Work.
62 |
63 | #### 2. Grant of Copyright License
64 |
65 | Subject to the terms and conditions of this License, each Contributor hereby
66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
67 | irrevocable copyright license to reproduce, prepare Derivative Works of,
68 | publicly display, publicly perform, sublicense, and distribute the Work and such
69 | Derivative Works in Source or Object form.
70 |
71 | #### 3. Grant of Patent License
72 |
73 | Subject to the terms and conditions of this License, each Contributor hereby
74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
75 | irrevocable (except as stated in this section) patent license to make, have
76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
77 | such license applies only to those patent claims licensable by such Contributor
78 | that are necessarily infringed by their Contribution(s) alone or by combination
79 | of their Contribution(s) with the Work to which such Contribution(s) was
80 | submitted. If You institute patent litigation against any entity (including a
81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
82 | Contribution incorporated within the Work constitutes direct or contributory
83 | patent infringement, then any patent licenses granted to You under this License
84 | for that Work shall terminate as of the date such litigation is filed.
85 |
86 | #### 4. Redistribution
87 |
88 | You may reproduce and distribute copies of the Work or Derivative Works thereof
89 | in any medium, with or without modifications, and in Source or Object form,
90 | provided that You meet the following conditions:
91 |
92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of
93 | this License; and
94 | * **(b)** You must cause any modified files to carry prominent notices stating that You
95 | changed the files; and
96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
97 | all copyright, patent, trademark, and attribution notices from the Source form
98 | of the Work, excluding those notices that do not pertain to any part of the
99 | Derivative Works; and
100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
101 | Derivative Works that You distribute must include a readable copy of the
102 | attribution notices contained within such NOTICE file, excluding those notices
103 | that do not pertain to any part of the Derivative Works, in at least one of the
104 | following places: within a NOTICE text file distributed as part of the
105 | Derivative Works; within the Source form or documentation, if provided along
106 | with the Derivative Works; or, within a display generated by the Derivative
107 | Works, if and wherever such third-party notices normally appear. The contents of
108 | the NOTICE file are for informational purposes only and do not modify the
109 | License. You may add Your own attribution notices within Derivative Works that
110 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
111 | provided that such additional attribution notices cannot be construed as
112 | modifying the License.
113 |
114 | You may add Your own copyright statement to Your modifications and may provide
115 | additional or different license terms and conditions for use, reproduction, or
116 | distribution of Your modifications, or for any such Derivative Works as a whole,
117 | provided Your use, reproduction, and distribution of the Work otherwise complies
118 | with the conditions stated in this License.
119 |
120 | #### 5. Submission of Contributions
121 |
122 | Unless You explicitly state otherwise, any Contribution intentionally submitted
123 | for inclusion in the Work by You to the Licensor shall be under the terms and
124 | conditions of this License, without any additional terms or conditions.
125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
126 | any separate license agreement you may have executed with Licensor regarding
127 | such Contributions.
128 |
129 | #### 6. Trademarks
130 |
131 | This License does not grant permission to use the trade names, trademarks,
132 | service marks, or product names of the Licensor, except as required for
133 | reasonable and customary use in describing the origin of the Work and
134 | reproducing the content of the NOTICE file.
135 |
136 | #### 7. Disclaimer of Warranty
137 |
138 | Unless required by applicable law or agreed to in writing, Licensor provides the
139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
141 | including, without limitation, any warranties or conditions of TITLE,
142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
143 | solely responsible for determining the appropriateness of using or
144 | redistributing the Work and assume any risks associated with Your exercise of
145 | permissions under this License.
146 |
147 | #### 8. Limitation of Liability
148 |
149 | In no event and under no legal theory, whether in tort (including negligence),
150 | contract, or otherwise, unless required by applicable law (such as deliberate
151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
152 | liable to You for damages, including any direct, indirect, special, incidental,
153 | or consequential damages of any character arising as a result of this License or
154 | out of the use or inability to use the Work (including but not limited to
155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
156 | any and all other commercial damages or losses), even if such Contributor has
157 | been advised of the possibility of such damages.
158 |
159 | #### 9. Accepting Warranty or Additional Liability
160 |
161 | While redistributing the Work or Derivative Works thereof, You may choose to
162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
163 | other liability obligations and/or rights consistent with this License. However,
164 | in accepting such obligations, You may act only on Your own behalf and on Your
165 | sole responsibility, not on behalf of any other Contributor, and only if You
166 | agree to indemnify, defend, and hold each Contributor harmless for any liability
167 | incurred by, or claims asserted against, such Contributor by reason of your
168 | accepting any such warranty or additional liability.
169 |
170 | _END OF TERMS AND CONDITIONS_
171 |
172 | ### APPENDIX: How to apply the Apache License to your work
173 |
174 | To apply the Apache License to your work, attach the following boilerplate
175 | notice, with the fields enclosed by brackets `[]` replaced with your own
176 | identifying information. (Don't include the brackets!) The text should be
177 | enclosed in the appropriate comment syntax for the file format. We also
178 | recommend that a file or class name and description of purpose be included on
179 | the same “printed page” as the copyright notice for easier identification within
180 | third-party archives.
181 |
182 | Copyright [yyyy] [name of copyright owner]
183 |
184 | Licensed under the Apache License, Version 2.0 (the "License");
185 | you may not use this file except in compliance with the License.
186 | You may obtain a copy of the License at
187 |
188 | http://www.apache.org/licenses/LICENSE-2.0
189 |
190 | Unless required by applicable law or agreed to in writing, software
191 | distributed under the License is distributed on an "AS IS" BASIS,
192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
193 | See the License for the specific language governing permissions and
194 | limitations under the License.
195 |
--------------------------------------------------------------------------------
/ci/environment-dev.yml:
--------------------------------------------------------------------------------
1 | name: spherely-dev
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - cxx-compiler
6 | - s2geometry>=0.11.1
7 | - libabseil
8 | - cmake
9 | - python
10 | - numpy
11 | - pybind11>=2.11.0
12 | - scikit-build-core
13 | - ninja
14 | - pytest
15 | - pip
16 | - geoarrow-pyarrow
17 |
--------------------------------------------------------------------------------
/ci/environment.yml:
--------------------------------------------------------------------------------
1 | name: spherely-dev
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - cxx-compiler
6 | - s2geometry>=0.11.1
7 | - s2geography>=0.2.0
8 | - libabseil
9 | - cmake
10 | - python
11 | - numpy
12 | - pybind11>=2.11.0
13 | - scikit-build-core
14 | - ninja
15 | - pytest
16 | - pip
17 | - geoarrow-pyarrow
18 |
--------------------------------------------------------------------------------
/ci/install_3rdparty.cmd:
--------------------------------------------------------------------------------
1 | :: Build and install absl, s2 and s2geography on Windows.
2 | ::
3 | :: This script requires environment variables to be set
4 | :: - DEPENDENCIES_DIR=/path/to/cached/prefix -- to build or use as cache
5 | :: - ABSL_VERSION
6 | :: - S2GEOMETRY_VERSION
7 | :: - S2GEOGRAPHY_VERSION
8 | :: - CXX_STANDARD
9 | ::
10 | :: This script assumes that library sources have been downloaded or copied in
11 | :: DEPENDENCIES_DIR (e.g., %DEPENDENCIES_DIR%/absl-src-%ABSL_VERSION%).
12 |
13 | set SRC_DIR=%DEPENDENCIES_DIR%\src
14 | set BUILD_DIR=%DEPENDENCIES_DIR%\build
15 | set INSTALL_DIR=%DEPENDENCIES_DIR%\dist
16 |
17 | if exist %INSTALL_DIR%\include\s2geography (
18 | echo Using cached install directory %INSTALL_DIR%
19 | exit /B 0
20 | )
21 |
22 | mkdir %SRC_DIR%
23 | mkdir %BUILD_DIR%
24 | mkdir %INSTALL_DIR%
25 |
26 | rem set ABSL_SRC_DIR=%DEPENDENCIES_DIR%\absl-src-%ABSL_VERSION%
27 | rem set S2GEOMETRY_SRC_DIR=%DEPENDENCIES_DIR%\s2geometry-src-%S2GEOMETRY_VERSION%
28 | set S2GEOGRAPHY_SRC_DIR=%DEPENDENCIES_DIR%\s2geography-src-%S2GEOGRAPHY_VERSION%
29 |
30 | rem set ABSL_BUILD_DIR=%BUILD_DIR%\absl-src-%ABSL_VERSION%
31 | rem set S2GEOMETRY_BUILD_DIR=%BUILD_DIR%\s2geometry-src-%S2GEOMETRY_VERSION%
32 | set S2GEOGRAPHY_BUILD_DIR=%BUILD_DIR%\s2geography-src-%S2GEOGRAPHY_VERSION%
33 |
34 | echo %CMAKE_PREFIX_PATH%
35 |
36 | echo "----- Installing cmake"
37 | pip install ninja cmake
38 |
39 | rem echo "----- Downloading, building and installing absl-%ABSL_VERSION%"
40 |
41 | rem cd %DEPENDENCIES_DIR%
42 | rem curl -o absl.tar.gz -L https://github.com/abseil/abseil-cpp/archive/refs/tags/%ABSL_VERSION%.tar.gz
43 | rem tar -xf absl.tar.gz -C %SRC_DIR%
44 |
45 | rem cmake -GNinja ^
46 | rem -S %SRC_DIR%/abseil-cpp-%ABSL_VERSION% ^
47 | rem -B %ABSL_BUILD_DIR% ^
48 | rem -DCMAKE_INSTALL_PREFIX=%INSTALL_DIR% ^
49 | rem -DCMAKE_POSITION_INDEPENDENT_CODE=ON ^
50 | rem -DCMAKE_CXX_STANDARD=%CXX_STANDARD% ^
51 | rem -DCMAKE_BUILD_TYPE=Release ^
52 | rem -DABSL_ENABLE_INSTALL=ON
53 |
54 | rem IF %ERRORLEVEL% NEQ 0 exit /B 1
55 | rem cmake --build %ABSL_BUILD_DIR%
56 | rem IF %ERRORLEVEL% NEQ 0 exit /B 2
57 | rem cmake --install %ABSL_BUILD_DIR%
58 |
59 | rem echo "----- Downloading, building and installing s2geometry-%S2GEOMETRY_VERSION%"
60 |
61 | rem echo %OPENSSL_ROOT_DIR%
62 |
63 | rem cd %DEPENDENCIES_DIR%
64 | rem curl -o s2geometry.tar.gz -L https://github.com/google/s2geometry/archive/refs/tags/v%S2GEOMETRY_VERSION%.tar.gz
65 | rem tar -xf s2geometry.tar.gz -C %SRC_DIR%
66 |
67 | rem cmake -GNinja ^
68 | rem -S %SRC_DIR%/s2geometry-%S2GEOMETRY_VERSION% ^
69 | rem -B %S2GEOMETRY_BUILD_DIR% ^
70 | rem -DCMAKE_INSTALL_PREFIX=%INSTALL_DIR% ^
71 | rem -DOPENSSL_ROOT_DIR=%OPENSSL_ROOT_DIR% ^
72 | rem -DBUILD_TESTS=OFF ^
73 | rem -DBUILD_EXAMPLES=OFF ^
74 | rem -UGOOGLETEST_ROOT ^
75 | rem -DCMAKE_CXX_STANDARD=%CXX_STANDARD% ^
76 | rem -DCMAKE_BUILD_TYPE=Release ^
77 | rem -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE ^
78 | rem -DBUILD_SHARED_LIBS=ON
79 |
80 | rem IF %ERRORLEVEL% NEQ 0 exit /B 3
81 | rem cmake --build %S2GEOMETRY_BUILD_DIR%
82 | rem IF %ERRORLEVEL% NEQ 0 exit /B 4
83 | rem cmake --install %S2GEOMETRY_BUILD_DIR%
84 |
85 | echo "----- Downloading, building and installing s2geography-%S2GEOGRAPHY_VERSION%"
86 |
87 | cd %DEPENDENCIES_DIR%
88 | curl -o s2geography.tar.gz -L https://github.com/paleolimbot/s2geography/archive/refs/tags/%S2GEOGRAPHY_VERSION%.tar.gz
89 | tar -xf s2geography.tar.gz -C %SRC_DIR%
90 |
91 | rem TODO: remove when fixed in s2geography
92 | rem (https://github.com/paleolimbot/s2geography/pull/53)
93 | cd %SRC_DIR%/s2geography-%S2GEOGRAPHY_VERSION%
94 | patch -i %PROJECT_DIR%\ci\s2geography-add-openssl-as-requirement.patch
95 |
96 | cmake -GNinja ^
97 | -S %SRC_DIR%/s2geography-%S2GEOGRAPHY_VERSION% ^
98 | -B %S2GEOGRAPHY_BUILD_DIR% ^
99 | -DCMAKE_INSTALL_PREFIX=%INSTALL_DIR% ^
100 | -DOPENSSL_ROOT_DIR=%OPENSSL_ROOT_DIR% ^
101 | -DS2GEOGRAPHY_BUILD_TESTS=OFF ^
102 | -DS2GEOGRAPHY_S2_SOURCE=AUTO ^
103 | -DS2GEOGRAPHY_BUILD_EXAMPLES=OFF ^
104 | -DCMAKE_CXX_STANDARD=%CXX_STANDARD% ^
105 | -DCMAKE_BUILD_TYPE=Release ^
106 | -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE ^
107 | -DBUILD_SHARED_LIBS=ON
108 |
109 | IF %ERRORLEVEL% NEQ 0 exit /B 5
110 | cmake --build %S2GEOGRAPHY_BUILD_DIR%
111 | IF %ERRORLEVEL% NEQ 0 exit /B 6
112 | cmake --install %S2GEOGRAPHY_BUILD_DIR%
113 |
--------------------------------------------------------------------------------
/ci/install_3rdparty.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Build and install absl, s2 and s2geography on posix systems.
4 | #
5 | # This script requires environment variables to be set
6 | # - DEPENDENCIES_DIR=/path/to/cached/prefix -- to build or use as cache
7 | # - ABSL_VERSION
8 | # - S2GEOMETRY_VERSION
9 | # - S2GEOGRAPHY_VERSION
10 | # - CXX_STANDARD
11 | #
12 | # This script assumes that library sources have been downloaded or copied in
13 | # DEPENDENCIES_DIR (e.g., $DEPENDENCIES_DIR/absl-src-$ABSL_VERSION).
14 | pushd .
15 |
16 | set -e
17 |
18 | if [ -z "$DEPENDENCIES_DIR" ]; then
19 | echo "DEPENDENCIES_DIR must be set"
20 | exit 1
21 | elif [ -z "$ABSL_VERSION" ]; then
22 | echo "ABSL_VERSION must be set"
23 | exit 1
24 | elif [ -z "$S2GEOMETRY_VERSION" ]; then
25 | echo "S2GEOMETRY_VERSION must be set"
26 | exit 1
27 | elif [ -z "$S2GEOGRAPHY_VERSION" ]; then
28 | echo "S2GEOGRAPHY_VERSION must be set"
29 | exit 1
30 | elif [ -z "$CXX_STANDARD" ]; then
31 | echo "CXX_STANDARD must be set"
32 | exit 1
33 | fi
34 |
35 | SRC_DIR=$DEPENDENCIES_DIR/src
36 | BUILD_DIR=$DEPENDENCIES_DIR/build
37 | INSTALL_DIR=$DEPENDENCIES_DIR/dist
38 |
39 | mkdir -p $SRC_DIR
40 | mkdir -p $BUILD_DIR
41 | mkdir -p $INSTALL_DIR
42 |
43 | ABSL_SRC_DIR=$DEPENDENCIES_DIR/absl-src-$ABSL_VERSION
44 | S2GEOMETRY_SRC_DIR=$DEPENDENCIES_DIR/s2geometry-src-$S2GEOMETRY_VERSION
45 | S2GEOGRAPHY_SRC_DIR=$DEPENDENCIES_DIR/s2geography-src-$S2GEOGRAPHY_VERSION
46 |
47 | ABSL_BUILD_DIR=$BUILD_DIR/absl-src-$ABSL_VERSION
48 | S2GEOMETRY_BUILD_DIR=$BUILD_DIR/s2geometry-src-$S2GEOMETRY_VERSION
49 | S2GEOGRAPHY_BUILD_DIR=$BUILD_DIR/s2geography-src-$S2GEOGRAPHY_VERSION
50 |
51 | build_install_dependencies(){
52 | echo "----- Installing cmake"
53 | pip install cmake
54 |
55 | echo "------ Clean build and install directories"
56 |
57 | rm -rf $BUILD_DIR/*
58 | rm -rf $INSTALL_DIR/*
59 |
60 | echo "----- Downloading, building and installing absl-$ABSL_VERSION"
61 |
62 | cd $DEPENDENCIES_DIR
63 | curl -o absl.tar.gz -L https://github.com/abseil/abseil-cpp/archive/refs/tags/$ABSL_VERSION.tar.gz
64 | tar -xf absl.tar.gz -C $SRC_DIR
65 | rm -f absl.tar.gz
66 |
67 | cmake -S $SRC_DIR/abseil-cpp-$ABSL_VERSION -B $ABSL_BUILD_DIR \
68 | -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \
69 | -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
70 | -DCMAKE_CXX_STANDARD=$CXX_STANDARD \
71 | -DCMAKE_BUILD_TYPE=Release \
72 | -DABSL_ENABLE_INSTALL=ON
73 |
74 | cmake --build $ABSL_BUILD_DIR
75 | cmake --install $ABSL_BUILD_DIR
76 |
77 | echo "----- Downloading, building and installing s2geometry-$S2GEOMETRY_VERSION"
78 |
79 | cd $DEPENDENCIES_DIR
80 | curl -o s2geometry.tar.gz -L https://github.com/google/s2geometry/archive/refs/tags/v$S2GEOMETRY_VERSION.tar.gz
81 | tar -xf s2geometry.tar.gz -C $SRC_DIR
82 | rm -f s2geometry.tar.gz
83 |
84 | cmake -S $SRC_DIR/s2geometry-$S2GEOMETRY_VERSION -B $S2GEOMETRY_BUILD_DIR \
85 | -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \
86 | -DBUILD_TESTS=OFF \
87 | -DBUILD_EXAMPLES=OFF \
88 | -UGOOGLETEST_ROOT \
89 | -DCMAKE_CXX_STANDARD=$CXX_STANDARD \
90 | -DCMAKE_BUILD_TYPE=Release \
91 | -DBUILD_SHARED_LIBS=ON
92 |
93 | cmake --build $S2GEOMETRY_BUILD_DIR
94 | cmake --install $S2GEOMETRY_BUILD_DIR
95 |
96 | echo "----- Downloading, building and installing s2geography-$S2GEOGRAPHY_VERSION"
97 |
98 | cd $DEPENDENCIES_DIR
99 | curl -o s2geography.tar.gz -L https://github.com/paleolimbot/s2geography/archive/refs/tags/$S2GEOGRAPHY_VERSION.tar.gz
100 | tar -xf s2geography.tar.gz -C $SRC_DIR
101 | rm -f s2geography.tar.gz
102 |
103 | # TODO: remove when fixed in s2geography
104 | # (https://github.com/paleolimbot/s2geography/pull/53)
105 | cd $SRC_DIR/s2geography-$S2GEOGRAPHY_VERSION
106 | if [ "$(uname)" == "Darwin" ]; then
107 | patch -p1 < $PROJECT_DIR/ci/s2geography-add-openssl-as-requirement.patch
108 | else
109 | patch -p1 < /project/ci/s2geography-add-openssl-as-requirement.patch
110 | fi
111 |
112 | cmake -S $SRC_DIR/s2geography-$S2GEOGRAPHY_VERSION -B $S2GEOGRAPHY_BUILD_DIR \
113 | -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR \
114 | -DS2GEOGRAPHY_BUILD_TESTS=OFF \
115 | -DS2GEOGRAPHY_S2_SOURCE=AUTO \
116 | -DS2GEOGRAPHY_BUILD_EXAMPLES=OFF \
117 | -DCMAKE_CXX_STANDARD=$CXX_STANDARD \
118 | -DCMAKE_BUILD_TYPE=Release \
119 | -DBUILD_SHARED_LIBS=ON
120 |
121 | cmake --build $S2GEOGRAPHY_BUILD_DIR
122 | cmake --install $S2GEOGRAPHY_BUILD_DIR
123 | }
124 |
125 |
126 | echo "----- Installing OpenSSL in Linux container"
127 |
128 | if [ "$(uname)" != "Darwin" ]; then
129 | # assume manylinux2014 https://cibuildwheel.pypa.io/en/stable/faq/
130 | # TODO: this is done outside of build_install_dependencies so it can
131 | # work with a cached install directory, but it doesn't prevent
132 | # installing an openssl version greater than the one used to build
133 | # libraries in build_install_dependencies (shoudn't be likely, though).
134 | yum install -y openssl-devel
135 | fi
136 |
137 |
138 | if [ -d "$INSTALL_DIR/include/s2geography" ]; then
139 | echo "----- Using cached install directory $INSTALL_DIR"
140 | else
141 | build_install_dependencies
142 | fi
143 |
144 | popd
145 |
--------------------------------------------------------------------------------
/ci/s2geography-add-include-dir.patch:
--------------------------------------------------------------------------------
1 | diff --git a/CMakeLists.txt b/CMakeLists.txt
2 | index d0d5e56..a520a1f 100644
3 | --- a/CMakeLists.txt
4 | +++ b/CMakeLists.txt
5 | @@ -212,6 +212,10 @@ add_library(s2geography
6 | set_target_properties(s2geography PROPERTIES
7 | POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
8 |
9 | +target_include_directories(s2geography PUBLIC
10 | + $
11 | + $)
12 | +
13 | target_compile_definitions(
14 | s2geography
15 | PUBLIC
16 |
--------------------------------------------------------------------------------
/ci/s2geography-add-openssl-as-requirement.patch:
--------------------------------------------------------------------------------
1 | From 7287508b986e65403b3352889c94249ee11a231a Mon Sep 17 00:00:00 2001
2 | From: Benoit Bovy
3 | Date: Wed, 11 Dec 2024 09:09:47 +0100
4 | Subject: [PATCH] another approach for openssl
5 |
6 | ---
7 | CMakeLists.txt | 6 +++++-
8 | 1 file changed, 5 insertions(+), 1 deletion(-)
9 |
10 | diff --git a/CMakeLists.txt b/CMakeLists.txt
11 | index 5fb3e93..0028c86 100644
12 | --- a/CMakeLists.txt
13 | +++ b/CMakeLists.txt
14 | @@ -173,6 +173,9 @@ elseif(${S2_SOURCE} STREQUAL "SYSTEM")
15 | endif()
16 | endif()
17 |
18 | +# --- OpenSSL
19 | +find_package(OpenSSL REQUIRED)
20 | +
21 | # --- Abseil (bundled build not supported)
22 |
23 | find_package(absl REQUIRED)
24 | @@ -271,7 +274,8 @@ if(MSVC)
25 | target_compile_options(s2geography PUBLIC /J)
26 | endif()
27 |
28 | -target_link_libraries(s2geography PUBLIC s2::s2 absl::memory absl::str_format)
29 | +target_link_libraries(s2geography PUBLIC s2::s2 absl::memory absl::str_format
30 | + OpenSSL::SSL OpenSSL::Crypto)
31 |
32 | # Set somewhat aggressive compiler warning flags
33 | if(S2GEOGRAPHY_EXTRA_WARNINGS)
34 | --
35 | 2.36.0
36 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = spherely
8 | SOURCEDIR = .
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/_static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benbovy/spherely/4f527296b3539865c37e7e477ec6ccba9cd5861e/docs/_static/favicon.ico
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benbovy/spherely/4f527296b3539865c37e7e477ec6ccba9cd5861e/docs/_static/spherely_logo_large.png
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_medium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benbovy/spherely/4f527296b3539865c37e7e477ec6ccba9cd5861e/docs/_static/spherely_logo_medium.png
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_noline.svg:
--------------------------------------------------------------------------------
1 |
2 |
35 |
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_noline_medium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benbovy/spherely/4f527296b3539865c37e7e477ec6ccba9cd5861e/docs/_static/spherely_logo_noline_medium.png
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_noline_notext.svg:
--------------------------------------------------------------------------------
1 |
2 |
28 |
--------------------------------------------------------------------------------
/docs/_static/spherely_logo_noline_notext_square.svg:
--------------------------------------------------------------------------------
1 |
2 |
28 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | .. _api:
2 |
3 | API reference
4 | =============
5 |
6 | .. currentmodule:: spherely
7 |
8 | .. _api_properties:
9 |
10 | Geography properties
11 | --------------------
12 |
13 | Functions that provide access to properties of :py:class:`~spherely.Geography`
14 | objects without side-effects (except for ``prepare`` and ``destroy_prepared``).
15 |
16 | .. autosummary::
17 | :toctree: _api_generated/
18 |
19 | GeographyType
20 | is_geography
21 | get_dimension
22 | get_type_id
23 | is_empty
24 | get_x
25 | get_y
26 | is_prepared
27 | prepare
28 | destroy_prepared
29 |
30 | .. _api_creation:
31 |
32 | Geography creation
33 | ------------------
34 |
35 | Functions that build new :py:class:`~spherely.Geography` objects from
36 | coordinates or existing geographies.
37 |
38 | .. autosummary::
39 | :toctree: _api_generated/
40 |
41 | create_point
42 | create_multipoint
43 | create_linestring
44 | create_multilinestring
45 | create_polygon
46 | create_multipolygon
47 | create_collection
48 |
49 | .. _api_io:
50 |
51 | Input/Output
52 | ------------
53 |
54 | Functions that convert :py:class:`~spherely.Geography` objects to/from an
55 | external format such as `WKT `_.
56 |
57 | .. autosummary::
58 | :toctree: _api_generated/
59 |
60 | from_wkt
61 | to_wkt
62 | from_wkb
63 | to_wkb
64 | from_geoarrow
65 | to_geoarrow
66 |
67 | .. _api_measurement:
68 |
69 | Measurement
70 | -----------
71 |
72 | Functions that compute measurements of one or more geographies.
73 |
74 | .. autosummary::
75 | :toctree: _api_generated/
76 |
77 | area
78 | distance
79 | length
80 | perimeter
81 |
82 | .. _api_predicates:
83 |
84 | Predicates
85 | ----------
86 |
87 | Functions that return ``True`` or ``False`` for some spatial relationship
88 | between two geographies.
89 |
90 | .. autosummary::
91 | :toctree: _api_generated/
92 |
93 | equals
94 | intersects
95 | touches
96 | contains
97 | within
98 | disjoint
99 | covers
100 | covered_by
101 |
102 | .. _api_overlays:
103 |
104 | Overlays (boolean operations)
105 | -----------------------------
106 |
107 | Functions that generate a new geography based on the combination of two
108 | geographies.
109 |
110 | .. autosummary::
111 | :toctree: _api_generated/
112 |
113 | union
114 | intersection
115 | difference
116 | symmetric_difference
117 |
118 | .. _api_constructive_ops:
119 |
120 | Constructive operations
121 | -----------------------
122 |
123 | Functions that generate a new geography based on input.
124 |
125 | .. autosummary::
126 | :toctree: _api_generated/
127 |
128 | centroid
129 | boundary
130 | convex_hull
131 |
--------------------------------------------------------------------------------
/docs/api_hidden.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: spherely
2 |
3 | :orphan:
4 |
5 | .. autosummary::
6 | :toctree: _api_generated/
7 |
8 | Geography
9 | Projection
10 | Projection.lnglat
11 | Projection.pseudo_mercator
12 | Projection.orthographic
13 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import spherely
4 |
5 | project = "spherely"
6 | author = "Spherely developers"
7 | copyright = "2022, Spherely Developers"
8 | # The short X.Y version.
9 | version = spherely.__version__.split("+")[0]
10 | # The full version, including alpha/beta/rc tags.
11 | release = spherely.__version__
12 |
13 | # -- General configuration ----------------------------------------------
14 |
15 | extensions = [
16 | "sphinx.ext.autodoc",
17 | "sphinx.ext.autosummary",
18 | "sphinx.ext.intersphinx",
19 | "sphinx.ext.napoleon",
20 | "myst_nb",
21 | ]
22 |
23 | intersphinx_mapping = {
24 | "python": ("https://docs.python.org/3", None),
25 | "numpy": ("https://numpy.org/doc/stable", None),
26 | "shapely": ("https://shapely.readthedocs.io/en/latest/", None),
27 | "pyarrow": ("https://arrow.apache.org/docs/", None),
28 | }
29 |
30 | napoleon_google_docstring = False
31 | napoleon_numpy_docstring = True
32 | napoleon_use_param = False
33 | napoleon_use_rtype = False
34 | napoleon_preprocess_types = True
35 | napoleon_type_aliases = {
36 | # general terms
37 | "sequence": ":term:`sequence`",
38 | "iterable": ":term:`iterable`",
39 | # numpy terms
40 | "array_like": ":term:`array_like`",
41 | "array-like": ":term:`array-like `",
42 | # objects without namespace: spherely
43 | "EARTH_RADIUS_METERS": "spherely.EARTH_RADIUS_METERS",
44 | # objects without namespace: numpy
45 | "ndarray": "~numpy.ndarray",
46 | "array": ":term:`array`",
47 | }
48 |
49 | source_suffix = [".rst", ".md"]
50 |
51 | master_doc = "index"
52 |
53 | language = "en"
54 |
55 | exclude_patterns = [
56 | "**.ipynb_checkpoints",
57 | "build/**.ipynb",
58 | ]
59 |
60 | templates_path = ["_templates"]
61 |
62 | highlight_language = "python"
63 |
64 | pygments_style = "sphinx"
65 |
66 | # -- Options for HTML output ----------------------------------------------
67 |
68 | html_theme = "sphinx_book_theme"
69 | html_title = ""
70 |
71 | html_theme_options = dict(
72 | repository_url="https://github.com/benbovy/spherely",
73 | repository_branch="main",
74 | path_to_docs="docs",
75 | use_edit_page_button=True,
76 | use_repository_button=True,
77 | use_issues_button=True,
78 | home_page_in_toc=False,
79 | )
80 |
81 | html_static_path = ["_static"]
82 | html_logo = "_static/spherely_logo_noline.svg"
83 | html_favicon = "_static/favicon.ico"
84 | htmlhelp_basename = "spherelydoc"
85 |
--------------------------------------------------------------------------------
/docs/environment.yml:
--------------------------------------------------------------------------------
1 |
2 | name: spherely-docs
3 | channels:
4 | - conda-forge
5 | dependencies:
6 | - python=3.11
7 | - numpy
8 | - cxx-compiler
9 | - cmake
10 | - make
11 | - s2geometry>=0.11.1
12 | - s2geography>=0.2.0
13 | - libabseil
14 | - sphinx
15 | - pydata-sphinx-theme=0.15.4
16 | - sphinx-book-theme=1.1.3
17 | - myst-nb
18 | - pip
19 | # TODO: install the library here when s2geography is packaged
20 | #- pip:
21 | # - ..
22 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Spherely Documentation
2 |
3 | Manipulation and analysis of geometric objects on the sphere.
4 |
5 | Spherely is the counterpart of [Shapely] (2.0+) for manipulation and analysis
6 | of spherical geometric objects. It is using the widely deployed open-source
7 | geometry library [s2geometry] via the library [s2geography] which provides a
8 | [GEOS] compatibility layer on top of s2geometry.
9 |
10 | **This library is at an early stage of development.**
11 |
12 | **Useful links**:
13 | [Home](http://spherely.readthedocs.io/) |
14 | [Code Repository](https://github.com/benbovy/spherely) |
15 | [Issues](https://github.com/benbovy/spherely/issues) |
16 | [Discussions](https://github.com/benbovy/spherely/discussions) |
17 | [Releases](https://github.com/benbovy/spherely/releases)
18 |
19 | ## Contents
20 |
21 | ```{toctree}
22 | :maxdepth: 1
23 |
24 | install
25 | api
26 | ```
27 |
28 | ## Acknowledgment
29 |
30 | The development of this project has been supported by two
31 | [NumFOCUS] Small Development Grants (GeoPandas 2022 round
32 | 1 and GeoPandas 2023 round 3).
33 |
34 | [Shapely]: https://shapely.readthedocs.io
35 | [s2geometry]: https://s2geometry.io
36 | [s2geography]: https://github.com/paleolimbot/s2geography
37 | [GEOS]: https://libgeos.org
38 | [NumFOCUS]: https://numfocus.org
39 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | (install)=
2 |
3 | # Installation
4 |
5 | ## Built distributions
6 |
7 | The easiest way to install Spherely is via its binary packages available for
8 | Linux, MacOS, and Windows platforms on [conda-forge](https://conda-forge.org/)
9 | and [PyPI](https://pypi.org/project/spherely/).
10 |
11 | ### Installation of Python binary wheels (PyPI)
12 |
13 | Install the last released binary wheel, e.g., using [pip](https://pip.pypa.io/):
14 |
15 | ``` sh
16 | $ pip install spherely
17 | ```
18 |
19 | ### Installation of Conda packages (conda-forge)
20 |
21 | Install the last released conda-forge package using
22 | [conda](https://docs.conda.io/projects/conda/en/stable/):
23 |
24 | ``` sh
25 | $ conda install spherely --channel conda-forge
26 | ```
27 |
28 | ## Installation from source
29 |
30 | Compiling and installing Spherely from source may be useful for development
31 | purpose and/or for building it against a specific version of S2Geography and/or
32 | S2Geometry.
33 |
34 | ### Requirements
35 |
36 | - Python
37 | - Numpy
38 | - [s2geography](https://github.com/paleolimbot/s2geography) v0.2.0 or higher
39 | - [s2geometry](https://github.com/google/s2geometry) v0.11.1 or higher
40 |
41 | Additional build dependencies:
42 |
43 | - C++ compiler supporting C++17 standard
44 | - CMake
45 | - [scikit-build-core](https://github.com/scikit-build/scikit-build-core)
46 |
47 | ### Cloning the source repository
48 |
49 | Spherely's source code can be downloaded by cloning its [source
50 | repository](https://github.com/benbovy/spherely):
51 |
52 | ```sh
53 | $ git clone https://github.com/benbovy/spherely
54 | $ cd spherely
55 | ```
56 |
57 | ### Setting up a development environment using pixi
58 |
59 | Spherely provides everything needed to manage its dependencies and run common
60 | tasks via [pixi](https://pixi.sh).
61 |
62 | If you have `pixi` installed, you can install a complete development environment
63 | for your platform simply by executing the following command from Spherely's
64 | project root directory:
65 |
66 | ```sh
67 | $ pixi install --environment all
68 | ```
69 |
70 | Running the command below from Spherely's root directory will install all
71 | required tools and dependencies (if not installed yet) in a local environment,
72 | build and install Spherely (if needed) and run the tests:
73 |
74 | ```sh
75 | $ pixi run tests
76 | ```
77 |
78 | All available tasks are detailed in the ``pyproject.toml`` file or listed via
79 | the following command:
80 |
81 | ```sh
82 | $ pixi task list
83 | ```
84 |
85 | ### Setting up a development environment using conda
86 |
87 | If you don't have `pixi` installed, you can follow the steps below to manually
88 | setup a conda environment for developing Spherely.
89 |
90 | After cloning Spherely's source repository, create a conda environment
91 | with the required (and development) dependencies using the
92 | `ci/environment-dev.yml` file:
93 |
94 | ```sh
95 | $ conda env create -f ci/environment-dev.yml
96 | $ conda activate spherely-dev
97 | ```
98 |
99 | Build and install Spherely:
100 |
101 | ```sh
102 | $ python -m pip install . -v --no-build-isolation
103 | ```
104 |
105 | Note that you can specify a build directory in order to avoid rebuilding the
106 | whole library from scratch each time after editing the code:
107 |
108 | ```sh
109 | $ python -m pip install . -v --no-build-isolation --config-settings build-dir=build/skbuild
110 | ```
111 |
112 | Run the tests:
113 |
114 | ```sh
115 | $ pytest . -v
116 | ```
117 |
118 | Spherely also uses [pre-commit](https://pre-commit.com/) for code
119 | auto-formatting and linting at every commit. After installing it, you can enable
120 | pre-commit hooks with the following command:
121 |
122 | ```sh
123 | $ pre-commit install
124 | ```
125 |
126 | (Note: you can skip the pre-commit checks with `git commit --no-verify`)
127 |
128 | ### Using the latest S2Geography version
129 |
130 | If you want to compile Spherely against the latest version of S2Geography, use:
131 |
132 | ```sh
133 | $ git clone https://github.com/paleolimbot/s2geography
134 | $ cmake \
135 | $ -S s2geography \
136 | $ -B s2geography/build \
137 | $ -DCMAKE_CXX_STANDARD=17 \
138 | $ -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX
139 | $ cmake --build s2geography/build
140 | $ cmake --install s2geography/build
141 | ```
142 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 | set SPHINXPROJ=spherely
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "scikit_build_core[rich]",
4 | "pybind11>=2.11",
5 | ]
6 | build-backend = "scikit_build_core.build"
7 |
8 | [project]
9 | name = "spherely"
10 | version = "0.1.0"
11 | description = "Manipulation and analysis of geometric objects on the sphere"
12 | keywords = ["gis", "geometry", "s2geometry", "shapely"]
13 | readme = "README.md"
14 | license = {text = "BSD 3-Clause"}
15 | authors = [
16 | {name = "Benoît Bovy"},
17 | ]
18 | maintainers = [
19 | {name = "Spherely contributors"},
20 | ]
21 | requires-python = ">=3.10"
22 | dependencies = ["numpy"]
23 |
24 | [project.urls]
25 | Home = "https://spherely.readthedocs.io"
26 | Repository = "https://github.com/benbovy/spherely"
27 |
28 | [project.optional-dependencies]
29 | test = ["pytest>=6.0"]
30 |
31 | [tool.scikit-build]
32 | sdist.exclude = [".github"]
33 | build-dir = "build/{wheel_tag}"
34 |
35 | [tool.mypy]
36 | files = ["tests", "src/spherely.pyi"]
37 | show_error_codes = true
38 | warn_unused_ignores = true
39 |
40 | [tool.pixi.project]
41 | channels = ["conda-forge"]
42 | platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"]
43 |
44 | [tool.pixi.environments]
45 | default = {features = [], solve-group = "default"}
46 | dev = {features = ["dev"], solve-group = "default"}
47 | test = {features = ["test", "dev"], solve-group = "default"}
48 | doc = {features = ["doc", "dev"], solve-group = "default"}
49 | lint = {features = ["lint", "dev"], solve-group = "default"}
50 | all = {features = ["lint", "doc", "test", "dev"], solve-group = "default"}
51 |
52 | [tool.pixi.dependencies]
53 | numpy = "*"
54 |
55 | [tool.pixi.feature.dev.dependencies]
56 | cxx-compiler = ">=1.9.0,<2"
57 | cmake = ">=3.31.5,<4"
58 | ninja = ">=1.12.1,<2"
59 | pybind11 = ">=2.13.6,<3"
60 | scikit-build-core = ">=0.10.7,<0.11"
61 | s2geometry = ">=0.11.1,<0.12"
62 | s2geography = ">=0.2.0,<0.3"
63 |
64 | [tool.pixi.feature.dev.pypi-dependencies]
65 | spherely = { path = ".", editable = true }
66 |
67 | [tool.pixi.feature.dev.pypi-options]
68 | no-build-isolation = ["spherely"]
69 |
70 | [tool.pixi.feature.test.dependencies]
71 | pytest = ">=8.3.4,<9"
72 | geoarrow-pyarrow = ">=0.1.2,<0.2"
73 |
74 | [tool.pixi.feature.doc.dependencies]
75 | sphinx = ">=8.1.3,<9"
76 | pydata-sphinx-theme = ">=0.16.1,<0.17"
77 | sphinx-book-theme = ">=1.1.3,<2"
78 | myst-nb = ">=1.1.2,<2"
79 |
80 | [tool.pixi.feature.lint.dependencies]
81 | pre-commit = ">=4.1.0,<5"
82 | mypy = ">=1.14.1,<2"
83 |
84 | [tool.pixi.feature.test.tasks]
85 | tests = "pytest tests --color=yes"
86 |
87 | [tool.pixi.feature.lint.tasks]
88 | precommit-install = "pre-commit install"
89 | mypy = "python -m mypy"
90 |
91 | [tool.pixi.feature.doc.tasks]
92 | build-doc = "sphinx-build -M html docs docs/build"
93 |
94 | [tool.pixi.feature.dev.tasks.compile-commands]
95 | cmd = "cmake -GNinja -S. -Bbuild/compile-commands -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && cp build/compile-commands/compile_commands.json ."
96 | inputs = ["CMakeLists.txt"]
97 | outputs = ["compile_commands.json"]
98 |
99 | [tool.pixi.feature.dev.tasks.configure]
100 | cmd = "cmake -GNinja -S. -Bbuild/python -DSKBUILD_PROJECT_NAME=spherely -DSKBUILD_PROJECT_VERSION=0.0.0"
101 | inputs = ["CMakeLists.txt"]
102 | outputs = ["build/python/CMakeFiles/"]
103 |
104 | [tool.pixi.feature.dev.tasks.compile]
105 | cmd = "cmake --build build/python --config Debug"
106 | depends-on = ["configure"]
107 | inputs = ["src/*"]
108 | outputs = ["build/python/spherely*"]
109 |
--------------------------------------------------------------------------------
/src/accessors-geog.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "constants.hpp"
5 | #include "creation.hpp"
6 | #include "geography.hpp"
7 | #include "pybind11.hpp"
8 |
9 | namespace py = pybind11;
10 | namespace s2geog = s2geography;
11 | using namespace spherely;
12 |
13 | PyObjectGeography centroid(PyObjectGeography a) {
14 | const auto& a_ptr = a.as_geog_ptr()->geog();
15 | auto s2_point = s2geog::s2_centroid(a_ptr);
16 | return make_py_geography(s2_point);
17 | }
18 |
19 | PyObjectGeography boundary(PyObjectGeography a) {
20 | const auto& a_ptr = a.as_geog_ptr()->geog();
21 | return make_py_geography(s2geog::s2_boundary(a_ptr));
22 | }
23 |
24 | PyObjectGeography convex_hull(PyObjectGeography a) {
25 | const auto& a_ptr = a.as_geog_ptr()->geog();
26 | return make_py_geography(s2geog::s2_convex_hull(a_ptr));
27 | }
28 |
29 | double distance(PyObjectGeography a,
30 | PyObjectGeography b,
31 | double radius = numeric_constants::EARTH_RADIUS_METERS) {
32 | const auto& a_index = a.as_geog_ptr()->geog_index();
33 | const auto& b_index = b.as_geog_ptr()->geog_index();
34 | return s2geog::s2_distance(a_index, b_index) * radius;
35 | }
36 |
37 | double area(PyObjectGeography a, double radius = numeric_constants::EARTH_RADIUS_METERS) {
38 | return s2geog::s2_area(a.as_geog_ptr()->geog()) * radius * radius;
39 | }
40 |
41 | double length(PyObjectGeography a, double radius = numeric_constants::EARTH_RADIUS_METERS) {
42 | return s2geog::s2_length(a.as_geog_ptr()->geog()) * radius;
43 | }
44 |
45 | double perimeter(PyObjectGeography a, double radius = numeric_constants::EARTH_RADIUS_METERS) {
46 | return s2geog::s2_perimeter(a.as_geog_ptr()->geog()) * radius;
47 | }
48 |
49 | void init_accessors(py::module& m) {
50 | m.attr("EARTH_RADIUS_METERS") = py::float_(numeric_constants::EARTH_RADIUS_METERS);
51 |
52 | m.def("centroid",
53 | py::vectorize(¢roid),
54 | py::arg("geography"),
55 | py::pos_only(),
56 | R"pbdoc(centroid(geography, /)
57 |
58 | Computes the centroid of each geography.
59 |
60 | Parameters
61 | ----------
62 | geography : :py:class:`Geography` or array_like
63 | Geography object(s).
64 |
65 | Returns
66 | -------
67 | Geography or array
68 | A single or an array of POINT Geography object(s).
69 |
70 | )pbdoc");
71 |
72 | m.def("boundary",
73 | py::vectorize(&boundary),
74 | py::arg("geography"),
75 | py::pos_only(),
76 | R"pbdoc(boundary(geography, /)
77 |
78 | Computes the boundary of each geography.
79 |
80 | Parameters
81 | ----------
82 | geography : :py:class:`Geography` or array_like
83 | Geography object(s).
84 |
85 | Returns
86 | -------
87 | Geography or array
88 | A single or an array of either (MULTI)POINT or (MULTI)LINESTRING
89 | Geography object(s).
90 |
91 | )pbdoc");
92 |
93 | m.def("convex_hull",
94 | py::vectorize(&convex_hull),
95 | py::arg("geography"),
96 | py::pos_only(),
97 | R"pbdoc(convex_hull(geography, /)
98 |
99 | Computes the convex hull of each geography.
100 |
101 | Parameters
102 | ----------
103 | geography : :py:class:`Geography` or array_like
104 | Geography object(s).
105 |
106 | Returns
107 | -------
108 | Geography or array
109 | A single or an array of POLYGON Geography object(s).
110 |
111 | )pbdoc");
112 |
113 | m.def("distance",
114 | py::vectorize(&distance),
115 | py::arg("a"),
116 | py::arg("b"),
117 | py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS,
118 | R"pbdoc(distance(a, b, radius=spherely.EARTH_RADIUS_METERS)
119 |
120 | Calculate the distance between two geographies.
121 |
122 | Parameters
123 | ----------
124 | a : :py:class:`Geography` or array_like
125 | Geography object(s).
126 | b : :py:class:`Geography` or array_like
127 | Geography object(s).
128 | radius : float, optional
129 | Radius of Earth in meters, default 6,371,010.
130 |
131 | Returns
132 | -------
133 | float or array
134 | Distance value(s), in meters.
135 |
136 | )pbdoc");
137 |
138 | m.def("area",
139 | py::vectorize(&area),
140 | py::arg("geography"),
141 | py::pos_only(),
142 | py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS,
143 | R"pbdoc(area(geography, /, radius=spherely.EARTH_RADIUS_METERS)
144 |
145 | Calculate the area of the geography.
146 |
147 | Parameters
148 | ----------
149 | geography : :py:class:`Geography` or array_like
150 | Geography object(s).
151 | radius : float, optional
152 | Radius of Earth in meters, default 6,371,010.
153 |
154 | Returns
155 | -------
156 | float or array
157 | Area value(s), in square meters.
158 |
159 | )pbdoc");
160 |
161 | m.def("length",
162 | py::vectorize(&length),
163 | py::arg("geography"),
164 | py::pos_only(),
165 | py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS,
166 | R"pbdoc(length(geography, /, radius=spherely.EARTH_RADIUS_METERS)
167 |
168 | Calculates the length of a line geography, returning zero for other types.
169 |
170 | Parameters
171 | ----------
172 | geography : :py:class:`Geography` or array_like
173 | Geography object(s).
174 | radius : float, optional
175 | Radius of Earth in meters, default 6,371,010.
176 |
177 | Returns
178 | -------
179 | float or array
180 | Length value(s), in meters.
181 |
182 | )pbdoc");
183 |
184 | m.def("perimeter",
185 | py::vectorize(&perimeter),
186 | py::arg("geography"),
187 | py::pos_only(),
188 | py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS,
189 | R"pbdoc(perimeter(geography, /, radius=spherely.EARTH_RADIUS_METERS)
190 |
191 | Calculates the perimeter of a polygon geography, returning zero for other types.
192 |
193 | Parameters
194 | ----------
195 | geography : :py:class:`Geography` or array_like
196 | Geography object(s).
197 | radius : float, optional
198 | Radius of Earth in meters, default 6,371,010.
199 |
200 | Returns
201 | -------
202 | float or array
203 | Perimeter value(s), in meters.
204 |
205 | )pbdoc");
206 | }
207 |
--------------------------------------------------------------------------------
/src/arrow_abi.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #ifdef __cplusplus
6 | extern "C" {
7 | #endif
8 |
9 | // Extra guard for versions of Arrow without the canonical guard
10 | #ifndef ARROW_FLAG_DICTIONARY_ORDERED
11 |
12 | #ifndef ARROW_C_DATA_INTERFACE
13 | #define ARROW_C_DATA_INTERFACE
14 |
15 | #define ARROW_FLAG_DICTIONARY_ORDERED 1
16 | #define ARROW_FLAG_NULLABLE 2
17 | #define ARROW_FLAG_MAP_KEYS_SORTED 4
18 |
19 | struct ArrowSchema {
20 | // Array type description
21 | const char* format;
22 | const char* name;
23 | const char* metadata;
24 | int64_t flags;
25 | int64_t n_children;
26 | struct ArrowSchema** children;
27 | struct ArrowSchema* dictionary;
28 |
29 | // Release callback
30 | void (*release)(struct ArrowSchema*);
31 | // Opaque producer-specific data
32 | void* private_data;
33 | };
34 |
35 | struct ArrowArray {
36 | // Array data description
37 | int64_t length;
38 | int64_t null_count;
39 | int64_t offset;
40 | int64_t n_buffers;
41 | int64_t n_children;
42 | const void** buffers;
43 | struct ArrowArray** children;
44 | struct ArrowArray* dictionary;
45 |
46 | // Release callback
47 | void (*release)(struct ArrowArray*);
48 | // Opaque producer-specific data
49 | void* private_data;
50 | };
51 |
52 | #endif // ARROW_C_DATA_INTERFACE
53 | #endif // ARROW_FLAG_DICTIONARY_ORDERED
54 |
55 | #ifdef __cplusplus
56 | }
57 | #endif
58 |
--------------------------------------------------------------------------------
/src/boolean-operations.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "constants.hpp"
5 | #include "creation.hpp"
6 | #include "geography.hpp"
7 | #include "pybind11.hpp"
8 |
9 | namespace py = pybind11;
10 | namespace s2geog = s2geography;
11 | using namespace spherely;
12 |
13 | class BooleanOp {
14 | public:
15 | BooleanOp(S2BooleanOperation::OpType op_type)
16 | : m_op_type(op_type), m_options(s2geog::GlobalOptions()) {
17 | // TODO make this configurable
18 | // m_options.polyline_layer_action = s2geography::GlobalOptions::OUTPUT_ACTION_IGNORE;
19 | }
20 |
21 | PyObjectGeography operator()(PyObjectGeography a, PyObjectGeography b) const {
22 | const auto& a_index = a.as_geog_ptr()->geog_index();
23 | const auto& b_index = b.as_geog_ptr()->geog_index();
24 | std::unique_ptr geog_out =
25 | s2geog::s2_boolean_operation(a_index, b_index, m_op_type, m_options);
26 |
27 | return make_py_geography(std::move(geog_out));
28 | }
29 |
30 | private:
31 | S2BooleanOperation::OpType m_op_type;
32 | s2geog::GlobalOptions m_options;
33 | };
34 |
35 | void init_boolean_operations(py::module& m) {
36 | m.def("union",
37 | py::vectorize(BooleanOp(S2BooleanOperation::OpType::UNION)),
38 | py::arg("a"),
39 | py::arg("b"),
40 | R"pbdoc(union(a, b)
41 |
42 | Computes the union of both geographies.
43 |
44 | Parameters
45 | ----------
46 | a, b : :py:class:`Geography` or array_like
47 | Geography object(s).
48 |
49 | Returns
50 | -------
51 | Geography or array
52 | New Geography object(s) representing the union of the input geographies.
53 |
54 | )pbdoc");
55 |
56 | m.def("intersection",
57 | py::vectorize(BooleanOp(S2BooleanOperation::OpType::INTERSECTION)),
58 | py::arg("a"),
59 | py::arg("b"),
60 | R"pbdoc(intersection(a, b)
61 |
62 | Computes the intersection of both geographies.
63 |
64 | Parameters
65 | ----------
66 | a, b : :py:class:`Geography` or array_like
67 | Geography object(s).
68 |
69 | Returns
70 | -------
71 | Geography or array
72 | New Geography object(s) representing the intersection of the input geographies.
73 |
74 | )pbdoc");
75 |
76 | m.def("difference",
77 | py::vectorize(BooleanOp(S2BooleanOperation::OpType::DIFFERENCE)),
78 | py::arg("a"),
79 | py::arg("b"),
80 | R"pbdoc(difference(a, b)
81 |
82 | Computes the difference of both geographies.
83 |
84 | Parameters
85 | ----------
86 | a, b : :py:class:`Geography` or array_like
87 | Geography object(s).
88 |
89 | Returns
90 | -------
91 | Geography or array
92 | New Geography object(s) representing the difference of the input geographies.
93 |
94 | )pbdoc");
95 |
96 | m.def("symmetric_difference",
97 | py::vectorize(BooleanOp(S2BooleanOperation::OpType::SYMMETRIC_DIFFERENCE)),
98 | py::arg("a"),
99 | py::arg("b"),
100 | R"pbdoc(symmetric_difference(a, b)
101 |
102 | Computes the symmetric difference of both geographies.
103 |
104 | Parameters
105 | ----------
106 | a, b : :py:class:`Geography` or array_like
107 | Geography object(s).
108 |
109 | Returns
110 | -------
111 | Geography or array
112 | New Geography object(s) representing the symmetric difference of
113 | the input geographies.
114 |
115 | )pbdoc");
116 | }
117 |
--------------------------------------------------------------------------------
/src/constants.hpp:
--------------------------------------------------------------------------------
1 | #include "s2/s2earth.h"
2 |
3 | namespace spherely {
4 |
5 | struct numeric_constants {
6 | static constexpr double EARTH_RADIUS_METERS = S2Earth::RadiusMeters();
7 | };
8 |
9 | } // namespace spherely
10 |
--------------------------------------------------------------------------------
/src/creation.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SPHERELY_CREATION_H_
2 | #define SPHERELY_CREATION_H_
3 |
4 | #include
5 | #include
6 |
7 | #include "geography.hpp"
8 | #include "pybind11.hpp"
9 |
10 | namespace spherely {
11 |
12 | //
13 | // ---- S2geometry / S2Geography / Spherely object wrapper utility functions
14 | //
15 |
16 | /*
17 | ** Wrap one or more s2geometry objects into a spherely::Geography object.
18 | **
19 | ** Example:
20 | **
21 | ** S2Point s2_obj(S2LatLng::FromDegrees(40.0, 5.0).ToPoint());
22 | ** auto geog_ptr = make_geography(s2_obj);
23 | **
24 | ** @tparam T The corresponding s2geography type (e.g., PointGeography for S2Point, etc)
25 | ** @tparam S The type of the s2geometry (vector of) object(s)
26 | ** @param s2_obj A single or a vector of s2geometry objects (e.g., S2Point, S2Polyline, etc.)
27 | ** @returns A new spherely::Geography object
28 | */
29 | template , bool> = true>
30 | inline std::unique_ptr make_geography(S &&s2_obj) {
31 | auto s2geog_ptr = std::make_unique(std::forward(s2_obj));
32 | return std::make_unique(std::move(s2geog_ptr));
33 | }
34 |
35 | /*
36 | ** Wrap a s2geography::Geography object into a spherely::Geography object.
37 | **
38 | ** @tparam T The s2geography type
39 | ** @param s2geog_ptr a pointer to the s2geography::Geography object
40 | ** @returns A new spherely::Geography object
41 | */
42 | template
43 | inline std::unique_ptr make_geography(std::unique_ptr s2geog_ptr) {
44 | return std::make_unique(std::move(s2geog_ptr));
45 | }
46 |
47 | /*
48 | ** Helper to create a Spherely Python Geography object directly from one or more
49 | ** S2Geometry objects.
50 | *
51 | ** Example:
52 | **
53 | ** S2Point s2_obj(S2LatLng::FromDegrees(40.0, 5.0).ToPoint());
54 | ** auto py_geog = make_py_geography(s2_obj);
55 | **
56 | ** @tparam T The corresponding s2geography type (e.g., PointGeography for S2Point, etc)
57 | ** @tparam S The type of the s2geometry (vector of) object(s)
58 | ** @param s2_obj A single or a vector of s2geometry objects (e.g., S2Point, S2Polyline, etc.)
59 | ** @returns A new Python Geography object (pybind11::object)
60 | */
61 | template , bool> = true>
62 | inline PyObjectGeography make_py_geography(S &&s2_obj) {
63 | auto geog_ptr = make_geography(std::forward(s2_obj));
64 | return PyObjectGeography::from_geog(std::move(geog_ptr));
65 | }
66 |
67 | /*
68 | ** Helper to create a shperely::Geography object from one s2geography::Geography
69 | ** object.
70 | *
71 | ** @tparam T The S2Geography type
72 | ** @param s2geog_ptr a pointer to the s2geography::Geography object
73 | ** @returns A new Python Geography object (pybind11::object)
74 | */
75 | template
76 | inline PyObjectGeography make_py_geography(std::unique_ptr s2geog_ptr) {
77 | auto geog_ptr = make_geography(std::move(s2geog_ptr));
78 | return PyObjectGeography::from_geog(std::move(geog_ptr));
79 | }
80 |
81 | } // namespace spherely
82 |
83 | #endif // SPHERELY_CREATION_H_
84 |
--------------------------------------------------------------------------------
/src/generate_spherely_vfunc_types.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import string
3 | from pathlib import Path
4 |
5 | from spherely import EARTH_RADIUS_METERS
6 |
7 |
8 | VFUNC_TYPE_SPECS = {
9 | "_VFunc_Nin1_Nout1": {"n_in": 1},
10 | "_VFunc_Nin2_Nout1": {"n_in": 2},
11 | "_VFunc_Nin2optradius_Nout1": {"n_in": 2, "radius": ("float", EARTH_RADIUS_METERS)},
12 | "_VFunc_Nin1optradius_Nout1": {"n_in": 1, "radius": ("float", EARTH_RADIUS_METERS)},
13 | "_VFunc_Nin1optprecision_Nout1": {"n_in": 1, "precision": ("int", 6)},
14 | }
15 |
16 | STUB_FILE_PATH = Path(__file__).parent / "spherely.pyi"
17 | BEGIN_MARKER = "# /// Begin types"
18 | END_MARKER = "# /// End types"
19 |
20 |
21 | def update_stub_file(path, **type_specs):
22 | stub_text = path.read_text(encoding="utf-8")
23 | try:
24 | start_idx = stub_text.index(BEGIN_MARKER)
25 | end_idx = stub_text.index(END_MARKER)
26 | except ValueError:
27 | raise SystemExit(
28 | f"Error: Markers '{BEGIN_MARKER}' and '{END_MARKER}' "
29 | f"were not found in stub file '{path}'"
30 | ) from None
31 |
32 | header = f"{BEGIN_MARKER}\n"
33 | code = "\n\n".join(
34 | _vfunctype_factory(name, **args) for name, args in type_specs.items()
35 | )
36 | updated_stub_text = stub_text[:start_idx] + header + code + stub_text[end_idx:]
37 | path.write_text(updated_stub_text, encoding="utf-8")
38 |
39 |
40 | def _vfunctype_factory(class_name, n_in, **optargs):
41 | """Create new VFunc types.
42 |
43 | Based on the number of input arrays and optional arguments and their types.
44 | """
45 | arg_names = list(string.ascii_lowercase[:n_in])
46 | if n_in == 1:
47 | arg_names[0] = "geography"
48 |
49 | class_code = [
50 | f"class {class_name}(",
51 | " Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]",
52 | "):",
53 | " @property",
54 | " def __name__(self) -> _NameType: ...",
55 | "",
56 | ]
57 | optarg_str = ", ".join(
58 | f"{arg_name}: {arg_type} = {arg_value}"
59 | for arg_name, (arg_type, arg_value) in optargs.items()
60 | )
61 |
62 | geog_types = ["Geography", "Iterable[Geography]"]
63 | for arg_types in itertools.product(geog_types, repeat=n_in):
64 | arg_str = ", ".join(
65 | f"{arg_name}: {arg_type}"
66 | for arg_name, arg_type in zip(arg_names, arg_types)
67 | )
68 | if n_in == 1:
69 | arg_str += ", /"
70 | return_type = (
71 | "_ScalarReturnType"
72 | if all(t == geog_types[0] for t in arg_types)
73 | else "npt.NDArray[_ArrayReturnDType]"
74 | )
75 | class_code.extend(
76 | [
77 | " @overload",
78 | " def __call__(",
79 | (
80 | f" self, {arg_str}, {optarg_str}"
81 | if optarg_str
82 | else f" self, {arg_str}"
83 | ),
84 | f" ) -> {return_type}: ...",
85 | "",
86 | ]
87 | )
88 | return "\n".join(class_code)
89 |
90 |
91 | if __name__ == "__main__":
92 | update_stub_file(path=STUB_FILE_PATH, **VFUNC_TYPE_SPECS)
93 |
--------------------------------------------------------------------------------
/src/geography.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SPHERELY_GEOGRAPHY_H_
2 | #define SPHERELY_GEOGRAPHY_H_
3 |
4 | #include
5 | #include
6 |
7 | #include
8 | #include
9 | #include
10 |
11 | namespace py = pybind11;
12 | namespace s2geog = s2geography;
13 |
14 | namespace spherely {
15 |
16 | using S2GeographyPtr = std::unique_ptr;
17 | using S2GeographyIndexPtr = std::unique_ptr;
18 |
19 | /*
20 | ** The registered Geography types
21 | */
22 | enum class GeographyType : std::int8_t {
23 | None = -1,
24 | Point,
25 | LineString,
26 | Polygon,
27 | MultiPoint,
28 | MultiLineString,
29 | MultiPolygon,
30 | GeometryCollection
31 | };
32 |
33 | /*
34 | ** Thin wrapper around s2geography::Geography.
35 | **
36 | ** This wrapper implements the following specific features (that might
37 | ** eventually move into s2geography::Geography?):
38 | **
39 | ** - Implement move semantics only.
40 | ** - add ``clone()`` method for explicit copy
41 | ** - Add ``geog_type()`` method for getting the geography type
42 | ** - Eagerly infer the geography type as well as other properties
43 | ** - Encapsulate a lazy ``s2geography::ShapeIndexGeography`` accessible via ``geog_index()``.
44 | **
45 | */
46 | class Geography {
47 | public:
48 | Geography(const Geography&) = delete;
49 | Geography(Geography&& geog)
50 | : m_s2geog_ptr(std::move(geog.m_s2geog_ptr)),
51 | m_is_empty(geog.is_empty()),
52 | m_geog_type(geog.geog_type()) {}
53 |
54 | Geography(S2GeographyPtr&& s2geog_ptr) : m_s2geog_ptr(std::move(s2geog_ptr)) {
55 | // TODO: template constructors with s2geography Geography subclass constraints (e.g., using
56 | // SFINAE or "if constexpr") may be more efficient than dynamic casting like done in
57 | // extract_geog_properties.
58 | extract_geog_properties();
59 | }
60 |
61 | Geography& operator=(const Geography&) = delete;
62 | Geography& operator=(Geography&& other) {
63 | m_s2geog_ptr = std::move(other.m_s2geog_ptr);
64 | m_is_empty = other.m_is_empty;
65 | m_geog_type = other.m_geog_type;
66 | return *this;
67 | }
68 |
69 | inline GeographyType geog_type() const noexcept {
70 | return m_geog_type;
71 | }
72 |
73 | inline const s2geog::Geography& geog() const noexcept {
74 | return *m_s2geog_ptr;
75 | }
76 |
77 | template
78 | inline const T* cast_geog() const noexcept {
79 | return reinterpret_cast(&geog());
80 | }
81 |
82 | inline const s2geog::ShapeIndexGeography& geog_index() {
83 | if (!m_s2geog_index_ptr) {
84 | m_s2geog_index_ptr = std::make_unique(geog());
85 | }
86 |
87 | return *m_s2geog_index_ptr;
88 | }
89 | inline void reset_index() {
90 | m_s2geog_index_ptr.reset();
91 | }
92 | inline bool has_index() const noexcept {
93 | return m_s2geog_index_ptr != nullptr;
94 | }
95 |
96 | inline int dimension() const {
97 | return m_s2geog_ptr->dimension();
98 | }
99 | inline int num_shapes() const {
100 | return m_s2geog_ptr->num_shapes();
101 | }
102 | inline bool is_empty() const noexcept {
103 | return m_is_empty;
104 | }
105 |
106 | Geography clone() const;
107 | std::unique_ptr clone_geog() const;
108 |
109 | py::tuple encode() const;
110 | static Geography decode(const py::tuple& encoded);
111 |
112 | private:
113 | S2GeographyPtr m_s2geog_ptr;
114 | S2GeographyIndexPtr m_s2geog_index_ptr;
115 | bool m_is_empty = false;
116 | GeographyType m_geog_type;
117 |
118 | // We don't want Geography to be default constructible, except internally via `clone()`
119 | // where there is no need to infer geography properties as we already know them.
120 | Geography() : m_is_empty(true) {}
121 |
122 | void extract_geog_properties();
123 | };
124 |
125 | /**
126 | * Custom exception that may be thrown when an empty Geography is found.
127 | */
128 | class EmptyGeographyException : public std::exception {
129 | private:
130 | std::string message;
131 |
132 | public:
133 | EmptyGeographyException(const char* msg) : message(msg) {}
134 |
135 | const char* what() const throw() {
136 | return message.c_str();
137 | }
138 | };
139 |
140 | // TODO: cleaner way? Already implemented elsewhere?
141 | inline std::string format_geog_type(GeographyType geog_type) {
142 | if (geog_type == GeographyType::Point) {
143 | return "POINT";
144 | } else if (geog_type == GeographyType::MultiPoint) {
145 | return "MULTIPOINT";
146 | } else if (geog_type == GeographyType::LineString) {
147 | return "LINESTRING";
148 | } else if (geog_type == GeographyType::MultiLineString) {
149 | return "MULTILINESTRING";
150 | } else if (geog_type == GeographyType::Polygon) {
151 | return "POLYGON";
152 | } else if (geog_type == GeographyType::MultiPolygon) {
153 | return "MULTIPOLYGON";
154 | } else if (geog_type == GeographyType::GeometryCollection) {
155 | return "GEOMETRYCOLLECTION";
156 | } else {
157 | return "UNKNOWN";
158 | }
159 | }
160 |
161 | /**
162 | * Check the type of a Geography object and maybe raise an exception.
163 | */
164 | inline void check_geog_type(const Geography& geog_obj, GeographyType geog_type) {
165 | if (geog_obj.geog_type() != geog_type) {
166 | auto expected = format_geog_type(geog_type);
167 | auto actual = format_geog_type(geog_obj.geog_type());
168 |
169 | throw py::type_error("invalid Geography type (expected " + expected + ", found " + actual +
170 | ")");
171 | }
172 | }
173 |
174 | } // namespace spherely
175 |
176 | #endif // SPHERELY_GEOGRAPHY_H_
177 |
--------------------------------------------------------------------------------
/src/io.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "constants.hpp"
5 | #include "creation.hpp"
6 | #include "geography.hpp"
7 | #include "pybind11.hpp"
8 |
9 | namespace py = pybind11;
10 | namespace s2geog = s2geography;
11 | using namespace spherely;
12 |
13 | class FromWKT {
14 | public:
15 | FromWKT(bool oriented, bool planar, float tessellate_tolerance = 100.0) {
16 | s2geog::geoarrow::ImportOptions options;
17 | options.set_oriented(oriented);
18 | if (planar) {
19 | auto tol =
20 | S1Angle::Radians(tessellate_tolerance / numeric_constants::EARTH_RADIUS_METERS);
21 | options.set_tessellate_tolerance(tol);
22 | }
23 | m_reader = std::make_shared(options);
24 | }
25 |
26 | PyObjectGeography operator()(py::str string) const {
27 | return make_py_geography(m_reader->read_feature(string));
28 | }
29 |
30 | private:
31 | std::shared_ptr m_reader;
32 | };
33 |
34 | class ToWKT {
35 | public:
36 | ToWKT(int precision = 6) {
37 | m_writer = std::make_shared(precision);
38 | }
39 |
40 | py::str operator()(PyObjectGeography obj) const {
41 | auto res = m_writer->write_feature(obj.as_geog_ptr()->geog());
42 | return py::str(res);
43 | }
44 |
45 | private:
46 | std::shared_ptr m_writer;
47 | };
48 |
49 | class FromWKB {
50 | public:
51 | FromWKB(bool oriented, bool planar, float tessellate_tolerance = 100.0) {
52 | s2geog::geoarrow::ImportOptions options;
53 | options.set_oriented(oriented);
54 | if (planar) {
55 | auto tol =
56 | S1Angle::Radians(tessellate_tolerance / numeric_constants::EARTH_RADIUS_METERS);
57 | options.set_tessellate_tolerance(tol);
58 | }
59 | m_reader = std::make_shared(options);
60 | }
61 |
62 | PyObjectGeography operator()(py::bytes bytes) const {
63 | return make_py_geography(m_reader->ReadFeature(bytes));
64 | }
65 |
66 | private:
67 | std::shared_ptr m_reader;
68 | };
69 |
70 | class ToWKB {
71 | public:
72 | ToWKB() {
73 | m_writer = std::make_shared();
74 | }
75 |
76 | py::bytes operator()(PyObjectGeography obj) const {
77 | return m_writer->WriteFeature(obj.as_geog_ptr()->geog());
78 | }
79 |
80 | private:
81 | std::shared_ptr m_writer;
82 | };
83 |
84 | void init_io(py::module& m) {
85 | m.def(
86 | "from_wkt",
87 | [](py::array_t string, bool oriented, bool planar, float tessellate_tolerance) {
88 | return py::vectorize(FromWKT(oriented, planar, tessellate_tolerance))(
89 | std::move(string));
90 | },
91 | py::arg("geography"),
92 | py::pos_only(),
93 | py::kw_only(),
94 | py::arg("oriented") = false,
95 | py::arg("planar") = false,
96 | py::arg("tessellate_tolerance") = 100.0,
97 | R"pbdoc(from_wkt(geography, /, *, oriented=False, planar=False, tessellate_tolerance=100.0)
98 |
99 | Creates geographies from the Well-Known Text (WKT) representation.
100 |
101 | Parameters
102 | ----------
103 | geography : str or array_like
104 | The WKT string(s) to convert.
105 | oriented : bool, default False
106 | Set to True if polygon ring directions are known to be correct
107 | (i.e., exterior rings are defined counter clockwise and interior
108 | rings are defined clockwise).
109 | By default (False), it will return the polygon with the smaller
110 | area.
111 | planar : bool, default False
112 | If set to True, the edges of linestrings and polygons are assumed
113 | to be linear on the plane. In that case, additional points will
114 | be added to the line while creating the geography objects, to
115 | ensure every point is within 100m of the original line.
116 | By default (False), it is assumed that the edges are spherical
117 | (i.e. represent the shortest path on the sphere between two points).
118 | tessellate_tolerance : float, default 100.0
119 | The maximum distance in meters that a point must be moved to
120 | satisfy the planar edge constraint. This is only used if `planar`
121 | is set to True.
122 |
123 | Returns
124 | -------
125 | Geography or array
126 | A single or an array of geography objects.
127 |
128 | )pbdoc");
129 |
130 | m.def(
131 | "to_wkt",
132 | [](py::array_t obj, int precision) {
133 | return py::vectorize(ToWKT(precision))(std::move(obj));
134 | },
135 | py::arg("geography"),
136 | py::pos_only(),
137 | py::arg("precision") = 6,
138 | R"pbdoc(to_wkt(geography, /, precision=6)
139 |
140 | Returns the WKT representation of each geography.
141 |
142 | Parameters
143 | ----------
144 | geography : :py:class:`Geography` or array_like
145 | Geography object(s).
146 | precision : int, default 6
147 | The number of decimal places to include in the output.
148 |
149 | Returns
150 | -------
151 | str or array
152 | A string or an array of strings.
153 |
154 | )pbdoc");
155 |
156 | m.def(
157 | "from_wkb",
158 | [](py::array_t bytes, bool oriented, bool planar, float tessellate_tolerance) {
159 | return py::vectorize(FromWKB(oriented, planar, tessellate_tolerance))(std::move(bytes));
160 | },
161 | py::arg("geography"),
162 | py::pos_only(),
163 | py::kw_only(),
164 | py::arg("oriented") = false,
165 | py::arg("planar") = false,
166 | py::arg("tessellate_tolerance") = 100.0,
167 | R"pbdoc(from_wkb(geography, /, *, oriented=False, planar=False, tessellate_tolerance=100.0)
168 |
169 | Creates geographies from the Well-Known Bytes (WKB) representation.
170 |
171 | Parameters
172 | ----------
173 | geography : bytes or array_like
174 | The WKB byte object(s) to convert.
175 | oriented : bool, default False
176 | Set to True if polygon ring directions are known to be correct
177 | (i.e., exterior rings are defined counter clockwise and interior
178 | rings are defined clockwise).
179 | By default (False), it will return the polygon with the smaller
180 | area.
181 | planar : bool, default False
182 | If set to True, the edges of linestrings and polygons are assumed
183 | to be linear on the plane. In that case, additional points will
184 | be added to the line while creating the geography objects, to
185 | ensure every point is within 100m of the original line.
186 | By default (False), it is assumed that the edges are spherical
187 | (i.e. represent the shortest path on the sphere between two points).
188 | tessellate_tolerance : float, default 100.0
189 | The maximum distance in meters that a point must be moved to
190 | satisfy the planar edge constraint. This is only used if `planar`
191 | is set to True.
192 |
193 | Returns
194 | -------
195 | Geography or array
196 | A single or an array of geography objects.
197 |
198 | )pbdoc");
199 |
200 | m.def("to_wkb",
201 | py::vectorize(ToWKB()),
202 | py::arg("geography"),
203 | py::pos_only(),
204 | R"pbdoc(to_wkb(geography, /)
205 |
206 | Returns the WKB representation of each geography.
207 |
208 | Parameters
209 | ----------
210 | geography : :py:class:`Geography` or array_like
211 | Geography object(s).
212 |
213 | Returns
214 | -------
215 | bytes or array
216 | A bytes object or an array of bytes.
217 |
218 | )pbdoc");
219 | }
220 |
--------------------------------------------------------------------------------
/src/predicates.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include
5 |
6 | #include "geography.hpp"
7 | #include "pybind11.hpp"
8 |
9 | namespace py = pybind11;
10 | namespace s2geog = s2geography;
11 | using namespace spherely;
12 |
13 | /*
14 | ** Functor for predicate bindings.
15 | */
16 | class Predicate {
17 | public:
18 | using FuncType = std::function;
21 |
22 | template
23 | Predicate(F&& func) : m_func(std::forward(func)) {}
24 |
25 | template
26 | Predicate(F&& func, const S2BooleanOperation::Options& options)
27 | : m_func(std::forward(func)), m_options(options) {}
28 |
29 | bool operator()(PyObjectGeography a, PyObjectGeography b) const {
30 | const auto& a_index = a.as_geog_ptr()->geog_index();
31 | const auto& b_index = b.as_geog_ptr()->geog_index();
32 | return m_func(a_index, b_index, m_options);
33 | }
34 |
35 | private:
36 | FuncType m_func;
37 | S2BooleanOperation::Options m_options;
38 | };
39 |
40 | /*
41 | ** A specialization of the `Predicate` class for the touches operation,
42 | ** as two `S2BooleanOpteration::Options` objects are necessary.
43 | */
44 | class TouchesPredicate {
45 | public:
46 | TouchesPredicate() : m_closed_options(), m_open_options() {
47 | m_closed_options.set_polyline_model(S2BooleanOperation::PolylineModel::CLOSED);
48 | m_closed_options.set_polygon_model(S2BooleanOperation::PolygonModel::CLOSED);
49 |
50 | m_open_options.set_polyline_model(S2BooleanOperation::PolylineModel::OPEN);
51 | m_open_options.set_polygon_model(S2BooleanOperation::PolygonModel::OPEN);
52 | }
53 |
54 | bool operator()(PyObjectGeography a, PyObjectGeography b) const {
55 | const auto& a_index = a.as_geog_ptr()->geog_index();
56 | const auto& b_index = b.as_geog_ptr()->geog_index();
57 |
58 | return s2geog::s2_intersects(a_index, b_index, m_closed_options) &&
59 | !s2geog::s2_intersects(a_index, b_index, m_open_options);
60 | }
61 |
62 | private:
63 | S2BooleanOperation::Options m_closed_options;
64 | S2BooleanOperation::Options m_open_options;
65 | };
66 |
67 | void init_predicates(py::module& m) {
68 | m.def("intersects",
69 | py::vectorize(Predicate(s2geog::s2_intersects)),
70 | py::arg("a"),
71 | py::arg("b"),
72 | R"pbdoc(intersects(a, b)
73 |
74 | Returns True if A and B share any portion of space.
75 |
76 | Intersects implies that overlaps, touches and within are True.
77 |
78 | Parameters
79 | ----------
80 | a, b : :py:class:`Geography` or array_like
81 | Geography object(s).
82 |
83 | Returns
84 | -------
85 | bool or array
86 |
87 | )pbdoc");
88 |
89 | m.def("equals",
90 | py::vectorize(Predicate(s2geog::s2_equals)),
91 | py::arg("a"),
92 | py::arg("b"),
93 | R"pbdoc(equals(a, b)
94 |
95 | Returns True if A and B are spatially equal.
96 |
97 | If A is within B and B is within A, A and B are considered equal. The
98 | ordering of points can be different.
99 |
100 | Parameters
101 | ----------
102 | a, b : :py:class:`Geography` or array_like
103 | Geography object(s).
104 |
105 | Returns
106 | -------
107 | bool or array
108 |
109 | )pbdoc");
110 |
111 | m.def("contains",
112 | py::vectorize(Predicate(s2geog::s2_contains)),
113 | py::arg("a"),
114 | py::arg("b"),
115 | R"pbdoc(contains(a, b)
116 |
117 | Returns True if B is completely inside A.
118 |
119 | Parameters
120 | ----------
121 | a, b : :py:class:`Geography` or array_like
122 | Geography object(s).
123 |
124 | Returns
125 | -------
126 | bool or array
127 |
128 | )pbdoc");
129 |
130 | m.def("within",
131 | py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index,
132 | const s2geog::ShapeIndexGeography& b_index,
133 | const S2BooleanOperation::Options& options) {
134 | return s2geog::s2_contains(b_index, a_index, options);
135 | })),
136 | py::arg("a"),
137 | py::arg("b"),
138 | R"pbdoc(within(a, b)
139 |
140 | Returns True if A is completely inside B.
141 |
142 | Parameters
143 | ----------
144 | a, b : :py:class:`Geography` or array_like
145 | Geography object(s).
146 |
147 | Returns
148 | -------
149 | bool or array
150 |
151 | )pbdoc");
152 |
153 | m.def("disjoint",
154 | py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index,
155 | const s2geog::ShapeIndexGeography& b_index,
156 | const S2BooleanOperation::Options& options) {
157 | return !s2geog::s2_intersects(a_index, b_index, options);
158 | })),
159 | py::arg("a"),
160 | py::arg("b"),
161 | R"pbdoc(disjoint(a, b)
162 |
163 | Returns True if A boundaries and interior does not intersect at all
164 | with those of B.
165 |
166 | Parameters
167 | ----------
168 | a, b : :py:class:`Geography` or array_like
169 | Geography object(s).
170 |
171 | Returns
172 | -------
173 | bool or array
174 |
175 | )pbdoc");
176 |
177 | m.def("touches",
178 | py::vectorize(TouchesPredicate()),
179 | py::arg("a"),
180 | py::arg("b"),
181 | R"pbdoc(touches(a, b)
182 |
183 | Returns True if A and B intersect, but their interiors do not intersect.
184 |
185 | A and B must have at least one point in common, where the common point
186 | lies in at least one boundary.
187 |
188 | Parameters
189 | ----------
190 | a, b : :py:class:`Geography` or array_like
191 | Geography object(s).
192 |
193 | Returns
194 | -------
195 | bool or array
196 |
197 | )pbdoc");
198 |
199 | S2BooleanOperation::Options closed_options;
200 | closed_options.set_polyline_model(S2BooleanOperation::PolylineModel::CLOSED);
201 | closed_options.set_polygon_model(S2BooleanOperation::PolygonModel::CLOSED);
202 |
203 | m.def("covers",
204 | py::vectorize(Predicate(
205 | [](const s2geog::ShapeIndexGeography& a_index,
206 | const s2geog::ShapeIndexGeography& b_index,
207 | const S2BooleanOperation::Options& options) {
208 | return s2geog::s2_contains(a_index, b_index, options);
209 | },
210 | closed_options)),
211 | py::arg("a"),
212 | py::arg("b"),
213 | R"pbdoc(covers(a, b)
214 |
215 | Returns True if every point in B lies inside the interior or boundary of A.
216 |
217 | Parameters
218 | ----------
219 | a, b : :py:class:`Geography` or array_like
220 | Geography object(s).
221 |
222 | Returns
223 | -------
224 | bool or array
225 |
226 | Notes
227 | -----
228 | If A and B are both polygons and share co-linear edges,
229 | `covers` currently returns expected results only when those
230 | shared edges are identical.
231 |
232 | )pbdoc");
233 |
234 | m.def("covered_by",
235 | py::vectorize(Predicate(
236 | [](const s2geog::ShapeIndexGeography& a_index,
237 | const s2geog::ShapeIndexGeography& b_index,
238 | const S2BooleanOperation::Options& options) {
239 | return s2geog::s2_contains(b_index, a_index, options);
240 | },
241 | closed_options)),
242 | py::arg("a"),
243 | py::arg("b"),
244 | R"pbdoc(covered_by(a, b)
245 |
246 | Returns True if every point in A lies inside the interior or boundary of B.
247 |
248 | Parameters
249 | ----------
250 | a, b : :py:class:`Geography` or array_like
251 | Geography object(s).
252 |
253 | Returns
254 | -------
255 | bool or array
256 |
257 | See Also
258 | --------
259 | covers
260 |
261 | )pbdoc");
262 | }
263 |
--------------------------------------------------------------------------------
/src/projections.cpp:
--------------------------------------------------------------------------------
1 | #include "projections.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | namespace py = pybind11;
8 |
9 | void init_projections(py::module& m) {
10 | py::class_ projection(m, "Projection", R"pbdoc(
11 | Lightweight wrapper for selecting common reference systems used to
12 | project Geography points or vertices.
13 |
14 | Cannot be instantiated directly.
15 |
16 | )pbdoc");
17 |
18 | projection
19 | .def_static("lnglat", &Projection::lnglat, R"pbdoc(lnglat()
20 |
21 | Selects the "plate carree" projection.
22 |
23 | This projection maps coordinates on the sphere to (longitude, latitude) pairs.
24 | The x coordinates (longitude) span [-180, 180] and the y coordinates (latitude)
25 | span [-90, 90].
26 |
27 | )pbdoc")
28 | .def_static("pseudo_mercator", &Projection::pseudo_mercator, R"pbdoc(pseudo_mercator()
29 |
30 | Selects the spherical Mercator projection.
31 |
32 | When used together with WGS84 coordinates, known as the "Web
33 | Mercator" or "WGS84/Pseudo-Mercator" projection.
34 |
35 | )pbdoc")
36 | .def_static("orthographic",
37 | &Projection::orthographic,
38 | py::arg("longitude"),
39 | py::arg("latitude"),
40 | R"pbdoc(orthographic(longitude, latitude)
41 |
42 | Selects an orthographic projection with the given centre point.
43 |
44 | The resulting coordinates depict a single hemisphere of the globe as
45 | it appears from outer space, centred on the given point.
46 |
47 | Parameters
48 | ----------
49 | longitude : float
50 | Longitude coordinate of the center point, in degrees.
51 | latitude : float
52 | Latitude coordinate of the center point, in degrees.
53 |
54 | )pbdoc");
55 | }
56 |
--------------------------------------------------------------------------------
/src/projections.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SPHERELY_PROJECTIONS_H_
2 | #define SPHERELY_PROJECTIONS_H_
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | namespace s2geog = s2geography;
9 |
10 | class Projection {
11 | public:
12 | Projection(std::shared_ptr projection)
13 | : m_s2_projection(std::move(projection)) {}
14 |
15 | std::shared_ptr s2_projection() {
16 | return m_s2_projection;
17 | }
18 |
19 | static Projection lnglat() {
20 | return Projection(std::move(s2geog::lnglat()));
21 | }
22 | static Projection pseudo_mercator() {
23 | return Projection(std::move(s2geog::pseudo_mercator()));
24 | }
25 | static Projection orthographic(double longitude, double latitude) {
26 | return Projection(
27 | std::move(s2geog::orthographic(S2LatLng::FromDegrees(latitude, longitude))));
28 | }
29 |
30 | private:
31 | std::shared_ptr m_s2_projection;
32 | };
33 |
34 | #endif // SPHERELY_PROJECTIONS_H_
35 |
--------------------------------------------------------------------------------
/src/pybind11.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SPHERELY_PYBIND11_H_
2 | #define SPHERELY_PYBIND11_H_
3 |
4 | /*
5 | ** Pybind11 patches and workarounds for operating on Python wrapped Geography
6 | ** objects through Numpy arrays and universal functions (using numpy.object
7 | ** dtype).
8 | **
9 | ** Somewhat hacky!
10 | */
11 |
12 | #include
13 | #include
14 |
15 | #include "geography.hpp"
16 |
17 | namespace py = pybind11;
18 |
19 | namespace spherely {
20 |
21 | // A ``pybind11::object`` that maybe points to a ``Geography`` C++ object.
22 | //
23 | // The main goal of this class is to be used as argument and/or return type of
24 | // spherely's vectorized functions that operate on Geography objects via the
25 | // numpy.object dtype.
26 | //
27 | // Instead of relying on Pybind11's implicit conversion mechanisms (copy), we
28 | // require explicit conversion from / to ``pybind11::object``.
29 | //
30 | //
31 | class PyObjectGeography : public py::object {
32 | public:
33 | static py::detail::type_info *geography_tinfo;
34 |
35 | bool check_type(bool throw_if_invalid = true) const {
36 | PyObject *source = ptr();
37 |
38 | // TODO: case of Python `None` and/or `NaN` (empty geography)
39 |
40 | // cache Geography type_info for performance
41 | if (!geography_tinfo) {
42 | // std::cout << "set pytype" << std::endl;
43 | geography_tinfo = py::detail::get_type_info(typeid(Geography));
44 | }
45 |
46 | PyTypeObject *source_type = Py_TYPE(source);
47 | if (!PyType_IsSubtype(source_type, geography_tinfo->type)) {
48 | if (throw_if_invalid) {
49 | throw py::type_error("not a Geography object");
50 | } else {
51 | return false;
52 | }
53 | }
54 |
55 | return true;
56 | }
57 |
58 | // Python -> C++ conversion
59 | //
60 | // Raises a ``TypeError`` on the Python side if the cast fails.
61 | //
62 | // Note: a raw pointer is used here because Pybind11's
63 | // `type_caster>` doesnt't support Python->C++
64 | // conversion (no `load` method) as it would imply that Python needs to give
65 | // up ownership of an object, which is not possible (the object might be
66 | // referenced elsewhere)
67 | //
68 | // Conversion shouldn't involve any copy. The cast is dynamic, though, as
69 | // needed since the numpy.object dtype can refer to any Python object.
70 | //
71 | Geography *as_geog_ptr() const {
72 | PyObject *source = ptr();
73 |
74 | // TODO: case of Python `None` and/or `NaN` (empty geography)
75 |
76 | check_type();
77 |
78 | auto inst = reinterpret_cast(source);
79 | return reinterpret_cast(inst->simple_value_holder[0]);
80 | }
81 |
82 | // C++ -> Python conversion
83 | //
84 | // Note: pybind11's `type_caster>` implements
85 | // move semantics (Python takes ownership).
86 | //
87 | template ::value, bool> = true>
88 | static PyObjectGeography from_geog(std::unique_ptr geog_ptr) {
89 | auto pyobj = py::cast(std::move(geog_ptr));
90 | auto pyobj_geog = static_cast(pyobj);
91 | return std::move(pyobj_geog);
92 | }
93 |
94 | // Just check whether the object is a Geography
95 | //
96 | bool is_geog_ptr() const {
97 | return check_type(false);
98 | }
99 | };
100 | } // namespace spherely
101 |
102 | namespace pybind11 {
103 | namespace detail {
104 |
105 | // Force pybind11 to allow PyObjectGeography and str as argument of vectorized
106 | // functions.
107 | //
108 | // Pybind11 doesn't support non-POD types as arguments for vectorized
109 | // functions because of its internal conversion mechanisms and also
110 | // because direct memory access requires a standard layout type.
111 | //
112 | // Here it is probably fine to make an exception since we require
113 | // explicit Python object <-> C++ Geography conversion and also because
114 | // with the numpy.object dtype the data are actually references to Python
115 | // objects (not the objects themselves).
116 | //
117 | // Caveat: be careful and use PyObjectGeography cast methods!
118 | //
119 | template <>
120 | struct vectorize_arg {
121 | using T = spherely::PyObjectGeography;
122 | // The wrapped function gets called with this type:
123 | using call_type = T;
124 | // Is this a vectorized argument?
125 | static constexpr bool vectorize = true;
126 | // Accept this type: an array for vectorized types, otherwise the type
127 | // as-is:
128 | using type = conditional_t, array::forcecast>, T>;
129 | };
130 |
131 | template <>
132 | struct vectorize_arg {
133 | using T = py::str;
134 | // The wrapped function gets called with this type:
135 | using call_type = T;
136 | // Is this a vectorized argument?
137 | static constexpr bool vectorize = true;
138 | // Accept this type: an array for vectorized types, otherwise the type
139 | // as-is:
140 | using type = conditional_t, array::forcecast>, T>;
141 | };
142 |
143 | template <>
144 | struct vectorize_arg {
145 | using T = py::bytes;
146 | // The wrapped function gets called with this type:
147 | using call_type = T;
148 | // Is this a vectorized argument?
149 | static constexpr bool vectorize = true;
150 | // Accept this type: an array for vectorized types, otherwise the type
151 | // as-is:
152 | using type = conditional_t, array::forcecast>, T>;
153 | };
154 |
155 | // Register PyObjectGeography and str as a valid numpy dtype (numpy.object alias)
156 | // from: https://github.com/pybind/pybind11/pull/1152
157 | template <>
158 | struct npy_format_descriptor {
159 | static constexpr auto name = _("object");
160 | enum { value = npy_api::NPY_OBJECT_ };
161 | static pybind11::dtype dtype() {
162 | if (auto ptr = npy_api::get().PyArray_DescrFromType_(value)) {
163 | return reinterpret_borrow(ptr);
164 | }
165 | pybind11_fail("Unsupported buffer format!");
166 | }
167 | };
168 |
169 | template <>
170 | struct npy_format_descriptor {
171 | static constexpr auto name = _("object");
172 | enum { value = npy_api::NPY_OBJECT_ };
173 | static pybind11::dtype dtype() {
174 | if (auto ptr = npy_api::get().PyArray_DescrFromType_(value)) {
175 | return reinterpret_borrow(ptr);
176 | }
177 | pybind11_fail("Unsupported buffer format!");
178 | }
179 | };
180 |
181 | template <>
182 | struct npy_format_descriptor {
183 | static constexpr auto name = _("object");
184 | enum { value = npy_api::NPY_OBJECT_ };
185 | static pybind11::dtype dtype() {
186 | if (auto ptr = npy_api::get().PyArray_DescrFromType_(value)) {
187 | return reinterpret_borrow(ptr);
188 | }
189 | pybind11_fail("Unsupported buffer format!");
190 | }
191 | };
192 |
193 | // Override signature type hint for vectorized Geography arguments
194 | template
195 | struct handle_type_name> {
196 | static constexpr auto name = _("Geography | array_like");
197 | };
198 |
199 | } // namespace detail
200 |
201 | // Specialization of ``pybind11::cast`` for PyObjectGeography and str (just a pass
202 | // through).
203 | //
204 | // Allows using PyObjectGeography and str as return type of vectorized functions.
205 | //
206 | template <
207 | typename T,
208 | typename detail::enable_if_t::value, int> = 0>
209 | object cast(T &&value) {
210 | return value;
211 | }
212 |
213 | template ::value, int> = 0>
214 | object cast(T &&value) {
215 | return value;
216 | }
217 |
218 | template ::value, int> = 0>
219 | object cast(T &&value) {
220 | return value;
221 | }
222 |
223 | } // namespace pybind11
224 |
225 | #endif // SPHERELY_PYBIND11_H_
226 |
--------------------------------------------------------------------------------
/src/spherely.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #define STRINGIFY(x) #x
4 | #define MACRO_STRINGIFY(x) STRINGIFY(x)
5 |
6 | namespace py = pybind11;
7 |
8 | void init_geography(py::module&);
9 | void init_creation(py::module&);
10 | void init_predicates(py::module&);
11 | void init_boolean_operations(py::module&);
12 | void init_accessors(py::module&);
13 | void init_io(py::module&);
14 | void init_geoarrow(py::module&);
15 | void init_projections(py::module&);
16 |
17 | PYBIND11_MODULE(spherely, m) {
18 | py::options options;
19 | options.disable_function_signatures();
20 |
21 | m.doc() = R"pbdoc(
22 | Spherely
23 | ---------
24 | .. currentmodule:: spherely
25 | .. autosummary::
26 | :toctree: _generate
27 | )pbdoc";
28 |
29 | init_geography(m);
30 | init_creation(m);
31 | init_predicates(m);
32 | init_boolean_operations(m);
33 | init_accessors(m);
34 | init_io(m);
35 | init_projections(m);
36 | init_geoarrow(m);
37 |
38 | #ifdef VERSION_INFO
39 | m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO);
40 | #else
41 | m.attr("__version__") = "dev";
42 | #endif
43 |
44 | #ifdef S2GEOGRAPHY_VERSION
45 | m.attr("__s2geography_version__") = MACRO_STRINGIFY(S2GEOGRAPHY_VERSION);
46 | #endif
47 | }
48 |
--------------------------------------------------------------------------------
/src/spherely.pyi:
--------------------------------------------------------------------------------
1 | from typing import (
2 | Annotated,
3 | Any,
4 | ClassVar,
5 | Generic,
6 | Iterable,
7 | Literal,
8 | Protocol,
9 | Sequence,
10 | TypeVar,
11 | overload,
12 | )
13 |
14 | import numpy as np
15 | import numpy.typing as npt
16 |
17 | __version__: str = ...
18 | __s2geography_version__: str = ...
19 | EARTH_RADIUS_METERS: float = ...
20 |
21 | class Geography:
22 | def __init__(self, *args, **kwargs) -> None: ...
23 |
24 | class GeographyType:
25 | __members__: ClassVar[dict] = ... # read-only
26 | LINESTRING: ClassVar[GeographyType] = ...
27 | NONE: ClassVar[GeographyType] = ...
28 | POINT: ClassVar[GeographyType] = ...
29 | POLYGON: ClassVar[GeographyType] = ...
30 | MULTIPOLYGON: ClassVar[GeographyType] = ...
31 | MULTIPOINT: ClassVar[GeographyType] = ...
32 | MULTILINESTRING: ClassVar[GeographyType] = ...
33 | GEOMETRYCOLLECTION: ClassVar[GeographyType] = ...
34 | __entries: ClassVar[dict] = ...
35 | def __init__(self, value: int) -> None: ...
36 | def __eq__(self, other: object) -> bool: ...
37 | def __getstate__(self) -> int: ...
38 | def __hash__(self) -> int: ...
39 | def __index__(self) -> int: ...
40 | def __int__(self) -> int: ...
41 | def __ne__(self, other: object) -> bool: ...
42 | def __setstate__(self, state: int) -> None: ...
43 | @property
44 | def name(self) -> str: ...
45 | @property
46 | def value(self) -> int: ...
47 |
48 | # Annotated type aliases
49 |
50 | PointGeography = Annotated[Geography, GeographyType.POINT]
51 | LineStringGeography = Annotated[Geography, GeographyType.LINESTRING]
52 | PolygonGeography = Annotated[Geography, GeographyType.POLYGON]
53 | MultiPointGeography = Annotated[Geography, GeographyType.MULTIPOINT]
54 | MultiLineStringGeography = Annotated[Geography, GeographyType.MULTILINESTRING]
55 | MultiPolygonGeography = Annotated[Geography, GeographyType.MULTIPOLYGON]
56 | GeometryCollection = Annotated[Geography, GeographyType.GEOMETRYCOLLECTION]
57 |
58 | # Projection class
59 |
60 | class Projection:
61 | @staticmethod
62 | def lnglat() -> Projection: ...
63 | @staticmethod
64 | def pseudo_mercator() -> Projection: ...
65 | @staticmethod
66 | def orthographic(longitude: float, latitude: float) -> Projection: ...
67 |
68 | # Numpy-like vectorized (universal) functions
69 |
70 | _NameType = TypeVar("_NameType", bound=str)
71 | _ScalarReturnType = TypeVar("_ScalarReturnType", bound=Any)
72 | _ArrayReturnDType = TypeVar("_ArrayReturnDType", bound=Any)
73 |
74 | # TODO: npt.NDArray[Geography] not supported yet
75 | # (see https://github.com/numpy/numpy/issues/24738)
76 | # (unless Geography is passed via Generic[...], see VFunc below)
77 | T_NDArray_Geography = npt.NDArray[Any]
78 |
79 | # The following types are auto-generated. Please don't edit them by hand.
80 | # Instead, update the generate_spherely_vfunc_types.py script and run it
81 | # to update the types.
82 | #
83 | # /// Begin types
84 | class _VFunc_Nin1_Nout1(Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]):
85 | @property
86 | def __name__(self) -> _NameType: ...
87 | @overload
88 | def __call__(self, geography: Geography, /) -> _ScalarReturnType: ...
89 | @overload
90 | def __call__(
91 | self, geography: Iterable[Geography], /
92 | ) -> npt.NDArray[_ArrayReturnDType]: ...
93 |
94 | class _VFunc_Nin2_Nout1(Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]):
95 | @property
96 | def __name__(self) -> _NameType: ...
97 | @overload
98 | def __call__(self, a: Geography, b: Geography) -> _ScalarReturnType: ...
99 | @overload
100 | def __call__(
101 | self, a: Geography, b: Iterable[Geography]
102 | ) -> npt.NDArray[_ArrayReturnDType]: ...
103 | @overload
104 | def __call__(
105 | self, a: Iterable[Geography], b: Geography
106 | ) -> npt.NDArray[_ArrayReturnDType]: ...
107 | @overload
108 | def __call__(
109 | self, a: Iterable[Geography], b: Iterable[Geography]
110 | ) -> npt.NDArray[_ArrayReturnDType]: ...
111 |
112 | class _VFunc_Nin2optradius_Nout1(
113 | Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]
114 | ):
115 | @property
116 | def __name__(self) -> _NameType: ...
117 | @overload
118 | def __call__(
119 | self, a: Geography, b: Geography, radius: float = 6371010.0
120 | ) -> _ScalarReturnType: ...
121 | @overload
122 | def __call__(
123 | self, a: Geography, b: Iterable[Geography], radius: float = 6371010.0
124 | ) -> npt.NDArray[_ArrayReturnDType]: ...
125 | @overload
126 | def __call__(
127 | self, a: Iterable[Geography], b: Geography, radius: float = 6371010.0
128 | ) -> npt.NDArray[_ArrayReturnDType]: ...
129 | @overload
130 | def __call__(
131 | self, a: Iterable[Geography], b: Iterable[Geography], radius: float = 6371010.0
132 | ) -> npt.NDArray[_ArrayReturnDType]: ...
133 |
134 | class _VFunc_Nin1optradius_Nout1(
135 | Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]
136 | ):
137 | @property
138 | def __name__(self) -> _NameType: ...
139 | @overload
140 | def __call__(
141 | self, geography: Geography, /, radius: float = 6371010.0
142 | ) -> _ScalarReturnType: ...
143 | @overload
144 | def __call__(
145 | self, geography: Iterable[Geography], /, radius: float = 6371010.0
146 | ) -> npt.NDArray[_ArrayReturnDType]: ...
147 |
148 | class _VFunc_Nin1optprecision_Nout1(
149 | Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]
150 | ):
151 | @property
152 | def __name__(self) -> _NameType: ...
153 | @overload
154 | def __call__(
155 | self, geography: Geography, /, precision: int = 6
156 | ) -> _ScalarReturnType: ...
157 | @overload
158 | def __call__(
159 | self, geography: Iterable[Geography], /, precision: int = 6
160 | ) -> npt.NDArray[_ArrayReturnDType]: ...
161 |
162 | # /// End types
163 |
164 | # Geography properties
165 |
166 | get_dimension: _VFunc_Nin1_Nout1[Literal["get_dimension"], Geography, Any]
167 | get_type_id: _VFunc_Nin1_Nout1[Literal["get_type_id"], int, np.int8]
168 |
169 | # Geography creation (scalar)
170 |
171 | def create_point(
172 | longitude: float | None = None, latitude: float | None = None
173 | ) -> Geography: ...
174 | def create_multipoint(
175 | points: Iterable[Sequence[float]] | Iterable[PointGeography],
176 | ) -> MultiPointGeography: ...
177 | def create_linestring(
178 | vertices: Iterable[Sequence[float]] | Iterable[PointGeography] | None = None,
179 | ) -> LineStringGeography: ...
180 | def create_multilinestring(
181 | vertices: (
182 | Iterable[Iterable[Sequence[float]]]
183 | | Iterable[Iterable[PointGeography]]
184 | | Iterable[LineStringGeography]
185 | ),
186 | ) -> MultiLineStringGeography: ...
187 | @overload
188 | def create_polygon(
189 | shell: None = None,
190 | holes: None = None,
191 | oriented: bool = False,
192 | ) -> PolygonGeography: ...
193 | @overload
194 | def create_polygon(
195 | shell: Iterable[Sequence[float]],
196 | holes: Iterable[Iterable[Sequence[float]]] | None = None,
197 | oriented: bool = False,
198 | ) -> PolygonGeography: ...
199 | @overload
200 | def create_polygon(
201 | shell: Iterable[PointGeography],
202 | holes: Iterable[Iterable[PointGeography]] | None = None,
203 | oriented: bool = False,
204 | ) -> PolygonGeography: ...
205 | def create_multipolygon(
206 | polygons: Iterable[PolygonGeography],
207 | ) -> MultiPolygonGeography: ...
208 | def create_collection(geographies: Iterable[Geography]) -> GeometryCollection: ...
209 |
210 | # Geography creation (vectorized)
211 |
212 | def points(
213 | longitude: npt.ArrayLike, latitude: npt.ArrayLike
214 | ) -> PointGeography | T_NDArray_Geography: ...
215 |
216 | # Geography utils
217 |
218 | is_geography: _VFunc_Nin1_Nout1[Literal["is_geography"], bool, bool]
219 | is_prepared: _VFunc_Nin1_Nout1[Literal["is_prepared"], bool, bool]
220 | prepare: _VFunc_Nin1_Nout1[Literal["prepare"], Geography, Any]
221 | destroy_prepared: _VFunc_Nin1_Nout1[Literal["destroy_prepared"], Geography, Any]
222 | is_empty: _VFunc_Nin1_Nout1[Literal["is_empty"], bool, bool]
223 |
224 | # predicates
225 |
226 | intersects: _VFunc_Nin2_Nout1[Literal["intersects"], bool, bool]
227 | equals: _VFunc_Nin2_Nout1[Literal["intersects"], bool, bool]
228 | contains: _VFunc_Nin2_Nout1[Literal["contains"], bool, bool]
229 | within: _VFunc_Nin2_Nout1[Literal["within"], bool, bool]
230 | disjoint: _VFunc_Nin2_Nout1[Literal["disjoint"], bool, bool]
231 | touches: _VFunc_Nin2_Nout1[Literal["touches"], bool, bool]
232 | covers: _VFunc_Nin2_Nout1[Literal["covers"], bool, bool]
233 | covered_by: _VFunc_Nin2_Nout1[Literal["covered_by"], bool, bool]
234 |
235 | # boolean operations
236 |
237 | union: _VFunc_Nin2_Nout1[Literal["union"], Geography, Geography]
238 | intersection: _VFunc_Nin2_Nout1[Literal["intersection"], Geography, Geography]
239 | difference: _VFunc_Nin2_Nout1[Literal["difference"], Geography, Geography]
240 | symmetric_difference: _VFunc_Nin2_Nout1[
241 | Literal["symmetric_difference"], Geography, Geography
242 | ]
243 |
244 | # coords
245 |
246 | get_x: _VFunc_Nin1_Nout1[Literal["get_x"], float, np.float64]
247 | get_y: _VFunc_Nin1_Nout1[Literal["get_y"], float, np.float64]
248 |
249 | # geography accessors
250 |
251 | centroid: _VFunc_Nin1_Nout1[Literal["centroid"], PointGeography, PointGeography]
252 | boundary: _VFunc_Nin1_Nout1[Literal["boundary"], Geography, Geography]
253 | convex_hull: _VFunc_Nin1_Nout1[
254 | Literal["convex_hull"], PolygonGeography, PolygonGeography
255 | ]
256 | distance: _VFunc_Nin2optradius_Nout1[Literal["distance"], float, np.float64]
257 | area: _VFunc_Nin1optradius_Nout1[Literal["area"], float, np.float64]
258 | length: _VFunc_Nin1optradius_Nout1[Literal["length"], float, np.float64]
259 | perimeter: _VFunc_Nin1optradius_Nout1[Literal["perimeter"], float, np.float64]
260 |
261 | # io functions
262 |
263 | to_wkt: _VFunc_Nin1optprecision_Nout1[Literal["to_wkt"], str, object]
264 | to_wkb: _VFunc_Nin1_Nout1[Literal["to_wkb"], bytes, object]
265 |
266 | @overload
267 | def from_wkt(
268 | geography: str,
269 | /,
270 | *,
271 | oriented: bool = False,
272 | planar: bool = False,
273 | tessellate_tolerance: float = 100.0,
274 | ) -> Geography: ...
275 | @overload
276 | def from_wkt(
277 | geography: list[str] | npt.NDArray[np.str_],
278 | /,
279 | *,
280 | oriented: bool = False,
281 | planar: bool = False,
282 | tessellate_tolerance: float = 100.0,
283 | ) -> T_NDArray_Geography: ...
284 | @overload
285 | def from_wkb(
286 | geography: bytes,
287 | /,
288 | *,
289 | oriented: bool = False,
290 | planar: bool = False,
291 | tessellate_tolerance: float = 100.0,
292 | ) -> Geography: ...
293 | @overload
294 | def from_wkb(
295 | geography: Iterable[bytes],
296 | /,
297 | *,
298 | oriented: bool = False,
299 | planar: bool = False,
300 | tessellate_tolerance: float = 100.0,
301 | ) -> T_NDArray_Geography: ...
302 |
303 | class ArrowSchemaExportable(Protocol):
304 | def __arrow_c_schema__(self) -> object: ...
305 |
306 | class ArrowArrayExportable(Protocol):
307 | def __arrow_c_array__(
308 | self, requested_schema: object | None = None
309 | ) -> tuple[object, object]: ...
310 |
311 | class ArrowArrayHolder(ArrowArrayExportable): ...
312 |
313 | def to_geoarrow(
314 | geographies: Geography | T_NDArray_Geography,
315 | /,
316 | *,
317 | output_schema: ArrowSchemaExportable | str | None = None,
318 | projection: Projection = Projection.lnglat(),
319 | planar: bool = False,
320 | tessellate_tolerance: float = 100.0,
321 | precision: int = 6,
322 | ) -> ArrowArrayExportable: ...
323 | def from_geoarrow(
324 | geographies: ArrowArrayExportable,
325 | /,
326 | *,
327 | oriented: bool = False,
328 | planar: bool = False,
329 | tessellate_tolerance: float = 100.0,
330 | projection: Projection = Projection.lnglat(),
331 | geometry_encoding: str | None = None,
332 | ) -> T_NDArray_Geography: ...
333 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benbovy/spherely/4f527296b3539865c37e7e477ec6ccba9cd5861e/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_accessors.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import numpy as np
4 | import pytest
5 |
6 | import spherely
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "geog, expected",
11 | [
12 | (spherely.create_point(0, 0), spherely.create_point(0, 0)),
13 | (
14 | spherely.create_linestring([(0, 0), (2, 0)]),
15 | spherely.create_point(1, 0),
16 | ),
17 | (
18 | spherely.create_polygon([(0, 0), (0, 2), (2, 2), (2, 0)]),
19 | spherely.create_point(1, 1),
20 | ),
21 | ],
22 | )
23 | def test_centroid(geog, expected) -> None:
24 | # scalar
25 | actual = spherely.centroid(geog)
26 | assert spherely.get_type_id(actual) == spherely.GeographyType.POINT.value
27 | # TODO add some way of testing almost equality
28 | # assert spherely.equals(actual, expected)
29 |
30 | # array
31 | actual = spherely.centroid([geog])
32 | assert isinstance(actual, np.ndarray)
33 | actual = actual[0]
34 | assert spherely.get_type_id(actual) == spherely.GeographyType.POINT.value
35 | # assert spherely.equals(actual, expected)
36 |
37 |
38 | @pytest.mark.parametrize(
39 | "geog, expected",
40 | [
41 | (spherely.create_point(0, 0), "GEOMETRYCOLLECTION EMPTY"),
42 | (
43 | spherely.create_linestring([(0, 0), (2, 0), (2, 2)]),
44 | "MULTIPOINT ((0 0), (2 2))",
45 | ),
46 | (
47 | spherely.create_polygon([(0, 0), (0, 2), (2, 2), (0.5, 1.5)]),
48 | "LINESTRING (0.5 1.5, 2 2, 0 2, 0 0, 0.5 1.5)",
49 | ),
50 | ],
51 | )
52 | def test_boundary(geog, expected) -> None:
53 | # scalar
54 | actual = spherely.boundary(geog)
55 | assert str(actual) == expected
56 |
57 | # array
58 | actual = spherely.boundary([geog])
59 | assert isinstance(actual, np.ndarray)
60 | assert str(actual[0]) == expected
61 |
62 |
63 | @pytest.mark.parametrize(
64 | "geog, expected",
65 | [
66 | (
67 | spherely.create_linestring([(0, 0), (2, 0), (2, 2)]),
68 | spherely.create_polygon([(0, 0), (2, 0), (2, 2)]),
69 | ),
70 | (
71 | spherely.create_polygon([(0, 0), (0, 2), (2, 2), (0.5, 1.5)]),
72 | spherely.create_polygon([(0, 0), (0, 2), (2, 2)]),
73 | ),
74 | ],
75 | )
76 | def test_convex_hull(geog, expected) -> None:
77 | # scalar
78 | actual = spherely.convex_hull(geog)
79 | assert spherely.get_type_id(actual) == spherely.GeographyType.POLYGON.value
80 | assert spherely.equals(actual, expected)
81 |
82 | # array
83 | actual = spherely.convex_hull([geog])
84 | assert isinstance(actual, np.ndarray)
85 | actual = actual[0]
86 | assert spherely.get_type_id(actual) == spherely.GeographyType.POLYGON.value
87 | assert spherely.equals(actual, expected)
88 |
89 |
90 | def test_is_empty():
91 | arr = spherely.from_wkt(
92 | [
93 | "POINT (0 0)",
94 | "POINT EMPTY",
95 | "LINESTRING (0 0, 1 1)",
96 | "LINESTRING EMPTY",
97 | "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))",
98 | "POLYGON EMPTY",
99 | "GEOMETRYCOLLECTION EMPTY",
100 | "GEOMETRYCOLLECTION (POINT EMPTY)",
101 | ]
102 | )
103 | result = spherely.is_empty(arr)
104 | expected = np.array([False, True, False, True, False, True, True, True])
105 | np.testing.assert_array_equal(result, expected)
106 |
107 |
108 | @pytest.mark.parametrize(
109 | "geog_a, geog_b, expected",
110 | [
111 | (
112 | spherely.create_point(0, 0),
113 | spherely.create_point(0, 90),
114 | np.pi / 2 * spherely.EARTH_RADIUS_METERS,
115 | ),
116 | (
117 | spherely.create_point(0, 90),
118 | spherely.create_point(90, 30),
119 | np.pi / 3 * spherely.EARTH_RADIUS_METERS,
120 | ),
121 | (
122 | spherely.create_polygon([(0, 0), (30, 60), (60, -30)]),
123 | spherely.create_point(0, 90),
124 | np.pi / 6 * spherely.EARTH_RADIUS_METERS,
125 | ),
126 | ],
127 | )
128 | def test_distance(geog_a, geog_b, expected) -> None:
129 | # scalar
130 | actual = spherely.distance(geog_a, geog_b)
131 | assert isinstance(actual, float)
132 | assert actual == pytest.approx(expected, 1e-9)
133 |
134 | # array
135 | actual = spherely.distance([geog_a], [geog_b])
136 | assert isinstance(actual, np.ndarray)
137 | actual = actual[0]
138 | assert isinstance(actual, float)
139 | assert actual == pytest.approx(expected, 1e-9)
140 |
141 |
142 | def test_distance_with_custom_radius() -> None:
143 | actual = spherely.distance(
144 | spherely.create_point(0, 90),
145 | spherely.create_point(0, 0),
146 | radius=1,
147 | )
148 | assert isinstance(actual, float)
149 | assert actual == pytest.approx(np.pi / 2)
150 |
151 |
152 | def test_area() -> None:
153 | # scalar
154 | geog = spherely.create_polygon([(0, 0), (90, 0), (0, 90), (0, 0)])
155 | result = spherely.area(geog, radius=1)
156 | assert isinstance(result, float)
157 | expected = 4 * math.pi / 8
158 | assert result == pytest.approx(expected, 1e-9)
159 |
160 | result = spherely.area(geog)
161 | assert result == pytest.approx(expected * spherely.EARTH_RADIUS_METERS**2, 1e-9)
162 |
163 | # array
164 | actual = spherely.area([geog], radius=1)
165 | assert isinstance(actual, np.ndarray)
166 | actual = actual[0]
167 | assert isinstance(actual, float)
168 | assert actual == pytest.approx(4 * math.pi / 8, 1e-9)
169 |
170 |
171 | @pytest.mark.parametrize(
172 | "geog",
173 | [
174 | "POINT (-64 45)",
175 | "POINT EMPTY",
176 | "LINESTRING (0 0, 1 1)",
177 | "LINESTRING EMPTY",
178 | "POLYGON EMPTY",
179 | ],
180 | )
181 | def test_area_empty(geog) -> None:
182 | assert spherely.area(spherely.from_wkt(geog)) == 0
183 |
184 |
185 | def test_length() -> None:
186 | geog = spherely.create_linestring([(0, 0), (1, 0)])
187 | result = spherely.length(geog, radius=1)
188 | assert isinstance(result, float)
189 | expected = 1.0 * np.pi / 180.0
190 | assert result == pytest.approx(expected, 1e-9)
191 |
192 | actual = spherely.length([geog], radius=1)
193 | assert isinstance(actual, np.ndarray)
194 | actual = actual[0]
195 | assert isinstance(actual, float)
196 | assert actual == pytest.approx(expected, 1e-9)
197 |
198 |
199 | @pytest.mark.parametrize(
200 | "geog",
201 | [
202 | "POINT (0 0)",
203 | "POINT EMPTY",
204 | "POLYGON EMPTY",
205 | "POLYGON ((0 0, 0 1, 1 0, 0 0))",
206 | ],
207 | )
208 | def test_length_invalid(geog) -> None:
209 | assert spherely.length(spherely.from_wkt(geog)) == 0.0
210 |
211 |
212 | def test_perimeter() -> None:
213 | geog = spherely.create_polygon([(0, 0), (0, 90), (90, 90), (90, 0), (0, 0)])
214 | result = spherely.perimeter(geog, radius=1)
215 | assert isinstance(result, float)
216 | expected = 3 * 90 * np.pi / 180.0
217 | assert result == pytest.approx(expected, 1e-9)
218 |
219 | actual = spherely.perimeter([geog], radius=1)
220 | assert isinstance(actual, np.ndarray)
221 | actual = actual[0]
222 | assert isinstance(actual, float)
223 | assert actual == pytest.approx(expected, 1e-9)
224 |
225 |
226 | @pytest.mark.parametrize(
227 | "geog", ["POINT (0 0)", "POINT EMPTY", "LINESTRING (0 0, 1 0)", "POLYGON EMPTY"]
228 | )
229 | def test_perimeter_invalid(geog) -> None:
230 | assert spherely.perimeter(spherely.from_wkt(geog)) == 0.0
231 |
--------------------------------------------------------------------------------
/tests/test_boolean_operations.py:
--------------------------------------------------------------------------------
1 | from packaging.version import Version
2 |
3 | import pytest
4 |
5 | import spherely
6 |
7 | poly1 = spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
8 | poly2 = spherely.from_wkt("POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))")
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "geog1, geog2, expected",
13 | [
14 | ("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"),
15 | ("POINT EMPTY", "POINT EMPTY", "GEOMETRYCOLLECTION EMPTY"),
16 | (
17 | "LINESTRING (-45 0, 0 0)",
18 | "LINESTRING (0 0, 0 10)",
19 | "LINESTRING (-45 0, 0 0, 0 10)",
20 | ),
21 | ],
22 | )
23 | def test_union(geog1, geog2, expected) -> None:
24 | result = spherely.union(spherely.from_wkt(geog1), spherely.from_wkt(geog2))
25 | assert str(result) == expected
26 |
27 |
28 | def test_union_polygon():
29 | result = spherely.union(poly1, poly2)
30 |
31 | expected_near = (
32 | spherely.area(spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))"))
33 | + spherely.area(spherely.from_wkt("POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))"))
34 | - spherely.area(spherely.from_wkt("POLYGON ((5 5, 10 5, 10 15, 5 10, 5 5))"))
35 | )
36 | pytest.approx(spherely.area(result), expected_near)
37 |
38 |
39 | @pytest.mark.parametrize(
40 | "geog1, geog2, expected",
41 | [
42 | ("POINT (30 10)", "POINT (30 10)", "POINT (30 10)"),
43 | (
44 | "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))",
45 | "LINESTRING (0 5, 10 5)",
46 | "LINESTRING (0 5, 10 5)",
47 | ),
48 | ],
49 | )
50 | def test_intersection(geog1, geog2, expected) -> None:
51 | result = spherely.intersection(spherely.from_wkt(geog1), spherely.from_wkt(geog2))
52 | assert str(result) == expected
53 |
54 |
55 | def test_intersection_empty() -> None:
56 | result = spherely.intersection(poly1, spherely.from_wkt("POLYGON EMPTY"))
57 | # assert spherely.is_empty(result)
58 | assert str(result) == "GEOMETRYCOLLECTION EMPTY"
59 |
60 | result = spherely.intersection(spherely.from_wkt("POLYGON EMPTY"), poly1)
61 | assert str(result) == "GEOMETRYCOLLECTION EMPTY"
62 |
63 | result = spherely.intersection(
64 | spherely.from_wkt("POINT (0 1)"), spherely.from_wkt("POINT (1 2)")
65 | )
66 | assert str(result) == "GEOMETRYCOLLECTION EMPTY"
67 |
68 |
69 | def test_intersection_lines() -> None:
70 | result = spherely.intersection(
71 | spherely.from_wkt("LINESTRING (-45 0, 45 0)"),
72 | spherely.from_wkt("LINESTRING (0 -10, 0 10)"),
73 | )
74 | assert str(result) == "POINT (0 0)"
75 | assert spherely.distance(result, spherely.from_wkt("POINT (0 0)")) == 0
76 |
77 |
78 | def test_intersection_polygons() -> None:
79 | result = spherely.intersection(poly1, poly2)
80 | # TODO precision could be higher with snap level
81 | precision = 2 if Version(spherely.__s2geography_version__) < Version("0.2.0") else 1
82 | assert (
83 | spherely.to_wkt(result, precision=precision)
84 | == "POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))"
85 | )
86 |
87 |
88 | def test_intersection_polygon_model() -> None:
89 | poly = spherely.from_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
90 | point = spherely.from_wkt("POINT (0 0)")
91 |
92 | result = spherely.intersection(poly, point)
93 | assert str(result) == "GEOMETRYCOLLECTION EMPTY"
94 |
95 | # TODO this will be different depending on the model
96 |
97 |
98 | @pytest.mark.parametrize(
99 | "geog1, geog2, expected",
100 | [
101 | ("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"),
102 | ("POINT EMPTY", "POINT EMPTY", "GEOMETRYCOLLECTION EMPTY"),
103 | (
104 | "LINESTRING (0 0, 45 0)",
105 | "LINESTRING (0 0, 45 0)",
106 | "GEOMETRYCOLLECTION EMPTY",
107 | ),
108 | ],
109 | )
110 | def test_difference(geog1, geog2, expected) -> None:
111 | result = spherely.difference(spherely.from_wkt(geog1), spherely.from_wkt(geog2))
112 | assert spherely.equals(result, spherely.from_wkt(expected))
113 |
114 |
115 | def test_difference_polygons() -> None:
116 | result = spherely.difference(poly1, poly2)
117 | expected_near = spherely.area(poly1) - spherely.area(
118 | spherely.from_wkt("POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))")
119 | )
120 | pytest.approx(spherely.area(result), expected_near)
121 |
122 |
123 | @pytest.mark.parametrize(
124 | "geog1, geog2, expected",
125 | [
126 | ("POINT (30 10)", "POINT EMPTY", "POINT (30 10)"),
127 | ("POINT (30 10)", "POINT (30 10)", "GEOMETRYCOLLECTION EMPTY"),
128 | ("POINT (30 10)", "POINT (30 20)", "MULTIPOINT ((30 20), (30 10))"),
129 | (
130 | "LINESTRING (0 0, 45 0)",
131 | "LINESTRING (0 0, 45 0)",
132 | "GEOMETRYCOLLECTION EMPTY",
133 | ),
134 | ],
135 | )
136 | def test_symmetric_difference(geog1, geog2, expected) -> None:
137 | result = spherely.symmetric_difference(
138 | spherely.from_wkt(geog1), spherely.from_wkt(geog2)
139 | )
140 | assert spherely.equals(result, spherely.from_wkt(expected))
141 |
142 |
143 | def test_symmetric_difference_polygons() -> None:
144 | result = spherely.symmetric_difference(poly1, poly2)
145 | expected_near = 2 * (
146 | spherely.area(poly1)
147 | - spherely.area(spherely.from_wkt("POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))"))
148 | )
149 | pytest.approx(spherely.area(result), expected_near)
150 |
--------------------------------------------------------------------------------
/tests/test_geoarrow.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pytest
4 |
5 | import spherely
6 |
7 |
8 | pa = pytest.importorskip("pyarrow")
9 | ga = pytest.importorskip("geoarrow.pyarrow")
10 |
11 |
12 | def test_from_geoarrow_wkt() -> None:
13 |
14 | arr = ga.as_wkt(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
15 |
16 | result = spherely.from_geoarrow(arr)
17 | expected = spherely.points([1, 2, 3], [1, 2, 3])
18 | # object equality does not yet work
19 | # np.testing.assert_array_equal(result, expected)
20 | assert spherely.equals(result, expected).all()
21 |
22 | # without extension type
23 | arr = pa.array(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
24 | result = spherely.from_geoarrow(arr, geometry_encoding="WKT")
25 | assert spherely.equals(result, expected).all()
26 |
27 |
28 | def test_from_geoarrow_wkb() -> None:
29 |
30 | arr = ga.as_wkt(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
31 | arr_wkb = ga.as_wkb(arr)
32 |
33 | result = spherely.from_geoarrow(arr_wkb)
34 | expected = spherely.points([1, 2, 3], [1, 2, 3])
35 | assert spherely.equals(result, expected).all()
36 |
37 | # without extension type
38 | arr_wkb = ga.as_wkb(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
39 | arr = arr_wkb.cast(pa.binary())
40 | result = spherely.from_geoarrow(arr, geometry_encoding="WKB")
41 | assert spherely.equals(result, expected).all()
42 |
43 |
44 | def test_from_geoarrow_native() -> None:
45 |
46 | arr = ga.as_wkt(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
47 | arr_point = ga.as_geoarrow(arr)
48 |
49 | result = spherely.from_geoarrow(arr_point)
50 | expected = spherely.points([1, 2, 3], [1, 2, 3])
51 | assert spherely.equals(result, expected).all()
52 |
53 |
54 | polygon_with_bad_hole_wkt = (
55 | "POLYGON "
56 | "((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),"
57 | "(30 20, 20 25, 20 15, 30 20))"
58 | )
59 |
60 |
61 | def test_from_geoarrow_oriented() -> None:
62 | # by default re-orients the inner ring
63 | arr = ga.as_geoarrow([polygon_with_bad_hole_wkt])
64 |
65 | result = spherely.from_geoarrow(arr)
66 | assert (
67 | str(result[0])
68 | == "POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (20 15, 20 25, 30 20, 20 15))"
69 | )
70 |
71 | # if we force to not orient, we get an error
72 | with pytest.raises(ValueError, match="Inconsistent loop orientations detected"):
73 | spherely.from_geoarrow(arr, oriented=True)
74 |
75 |
76 | def test_from_wkt_planar() -> None:
77 | arr = ga.as_geoarrow(["LINESTRING (-64 45, 0 45)"])
78 | result = spherely.from_geoarrow(arr)
79 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) > 10000.0
80 |
81 | result = spherely.from_geoarrow(arr, planar=True)
82 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 100.0
83 |
84 | result = spherely.from_geoarrow(arr, planar=True, tessellate_tolerance=10)
85 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 10.0
86 |
87 |
88 | def test_from_geoarrow_projection() -> None:
89 | arr = ga.as_wkt(["POINT (1 0)", "POINT(0 1)"])
90 |
91 | result = spherely.from_geoarrow(
92 | arr, projection=spherely.Projection.orthographic(0, 0)
93 | )
94 | expected = spherely.points([90, 0], [0, 90])
95 | # TODO use equality when we support precision / snapping
96 | # assert spherely.equals(result, expected).all()
97 | assert (spherely.to_wkt(result) == spherely.to_wkt(expected)).all()
98 |
99 |
100 | def test_from_geoarrow_no_extension_type() -> None:
101 | arr = pa.array(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
102 |
103 | with pytest.raises(ValueError, match="Expected extension type"):
104 | spherely.from_geoarrow(arr)
105 |
106 |
107 | def test_from_geoarrow_invalid_encoding() -> None:
108 | arr = pa.array(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
109 |
110 | with pytest.raises(ValueError, match="'geometry_encoding' should be one"):
111 | spherely.from_geoarrow(arr, geometry_encoding="point")
112 |
113 |
114 | def test_from_geoarrow_no_arrow_object() -> None:
115 | with pytest.raises(ValueError, match="input should be an Arrow-compatible array"):
116 | spherely.from_geoarrow(np.array(["POINT (1 1)"], dtype=object)) # type: ignore
117 |
118 |
119 | def test_to_geoarrow() -> None:
120 | arr = spherely.points([1, 2, 3], [1, 2, 3])
121 | res = spherely.to_geoarrow(
122 | arr, output_schema=ga.point().with_coord_type(ga.CoordType.INTERLEAVED)
123 | )
124 | assert isinstance(res, spherely.ArrowArrayHolder)
125 | assert hasattr(res, "__arrow_c_array__")
126 |
127 | arr_pa = pa.array(res)
128 | coords = np.asarray(arr_pa.storage.values)
129 | expected = np.array([1, 1, 2, 2, 3, 3], dtype="float64")
130 | np.testing.assert_allclose(coords, expected)
131 |
132 |
133 | def test_to_geoarrow_wkt() -> None:
134 | arr = spherely.points([1, 2, 3], [1, 2, 3])
135 | result = pa.array(spherely.to_geoarrow(arr, output_schema=ga.wkt()))
136 | expected = pa.array(["POINT (1 1)", "POINT (2 2)", "POINT (3 3)"])
137 | assert result.storage.equals(expected)
138 |
139 |
140 | def test_to_geoarrow_wkb() -> None:
141 | arr = spherely.points([1, 2, 3], [1, 2, 3])
142 | result = pa.array(spherely.to_geoarrow(arr, output_schema=ga.wkb()))
143 | # the conversion from lon/lat values to S2 points and back gives some floating
144 | # point differences, and output to WKB does not do any rounding,
145 | # therefore checking exact values here
146 | expected = ga.as_wkb(
147 | [
148 | "POINT (0.9999999999999998 1)",
149 | "POINT (2 1.9999999999999996)",
150 | "POINT (3.0000000000000004 3.0000000000000004)",
151 | ]
152 | )
153 | assert result.equals(expected)
154 |
155 |
156 | def test_wkt_roundtrip() -> None:
157 | wkt = [
158 | "POINT (30 10)",
159 | "LINESTRING (30 10, 10 30, 40 40)",
160 | "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))",
161 | "POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))",
162 | "MULTIPOINT ((10 40), (40 30), (20 20), (30 10))",
163 | "MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))",
164 | "MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))",
165 | "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)))",
166 | "GEOMETRYCOLLECTION (POINT (40 10), LINESTRING (10 10, 20 20, 10 40), POLYGON ((40 40, 20 45, 45 30, 40 40)))",
167 | ]
168 |
169 | arr = spherely.from_geoarrow(ga.as_wkt(wkt))
170 | result = pa.array(spherely.to_geoarrow(arr, output_schema=ga.wkt()))
171 | np.testing.assert_array_equal(result, wkt)
172 |
173 |
174 | def test_to_geoarrow_no_output_encoding() -> None:
175 | arr = spherely.points([1, 2, 3], [1, 2, 3])
176 |
177 | with pytest.raises(ValueError, match="'output_schema' should be specified"):
178 | spherely.to_geoarrow(arr)
179 |
180 |
181 | def test_to_geoarrow_invalid_output_schema() -> None:
182 | arr = spherely.points([1, 2, 3], [1, 2, 3])
183 | with pytest.raises(
184 | ValueError, match="'output_schema' should be an Arrow-compatible schema"
185 | ):
186 | spherely.to_geoarrow(arr, output_schema="WKT")
187 |
188 | with pytest.raises(ValueError, match="Did you pass a valid schema"):
189 | spherely.to_geoarrow(arr, output_schema=pa.schema([("test", pa.int64())]))
190 |
191 |
192 | def test_to_geoarrow_projected() -> None:
193 | arr = spherely.points([1, 2, 3], [1, 2, 3])
194 | point_schema = ga.point().with_coord_type(ga.CoordType.INTERLEAVED)
195 | result = pa.array(
196 | spherely.to_geoarrow(
197 | arr, output_schema=point_schema, projection=spherely.Projection.lnglat()
198 | )
199 | )
200 |
201 | coords = np.asarray(result.storage.values)
202 | expected = np.array([1, 1, 2, 2, 3, 3], dtype="float64")
203 | np.testing.assert_allclose(coords, expected)
204 |
205 | # Output to pseudo mercator - generation of expected result
206 | # import pyproj
207 | # trans = pyproj.Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
208 | # trans.transform([1, 2, 3], [1, 2, 3])
209 | result = pa.array(
210 | spherely.to_geoarrow(
211 | arr,
212 | output_schema=point_schema,
213 | projection=spherely.Projection.pseudo_mercator(),
214 | )
215 | )
216 | coords = np.asarray(result.storage.values)
217 | expected = np.array(
218 | [
219 | 111319.49079327357,
220 | 111325.1428663851,
221 | 222638.98158654713,
222 | 222684.20850554405,
223 | 333958.4723798207,
224 | 334111.1714019596,
225 | ],
226 | dtype="float64",
227 | )
228 | np.testing.assert_allclose(coords, expected)
229 |
230 | # Output to orthographic
231 | result = pa.array(
232 | spherely.to_geoarrow(
233 | arr,
234 | output_schema=point_schema,
235 | projection=spherely.Projection.orthographic(0.0, 0.0),
236 | )
237 | )
238 | coords = np.asarray(result.storage.values)
239 | expected = np.array(
240 | [0.01744975, 0.01745241, 0.03487824, 0.0348995, 0.05226423, 0.05233596],
241 | dtype="float64",
242 | )
243 | np.testing.assert_allclose(coords, expected, rtol=1e-06)
244 |
--------------------------------------------------------------------------------
/tests/test_geography.py:
--------------------------------------------------------------------------------
1 | import pickle
2 |
3 | import pytest
4 | import numpy as np
5 |
6 | import spherely
7 |
8 |
9 | def test_geography_type() -> None:
10 | assert spherely.GeographyType.NONE.value == -1
11 | assert spherely.GeographyType.POINT.value == 0
12 | assert spherely.GeographyType.LINESTRING.value == 1
13 | assert spherely.GeographyType.POLYGON.value == 2
14 | assert spherely.GeographyType.MULTIPOINT.value == 3
15 | assert spherely.GeographyType.MULTILINESTRING.value == 4
16 | assert spherely.GeographyType.MULTIPOLYGON.value == 5
17 | assert spherely.GeographyType.GEOMETRYCOLLECTION.value == 6
18 |
19 |
20 | def test_is_geography() -> None:
21 | arr = np.array([1, 2.33, spherely.create_point(30, 6)])
22 |
23 | actual = spherely.is_geography(arr)
24 | expected = np.array([False, False, True])
25 | np.testing.assert_array_equal(actual, expected)
26 |
27 |
28 | def test_not_geography_raise() -> None:
29 | arr = np.array([1, 2.33, spherely.create_point(30, 6)])
30 |
31 | with pytest.raises(TypeError, match="not a Geography object"):
32 | spherely.get_dimension(arr)
33 |
34 |
35 | def test_get_type_id() -> None:
36 | # array
37 | geog = np.array(
38 | [
39 | spherely.create_point(45, 50),
40 | spherely.create_multipoint([(5, 50), (6, 51)]),
41 | spherely.create_linestring([(5, 50), (6, 51)]),
42 | spherely.create_multilinestring([[(5, 50), (6, 51)], [(15, 60), (16, 61)]]),
43 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
44 | # with hole
45 | spherely.create_polygon(
46 | shell=[(5, 60), (6, 60), (6, 50), (5, 50)],
47 | holes=[[(5.1, 59), (5.9, 59), (5.9, 51), (5.1, 51)]],
48 | ),
49 | spherely.create_multipolygon(
50 | [
51 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
52 | spherely.create_polygon(
53 | [(10, 100), (10, 160), (11, 160), (11, 100)]
54 | ),
55 | ]
56 | ),
57 | spherely.create_collection([spherely.create_point(40, 50)]),
58 | ]
59 | )
60 | actual = spherely.get_type_id(geog)
61 | expected = np.array(
62 | [
63 | spherely.GeographyType.POINT.value,
64 | spherely.GeographyType.MULTIPOINT.value,
65 | spherely.GeographyType.LINESTRING.value,
66 | spherely.GeographyType.MULTILINESTRING.value,
67 | spherely.GeographyType.POLYGON.value,
68 | spherely.GeographyType.POLYGON.value,
69 | spherely.GeographyType.MULTIPOLYGON.value,
70 | spherely.GeographyType.GEOMETRYCOLLECTION.value,
71 | ]
72 | )
73 | np.testing.assert_array_equal(actual, expected)
74 |
75 | # scalar
76 | geog2 = spherely.create_point(45, 50)
77 | assert spherely.get_type_id(geog2) == spherely.GeographyType.POINT.value
78 |
79 |
80 | def test_get_dimension() -> None:
81 | # test n-d array
82 | expected = np.array([[0, 0], [1, 0]], dtype=np.int32)
83 | geog = np.array(
84 | [
85 | [spherely.create_point(5, 40), spherely.create_point(6, 30)],
86 | [
87 | spherely.create_linestring([(5, 50), (6, 51)]),
88 | spherely.create_point(4, 20),
89 | ],
90 | ]
91 | )
92 | actual = spherely.get_dimension(geog)
93 | np.testing.assert_array_equal(actual, expected)
94 |
95 | # test scalar
96 | assert spherely.get_dimension(spherely.create_point(5, 40)) == 0
97 |
98 |
99 | @pytest.mark.parametrize(
100 | "empty_geog, expected",
101 | [
102 | (spherely.create_point(), 0),
103 | (spherely.create_linestring(), 1),
104 | (spherely.create_polygon(), 2),
105 | (spherely.create_collection([]), -1),
106 | ],
107 | )
108 | def test_get_dimension_empty(empty_geog, expected) -> None:
109 | assert spherely.get_dimension(empty_geog) == expected
110 |
111 |
112 | def test_get_dimension_collection() -> None:
113 | geog = spherely.create_collection(
114 | [spherely.create_point(0, 0), spherely.create_polygon([(0, 0), (1, 1), (2, 0)])]
115 | )
116 | assert spherely.get_dimension(geog) == 2
117 |
118 |
119 | def test_get_x_y() -> None:
120 | # scalar
121 | a = spherely.create_point(1.5, 2.6)
122 | assert spherely.get_x(a) == pytest.approx(1.5, abs=1e-14)
123 | assert spherely.get_y(a) == pytest.approx(2.6, abs=1e-14)
124 |
125 | # array
126 | arr = np.array(
127 | [
128 | spherely.create_point(0, 1),
129 | spherely.create_point(1, 2),
130 | spherely.create_point(2, 3),
131 | ]
132 | )
133 |
134 | actual = spherely.get_x(arr)
135 | expected = np.array([0, 1, 2], dtype="float64")
136 | np.testing.assert_allclose(actual, expected)
137 |
138 | actual = spherely.get_y(arr)
139 | expected = np.array([1, 2, 3], dtype="float64")
140 | np.testing.assert_allclose(actual, expected)
141 |
142 | # only points are supported
143 | with pytest.raises(ValueError):
144 | spherely.get_x(spherely.create_linestring([(0, 1), (1, 2)]))
145 |
146 | with pytest.raises(ValueError):
147 | spherely.get_y(spherely.create_linestring([(0, 1), (1, 2)]))
148 |
149 |
150 | def test_prepare() -> None:
151 | # test array
152 | geog = np.array(
153 | [spherely.create_point(50, 45), spherely.create_linestring([(5, 50), (6, 51)])]
154 | )
155 | np.testing.assert_array_equal(spherely.is_prepared(geog), np.array([False, False]))
156 |
157 | spherely.prepare(geog)
158 | np.testing.assert_array_equal(spherely.is_prepared(geog), np.array([True, True]))
159 |
160 | spherely.destroy_prepared(geog)
161 | np.testing.assert_array_equal(spherely.is_prepared(geog), np.array([False, False]))
162 |
163 | # test scalar
164 | geog2 = spherely.points(45, 50)
165 | assert spherely.is_prepared(geog2) is False
166 |
167 | spherely.prepare(geog2)
168 | assert spherely.is_prepared(geog2) is True
169 |
170 | spherely.destroy_prepared(geog2)
171 | assert spherely.is_prepared(geog2) is False
172 |
173 |
174 | def test_equality() -> None:
175 | p1 = spherely.create_point(1, 1)
176 | p2 = spherely.create_point(1, 1)
177 | p3 = spherely.create_point(2, 2)
178 |
179 | assert p1 == p1
180 | assert p1 == p2
181 | assert not p1 == p3
182 |
183 | line1 = spherely.create_linestring([(1, 1), (2, 2), (3, 3)])
184 | line2 = spherely.create_linestring([(3, 3), (2, 2), (1, 1)])
185 |
186 | assert line1 == line2
187 |
188 | poly1 = spherely.create_polygon([(1, 1), (3, 1), (2, 3)])
189 | poly2 = spherely.create_polygon([(2, 3), (1, 1), (3, 1)])
190 | poly3 = spherely.create_polygon([(2, 3), (3, 1), (1, 1)])
191 |
192 | assert p1 != poly1
193 | assert line1 != poly1
194 | assert poly1 == poly2
195 | assert poly2 == poly3
196 | assert poly1 == poly3
197 |
198 | coll1 = (spherely.create_collection([spherely.create_point(40, 50)]),)
199 | coll2 = (spherely.create_collection([spherely.create_point(40, 50)]),)
200 |
201 | assert coll1 == coll2
202 |
203 |
204 | @pytest.mark.parametrize(
205 | "geog",
206 | [
207 | spherely.create_point(45, 50),
208 | spherely.create_multipoint([(5, 50), (6, 51)]),
209 | spherely.create_linestring([(5, 50), (6, 51)]),
210 | spherely.create_multilinestring([[(5, 50), (6, 51)], [(15, 60), (16, 61)]]),
211 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
212 | spherely.create_multipolygon(
213 | [
214 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
215 | spherely.create_polygon([(10, 100), (10, 160), (11, 160), (11, 100)]),
216 | ]
217 | ),
218 | spherely.create_collection([spherely.create_point(40, 50)]),
219 | # empty geography
220 | spherely.create_point(),
221 | spherely.create_linestring(),
222 | spherely.create_polygon(),
223 | ],
224 | )
225 | def test_pickle_roundtrip(geog):
226 | roundtripped = pickle.loads(pickle.dumps(geog))
227 |
228 | assert spherely.get_type_id(roundtripped) == spherely.get_type_id(geog)
229 | assert spherely.to_wkt(roundtripped) == spherely.to_wkt(geog)
230 | assert roundtripped == geog
231 |
--------------------------------------------------------------------------------
/tests/test_io.py:
--------------------------------------------------------------------------------
1 | import struct
2 |
3 | import numpy as np
4 | import pytest
5 |
6 | import spherely
7 |
8 |
9 | def test_from_wkt() -> None:
10 | result = spherely.from_wkt(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"])
11 | expected = spherely.points([1, 2, 3], [1, 2, 3])
12 | # object equality does not yet work
13 | # np.testing.assert_array_equal(result, expected)
14 | assert spherely.equals(result, expected).all()
15 |
16 | # from explicit object dtype
17 | result = spherely.from_wkt(
18 | np.array(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"], dtype=object)
19 | )
20 | assert spherely.equals(result, expected).all()
21 |
22 | # from numpy string dtype
23 | result = spherely.from_wkt(
24 | np.array(["POINT (1 1)", "POINT(2 2)", "POINT(3 3)"], dtype="U")
25 | )
26 | assert spherely.equals(result, expected).all()
27 |
28 |
29 | def test_from_wkt_invalid() -> None:
30 | # TODO can we provide better error type?
31 | with pytest.raises(RuntimeError):
32 | spherely.from_wkt(["POINT (1)"])
33 |
34 |
35 | def test_from_wkt_wrong_type() -> None:
36 | with pytest.raises(TypeError, match="expected bytes, int found"):
37 | spherely.from_wkt([1]) # type: ignore
38 |
39 | # TODO support missing values
40 | with pytest.raises(TypeError, match="expected bytes, NoneType found"):
41 | spherely.from_wkt(["POINT (1 1)", None]) # type: ignore
42 |
43 |
44 | polygon_with_bad_hole_wkt = (
45 | "POLYGON "
46 | "((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),"
47 | "(30 20, 20 25, 20 15, 30 20))"
48 | )
49 |
50 |
51 | def test_from_wkt_oriented() -> None:
52 | # by default re-orients the inner ring
53 | result = spherely.from_wkt(polygon_with_bad_hole_wkt)
54 | assert (
55 | str(result)
56 | == "POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (20 15, 20 25, 30 20, 20 15))"
57 | )
58 |
59 | # if we force to not orient, we get an error
60 | with pytest.raises(RuntimeError, match="Inconsistent loop orientations detected"):
61 | spherely.from_wkt(polygon_with_bad_hole_wkt, oriented=True)
62 |
63 |
64 | def test_from_wkt_planar() -> None:
65 | result = spherely.from_wkt("LINESTRING (-64 45, 0 45)")
66 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) > 10000
67 |
68 | result = spherely.from_wkt("LINESTRING (-64 45, 0 45)", planar=True)
69 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 100
70 |
71 | result = spherely.from_wkt(
72 | "LINESTRING (-64 45, 0 45)", planar=True, tessellate_tolerance=10
73 | )
74 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 10
75 |
76 |
77 | def test_to_wkt() -> None:
78 | arr = spherely.points([1.1, 2, 3], [1.1, 2, 3])
79 | result = spherely.to_wkt(arr)
80 | expected = np.array(["POINT (1.1 1.1)", "POINT (2 2)", "POINT (3 3)"], dtype=object)
81 | np.testing.assert_array_equal(result, expected)
82 |
83 |
84 | def test_to_wkt_precision() -> None:
85 | arr = spherely.points([0.12345], [0.56789])
86 | result = spherely.to_wkt(arr)
87 | assert result[0] == "POINT (0.12345 0.56789)"
88 |
89 | result = spherely.to_wkt(arr, precision=2)
90 | assert result[0] == "POINT (0.12 0.57)"
91 |
92 |
93 | POINT11_WKB = struct.pack(" None:
104 | result = spherely.from_wkb([POINT11_WKB, POINT_NAN_WKB, MULTIPOINT_NAN_WKB])
105 | # empty MultiPoint is converted to empty Point
106 | expected = spherely.from_wkt(["POINT (1 1)", "POINT EMPTY", "POINT EMPTY"])
107 | assert spherely.equals(result, expected).all()
108 |
109 | result2 = spherely.from_wkb(GEOMETRYCOLLECTION_NAN_WKB)
110 | assert str(result2) == "GEOMETRYCOLLECTION (POINT EMPTY)"
111 |
112 |
113 | def test_from_wkb_invalid() -> None:
114 | with pytest.raises(RuntimeError, match="Expected endian byte"):
115 | spherely.from_wkb(b"")
116 |
117 | with pytest.raises(RuntimeError):
118 | spherely.from_wkb([b"\x01\x01\x00\x00\x00\x00"])
119 |
120 | # TODO should this raise an error?
121 | # with pytest.raises(RuntimeError):
122 | result = spherely.from_wkb(INVALID_WKB)
123 | assert str(result) == "POLYGON ((108.7761 -10.2852, 108.7761 -10.2852))"
124 |
125 |
126 | def test_from_wkb_invalid_type() -> None:
127 | with pytest.raises(TypeError, match="expected bytes, str found"):
128 | spherely.from_wkb("POINT (1 1)") # type: ignore
129 |
130 |
131 | @pytest.mark.parametrize(
132 | "geog",
133 | [
134 | spherely.create_point(45, 50),
135 | spherely.create_multipoint([(5, 50), (6, 51)]),
136 | spherely.create_linestring([(5, 50), (6, 51)]),
137 | spherely.create_multilinestring([[(5, 50), (6, 51)], [(15, 60), (16, 61)]]),
138 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
139 | # with hole
140 | spherely.create_polygon(
141 | shell=[(5, 60), (6, 60), (6, 50), (5, 50)],
142 | holes=[[(5.1, 59), (5.9, 59), (5.9, 51), (5.1, 51)]],
143 | ),
144 | spherely.create_multipolygon(
145 | [
146 | spherely.create_polygon([(5, 50), (5, 60), (6, 60), (6, 51)]),
147 | spherely.create_polygon([(10, 100), (10, 160), (11, 160), (11, 100)]),
148 | ]
149 | ),
150 | spherely.create_collection([spherely.create_point(40, 50)]),
151 | spherely.create_collection(
152 | [
153 | spherely.create_point(0, 0),
154 | spherely.create_linestring([(0, 0), (1, 1)]),
155 | spherely.create_polygon([(0, 0), (1, 0), (1, 1)]),
156 | ]
157 | ),
158 | ],
159 | )
160 | def test_wkb_roundtrip(geog) -> None:
161 | wkb = spherely.to_wkb(geog)
162 | result = spherely.from_wkb(wkb)
163 | # roundtrip through Geography unit vector is not exact, so equals can fail
164 | # TODO properly test this once `equals` supports snapping/precision
165 | # assert spherely.equals(result, geog)
166 | assert str(result) == str(geog)
167 |
168 |
169 | def test_from_wkb_oriented() -> None:
170 | # WKB for POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)) -> non-CCW box
171 | wkb = bytes.fromhex(
172 | "010300000001000000050000000000000000000000000000000000000000000000000000000000000000002440000000000000244000000000000024400000000000002440000000000000000000000000000000000000000000000000"
173 | ) # noqa: E501
174 |
175 | result = spherely.from_wkb(wkb)
176 | # by default re-oriented to take the smaller polygon
177 | assert str(result) == "POLYGON ((10 0, 10 10, 0 10, 0 0, 10 0))"
178 | assert spherely.within(spherely.create_point(5, 5), result)
179 |
180 | result = spherely.from_wkb(wkb, oriented=True)
181 | assert str(result) == "POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))"
182 | assert not spherely.within(spherely.create_point(5, 5), result)
183 |
184 |
185 | def test_from_wkb_planar() -> None:
186 | wkb = spherely.to_wkb(spherely.from_wkt("LINESTRING (-64 45, 0 45)"))
187 |
188 | result = spherely.from_wkb(wkb)
189 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) > 10000
190 |
191 | result = spherely.from_wkb(wkb, planar=True)
192 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 100
193 |
194 | result = spherely.from_wkb(wkb, planar=True, tessellate_tolerance=10)
195 | assert spherely.distance(result, spherely.create_point(-30.1, 45)) < 10
196 |
--------------------------------------------------------------------------------
/tests/test_predicates.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import numpy as np
4 | import numpy.typing as npt
5 |
6 | import spherely
7 | import pytest
8 |
9 |
10 | def test_intersects() -> None:
11 | # test array + scalar
12 | a = np.array(
13 | [
14 | spherely.create_linestring([(40, 8), (60, 8)]),
15 | spherely.create_linestring([(20, 0), (30, 0)]),
16 | ]
17 | )
18 | b = spherely.create_linestring([(50, 5), (50, 10)])
19 |
20 | actual = spherely.intersects(a, b)
21 | expected = np.array([True, False])
22 | np.testing.assert_array_equal(actual, expected)
23 |
24 | # two scalars
25 | a2 = spherely.create_point(50, 8)
26 | b2 = spherely.create_point(20, 5)
27 | assert not spherely.intersects(a2, b2)
28 |
29 |
30 | def test_equals() -> None:
31 | # test array + scalar
32 | a = np.array(
33 | [
34 | spherely.create_linestring([(40, 8), (60, 8)]),
35 | spherely.create_linestring([(20, 0), (30, 0)]),
36 | ]
37 | )
38 | b = spherely.create_point(50, 8)
39 |
40 | actual = spherely.equals(a, b)
41 | expected = np.array([False, False])
42 | np.testing.assert_array_equal(actual, expected)
43 |
44 | # two scalars
45 | a2 = spherely.create_point(50, 8)
46 | b2 = spherely.create_point(50, 8)
47 | assert spherely.equals(a2, b2)
48 |
49 |
50 | def test_contains() -> None:
51 | # test array + scalar
52 | a = np.array(
53 | [
54 | spherely.create_linestring([(40, 8), (60, 8)]),
55 | spherely.create_linestring([(20, 0), (30, 0)]),
56 | ]
57 | )
58 | b = spherely.create_point(40, 8)
59 |
60 | actual = spherely.contains(a, b)
61 | expected = np.array([True, False])
62 | np.testing.assert_array_equal(actual, expected)
63 |
64 | # two scalars
65 | a2 = spherely.create_linestring([(50, 8), (60, 8)])
66 | b2 = spherely.create_point(50, 8)
67 | assert spherely.contains(a2, b2)
68 |
69 |
70 | def test_contains_polygon() -> None:
71 | # plain vs. hole polygon
72 | poly_plain = spherely.create_polygon(shell=[(0, 0), (4, 0), (4, 4), (0, 4)])
73 |
74 | poly_hole = spherely.create_polygon(
75 | shell=[(0, 0), (4, 0), (4, 4), (0, 4)],
76 | holes=[[(1, 1), (3, 1), (3, 3), (1, 3)]],
77 | )
78 |
79 | assert spherely.contains(poly_plain, spherely.create_point(2, 2))
80 | assert not spherely.contains(poly_hole, spherely.create_point(2, 2))
81 |
82 |
83 | def test_within() -> None:
84 | # test array + scalar
85 | a = spherely.create_point(40, 8)
86 | b = np.array(
87 | [
88 | spherely.create_linestring([(40, 8), (60, 8)]),
89 | spherely.create_linestring([(20, 0), (30, 0)]),
90 | ]
91 | )
92 |
93 | actual = spherely.within(a, b)
94 | expected = np.array([True, False])
95 | np.testing.assert_array_equal(actual, expected)
96 |
97 | # two scalars
98 | a2 = spherely.create_point(50, 8)
99 | b2 = spherely.create_linestring([(50, 8), (60, 8)])
100 | assert spherely.within(a2, b2)
101 |
102 |
103 | def test_within_polygon() -> None:
104 | # plain vs. hole polygon
105 | poly_plain = spherely.create_polygon(shell=[(0, 0), (4, 0), (4, 4), (0, 4)])
106 |
107 | poly_hole = spherely.create_polygon(
108 | shell=[(0, 0), (4, 0), (4, 4), (0, 4)],
109 | holes=[[(1, 1), (3, 1), (3, 3), (1, 3)]],
110 | )
111 |
112 | assert spherely.within(spherely.create_point(2, 2), poly_plain)
113 | assert not spherely.within(spherely.create_point(2, 2), poly_hole)
114 |
115 |
116 | def test_disjoint() -> None:
117 | a = spherely.create_point(40, 9)
118 | b = np.array(
119 | [
120 | spherely.create_linestring([(40, 8), (60, 8)]),
121 | spherely.create_linestring([(20, 0), (30, 0)]),
122 | ]
123 | )
124 |
125 | actual = spherely.disjoint(a, b)
126 | expected = np.array([True, True])
127 | np.testing.assert_array_equal(actual, expected)
128 |
129 | # two scalars
130 | a2 = spherely.create_point(50, 9)
131 | b2 = spherely.create_linestring([(50, 8), (60, 8)])
132 | assert spherely.disjoint(a2, b2)
133 |
134 |
135 | def test_touches() -> None:
136 | a = spherely.create_polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
137 | b = np.array(
138 | [
139 | spherely.create_polygon([(1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0)]),
140 | spherely.create_polygon([(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5)]),
141 | ]
142 | )
143 |
144 | actual = spherely.touches(a, b)
145 | expected = np.array([True, False])
146 | np.testing.assert_array_equal(actual, expected)
147 |
148 | a_p = spherely.create_point(1.0, 1.0)
149 | b_p = spherely.create_point(1.0, 1.0)
150 | # Points do not have a boundary, so they cannot touch per definition
151 | # This is consistent with PostGIS for example
152 | # (cmp. https://postgis.net/docs/ST_Touches.html)
153 | assert not spherely.touches(a_p, b_p)
154 |
155 | b_line = spherely.create_linestring([(1.0, 1.0), (1.0, 2.0)])
156 | assert spherely.touches(a_p, b_line)
157 |
158 |
159 | @pytest.fixture
160 | def parent_poly() -> spherely.Geography:
161 | return spherely.create_polygon(
162 | [
163 | (-118.0, 60.0),
164 | (-118.0, 40.0),
165 | (-118.0, 23.0),
166 | (34.0, 23.0),
167 | (34.0, 40.0),
168 | (34.0, 60.0),
169 | ]
170 | )
171 |
172 |
173 | @pytest.fixture
174 | def geographies_covers_contains() -> npt.NDArray[Any]:
175 | return np.array(
176 | [
177 | # Basic point covers tests, outside, on boundary and interior
178 | spherely.create_point(-120.0, 70.0),
179 | spherely.create_point(-118.0, 41.0),
180 | spherely.create_point(-116.0, 37.0),
181 | # Basic polyline tests, crossing, on boundary and interior
182 | spherely.create_linestring([(-120.0, 70.0), (-116.0, 37.0)]),
183 | spherely.create_linestring([(-118.0, 41.0), (-118.0, 23.0)]),
184 | spherely.create_linestring([(-117.0, 39.0), (-115.0, 37.0)]),
185 | # Basic polygon test, crossing, shared boundary and interior
186 | spherely.create_polygon(
187 | [(-120.0, 41.0), (-120.0, 35.0), (-115.0, 35.0), (-115.0, 41.0)]
188 | ),
189 | # TODO: This case is currently not fully correct. Supplying a
190 | # polygon `a` and `b` and checking whether `a` covers `b`,
191 | # where `a` and `b` share co-linear edges only works
192 | # when the edges between them are identical.
193 | # An example of this breaking would be a polygon `a`,
194 | # consisting of the edges AB, BC and CA and a polygon `b`,
195 | # consisting of the edges AB*, B*C* and C*A, where B* and C* reside
196 | # somewhere on the edge AB and BC, respectively.
197 | # In this case, s2geometry tries to resolve the co-linearity by
198 | # symbolic perturbation, where B* and C* are moved by
199 | # an infinitesimal amount. However, the resulting geometry may then not
200 | # be covered anymore, even if it would be in reality. Therefor,
201 | # these tests assume identical shared edges between polygons `a` and `b`,
202 | # which does work as intended.
203 | spherely.create_polygon(
204 | [(-118.0, 40.0), (-118.0, 23.0), (34.0, 23.0), (34.0, 40.0)]
205 | ),
206 | spherely.create_polygon(
207 | [(-117.0, 40.0), (-117.0, 35.0), (-115.0, 35.0), (-115.0, 40.0)]
208 | ),
209 | ]
210 | )
211 |
212 |
213 | @pytest.fixture
214 | def geographies_covers_with_labels(
215 | geographies_covers_contains,
216 | ) -> tuple[npt.NDArray[Any], npt.NDArray[np.bool_]]:
217 | return (
218 | geographies_covers_contains,
219 | np.array([False, True, True, False, True, True, False, True, True]),
220 | )
221 |
222 |
223 | @pytest.fixture
224 | def geographies_contains_with_labels(
225 | geographies_covers_contains,
226 | ) -> tuple[npt.NDArray[Any], npt.NDArray[np.bool_]]:
227 | return (
228 | geographies_covers_contains,
229 | np.array([False, False, True, False, False, True, False, True, True]),
230 | )
231 |
232 |
233 | @pytest.mark.skip(
234 | reason="Testing whether a polygon contains a points on its boundary \
235 | currently returns true, although it should be false"
236 | )
237 | def test_contains_edge_cases(parent_poly, geographies_contains_with_labels) -> None:
238 | polys_to_check, expected_labels = geographies_contains_with_labels
239 |
240 | actual = spherely.contains(parent_poly, polys_to_check)
241 | np.testing.assert_array_equal(actual, expected_labels)
242 |
243 |
244 | def test_covers(parent_poly, geographies_covers_with_labels) -> None:
245 | polys_to_check, expected_labels = geographies_covers_with_labels
246 |
247 | actual = spherely.covers(parent_poly, polys_to_check)
248 | np.testing.assert_array_equal(actual, expected_labels)
249 |
250 |
251 | def test_covered_by(parent_poly, geographies_covers_with_labels) -> None:
252 | polys_to_check, expected_labels = geographies_covers_with_labels
253 |
254 | actual = spherely.covered_by(polys_to_check, parent_poly)
255 | np.testing.assert_array_equal(actual, expected_labels)
256 |
--------------------------------------------------------------------------------