diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index c792242..c0d58c5 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -15,8 +15,15 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" + - name: Cache pre-commit + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit- - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 57f6308..88a9802 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -28,6 +28,20 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Get pip cache dir + id: pip-cache + shell: bash + run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + - name: Add requirements run: python -m pip install --upgrade wheel setuptools diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3db26ec..0e0fcf2 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -44,40 +44,42 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-14] steps: - uses: actions/checkout@v4 with: submodules: true - - name: Set up QEMU - if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v2 + - name: Cache cibuildwheel + uses: actions/cache@v4 with: - platforms: all + path: | + ~/Library/Caches/pip + ~/.cache/pip + ~/AppData/Local/pip/Cache + key: cibw-pip-${{ matrix.os }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + cibw-pip-${{ matrix.os }}- - uses: pypa/cibuildwheel@v2.22 env: - # CIBW_ARCHS: auto64 - CIBW_ARCHS_LINUX: x86_64 aarch64 - CIBW_ARCHS_WINDOWS: AMD64 # ARM64 - CIBW_ARCHS_MACOS: x86_64 arm64 - CIBW_BEFORE_BUILD: pip install numpy fire --prefer-binary - # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip - CIBW_SKIP: pp* *i686 *musllinux* + CIBW_ARCHS_MACOS: universal2 + CIBW_ARCHS_WINDOWS: AMD64 ARM64 + CIBW_SKIP: "pp* *musllinux* *_i686" CIBW_TEST_SKIP: "*macosx* *win* *aarch64" + CIBW_BEFORE_BUILD: "pip install --prefer-binary numpy fire pytest" - name: Verify clean directory run: git diff --exit-code shell: bash - - name: Upload wheels - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4 with: - name: cibw-wheels-${{ matrix.os }} + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl + upload_all: name: Upload if release needs: [build_wheels, build_sdist] diff --git a/.gitmodules b/.gitmodules index 21f5cc7..ae16b31 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "pygeobuf"] path = pygeobuf url = https://github.com/pygeobuf/pygeobuf.git -[submodule "headers"] - path = headers - url = https://github.com/cubao/headers.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88b5602..95aea89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: # Changes tabs to spaces - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.13 + rev: v1.5.6 hooks: - id: remove-tabs exclude: ^(docs|Makefile|benchmarks/Makefile) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f01f0d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,68 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +C++ port of [mapbox/geobuf](https://github.com/mapbox/geobuf) with Python bindings via pybind11. Geobuf is a compact binary encoding for GeoJSON using Protocol Buffers. + +## Build Commands + +```bash +# Initialize submodules (required first time) +git submodule update --init --recursive + +# Build Python extension (editable install) +make build + +# Run all C++ tests +make test_all + +# Run Python tests +make pytest +# Or directly: pytest tests/test_basic.py + +# Lint code +make lint + +# Roundtrip tests (compare C++ vs JS implementations) +make roundtrip_test_cpp +make roundtrip_test_js + +# CLI tests +make cli_test +``` + +## Architecture + +### Core C++ Library (`src/geobuf/`) + +- **geobuf.hpp/cpp**: Main `Encoder` and `Decoder` classes for GeoJSON ↔ Geobuf (protobuf) conversion +- **geobuf_index.hpp**: `GeobufIndex` class for spatial indexing and random access to features in large Geobuf files using memory-mapped I/O and packed R-tree +- **planet.hpp**: `Planet` class wrapping feature collections with spatial query support via `PackedRTree` +- **geojson_helpers.hpp**: JSON normalization utilities (rounding, sorting keys, denoising) +- **rapidjson_helpers.hpp**: RapidJSON wrapper utilities + +### Python Bindings (`src/`) + +- **main.cpp**: pybind11 module definition exposing `Encoder`, `Decoder`, `GeobufIndex`, `Planet`, `PackedRTree` +- **pybind11_geojson.cpp**: Bindings for mapbox::geojson types (Point, LineString, Polygon, Feature, FeatureCollection) +- **pybind11_rapidjson.cpp**: Bindings for RapidJSON value types + +### Key Dependencies (all header-only, in submodules) + +- `rapidjson`: JSON parsing/serialization +- `geojson-cpp` (forked): GeoJSON representation with Z-coordinate and custom_properties support +- `protozero`: Protocol Buffer encoding/decoding +- `geometry.hpp` (forked): Geometry types + +### Python Package (`src/pybind11_geobuf/`) + +- CLI via `python -m pybind11_geobuf` with commands: `json2geobuf`, `geobuf2json`, `pbf_decode`, `normalize_json`, `round_trip`, `is_subset_of` + +## Key Classes + +- `mapbox::geobuf::Encoder`: Encodes GeoJSON to compact Geobuf format with configurable precision +- `mapbox::geobuf::Decoder`: Decodes Geobuf back to GeoJSON, supports partial decoding (header, individual features) +- `cubao::GeobufIndex`: Enables random access to features in large Geobuf files without loading entire file +- `cubao::Planet`: Feature collection with built-in spatial index for bounding box queries diff --git a/CMakeLists.txt b/CMakeLists.txt index 9de33a7..25b1bdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-pragma-once-outside-header") endif() +if(MSVC) + add_compile_options(/bigobj) +endif() + add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-DPROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}") add_definitions(-DPROJECT_BINARY_DIR="${PROJECT_BINARY_DIR}") @@ -34,7 +38,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 17) -include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/headers/include) +execute_process( + COMMAND "${Python_EXECUTABLE}" -c "import cubao_headers; print(cubao_headers.get_include())" + OUTPUT_VARIABLE CUBAO_HEADERS_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) +include_directories(SYSTEM ${CUBAO_HEADERS_INCLUDE_DIR}) include_directories(${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/geobuf) option(BUILD_SHARED_LIBS "Build shared library." OFF) @@ -44,7 +52,7 @@ file(GLOB_RECURSE SOURCES src/**/*.cpp) add_library(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_link_libraries(${PROJECT_NAME} ${CONAN_LIBS}) set_target_properties(${PROJECT_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden") -install(TARGETS ${PROJECT_NAME} DESTINATION ${PROJECT_NAME}) +# Don't install static library to wheel - delocate-wheel can't handle .a files add_executable(pbf_decoder src/geobuf/pbf_decoder.cpp) target_compile_definitions(pbf_decoder PUBLIC -DPBF_DECODER_ENABLE_MAIN) diff --git a/MANIFEST.in b/MANIFEST.in index 093cce6..8348bb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,5 @@ include README.md LICENSE pybind11/LICENSE include CMakeLists.txt -graft headers/include -graft pybind11/include -graft pybind11/tools graft src graft examples graft tests diff --git a/Makefile b/Makefile index 92705cf..2a31df1 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,9 @@ cli_test: cli_test1 cli_test2 cli_test3 cli_test4 restub: pybind11-stubgen pybind11_geobuf._core -o stubs cp -rf stubs/pybind11_geobuf/_core src/pybind11_geobuf + # Fix duplicate parameter names generated by pybind11-stubgen + sed -i '' 's/new_value: \.\.\., std: \.\.\., std: \.\.\., mapbox: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., std: \.\.\., mapbox: \.\.\./new_value: .../g' src/pybind11_geobuf/_core/geojson.pyi + pre-commit run --files src/pybind11_geobuf/_core/*.pyi || pre-commit run --files src/pybind11_geobuf/_core/*.pyi test_all: @cd build && for t in $(wildcard $(BUILD_DIR)/test_*); do echo $$t && eval $$t >/dev/null 2>&1 && echo 'ok' || echo $(RED)Not Ok$(NC); done diff --git a/headers b/headers deleted file mode 160000 index 0a21abb..0000000 --- a/headers +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a21abb45cd8269b08363d638903dd4ff1de048b diff --git a/pyproject.toml b/pyproject.toml index 83554da..5f53b98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,21 @@ [build-system] -requires = ["scikit-build-core>=0.3.3", "pybind11"] +requires = ["scikit-build-core>=0.3.3", "pybind11", "cubao-headers>=0.1.0"] build-backend = "scikit_build_core.build" [project] name = "pybind11_geobuf" -version = "0.2.3" +version = "0.2.4" description="c++ geobuf with python binding" readme = "README.md" authors = [ { name = "district10", email = "dvorak4tzx@gmail.com" }, ] +dependencies = [ + "fire", + "loguru", + "numpy", +] requires-python = ">=3.7" classifiers = [ "Development Status :: 4 - Beta", @@ -22,6 +27,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] @@ -30,7 +37,7 @@ Homepage = "https://geobuf-cpp.readthedocs.io" [project.optional-dependencies] -test = ["pytest", "scipy"] +test = ["pytest"] [tool.scikit-build] diff --git a/src/geobuf/geojson_transform.hpp b/src/geobuf/geojson_transform.hpp new file mode 100644 index 0000000..80e7a9f --- /dev/null +++ b/src/geobuf/geojson_transform.hpp @@ -0,0 +1,268 @@ +#pragma once + +// https://github.com/microsoft/vscode-cpptools/issues/9692 +#if __INTELLISENSE__ +#undef __ARM_NEON +#undef __ARM_NEON__ +#endif + +#include "geojson_helpers.hpp" +#include +#include + +namespace cubao +{ + +// Matrix transform function type +using MatrixTransformFn = std::function)>; + +// Forward declarations +inline void transform_coords(std::vector &coords, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::point &pt, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::multi_point &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::line_string &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::linear_ring &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::multi_line_string &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::polygon &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::multi_polygon &geom, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::geometry_collection &gc, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::geometry &g, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::feature &f, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::feature_collection &fc, + const MatrixTransformFn &fn); +inline void transform_coords(mapbox::geojson::geojson &geojson, + const MatrixTransformFn &fn); + +// Implementation for vector of points +inline void transform_coords(std::vector &coords, + const MatrixTransformFn &fn) +{ + if (coords.empty()) { + return; + } + auto matrix = as_row_vectors(coords); + fn(matrix); +} + +// Implementation for single point +inline void transform_coords(mapbox::geojson::point &pt, + const MatrixTransformFn &fn) +{ + auto matrix = as_row_vectors(pt); + fn(matrix); +} + +// Implementation for multi_point (inherits from vector) +inline void transform_coords(mapbox::geojson::multi_point &geom, + const MatrixTransformFn &fn) +{ + transform_coords(static_cast &>(geom), + fn); +} + +// Implementation for line_string (inherits from vector) +inline void transform_coords(mapbox::geojson::line_string &geom, + const MatrixTransformFn &fn) +{ + transform_coords(static_cast &>(geom), + fn); +} + +// Implementation for linear_ring (inherits from vector) +inline void transform_coords(mapbox::geojson::linear_ring &geom, + const MatrixTransformFn &fn) +{ + transform_coords(static_cast &>(geom), + fn); +} + +// Implementation for multi_line_string +inline void transform_coords(mapbox::geojson::multi_line_string &geom, + const MatrixTransformFn &fn) +{ + for (auto &ls : geom) { + transform_coords(ls, fn); + } +} + +// Implementation for polygon +inline void transform_coords(mapbox::geojson::polygon &geom, + const MatrixTransformFn &fn) +{ + for (auto &ring : geom) { + transform_coords(ring, fn); + } +} + +// Implementation for multi_polygon +inline void transform_coords(mapbox::geojson::multi_polygon &geom, + const MatrixTransformFn &fn) +{ + for (auto &poly : geom) { + for (auto &ring : poly) { + transform_coords(ring, fn); + } + } +} + +// Implementation for geometry_collection +inline void transform_coords(mapbox::geojson::geometry_collection &gc, + const MatrixTransformFn &fn) +{ + for (auto &g : gc) { + transform_coords(g, fn); + } +} + +// Implementation for geometry (recursive traversal using match) +inline void transform_coords(mapbox::geojson::geometry &g, + const MatrixTransformFn &fn) +{ + g.match([&](mapbox::geojson::point &pt) { transform_coords(pt, fn); }, + [&](mapbox::geojson::multi_point &mp) { transform_coords(mp, fn); }, + [&](mapbox::geojson::line_string &ls) { transform_coords(ls, fn); }, + [&](mapbox::geojson::linear_ring &lr) { transform_coords(lr, fn); }, + [&](mapbox::geojson::multi_line_string &mls) { + transform_coords(mls, fn); + }, + [&](mapbox::geojson::polygon &poly) { transform_coords(poly, fn); }, + [&](mapbox::geojson::multi_polygon &mpoly) { + transform_coords(mpoly, fn); + }, + [&](mapbox::geojson::geometry_collection &gc) { + transform_coords(gc, fn); + }, + [](auto &) {}); +} + +// Implementation for feature +inline void transform_coords(mapbox::geojson::feature &f, + const MatrixTransformFn &fn) +{ + transform_coords(f.geometry, fn); +} + +// Implementation for feature collection +inline void transform_coords(mapbox::geojson::feature_collection &fc, + const MatrixTransformFn &fn) +{ + for (auto &f : fc) { + transform_coords(f, fn); + } +} + +// Implementation for geojson variant +inline void transform_coords(mapbox::geojson::geojson &geojson, + const MatrixTransformFn &fn) +{ + geojson.match( + [&](mapbox::geojson::geometry &g) { transform_coords(g, fn); }, + [&](mapbox::geojson::feature &f) { transform_coords(f, fn); }, + [&](mapbox::geojson::feature_collection &fc) { + transform_coords(fc, fn); + }, + [](auto &) {}); +} + +// Preset transform function objects + +// WGS84 to ENU transform +struct Wgs84ToEnu +{ + Eigen::Vector3d anchor_lla; + bool cheap_ruler = true; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + coords = lla2enu(coords, anchor_lla, cheap_ruler); + } +}; + +// ENU to WGS84 transform +struct EnuToWgs84 +{ + Eigen::Vector3d anchor_lla; + bool cheap_ruler = true; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + coords = enu2lla(coords, anchor_lla, cheap_ruler); + } +}; + +// Affine transform (4x4 matrix) +struct AffineTransform +{ + Eigen::Matrix4d T; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + apply_transform_inplace(T, coords); + } +}; + +// 3D rotation transform +struct Rotation3D +{ + Eigen::Matrix3d R; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + coords = (R * coords.transpose()).transpose(); + } +}; + +// 3D translation transform +struct Translation3D +{ + Eigen::Vector3d offset; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + coords.rowwise() += offset.transpose(); + } +}; + +// 3D scale transform +struct Scale3D +{ + Eigen::Vector3d scale; + + void operator()(Eigen::Ref coords) const + { + if (coords.rows() == 0) { + return; + } + coords.col(0) *= scale[0]; + coords.col(1) *= scale[1]; + coords.col(2) *= scale[2]; + } +}; + +} // namespace cubao diff --git a/src/pybind11_geobuf/_core/__init__.pyi b/src/pybind11_geobuf/_core/__init__.pyi index 45e2943..20bd49d 100644 --- a/src/pybind11_geobuf/_core/__init__.pyi +++ b/src/pybind11_geobuf/_core/__init__.pyi @@ -1,11 +1,12 @@ from __future__ import annotations +import collections.abc import numpy -import pybind11_stubgen.typing_ext +import numpy.typing import typing from . import geojson from . import tf -__all__ = [ +__all__: list[str] = [ "Decoder", "Encoder", "GeobufIndex", @@ -65,7 +66,7 @@ class Decoder: """ def decode_feature( self, bytes: str, only_geometry: bool = False, only_properties: bool = False - ) -> geojson.Feature | None: + ) -> pybind11_geobuf._core.geojson.Feature | None: """ Decode Protocol Buffer (PBF) feature. @@ -151,9 +152,9 @@ class Encoder: def __init__( self, *, - max_precision: int = 1000000, + max_precision: typing.SupportsInt = 1000000, only_xy: bool = False, - round_z: int | None = None, + round_z: typing.SupportsInt | None = None, ) -> None: """ Initialize an Encoder object. @@ -293,8 +294,12 @@ class GeobufIndex: """ @typing.overload def decode_feature( - self, index: int, *, only_geometry: bool = False, only_properties: bool = False - ) -> geojson.Feature | None: + self, + index: typing.SupportsInt, + *, + only_geometry: bool = False, + only_properties: bool = False, + ) -> pybind11_geobuf._core.geojson.Feature | None: """ Decode a feature from the Geobuf file. @@ -309,7 +314,7 @@ class GeobufIndex: @typing.overload def decode_feature( self, bytes: str, *, only_geometry: bool = False, only_properties: bool = False - ) -> geojson.Feature | None: + ) -> pybind11_geobuf._core.geojson.Feature | None: """ Decode a feature from bytes. @@ -323,7 +328,7 @@ class GeobufIndex: """ def decode_feature_of_id( self, id: str, *, only_geometry: bool = False, only_properties: bool = False - ) -> geojson.Feature | None: + ) -> pybind11_geobuf._core.geojson.Feature | None: """ Decode a feature by its ID. @@ -337,7 +342,7 @@ class GeobufIndex: """ def decode_features( self, - index: list[int], + index: collections.abc.Sequence[typing.SupportsInt], *, only_geometry: bool = False, only_properties: bool = False, @@ -382,7 +387,9 @@ class GeobufIndex: Returns: None """ - def mmap_bytes(self, offset: int, length: int) -> bytes | None: + def mmap_bytes( + self, offset: typing.SupportsInt, length: typing.SupportsInt + ) -> bytes | None: """ Read bytes from the memory-mapped file. @@ -418,8 +425,8 @@ class GeobufIndex: """ def query( self, - arg0: numpy.ndarray[numpy.float64[2, 1]], - arg1: numpy.ndarray[numpy.float64[2, 1]], + arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[2, 1]"], + arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[2, 1]"], ) -> set[int]: """ Query features within a bounding box. @@ -490,7 +497,9 @@ class NodeItem: """ Check if this node's bounding box intersects with another node's bounding box """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[4, 1]]: + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[4, 1]"]: """ Convert the node's bounding box to a numpy array [minX, minY, maxX, maxY] """ @@ -532,7 +541,11 @@ class NodeItem: class PackedRTree: def search( - self, min_x: float, min_y: float, max_x: float, max_y: float + self, + min_x: typing.SupportsFloat, + min_y: typing.SupportsFloat, + max_x: typing.SupportsFloat, + max_y: typing.SupportsFloat, ) -> list[int]: """ Search for items within the given bounding box. @@ -547,7 +560,9 @@ class PackedRTree: list: List of offsets of items within the bounding box. """ @property - def extent(self) -> numpy.ndarray[numpy.float64[4, 1]]: ... + def extent( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[4, 1]"]: ... @property def node_size(self) -> int: ... @property @@ -582,7 +597,9 @@ class Planet: Returns: None """ - def copy(self, arg0: numpy.ndarray[numpy.int32[m, 1]]) -> geojson.FeatureCollection: + def copy( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.int32, "[m, 1]"] + ) -> geojson.FeatureCollection: """ Create a deep copy of the Planet object. @@ -591,7 +608,9 @@ class Planet: """ def crop( self, - polygon: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + polygon: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 2]", "flags.c_contiguous" + ], *, clipping_mode: str = "longest", strip_properties: bool = False, @@ -634,9 +653,9 @@ class Planet: """ def query( self, - min: numpy.ndarray[numpy.float64[2, 1]], - max: numpy.ndarray[numpy.float64[2, 1]], - ) -> numpy.ndarray[numpy.int32[m, 1]]: + min: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[2, 1]"], + max: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[2, 1]"], + ) -> typing.Annotated[numpy.typing.NDArray[numpy.int32], "[m, 1]"]: """ Query features within the given bounding box. @@ -682,11 +701,11 @@ class rapidjson: def __getstate__(self) -> int: ... def __hash__(self) -> int: ... def __index__(self) -> int: ... - def __init__(self, value: int) -> None: ... + def __init__(self, value: typing.SupportsInt) -> None: ... def __int__(self) -> int: ... def __ne__(self, other: typing.Any) -> bool: ... def __repr__(self) -> str: ... - def __setstate__(self, state: int) -> None: ... + def __setstate__(self, state: typing.SupportsInt) -> None: ... def __str__(self) -> str: ... @property def name(self) -> str: ... @@ -825,19 +844,19 @@ class rapidjson: """ Set the value to an empty array """ - def SetDouble(self, arg0: float) -> rapidjson: + def SetDouble(self, arg0: typing.SupportsFloat) -> rapidjson: """ Set the value to a double """ - def SetFloat(self, arg0: float) -> rapidjson: + def SetFloat(self, arg0: typing.SupportsFloat) -> rapidjson: """ Set the value to a float """ - def SetInt(self, arg0: int) -> rapidjson: + def SetInt(self, arg0: typing.SupportsInt) -> rapidjson: """ Set the value to an integer """ - def SetInt64(self, arg0: int) -> rapidjson: + def SetInt64(self, arg0: typing.SupportsInt) -> rapidjson: """ Set the value to a 64-bit integer """ @@ -849,11 +868,11 @@ class rapidjson: """ Set the value to an empty object """ - def SetUint(self, arg0: int) -> rapidjson: + def SetUint(self, arg0: typing.SupportsInt) -> rapidjson: """ Set the value to an unsigned integer """ - def SetUint64(self, arg0: int) -> rapidjson: + def SetUint64(self, arg0: typing.SupportsInt) -> rapidjson: """ Set the value to a 64-bit unsigned integer """ @@ -887,7 +906,7 @@ class rapidjson: Delete a member by key """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete an array element by index """ @@ -901,7 +920,7 @@ class rapidjson: Get a member value by key """ @typing.overload - def __getitem__(self, arg0: int) -> rapidjson: + def __getitem__(self, arg0: typing.SupportsInt) -> rapidjson: """ Get an array element by index """ @@ -925,7 +944,7 @@ class rapidjson: Compare two RapidJSON values for inequality """ @typing.overload - def __setitem__(self, index: int, value: typing.Any) -> typing.Any: + def __setitem__(self, index: typing.SupportsInt, value: typing.Any) -> typing.Any: """ Set array element by index """ @@ -991,9 +1010,9 @@ class rapidjson: *, sort_keys: bool = True, strip_geometry_z_0: bool = True, - round_geojson_non_geometry: int | None = 3, + round_geojson_non_geometry: typing.SupportsInt | None = 3, round_geojson_geometry: typing.Annotated[ - list[int], pybind11_stubgen.typing_ext.FixedSize(3) + collections.abc.Sequence[typing.SupportsInt], "FixedSize(3)" ] | None = [8, 8, 3], denoise_double_0: bool = True, @@ -1010,7 +1029,11 @@ class rapidjson: Append value to array """ def round( - self, *, precision: float = 3, depth: int = 32, skip_keys: list[str] = [] + self, + *, + precision: typing.SupportsFloat = 3, + depth: typing.SupportsInt = 32, + skip_keys: collections.abc.Sequence[str] = [], ) -> rapidjson: """ Round numeric values in the JSON @@ -1019,13 +1042,15 @@ class rapidjson: self, *, precision: typing.Annotated[ - list[int], pybind11_stubgen.typing_ext.FixedSize(3) + collections.abc.Sequence[typing.SupportsInt], "FixedSize(3)" ] = [8, 8, 3], ) -> rapidjson: """ Round geometry coordinates in GeoJSON """ - def round_geojson_non_geometry(self, *, precision: int = 3) -> rapidjson: + def round_geojson_non_geometry( + self, *, precision: typing.SupportsInt = 3 + ) -> rapidjson: """ Round non-geometry numeric values in GeoJSON """ @@ -1073,10 +1098,10 @@ def normalize_json( sort_keys: bool = True, denoise_double_0: bool = True, strip_geometry_z_0: bool = True, - round_non_geojson: int | None = 3, - round_geojson_non_geometry: int | None = 3, + round_non_geojson: typing.SupportsInt | None = 3, + round_geojson_non_geometry: typing.SupportsInt | None = 3, round_geojson_geometry: typing.Annotated[ - list[int], pybind11_stubgen.typing_ext.FixedSize(3) + collections.abc.Sequence[typing.SupportsInt], "FixedSize(3)" ] | None = [8, 8, 3], ) -> bool: @@ -1105,10 +1130,10 @@ def normalize_json( sort_keys: bool = True, denoise_double_0: bool = True, strip_geometry_z_0: bool = True, - round_non_geojson: int | None = 3, - round_geojson_non_geometry: int | None = 3, + round_non_geojson: typing.SupportsInt | None = 3, + round_geojson_non_geometry: typing.SupportsInt | None = 3, round_geojson_geometry: typing.Annotated[ - list[int], pybind11_stubgen.typing_ext.FixedSize(3) + collections.abc.Sequence[typing.SupportsInt], "FixedSize(3)" ] | None = [8, 8, 3], ) -> rapidjson: @@ -1170,4 +1195,4 @@ def str2json2str( Optional[str]: Converted JSON string, or None if input is invalid. """ -__version__: str = "0.2.2" +__version__: str = "0.2.4" diff --git a/src/pybind11_geobuf/_core/geojson.pyi b/src/pybind11_geobuf/_core/geojson.pyi index 8c8e58e..a813db5 100644 --- a/src/pybind11_geobuf/_core/geojson.pyi +++ b/src/pybind11_geobuf/_core/geojson.pyi @@ -1,9 +1,11 @@ from __future__ import annotations +import collections.abc import numpy +import numpy.typing import pybind11_geobuf._core import typing -__all__ = [ +__all__: list[str] = [ "Feature", "FeatureCollection", "FeatureList", @@ -80,17 +82,26 @@ class Feature: """ Set a custom property value by key """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> Feature: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the feature geometry """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the feature """ @@ -122,7 +133,7 @@ class Feature: *, indent: bool = False, sort_keys: bool = False, - precision: int = 8, + precision: typing.SupportsInt = 8, only_xy: bool = False, ) -> bool: """ @@ -197,11 +208,11 @@ class Feature: """ Set the feature ID """ - def items(self) -> typing.Iterator[tuple[str, value]]: + def items(self) -> collections.abc.Iterator[tuple[str, value]]: """ Get an iterator over custom property items """ - def keys(self) -> typing.Iterator[str]: + def keys(self) -> collections.abc.Iterator[str]: """ Get an iterator over custom property keys """ @@ -234,17 +245,50 @@ class Feature: """ Set a property value by key """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> Feature: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> Feature: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> Feature: """ Round the coordinates of the feature geometry """ + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Feature: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Feature: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ def to_geobuf( - self, *, precision: int = 8, only_xy: bool = False, round_z: int | None = None + self, + *, + precision: typing.SupportsInt = 8, + only_xy: bool = False, + round_z: typing.SupportsInt | None = None, ) -> bytes: """ Convert the feature to Geobuf bytes """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the feature geometry to a numpy array """ @@ -252,6 +296,25 @@ class Feature: """ Convert the feature to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Feature: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> Feature: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Feature: + """ + Translate all coordinates by offset vector + """ class FeatureCollection(FeatureList): def __call__(self) -> typing.Any: @@ -272,7 +335,7 @@ class FeatureCollection(FeatureList): Delete a custom property """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -287,7 +350,7 @@ class FeatureCollection(FeatureList): Get a custom property by key """ @typing.overload - def __getitem__(self, arg0: int) -> Feature: + def __getitem__(self, arg0: typing.SupportsInt) -> Feature: """ Get a feature from the collection by index """ @@ -307,7 +370,7 @@ class FeatureCollection(FeatureList): Initialize a FeatureCollection from another FeatureCollection """ @typing.overload - def __init__(self, N: int) -> None: + def __init__(self, N: typing.SupportsInt) -> None: """ Initialize a FeatureCollection with N empty features """ @@ -317,7 +380,7 @@ class FeatureCollection(FeatureList): Set a custom property """ @typing.overload - def __setitem__(self, arg0: int, arg1: Feature) -> None: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Feature) -> None: """ Set a feature in the collection at the specified index """ @@ -326,6 +389,12 @@ class FeatureCollection(FeatureList): """ Assign list elements using a slice object """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> FeatureCollection: + """ + Apply 4x4 affine transformation matrix + """ def clone(self) -> FeatureCollection: """ Create a clone of the object @@ -350,7 +419,7 @@ class FeatureCollection(FeatureList): *, indent: bool = False, sort_keys: bool = False, - precision: int = 8, + precision: typing.SupportsInt = 8, only_xy: bool = False, ) -> bool: """ @@ -366,11 +435,11 @@ class FeatureCollection(FeatureList): """ Load the FeatureCollection from a RapidJSON value """ - def items(self) -> typing.Iterator[tuple[str, value]]: + def items(self) -> collections.abc.Iterator[tuple[str, value]]: """ Return an iterator over the items of custom properties """ - def keys(self) -> typing.Iterator[str]: + def keys(self) -> collections.abc.Iterator[str]: """ Return an iterator over the keys of custom properties """ @@ -378,16 +447,47 @@ class FeatureCollection(FeatureList): """ Load the FeatureCollection from a file (GeoJSON or Geobuf) """ - def resize(self, arg0: int) -> FeatureCollection: + def resize(self, arg0: typing.SupportsInt) -> FeatureCollection: """ Resize the FeatureCollection to contain N features """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> FeatureCollection: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> FeatureCollection: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> FeatureCollection: """ Round the coordinates of all features in the collection """ + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> FeatureCollection: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> FeatureCollection: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ def to_geobuf( - self, *, precision: int = 8, only_xy: bool = False, round_z: int | None = None + self, + *, + precision: typing.SupportsInt = 8, + only_xy: bool = False, + round_z: typing.SupportsInt | None = None, ) -> bytes: """ Convert the FeatureCollection to Geobuf bytes @@ -396,7 +496,26 @@ class FeatureCollection(FeatureList): """ Convert the FeatureCollection to a RapidJSON value """ - def values(self) -> typing.Iterator[value]: + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> FeatureCollection: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> FeatureCollection: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> FeatureCollection: + """ + Translate all coordinates by offset vector + """ + def values(self) -> collections.abc.Iterator[value]: """ Return an iterator over the values of custom properties """ @@ -413,7 +532,7 @@ class FeatureList: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -429,7 +548,7 @@ class FeatureList: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> Feature: ... + def __getitem__(self, arg0: typing.SupportsInt) -> Feature: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -438,12 +557,12 @@ class FeatureList: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[Feature]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[Feature]: ... def __len__(self) -> int: ... def __ne__(self, arg0: FeatureList) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: Feature) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: Feature) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: FeatureList) -> None: """ @@ -467,11 +586,11 @@ class FeatureList: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ - def insert(self, i: int, x: Feature) -> None: + def insert(self, i: typing.SupportsInt, x: Feature) -> None: """ Insert an item at a given position. """ @@ -481,7 +600,7 @@ class FeatureList: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> Feature: + def pop(self, i: typing.SupportsInt) -> Feature: """ Remove and return the item at index ``i`` """ @@ -532,6 +651,12 @@ class GeoJSON: """ Check if two GeoJSON objects are not equal """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> GeoJSON: + """ + Apply 4x4 affine transformation matrix + """ def as_feature(self) -> ...: """ Get this GeoJSON object as a feature (if it is one) @@ -550,10 +675,10 @@ class GeoJSON: """ def crop( self, - polygon: numpy.ndarray[numpy.float64[m, 3]], + polygon: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 3]"], *, clipping_mode: str = "longest", - max_z_offset: float | None = None, + max_z_offset: typing.SupportsFloat | None = None, ) -> ...: """ Crop the GeoJSON object using a polygon @@ -568,7 +693,7 @@ class GeoJSON: *, indent: bool = False, sort_keys: bool = False, - precision: int = 8, + precision: typing.SupportsInt = 8, only_xy: bool = False, ) -> bool: """ @@ -598,12 +723,43 @@ class GeoJSON: """ Load a GeoJSON object from a file """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> GeoJSON: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> GeoJSON: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> GeoJSON: """ Round coordinates to specified decimal places """ + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> GeoJSON: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> GeoJSON: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ def to_geobuf( - self, *, precision: int = 8, only_xy: bool = False, round_z: int | None = None + self, + *, + precision: typing.SupportsInt = 8, + only_xy: bool = False, + round_z: typing.SupportsInt | None = None, ) -> bytes: """ Encode the GeoJSON object to a Geobuf byte string @@ -612,6 +768,25 @@ class GeoJSON: """ Convert the GeoJSON object to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> GeoJSON: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> GeoJSON: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> GeoJSON: + """ + Translate all coordinates by offset vector + """ class Geometry(GeometryBase): __hash__: typing.ClassVar[None] = None @@ -700,7 +875,7 @@ class Geometry(GeometryBase): """ Initialize from a Python dictionary """ - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> collections.abc.Iterator[str]: """ Get an iterator over the custom property keys """ @@ -720,6 +895,12 @@ class Geometry(GeometryBase): """ Pickle support for Geometry objects """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> Geometry: + """ + Apply 4x4 affine transformation matrix + """ def as_geometry_collection(self) -> ...: """ Get this geometry as a geometry_collection (if it is one) @@ -742,10 +923,11 @@ class Geometry(GeometryBase): """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the geometry coordinates @@ -758,7 +940,9 @@ class Geometry(GeometryBase): """ Get this geometry as a polygon (if it is one) """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Get the bounding box of the geometry """ @@ -790,7 +974,7 @@ class Geometry(GeometryBase): *, indent: bool = False, sort_keys: bool = False, - precision: int = 8, + precision: typing.SupportsInt = 8, only_xy: bool = False, ) -> bool: """ @@ -801,7 +985,10 @@ class Geometry(GeometryBase): Decode a Geobuf byte string into a geometry """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> Geometry: """ Set geometry coordinates from a numpy array @@ -846,11 +1033,11 @@ class Geometry(GeometryBase): """ Check if this geometry is of type polygon """ - def items(self) -> typing.Iterator[tuple[str, ...]]: + def items(self) -> collections.abc.Iterator[tuple[str, ...]]: """ Get an iterator over the custom property items """ - def keys(self) -> typing.Iterator[str]: + def keys(self) -> collections.abc.Iterator[str]: """ Get an iterator over the custom property keys """ @@ -868,13 +1055,18 @@ class Geometry(GeometryBase): Add a point to the geometry """ @typing.overload - def push_back(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> Geometry: + def push_back( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> Geometry: """ Add a point to the geometry """ @typing.overload def push_back( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> Geometry: """ Add multiple points to the geometry @@ -894,21 +1086,54 @@ class Geometry(GeometryBase): """ Add a line string to a multi-line string geometry """ - def resize(self, arg0: int) -> Geometry: + def resize(self, arg0: typing.SupportsInt) -> Geometry: """ Resize the geometry """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> Geometry: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> Geometry: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> Geometry: """ Round coordinates to specified decimal places """ + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Geometry: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Geometry: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ def to_geobuf( - self, *, precision: int = 8, only_xy: bool = False, round_z: int | None = None + self, + *, + precision: typing.SupportsInt = 8, + only_xy: bool = False, + round_z: typing.SupportsInt | None = None, ) -> bytes: """ Encode the geometry to a Geobuf byte string """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert geometry coordinates to a numpy array """ @@ -916,11 +1141,30 @@ class Geometry(GeometryBase): """ Convert the geometry to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Geometry: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> Geometry: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Geometry: + """ + Translate all coordinates by offset vector + """ def type(self) -> str: """ Get the type of the geometry """ - def values(self) -> typing.Iterator[...]: + def values(self) -> collections.abc.Iterator[...]: """ Get an iterator over the custom property values """ @@ -952,7 +1196,7 @@ class GeometryCollection(GeometryList): Copy constructor for GeometryCollection """ @typing.overload - def __init__(self, N: int) -> None: + def __init__(self, N: typing.SupportsInt) -> None: """ Construct a GeometryCollection with N empty geometries """ @@ -961,42 +1205,56 @@ class GeometryCollection(GeometryList): Check if two GeometryCollections are not equal """ @typing.overload - def __setitem__(self, arg0: int, arg1: Geometry) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: Geometry + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: Point) -> GeometryCollection: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Point) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: MultiPoint) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: MultiPoint + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: LineString) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: LineString + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: MultiLineString) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: MultiLineString + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: Polygon) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: Polygon + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: MultiPolygon) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: MultiPolygon + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @typing.overload - def __setitem__(self, arg0: int, arg1: GeometryCollection) -> GeometryCollection: + def __setitem__( + self, arg0: typing.SupportsInt, arg1: GeometryCollection + ) -> GeometryCollection: """ Set a geometry in the GeometryCollection by index """ @@ -1004,6 +1262,12 @@ class GeometryCollection(GeometryList): """ Pickle support for GeometryCollection """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> GeometryCollection: + """ + Apply 4x4 affine transformation matrix + """ def clear(self) -> GeometryCollection: """ Clear all geometries from the GeometryCollection @@ -1062,18 +1326,64 @@ class GeometryCollection(GeometryList): """ Add a new geometry to the GeometryCollection """ - def resize(self, arg0: int) -> GeometryCollection: + def resize(self, arg0: typing.SupportsInt) -> GeometryCollection: """ Resize the GeometryCollection to contain N geometries """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> GeometryCollection: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> GeometryCollection: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> GeometryCollection: """ Round the coordinates of all geometries in the GeometryCollection """ + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> GeometryCollection: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> GeometryCollection: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ def to_rapidjson(self) -> pybind11_geobuf._core.rapidjson: """ Convert the GeometryCollection to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> GeometryCollection: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> GeometryCollection: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> GeometryCollection: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -1091,7 +1401,7 @@ class GeometryList: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -1107,7 +1417,7 @@ class GeometryList: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> Geometry: ... + def __getitem__(self, arg0: typing.SupportsInt) -> Geometry: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -1116,12 +1426,12 @@ class GeometryList: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[Geometry]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[Geometry]: ... def __len__(self) -> int: ... def __ne__(self, arg0: GeometryList) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: Geometry) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: Geometry) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: GeometryList) -> None: """ @@ -1145,11 +1455,11 @@ class GeometryList: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ - def insert(self, i: int, x: Geometry) -> None: + def insert(self, i: typing.SupportsInt, x: Geometry) -> None: """ Insert an item at a given position. """ @@ -1159,7 +1469,7 @@ class GeometryList: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> Geometry: + def pop(self, i: typing.SupportsInt) -> Geometry: """ Remove and return the item at index ``i`` """ @@ -1186,7 +1496,7 @@ class LineString(coordinates): """ Check if two LineStrings are equal """ - def __getitem__(self, arg0: int) -> Point: + def __getitem__(self, arg0: typing.SupportsInt) -> Point: """ Get a point from the geometry by index """ @@ -1198,12 +1508,15 @@ class LineString(coordinates): """ @typing.overload def __init__( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> None: """ Initialize from a numpy array of points """ - def __iter__(self) -> typing.Iterator[Point]: + def __iter__(self) -> collections.abc.Iterator[Point]: """ Iterate over the points in the geometry """ @@ -1216,14 +1529,16 @@ class LineString(coordinates): Check if two LineStrings are not equal """ @typing.overload - def __setitem__(self, arg0: int, arg1: Point) -> Point: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Point) -> Point: """ Set a point in the geometry by index """ @typing.overload def __setitem__( - self, arg0: int, arg1: numpy.ndarray[numpy.float64[m, 1]] - ) -> numpy.ndarray[numpy.float64[m, 1]]: + self, + arg0: typing.SupportsInt, + arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"], + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Set a point in the geometry by index using a vector """ @@ -1231,17 +1546,26 @@ class LineString(coordinates): """ Pickle support for serialization """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> LineString: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the geometry points """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the geometry """ @@ -1264,7 +1588,10 @@ class LineString(coordinates): Remove duplicate consecutive points based on their XYZ coordinates """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> LineString: """ Set the geometry points from a numpy array @@ -1283,19 +1610,50 @@ class LineString(coordinates): Add a point to the end of the geometry """ @typing.overload - def push_back(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> LineString: + def push_back( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> LineString: """ Add a point to the end of the geometry using a vector """ - def resize(self, arg0: int) -> LineString: + def resize(self, arg0: typing.SupportsInt) -> LineString: """ Resize the geometry to the specified size """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> LineString: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> LineString: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> LineString: """ Round coordinates to specified decimal places """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> LineString: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> LineString: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the geometry points to a numpy array """ @@ -1303,6 +1661,25 @@ class LineString(coordinates): """ Convert to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> LineString: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> LineString: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> LineString: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -1324,7 +1701,7 @@ class LineStringList: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -1340,7 +1717,7 @@ class LineStringList: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> LineString: ... + def __getitem__(self, arg0: typing.SupportsInt) -> LineString: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -1349,12 +1726,12 @@ class LineStringList: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[LineString]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[LineString]: ... def __len__(self) -> int: ... def __ne__(self, arg0: LineStringList) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: LineString) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: LineString) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: LineStringList) -> None: """ @@ -1378,11 +1755,11 @@ class LineStringList: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ - def insert(self, i: int, x: LineString) -> None: + def insert(self, i: typing.SupportsInt, x: LineString) -> None: """ Insert an item at a given position. """ @@ -1392,7 +1769,7 @@ class LineStringList: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> LineString: + def pop(self, i: typing.SupportsInt) -> LineString: """ Remove and return the item at index ``i`` """ @@ -1419,7 +1796,7 @@ class LinearRing(coordinates): """ Check if two LinearRings are equal """ - def __getitem__(self, arg0: int) -> Point: + def __getitem__(self, arg0: typing.SupportsInt) -> Point: """ Get a point from the geometry by index """ @@ -1427,7 +1804,7 @@ class LinearRing(coordinates): """ Default constructor for LinearRing """ - def __iter__(self) -> typing.Iterator[Point]: + def __iter__(self) -> collections.abc.Iterator[Point]: """ Iterate over the points in the geometry """ @@ -1440,23 +1817,26 @@ class LinearRing(coordinates): Check if two LinearRings are not equal """ @typing.overload - def __setitem__(self, arg0: int, arg1: Point) -> Point: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Point) -> Point: """ Set a point in the geometry by index """ @typing.overload def __setitem__( - self, arg0: int, arg1: numpy.ndarray[numpy.float64[m, 1]] - ) -> numpy.ndarray[numpy.float64[m, 1]]: + self, + arg0: typing.SupportsInt, + arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"], + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Set a point in the geometry by index using a vector """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the geometry points @@ -1470,7 +1850,10 @@ class LinearRing(coordinates): Create a clone of the object """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> LinearRing: """ Set the geometry points from a numpy array @@ -1485,11 +1868,15 @@ class LinearRing(coordinates): Add a point to the end of the geometry """ @typing.overload - def push_back(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> LinearRing: + def push_back( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> LinearRing: """ Add a point to the end of the geometry using a vector """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the geometry points to a numpy array """ @@ -1509,7 +1896,7 @@ class LinearRingList: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -1525,7 +1912,7 @@ class LinearRingList: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> LinearRing: ... + def __getitem__(self, arg0: typing.SupportsInt) -> LinearRing: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -1534,12 +1921,12 @@ class LinearRingList: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[LinearRing]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[LinearRing]: ... def __len__(self) -> int: ... def __ne__(self, arg0: LinearRingList) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: LinearRing) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: LinearRing) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: LinearRingList) -> None: """ @@ -1563,11 +1950,11 @@ class LinearRingList: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ - def insert(self, i: int, x: LinearRing) -> None: + def insert(self, i: typing.SupportsInt, x: LinearRing) -> None: """ Insert an item at a given position. """ @@ -1577,7 +1964,7 @@ class LinearRingList: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> LinearRing: + def pop(self, i: typing.SupportsInt) -> LinearRing: """ Remove and return the item at index ``i`` """ @@ -1604,7 +1991,7 @@ class MultiLineString(LineStringList): """ Check if two MultiLineStrings are equal """ - def __getitem__(self, arg0: int) -> LineString: + def __getitem__(self, arg0: typing.SupportsInt) -> LineString: """ Get a linear ring by index """ @@ -1626,12 +2013,15 @@ class MultiLineString(LineStringList): """ @typing.overload def __init__( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> None: """ Initialize from a numpy array of points """ - def __iter__(self) -> typing.Iterator[LineString]: + def __iter__(self) -> collections.abc.Iterator[LineString]: """ Return an iterator over the linear rings in the geometry """ @@ -1645,9 +2035,13 @@ class MultiLineString(LineStringList): """ def __setitem__( self, - arg0: int, - arg1: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous], - ) -> numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous]: + arg0: typing.SupportsInt, + arg1: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ]: """ Set a linear ring by index using a numpy array of points """ @@ -1655,17 +2049,26 @@ class MultiLineString(LineStringList): """ Pickle support for the geometry """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> MultiLineString: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Return a numpy view of the geometry's points """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the geometry """ @@ -1682,7 +2085,10 @@ class MultiLineString(LineStringList): Remove duplicate consecutive points based on their XYZ coordinates """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> MultiLineString: """ Set the geometry from a numpy array of points @@ -1697,7 +2103,10 @@ class MultiLineString(LineStringList): """ @typing.overload def push_back( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> MultiLineString: """ Add a new linear ring from a numpy array of points @@ -1707,11 +2116,40 @@ class MultiLineString(LineStringList): """ Add a new linear ring """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> MultiLineString: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> MultiLineString: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> MultiLineString: """ Round the coordinates of the geometry """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiLineString: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiLineString: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the geometry to a numpy array """ @@ -1719,6 +2157,25 @@ class MultiLineString(LineStringList): """ Convert the geometry to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiLineString: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> MultiLineString: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiLineString: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -1743,7 +2200,7 @@ class MultiPoint(coordinates): """ Check if two MultiPoints are equal """ - def __getitem__(self, arg0: int) -> Point: + def __getitem__(self, arg0: typing.SupportsInt) -> Point: """ Get a point from the geometry by index """ @@ -1755,12 +2212,15 @@ class MultiPoint(coordinates): """ @typing.overload def __init__( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> None: """ Initialize from a numpy array of points """ - def __iter__(self) -> typing.Iterator[Point]: + def __iter__(self) -> collections.abc.Iterator[Point]: """ Iterate over the points in the geometry """ @@ -1773,14 +2233,16 @@ class MultiPoint(coordinates): Check if two MultiPoints are not equal """ @typing.overload - def __setitem__(self, arg0: int, arg1: Point) -> Point: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Point) -> Point: """ Set a point in the geometry by index """ @typing.overload def __setitem__( - self, arg0: int, arg1: numpy.ndarray[numpy.float64[m, 1]] - ) -> numpy.ndarray[numpy.float64[m, 1]]: + self, + arg0: typing.SupportsInt, + arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"], + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Set a point in the geometry by index using a vector """ @@ -1788,17 +2250,26 @@ class MultiPoint(coordinates): """ Pickle support for serialization """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> MultiPoint: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the geometry points """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the geometry """ @@ -1815,7 +2286,10 @@ class MultiPoint(coordinates): Remove duplicate consecutive points based on their XYZ coordinates """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> MultiPoint: """ Set the geometry points from a numpy array @@ -1834,19 +2308,50 @@ class MultiPoint(coordinates): Add a point to the end of the geometry """ @typing.overload - def push_back(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> MultiPoint: + def push_back( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> MultiPoint: """ Add a point to the end of the geometry using a vector """ - def resize(self, arg0: int) -> MultiPoint: + def resize(self, arg0: typing.SupportsInt) -> MultiPoint: """ Resize the geometry to the specified size """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> MultiPoint: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> MultiPoint: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> MultiPoint: """ Round coordinates to specified decimal places """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiPoint: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiPoint: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the geometry points to a numpy array """ @@ -1854,6 +2359,25 @@ class MultiPoint(coordinates): """ Convert to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiPoint: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> MultiPoint: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiPoint: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -1878,7 +2402,7 @@ class MultiPolygon(PolygonList): """ Check if two MultiPolygons are equal """ - def __getitem__(self, arg0: int) -> Polygon: + def __getitem__(self, arg0: typing.SupportsInt) -> Polygon: """ Get a Polygon from the MultiPolygon by index """ @@ -1900,12 +2424,15 @@ class MultiPolygon(PolygonList): """ @typing.overload def __init__( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> None: """ Construct MultiPolygon from a numpy array of points """ - def __iter__(self) -> typing.Iterator[Polygon]: + def __iter__(self) -> collections.abc.Iterator[Polygon]: """ Return an iterator over the Polygons in the MultiPolygon """ @@ -1918,15 +2445,17 @@ class MultiPolygon(PolygonList): Check if two MultiPolygons are not equal """ @typing.overload - def __setitem__(self, arg0: int, arg1: Polygon) -> Polygon: + def __setitem__(self, arg0: typing.SupportsInt, arg1: Polygon) -> Polygon: """ Set a Polygon in the MultiPolygon by index """ @typing.overload def __setitem__( self, - arg0: int, - arg1: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous], + arg0: typing.SupportsInt, + arg1: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> Polygon: """ Set a Polygon in the MultiPolygon by index using a numpy array @@ -1935,17 +2464,26 @@ class MultiPolygon(PolygonList): """ Pickle support for MultiPolygon """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> MultiPolygon: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Return a numpy view of the MultiPolygon coordinates """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the MultiPolygon """ @@ -1958,7 +2496,10 @@ class MultiPolygon(PolygonList): Create a clone of the object """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> MultiPolygon: """ Set MultiPolygon coordinates from a numpy array @@ -1973,7 +2514,10 @@ class MultiPolygon(PolygonList): """ @typing.overload def push_back( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> MultiPolygon: """ Add a new Polygon to the MultiPolygon from a numpy array @@ -1983,11 +2527,40 @@ class MultiPolygon(PolygonList): """ Add a new Polygon to the MultiPolygon """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> MultiPolygon: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> MultiPolygon: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> MultiPolygon: """ Round the coordinates of the MultiPolygon """ - def to_numpy(self: Polygon) -> numpy.ndarray[numpy.float64[m, 3]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiPolygon: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiPolygon: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self: Polygon, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert MultiPolygon to a numpy array """ @@ -1995,6 +2568,25 @@ class MultiPolygon(PolygonList): """ Convert the MultiPolygon to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> MultiPolygon: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> MultiPolygon: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> MultiPolygon: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -2019,7 +2611,7 @@ class Point: """ Check if two Points are equal """ - def __getitem__(self, index: int) -> float: + def __getitem__(self, index: typing.SupportsInt) -> float: """ Get the coordinate value at the specified index (0: x, 1: y, 2: z) """ @@ -2030,16 +2622,23 @@ class Point: Initialize an empty Point """ @typing.overload - def __init__(self, x: float, y: float, z: float = 0.0) -> None: + def __init__( + self, + x: typing.SupportsFloat, + y: typing.SupportsFloat, + z: typing.SupportsFloat = 0.0, + ) -> None: """ Initialize a Point with coordinates (x, y, z) """ @typing.overload - def __init__(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> None: + def __init__( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> None: """ Initialize a Point from a numpy array or vector """ - def __iter__(self) -> typing.Iterator[float]: + def __iter__(self) -> collections.abc.Iterator[float]: """ Return an iterator over the point's coordinates """ @@ -2051,7 +2650,9 @@ class Point: """ Check if two Points are not equal """ - def __setitem__(self, index: int, value: float) -> float: + def __setitem__( + self, index: typing.SupportsInt, value: typing.SupportsFloat + ) -> float: """ Set the coordinate value at the specified index (0: x, 1: y, 2: z) """ @@ -2059,13 +2660,23 @@ class Point: """ Enable pickling support for Point objects """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> Point: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[numpy.float64[3, 1], numpy.ndarray.flags.writeable]: + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[3, 1]", "flags.writeable" + ]: """ Get a numpy view of the point coordinates """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Get the bounding box of the point """ @@ -2081,7 +2692,9 @@ class Point: """ Remove duplicate consecutive points based on their XYZ coordinates """ - def from_numpy(self, arg0: numpy.ndarray[numpy.float64[m, 1]]) -> Point: + def from_numpy( + self, arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"] + ) -> Point: """ Set point coordinates from a numpy array """ @@ -2089,11 +2702,40 @@ class Point: """ Create a Point from a RapidJSON value """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> Point: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> Point: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> Point: """ Round coordinates to specified decimal places """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[3, 1]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Point: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Point: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 1]"]: """ Convert point coordinates to a numpy array """ @@ -2101,6 +2743,25 @@ class Point: """ Convert the Point to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Point: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> Point: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Point: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -2112,21 +2773,21 @@ class Point: Get or set the x-coordinate of the point """ @x.setter - def x(self, arg1: float) -> None: ... + def x(self, arg1: typing.SupportsFloat) -> None: ... @property def y(self) -> float: """ Get or set the y-coordinate of the point """ @y.setter - def y(self, arg1: float) -> None: ... + def y(self, arg1: typing.SupportsFloat) -> None: ... @property def z(self) -> float: """ Get or set the z-coordinate of the point """ @z.setter - def z(self, arg1: float) -> None: ... + def z(self, arg1: typing.SupportsFloat) -> None: ... class Polygon(LinearRingList): __hash__: typing.ClassVar[None] = None @@ -2146,7 +2807,7 @@ class Polygon(LinearRingList): """ Check if two Polygons are equal """ - def __getitem__(self, arg0: int) -> LinearRing: + def __getitem__(self, arg0: typing.SupportsInt) -> LinearRing: """ Get a linear ring by index """ @@ -2168,12 +2829,15 @@ class Polygon(LinearRingList): """ @typing.overload def __init__( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> None: """ Initialize from a numpy array of points """ - def __iter__(self) -> typing.Iterator[LinearRing]: + def __iter__(self) -> collections.abc.Iterator[LinearRing]: """ Return an iterator over the linear rings in the geometry """ @@ -2187,9 +2851,13 @@ class Polygon(LinearRingList): """ def __setitem__( self, - arg0: int, - arg1: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous], - ) -> numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous]: + arg0: typing.SupportsInt, + arg1: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ]: """ Set a linear ring by index using a numpy array of points """ @@ -2197,17 +2865,26 @@ class Polygon(LinearRingList): """ Pickle support for the geometry """ + def affine( + self, T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"] + ) -> Polygon: + """ + Apply 4x4 affine transformation matrix + """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Return a numpy view of the geometry's points """ - def bbox(self, *, with_z: bool = False) -> numpy.ndarray[numpy.float64[m, 1]]: + def bbox( + self, *, with_z: bool = False + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: """ Compute the bounding box of the geometry """ @@ -2224,7 +2901,10 @@ class Polygon(LinearRingList): Remove duplicate consecutive points based on their XYZ coordinates """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> Polygon: """ Set the geometry from a numpy array of points @@ -2239,7 +2919,10 @@ class Polygon(LinearRingList): """ @typing.overload def push_back( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> Polygon: """ Add a new linear ring from a numpy array of points @@ -2249,11 +2932,40 @@ class Polygon(LinearRingList): """ Add a new linear ring """ - def round(self, *, lon: int = 8, lat: int = 8, alt: int = 3) -> Polygon: + def rotate( + self, R: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 3]"] + ) -> Polygon: + """ + Apply 3x3 rotation matrix to all coordinates + """ + def round( + self, + *, + lon: typing.SupportsInt = 8, + lat: typing.SupportsInt = 8, + alt: typing.SupportsInt = 3, + ) -> Polygon: """ Round the coordinates of the geometry """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def scale( + self, scale: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Polygon: + """ + Scale all coordinates by factors [sx, sy, sz] + """ + def to_enu( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Polygon: + """ + Convert WGS84 (lon,lat,alt) to ENU coordinates + """ + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert the geometry to a numpy array """ @@ -2261,6 +2973,25 @@ class Polygon(LinearRingList): """ Convert the geometry to a RapidJSON value """ + def to_wgs84( + self, + anchor: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], + *, + cheap_ruler: bool = True, + ) -> Polygon: + """ + Convert ENU coordinates to WGS84 (lon,lat,alt) + """ + def transform(self, fn: typing.Any) -> Polygon: + """ + Apply transform function to all coordinates (Nx3 numpy array) + """ + def translate( + self, offset: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + ) -> Polygon: + """ + Translate all coordinates by offset vector + """ @property def __geo_interface__(self) -> typing.Any: """ @@ -2278,7 +3009,7 @@ class PolygonList: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -2294,7 +3025,7 @@ class PolygonList: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> Polygon: ... + def __getitem__(self, arg0: typing.SupportsInt) -> Polygon: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -2303,12 +3034,12 @@ class PolygonList: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[Polygon]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[Polygon]: ... def __len__(self) -> int: ... def __ne__(self, arg0: PolygonList) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: Polygon) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: Polygon) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: PolygonList) -> None: """ @@ -2332,11 +3063,11 @@ class PolygonList: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ - def insert(self, i: int, x: Polygon) -> None: + def insert(self, i: typing.SupportsInt, x: Polygon) -> None: """ Insert an item at a given position. """ @@ -2346,7 +3077,7 @@ class PolygonList: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> Polygon: + def pop(self, i: typing.SupportsInt) -> Polygon: """ Remove and return the item at index ``i`` """ @@ -2366,7 +3097,7 @@ class coordinates: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -2382,7 +3113,7 @@ class coordinates: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> ...: ... + def __getitem__(self, arg0: typing.SupportsInt) -> ...: ... @typing.overload def __init__(self) -> None: ... @typing.overload @@ -2391,12 +3122,12 @@ class coordinates: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... - def __iter__(self) -> typing.Iterator[...]: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... + def __iter__(self) -> collections.abc.Iterator[...]: ... def __len__(self) -> int: ... def __ne__(self, arg0: coordinates) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: ...) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: ...) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: coordinates) -> None: """ @@ -2408,10 +3139,11 @@ class coordinates: """ def as_numpy( self, - ) -> numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + ) -> typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ]: """ Get a numpy view of the coordinates @@ -2430,17 +3162,20 @@ class coordinates: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ def from_numpy( - self, arg0: numpy.ndarray[numpy.float64[m, n], numpy.ndarray.flags.c_contiguous] + self, + arg0: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, n]", "flags.c_contiguous" + ], ) -> coordinates: """ Set coordinates from a numpy array """ - def insert(self, i: int, x: ...) -> None: + def insert(self, i: typing.SupportsInt, x: ...) -> None: """ Insert an item at a given position. """ @@ -2450,7 +3185,7 @@ class coordinates: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> ...: + def pop(self, i: typing.SupportsInt) -> ...: """ Remove and return the item at index ``i`` """ @@ -2458,23 +3193,25 @@ class coordinates: """ Remove the first item from the list whose value is x. It is an error if there is no such item. """ - def to_numpy(self) -> numpy.ndarray[numpy.float64[m, 3]]: + def to_numpy( + self, + ) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: """ Convert coordinates to a numpy array """ class value: class ItemsView: - def __iter__(self) -> typing.Iterator: ... + def __iter__(self) -> collections.abc.Iterator: ... def __len__(self) -> int: ... class KeysView: def __contains__(self, arg0: typing.Any) -> bool: ... - def __iter__(self) -> typing.Iterator: ... + def __iter__(self) -> collections.abc.Iterator: ... def __len__(self) -> int: ... class ValuesView: - def __iter__(self) -> typing.Iterator: ... + def __iter__(self) -> collections.abc.Iterator: ... def __len__(self) -> int: ... class array_type: @@ -2492,7 +3229,7 @@ class value: Return true the container contains ``x`` """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete the list elements at index ``i`` """ @@ -2508,9 +3245,9 @@ class value: Retrieve list elements using a slice object """ @typing.overload - def __getitem__(self, arg0: int) -> value: ... + def __getitem__(self, arg0: typing.SupportsInt) -> value: ... @typing.overload - def __getitem__(self, arg0: int) -> value: + def __getitem__(self, arg0: typing.SupportsInt) -> value: """ Get an item from the GeoJSON array by index """ @@ -2522,7 +3259,7 @@ class value: Copy constructor """ @typing.overload - def __init__(self, arg0: typing.Iterable) -> None: ... + def __init__(self, arg0: collections.abc.Iterable) -> None: ... @typing.overload def __init__(self) -> None: """ @@ -2533,18 +3270,18 @@ class value: """ Construct a GeoJSON array from a Python iterable """ - def __iter__(self) -> typing.Iterator[value]: ... + def __iter__(self) -> collections.abc.Iterator[value]: ... def __len__(self) -> int: ... def __ne__(self, arg0: value.array_type) -> bool: ... @typing.overload - def __setitem__(self, arg0: int, arg1: value) -> None: ... + def __setitem__(self, arg0: typing.SupportsInt, arg1: value) -> None: ... @typing.overload def __setitem__(self, arg0: slice, arg1: value.array_type) -> None: """ Assign list elements using a slice object """ @typing.overload - def __setitem__(self, arg0: int, arg1: typing.Any) -> value: + def __setitem__(self, arg0: typing.SupportsInt, arg1: typing.Any) -> value: """ Set an item in the GeoJSON array by index """ @@ -2572,7 +3309,7 @@ class value: Extend the list by appending all the items in the given list """ @typing.overload - def extend(self, L: typing.Iterable) -> None: + def extend(self, L: collections.abc.Iterable) -> None: """ Extend the list by appending all the items in the given list """ @@ -2582,7 +3319,7 @@ class value: """ Set the GeoJSON array from a RapidJSON value """ - def insert(self, i: int, x: value) -> None: + def insert(self, i: typing.SupportsInt, x: value) -> None: """ Insert an item at a given position. """ @@ -2592,7 +3329,7 @@ class value: Remove and return the last item """ @typing.overload - def pop(self, i: int) -> value: + def pop(self, i: typing.SupportsInt) -> value: """ Remove and return the item at index ``i`` """ @@ -2632,7 +3369,7 @@ class value: """ Construct a GeoJSON object from a Python dict """ - def __iter__(self) -> typing.Iterator[str]: ... + def __iter__(self) -> collections.abc.Iterator[str]: ... def __len__(self) -> int: ... @typing.overload def __setitem__(self, arg0: str, arg1: value) -> None: ... @@ -2654,14 +3391,14 @@ class value: @typing.overload def items(self) -> value.ItemsView: ... @typing.overload - def items(self) -> typing.Iterator[tuple[str, value]]: + def items(self) -> collections.abc.Iterator[tuple[str, value]]: """ Get an iterator over the items (key-value pairs) of the GeoJSON object """ @typing.overload def keys(self) -> value.KeysView: ... @typing.overload - def keys(self) -> typing.Iterator[str]: + def keys(self) -> collections.abc.Iterator[str]: """ Get an iterator over the keys of the GeoJSON object """ @@ -2672,7 +3409,7 @@ class value: @typing.overload def values(self) -> value.ValuesView: ... @typing.overload - def values(self) -> typing.Iterator[value]: + def values(self) -> collections.abc.Iterator[value]: """ Get an iterator over the values of the GeoJSON object """ @@ -2719,12 +3456,12 @@ class value: Delete an item from the GeoJSON object by key """ @typing.overload - def __delitem__(self, arg0: int) -> None: + def __delitem__(self, arg0: typing.SupportsInt) -> None: """ Delete an item from the GeoJSON array by index """ @typing.overload - def __getitem__(self, arg0: int) -> value: + def __getitem__(self, arg0: typing.SupportsInt) -> value: """ Get an item from the GeoJSON array by index """ @@ -2753,7 +3490,7 @@ class value: Set an item in the GeoJSON object by key """ @typing.overload - def __setitem__(self, arg0: int, arg1: typing.Any) -> typing.Any: + def __setitem__(self, arg0: typing.SupportsInt, arg1: typing.Any) -> typing.Any: """ Set an item in the GeoJSON array by index """ @@ -2785,11 +3522,11 @@ class value: """ Check if the GeoJSON value is an object """ - def items(self) -> typing.Iterator[tuple[str, value]]: + def items(self) -> collections.abc.Iterator[tuple[str, value]]: """ Get an iterator over the items of the GeoJSON object """ - def keys(self) -> typing.Iterator[str]: + def keys(self) -> collections.abc.Iterator[str]: """ Get an iterator over the keys of the GeoJSON object """ @@ -2809,7 +3546,7 @@ class value: """ Convert the GeoJSON value to a RapidJSON value """ - def values(self) -> typing.Iterator[value]: + def values(self) -> collections.abc.Iterator[value]: """ Get an iterator over the values of the GeoJSON object """ diff --git a/src/pybind11_geobuf/_core/tf.pyi b/src/pybind11_geobuf/_core/tf.pyi index bd4e0a3..4170630 100644 --- a/src/pybind11_geobuf/_core/tf.pyi +++ b/src/pybind11_geobuf/_core/tf.pyi @@ -1,8 +1,9 @@ from __future__ import annotations import numpy +import numpy.typing import typing -__all__ = [ +__all__: list[str] = [ "R_ecef_enu", "T_ecef_enu", "apply_transform", @@ -16,65 +17,143 @@ __all__ = [ "lla2enu", ] -def R_ecef_enu(lon: float, lat: float) -> numpy.ndarray[numpy.float64[3, 3]]: ... +def R_ecef_enu( + lon: typing.SupportsFloat, lat: typing.SupportsFloat +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 3]"]: + """ + Get rotation matrix from ECEF to ENU coordinate system. + """ + @typing.overload def T_ecef_enu( - lon: float, lat: float, alt: float -) -> numpy.ndarray[numpy.float64[4, 4]]: ... + lon: typing.SupportsFloat, lat: typing.SupportsFloat, alt: typing.SupportsFloat +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[4, 4]"]: + """ + Get transformation matrix from ECEF to ENU coordinate system. + """ + @typing.overload def T_ecef_enu( - lla: numpy.ndarray[numpy.float64[3, 1]], -) -> numpy.ndarray[numpy.float64[4, 4]]: ... + lla: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[4, 4]"]: + """ + Get transformation matrix from ECEF to ENU coordinate system using LLA vector. + """ + def apply_transform( - T: numpy.ndarray[numpy.float64[4, 4]], - coords: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], -) -> numpy.ndarray[numpy.float64[m, 3]]: ... + T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"], + coords: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Apply transformation matrix to coordinates. + """ + def apply_transform_inplace( - T: numpy.ndarray[numpy.float64[4, 4]], - coords: numpy.ndarray[ - numpy.float64[m, 3], - numpy.ndarray.flags.writeable, - numpy.ndarray.flags.c_contiguous, + T: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[4, 4]"], + coords: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], + "[m, 3]", + "flags.writeable", + "flags.c_contiguous", ], *, - batch_size: int = 1000, -) -> None: ... -def cheap_ruler_k(latitude: float) -> numpy.ndarray[numpy.float64[3, 1]]: ... + batch_size: typing.SupportsInt = 1000, +) -> None: + """ + Apply transformation matrix to coordinates in-place. + """ + +def cheap_ruler_k( + latitude: typing.SupportsFloat, +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 1]"]: + """ + Get the cheap ruler's unit conversion factor for a given latitude. + """ + def ecef2enu( - ecefs: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + ecefs: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], *, - anchor_lla: numpy.ndarray[numpy.float64[3, 1]] | None = None, + anchor_lla: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + | None = None, cheap_ruler: bool = False, -) -> numpy.ndarray[numpy.float64[m, 3]]: ... +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert ECEF to ENU (East, North, Up) coordinates. + """ + @typing.overload -def ecef2lla(x: float, y: float, z: float) -> numpy.ndarray[numpy.float64[3, 1]]: ... +def ecef2lla( + x: typing.SupportsFloat, y: typing.SupportsFloat, z: typing.SupportsFloat +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 1]"]: + """ + Convert ECEF coordinates to LLA (Longitude, Latitude, Altitude). + """ + @typing.overload def ecef2lla( - ecefs: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], -) -> numpy.ndarray[numpy.float64[m, 3]]: ... + ecefs: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert multiple ECEF coordinates to LLA (Longitude, Latitude, Altitude). + """ + def enu2ecef( - enus: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + enus: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], *, - anchor_lla: numpy.ndarray[numpy.float64[3, 1]], + anchor_lla: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], cheap_ruler: bool = False, -) -> numpy.ndarray[numpy.float64[m, 3]]: ... +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert ENU (East, North, Up) to ECEF coordinates. + """ + def enu2lla( - enus: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + enus: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], *, - anchor_lla: numpy.ndarray[numpy.float64[3, 1]], + anchor_lla: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"], cheap_ruler: bool = True, -) -> numpy.ndarray[numpy.float64[m, 3]]: ... +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert ENU (East, North, Up) to LLA (Longitude, Latitude, Altitude) coordinates. + """ + @typing.overload def lla2ecef( - lon: float, lat: float, alt: float -) -> numpy.ndarray[numpy.float64[3, 1]]: ... + lon: typing.SupportsFloat, lat: typing.SupportsFloat, alt: typing.SupportsFloat +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 1]"]: + """ + Convert LLA (Longitude, Latitude, Altitude) to ECEF coordinates. + """ + @typing.overload def lla2ecef( - llas: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], -) -> numpy.ndarray[numpy.float64[m, 3]]: ... + llas: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert multiple LLA (Longitude, Latitude, Altitude) to ECEF coordinates. + """ + def lla2enu( - llas: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + llas: typing.Annotated[ + numpy.typing.NDArray[numpy.float64], "[m, 3]", "flags.c_contiguous" + ], *, - anchor_lla: numpy.ndarray[numpy.float64[3, 1]] | None = None, + anchor_lla: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[3, 1]"] + | None = None, cheap_ruler: bool = True, -) -> numpy.ndarray[numpy.float64[m, 3]]: ... +) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 3]"]: + """ + Convert LLA (Longitude, Latitude, Altitude) to ENU (East, North, Up) coordinates. + """ diff --git a/src/pybind11_geojson.cpp b/src/pybind11_geojson.cpp index 8714278..7c95a98 100644 --- a/src/pybind11_geojson.cpp +++ b/src/pybind11_geojson.cpp @@ -8,6 +8,7 @@ #include "geobuf/geojson_cropping.hpp" #include "geobuf/geojson_helpers.hpp" +#include "geobuf/geojson_transform.hpp" #include "geobuf/pybind11_helpers.hpp" #include "geobuf/rapidjson_helpers.hpp" @@ -104,6 +105,112 @@ void bind_geojson(py::module &geojson) "clone", [](const Type &self) -> Type { return self; }, \ "Create a clone of the object") +// Transform methods macros +#define GEOMETRY_TRANSFORM(geom_type) \ + .def( \ + "transform", \ + [](mapbox::geojson::geom_type &self, \ + const py::object &fn) -> mapbox::geojson::geom_type & { \ + transform_coords(self, [&](Eigen::Ref coords) { \ + py::gil_scoped_acquire acquire; \ + auto arr = \ + py::array_t({coords.rows(), (Eigen::Index)3}, \ + {sizeof(double) * 3, sizeof(double)}, \ + coords.data(), py::none()); \ + auto result = fn(arr); \ + if (!result.is_none()) { \ + auto mat = result.cast(); \ + coords = mat; \ + } \ + }); \ + return self; \ + }, \ + "fn"_a, rvp::reference_internal, \ + "Apply transform function to all coordinates (Nx3 numpy array)") + +#define GEOMETRY_TO_ENU(geom_type) \ + .def( \ + "to_enu", \ + [](mapbox::geojson::geom_type &self, const Eigen::Vector3d &anchor, \ + bool cheap_ruler) -> mapbox::geojson::geom_type & { \ + Wgs84ToEnu xform{anchor, cheap_ruler}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "anchor"_a, py::kw_only(), "cheap_ruler"_a = true, \ + rvp::reference_internal, \ + "Convert WGS84 (lon,lat,alt) to ENU coordinates") + +#define GEOMETRY_TO_WGS84(geom_type) \ + .def( \ + "to_wgs84", \ + [](mapbox::geojson::geom_type &self, const Eigen::Vector3d &anchor, \ + bool cheap_ruler) -> mapbox::geojson::geom_type & { \ + EnuToWgs84 xform{anchor, cheap_ruler}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "anchor"_a, py::kw_only(), "cheap_ruler"_a = true, \ + rvp::reference_internal, \ + "Convert ENU coordinates to WGS84 (lon,lat,alt)") + +#define GEOMETRY_ROTATE(geom_type) \ + .def( \ + "rotate", \ + [](mapbox::geojson::geom_type &self, \ + const Eigen::Matrix3d &R) -> mapbox::geojson::geom_type & { \ + Rotation3D xform{R}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "R"_a, rvp::reference_internal, \ + "Apply 3x3 rotation matrix to all coordinates") + +#define GEOMETRY_TRANSLATE(geom_type) \ + .def( \ + "translate", \ + [](mapbox::geojson::geom_type &self, \ + const Eigen::Vector3d &offset) -> mapbox::geojson::geom_type & { \ + Translation3D xform{offset}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "offset"_a, rvp::reference_internal, \ + "Translate all coordinates by offset vector") + +#define GEOMETRY_SCALE(geom_type) \ + .def( \ + "scale", \ + [](mapbox::geojson::geom_type &self, \ + const Eigen::Vector3d &s) -> mapbox::geojson::geom_type & { \ + Scale3D xform{s}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "scale"_a, rvp::reference_internal, \ + "Scale all coordinates by factors [sx, sy, sz]") + +#define GEOMETRY_AFFINE(geom_type) \ + .def( \ + "affine", \ + [](mapbox::geojson::geom_type &self, \ + const Eigen::Matrix4d &T) -> mapbox::geojson::geom_type & { \ + AffineTransform xform{T}; \ + transform_coords(self, xform); \ + return self; \ + }, \ + "T"_a, rvp::reference_internal, \ + "Apply 4x4 affine transformation matrix") + +#define GEOMETRY_TRANSFORM_METHODS(geom_type) \ + GEOMETRY_TRANSFORM(geom_type) \ + GEOMETRY_TO_ENU(geom_type) \ + GEOMETRY_TO_WGS84(geom_type) \ + GEOMETRY_ROTATE(geom_type) \ + GEOMETRY_TRANSLATE(geom_type) \ + GEOMETRY_SCALE(geom_type) \ + GEOMETRY_AFFINE(geom_type) + py::class_(geojson, "GeoJSON", py::module_local()) is_geojson_type(geometry) // is_geojson_type(feature) // @@ -147,6 +254,7 @@ void bind_geojson(py::module &geojson) rvp::reference_internal, "Round coordinates to specified decimal places") // GEOMETRY_DEDUPLICATE_XYZ(geojson) // + GEOMETRY_TRANSFORM_METHODS(geojson) // .def( "from_rapidjson", [](mapbox::geojson::geojson &self, @@ -581,6 +689,7 @@ void bind_geojson(py::module &geojson) "Get an iterator over the custom property keys") GEOMETRY_ROUND_COORDS(geometry) GEOMETRY_DEDUPLICATE_XYZ(geometry) + GEOMETRY_TRANSFORM_METHODS(geometry) .def_property_readonly( "__geo_interface__", [](const mapbox::geojson::geometry &self) -> py::object { @@ -762,6 +871,7 @@ void bind_geojson(py::module &geojson) "Enable pickling support for Point objects") // GEOMETRY_ROUND_COORDS(point) // GEOMETRY_DEDUPLICATE_XYZ(point) // + GEOMETRY_TRANSFORM_METHODS(point) // .def_property_readonly( "__geo_interface__", [](const mapbox::geojson::point &self) -> py::object { @@ -948,6 +1058,7 @@ void bind_geojson(py::module &geojson) "Pickle support for serialization") \ GEOMETRY_ROUND_COORDS(geom_type) \ GEOMETRY_DEDUPLICATE_XYZ(geom_type) \ + GEOMETRY_TRANSFORM_METHODS(geom_type) \ .def_property_readonly( \ "__geo_interface__", \ [](const mapbox::geojson::geom_type &self) -> py::object { \ @@ -1157,6 +1268,7 @@ void bind_geojson(py::module &geojson) py::kw_only(), "lon"_a = 8, "lat"_a = 8, "alt"_a = 3, \ rvp::reference_internal, "Round the coordinates of the geometry") \ GEOMETRY_DEDUPLICATE_XYZ(geom_type) \ + GEOMETRY_TRANSFORM_METHODS(geom_type) \ .def( \ "bbox", \ [](const mapbox::geojson::geom_type &self, bool with_z) \ @@ -1364,6 +1476,7 @@ void bind_geojson(py::module &geojson) py::kw_only(), "lon"_a = 8, "lat"_a = 8, "alt"_a = 3, rvp::reference_internal, "Round the coordinates of the MultiPolygon") + GEOMETRY_TRANSFORM_METHODS(multi_polygon) .def_property_readonly( "__geo_interface__", [](const mapbox::geojson::multi_polygon &self) -> py::object { @@ -1505,6 +1618,7 @@ void bind_geojson(py::module &geojson) rvp::reference_internal, "Round the coordinates of all geometries in the GeometryCollection") GEOMETRY_DEDUPLICATE_XYZ(geometry_collection) + GEOMETRY_TRANSFORM_METHODS(geometry_collection) .def_property_readonly( "__geo_interface__", [](const mapbox::geojson::geometry_collection &self) -> py::object { @@ -2186,7 +2300,7 @@ void bind_geojson(py::module &geojson) py::kw_only(), "lon"_a = 8, "lat"_a = 8, "alt"_a = 3, rvp::reference_internal, "Round the coordinates of the feature geometry") // - GEOMETRY_DEDUPLICATE_XYZ(feature) + GEOMETRY_DEDUPLICATE_XYZ(feature) GEOMETRY_TRANSFORM_METHODS(feature) // ; @@ -2239,6 +2353,7 @@ void bind_geojson(py::module &geojson) rvp::reference_internal, "Round the coordinates of all features in the collection") GEOMETRY_DEDUPLICATE_XYZ(feature_collection) + GEOMETRY_TRANSFORM_METHODS(feature_collection) // round // .def( diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 0000000..b422bac --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from pybind11_geobuf import geojson + + +def sample_coords(): + """Sample WGS84 coordinates (lon, lat, alt)""" + return np.array( + [ + [120.40317479950272, 31.416966084052177, 1.0], + [120.28451900911591, 31.30578266928819, 2.0], + [120.35592249359615, 31.21781895672254, 3.0], + [120.67093786630113, 31.299502266522722, 4.0], + ] + ) + + +def sample_anchor(): + """Sample anchor point for ENU conversion""" + return np.array([120.4, 31.3, 0.0]) + + +class TestTransformPoint: + def test_transform_custom_function(self): + pt = geojson.Point(1.0, 2.0, 3.0) + + def double_coords(coords): + coords[:] *= 2 + + pt.transform(double_coords) + assert pt() == [2.0, 4.0, 6.0] + + def test_transform_return_new_array(self): + pt = geojson.Point(1.0, 2.0, 3.0) + + def add_offset(coords): + return coords + 10 + + pt.transform(add_offset) + assert pt() == [11.0, 12.0, 13.0] + + def test_to_enu_to_wgs84_roundtrip(self): + pt = geojson.Point(120.4, 31.3, 100.0) + original = pt.to_numpy().copy() + anchor = np.array([120.4, 31.3, 0.0]) + + pt.to_enu(anchor) + # After to_enu, coordinates should be near origin in ENU + enu_coords = pt.to_numpy() + assert np.abs(enu_coords[0]) < 1.0 # east ~0 + assert np.abs(enu_coords[1]) < 1.0 # north ~0 + assert np.abs(enu_coords[2] - 100.0) < 0.01 # up ~100 + + pt.to_wgs84(anchor) + np.testing.assert_allclose(pt.to_numpy(), original, rtol=1e-6) + + def test_translate(self): + pt = geojson.Point(1.0, 2.0, 3.0) + pt.translate(np.array([10.0, 20.0, 30.0])) + assert pt() == [11.0, 22.0, 33.0] + + def test_scale(self): + pt = geojson.Point(1.0, 2.0, 3.0) + pt.scale(np.array([2.0, 3.0, 4.0])) + assert pt() == [2.0, 6.0, 12.0] + + def test_rotate(self): + pt = geojson.Point(1.0, 0.0, 0.0) + # Rotate 90 degrees around Z axis + R = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]], dtype=float) + pt.rotate(R) + np.testing.assert_allclose(pt.to_numpy(), [0.0, 1.0, 0.0], atol=1e-10) + + def test_affine(self): + pt = geojson.Point(1.0, 2.0, 3.0) + T = np.eye(4) + T[:3, 3] = [10, 20, 30] # translation + pt.affine(T) + assert pt() == [11.0, 22.0, 33.0] + pt = geojson.Point(1.0, 2.0, 3.0) + T[:, 0] *= 5 + T[:, 1] *= 2 + pt.affine(T) + assert pt() == [15.0, 24.0, 33.0] + + +class TestTransformLineString: + def test_transform_custom_function(self): + ls = geojson.LineString(sample_coords()) + original_shape = ls.to_numpy().shape + + def add_noise(coords): + coords[:, 2] += 10.0 + + ls.transform(add_noise) + result = ls.to_numpy() + assert result.shape == original_shape + np.testing.assert_allclose(result[:, 2], sample_coords()[:, 2] + 10.0) + + def test_to_enu_to_wgs84_roundtrip(self): + ls = geojson.LineString(sample_coords()) + original = ls.to_numpy().copy() + anchor = sample_anchor() + + ls.to_enu(anchor) + # After to_enu, coords should be in local ENU frame + enu_coords = ls.to_numpy() + assert not np.allclose(enu_coords, original) # should be different + + ls.to_wgs84(anchor) + np.testing.assert_allclose(ls.to_numpy(), original, rtol=1e-6) + + def test_translate(self): + ls = geojson.LineString([[0, 0, 0], [1, 1, 1]]) + ls.translate(np.array([10.0, 20.0, 30.0])) + expected = [[10, 20, 30], [11, 21, 31]] + np.testing.assert_allclose(ls.to_numpy(), expected) + + def test_chain_operations(self): + ls = geojson.LineString([[0, 0, 0], [1, 1, 1]]) + result = ls.translate(np.array([1, 1, 1])).scale(np.array([2, 2, 2])) + assert result == ls # chain should return self + expected = [[2, 2, 2], [4, 4, 4]] + np.testing.assert_allclose(ls.to_numpy(), expected) + + +class TestTransformMultiPoint: + def test_transform(self): + mp = geojson.MultiPoint(sample_coords()) + mp.translate(np.array([1.0, 2.0, 3.0])) + result = mp.to_numpy() + expected = sample_coords() + np.array([1.0, 2.0, 3.0]) + np.testing.assert_allclose(result, expected) + + +class TestTransformPolygon: + def test_transform(self): + coords = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]] + poly = geojson.Polygon(coords) + poly.translate(np.array([10.0, 20.0, 30.0])) + result = poly.to_numpy() + expected = np.array(coords) + np.array([10.0, 20.0, 30.0]) + np.testing.assert_allclose(result, expected) + + +class TestTransformMultiLineString: + def test_transform(self): + coords = sample_coords() + mls = geojson.MultiLineString(coords) + mls.push_back(coords * 2) + + mls.translate(np.array([1.0, 1.0, 1.0])) + + # Both line strings should be translated + for ls in mls: + assert ls.to_numpy()[0, 2] != 1.0 # z was 1 or 2, now +1 + + +class TestTransformMultiPolygon: + def test_transform(self): + coords = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 0, 0]] + mpoly = geojson.MultiPolygon(coords) + mpoly.translate(np.array([5.0, 5.0, 5.0])) + expected = np.array(coords) + np.array([5.0, 5.0, 5.0]) + # MultiPolygon.to_numpy returns the first polygon's first ring + np.testing.assert_allclose(mpoly.as_numpy(), expected) + + +class TestTransformGeometryCollection: + def test_transform(self): + gc = geojson.GeometryCollection() + gc.push_back(geojson.Point(1, 2, 3)) + gc.push_back(geojson.LineString([[0, 0, 0], [1, 1, 1]])) + + gc.translate(np.array([10.0, 10.0, 10.0])) + + pt = gc[0].as_point() + assert pt() == [11.0, 12.0, 13.0] + + ls = gc[1].as_line_string() + np.testing.assert_allclose(ls.to_numpy(), [[10, 10, 10], [11, 11, 11]]) + + +class TestTransformFeature: + def test_transform(self): + f = geojson.Feature() + f.geometry(geojson.LineString(sample_coords())) + original = f.to_numpy().copy() + + f.translate(np.array([0.0, 0.0, 100.0])) + result = f.to_numpy() + + np.testing.assert_allclose(result[:, :2], original[:, :2]) + np.testing.assert_allclose(result[:, 2], original[:, 2] + 100.0) + + def test_to_enu_to_wgs84_roundtrip(self): + f = geojson.Feature() + f.geometry(geojson.LineString(sample_coords())) + original = f.to_numpy().copy() + anchor = sample_anchor() + + f.to_enu(anchor).to_wgs84(anchor) + np.testing.assert_allclose(f.to_numpy(), original, rtol=1e-6) + + +class TestTransformFeatureCollection: + def test_transform(self): + fc = geojson.FeatureCollection() + f1 = geojson.Feature() + f1.geometry(geojson.Point(1, 2, 3)) + f2 = geojson.Feature() + f2.geometry(geojson.LineString([[0, 0, 0], [1, 1, 1]])) + fc.append(f1) + fc.append(f2) + + fc.translate(np.array([10.0, 10.0, 10.0])) + + assert fc[0].geometry().as_point()() == [11.0, 12.0, 13.0] + np.testing.assert_allclose( + fc[1].geometry().as_line_string().to_numpy(), [[10, 10, 10], [11, 11, 11]] + ) + + def test_to_enu_to_wgs84_roundtrip(self): + fc = geojson.FeatureCollection() + f = geojson.Feature() + f.geometry(geojson.LineString(sample_coords())) + fc.append(f) + + original = fc[0].to_numpy().copy() + anchor = sample_anchor() + + fc.to_enu(anchor).to_wgs84(anchor) + np.testing.assert_allclose(fc[0].to_numpy(), original, rtol=1e-6) + + +class TestTransformGeometry: + def test_transform_point(self): + g = geojson.Geometry(geojson.Point(1, 2, 3)) + g.translate(np.array([10.0, 10.0, 10.0])) + assert g.as_point()() == [11.0, 12.0, 13.0] + + def test_transform_line_string(self): + g = geojson.Geometry(geojson.LineString([[0, 0, 0], [1, 1, 1]])) + g.scale(np.array([2.0, 2.0, 2.0])) + np.testing.assert_allclose( + g.as_line_string().to_numpy(), [[0, 0, 0], [2, 2, 2]] + ) + + +class TestTransformGeoJSON: + def test_transform_geometry(self): + gj = geojson.GeoJSON(geojson.Geometry(geojson.Point(1, 2, 3))) + gj.translate(np.array([10.0, 10.0, 10.0])) + assert gj.as_geometry().as_point()() == [11.0, 12.0, 13.0] + + def test_transform_feature(self): + f = geojson.Feature() + f.geometry(geojson.Point(1, 2, 3)) + gj = geojson.GeoJSON(f) + gj.translate(np.array([10.0, 10.0, 10.0])) + assert gj.as_feature().geometry().as_point()() == [11.0, 12.0, 13.0] + + +class TestCheapRulerOption: + def test_cheap_ruler_vs_full(self): + ls = geojson.LineString(sample_coords()) + anchor = sample_anchor() + + # With cheap_ruler=True (default) + ls_cheap = ls.clone() + ls_cheap.to_enu(anchor, cheap_ruler=True) + + # With cheap_ruler=False + ls_full = ls.clone() + ls_full.to_enu(anchor, cheap_ruler=False) + + # Results should be similar but not identical + cheap_coords = ls_cheap.to_numpy() + full_coords = ls_full.to_numpy() + + # X and Y should be close (within a few hundred meters for typical scenarios) + # Z differs significantly because cheap_ruler preserves original Z while + # full transform converts through ECEF which changes altitude reference + # Use absolute tolerance of 100m for comparison since cheap_ruler is an approximation + # and the test data spans ~30km, so 100m error is reasonable + np.testing.assert_allclose(cheap_coords[:, :2], full_coords[:, :2], atol=100) + + +def test_chain_multiple_transforms(): + """Test chaining multiple transform operations""" + fc = geojson.FeatureCollection() + f = geojson.Feature() + f.geometry(geojson.LineString(sample_coords())) + fc.append(f) + + anchor = sample_anchor() + + # Chain: to_enu -> translate -> rotate -> to_wgs84 + R = np.eye(3) # identity rotation + + # This should work without errors + result = fc.to_enu(anchor).translate(np.array([100.0, 100.0, 0.0])).rotate(R) + + # Verify result is the same object (fluent interface) + assert result is fc + + +def test_transform_preserves_properties(): + """Test that transform operations preserve feature properties""" + f = geojson.Feature() + f.geometry(geojson.LineString([[0, 0, 0], [1, 1, 1]])) + f.properties({"name": "test", "value": 42}) + + f.translate(np.array([10.0, 10.0, 10.0])) + + # Properties should be unchanged + assert f.properties()["name"]() == "test" + assert f.properties()["value"]() == 42 + + # Geometry should be translated + np.testing.assert_allclose(f.to_numpy(), [[10, 10, 10], [11, 11, 11]]) + + +if __name__ == "__main__": + import os + import sys + + os.chdir(os.path.dirname(__file__)) + sys.exit(pytest.main([__file__, "-v", "-x"])) diff --git a/version.h.in b/version.h.in deleted file mode 100644 index a534895..0000000 --- a/version.h.in +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef @PROJECT_NAME_UPPERCASE@_VERSION_H -#define @PROJECT_NAME_UPPERCASE@_VERSION_H - -#define @PROJECT_NAME_UPPERCASE@_MAJOR_VERSION (@MAJOR_VERSION@) -#define @PROJECT_NAME_UPPERCASE@_MINOR_VERSION (@MINOR_VERSION@) -#define @PROJECT_NAME_UPPERCASE@_PATCH_VERSION (@PATCH_VERSION@) -#define @PROJECT_NAME_UPPERCASE@_VERSION "@PROJECT_VERSION@" - -#define DATA_DIR "@PROJECT_SOURCE_DIR@/data" -#define PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@" -#define PROJECT_BINARY_DIR "@PROJECT_BINARY_DIR@" - -#define USERNAME_HOSTNAME "@USERNAME_HOSTNAME@" -#define GIT_BRANCH "@GIT_BRANCH@" -#define GIT_COMMIT_HASH "@GIT_COMMIT_HASH@" -#define GIT_COMMIT_COUNT GIT_COMMIT_COUNT -#define GIT_DIFF_NAME_ONLY "@GIT_DIFF_NAME_ONLY@" - -#endif // @PROJECT_NAME_UPPERCASE@_VERSION_H