diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 4abd572..ec952b1 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -10,10 +10,10 @@ on: jobs: build: strategy: - fail-fast: false + fail-fast: true matrix: - platform: [ubuntu-20.04, windows-2019, macos-12] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + platform: [ubuntu-20.04, windows-2019, macos-13] + python-version: ["3.9"] runs-on: ${{ matrix.platform }} diff --git a/.gitignore b/.gitignore index 39aeec0..07eb143 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ _generate/ wheelhouse !test.py site +stubs diff --git a/.gitmodules b/.gitmodules index 8197afb..0bb58c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "pybind11"] - path = pybind11 - url = https://github.com/pybind/pybind11.git - branch = master [submodule "headers"] path = headers url = https://github.com/cubao/headers.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a3e827..e6b4471 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,18 +33,14 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace -# Black, the code formatter, natively supports pre-commit -- repo: https://github.com/psf/black - rev: 22.3.0 +# Check linting and style issues +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.6.5" hooks: - - id: black - exclude: ^(docs) - -# Sort your imports in a standard form -- repo: https://github.com/PyCQA/isort - rev: 5.11.5 - hooks: - - id: isort + - id: ruff + args: ["--fix", "--show-fixes"] + - id: ruff-format + exclude: ^(docs) # Upgrade older Python syntax - repo: https://github.com/asottile/pyupgrade @@ -60,12 +56,6 @@ repos: - id: remove-tabs exclude: ^(docs|Makefile) -- repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - additional_dependencies: [flake8-bugbear] - # CMake formatting - repo: https://github.com/cheshirekow/cmake-format-precommit rev: v0.6.13 diff --git a/CMakeLists.txt b/CMakeLists.txt index 22fce55..06609e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,20 @@ -cmake_minimum_required(VERSION 3.4...3.18) -project(fast_crossing) +cmake_minimum_required(VERSION 3.15...3.26) +if(NOT DEFINED SKBUILD_PROJECT_NAME) + set(SKBUILD_PROJECT_NAME "fast_crossing") +endif() +if(NOT DEFINED PROJECT_VERSION) + set(PROJECT_VERSION "dev") +endif() +# https://scikit-build-core.readthedocs.io/en/latest/cmakelists.html#accessing-information +project( + ${SKBUILD_PROJECT_NAME} + VERSION ${SKBUILD_PROJECT_VERSION} + LANGUAGES CXX) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_CXX_STANDARD 17) if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "") set(CMAKE_BUILD_TYPE @@ -23,11 +35,16 @@ endif() include_directories(${PROJECT_SOURCE_DIR}/headers/include ${PROJECT_SOURCE_DIR}/headers/include/cubao) -set(CMAKE_CXX_STANDARD 17) set(PYBIND11_CPP_STANDARD -std=c++17) -add_subdirectory(pybind11) -pybind11_add_module(_pybind11_fast_crossing src/main.cpp) +# https://scikit-build-core.readthedocs.io/en/latest/getting_started.html +find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) +find_package(pybind11 CONFIG REQUIRED) +include_directories(headers/include) -target_compile_definitions(_pybind11_fast_crossing - PRIVATE VERSION_INFO=${FAST_CROSSING_VERSION_INFO}) +file(GLOB SRCS src/*.cpp) +python_add_library(_core MODULE ${SRCS} WITH_SOABI) +target_link_libraries(_core PRIVATE pybind11::headers) +target_include_directories(_core PRIVATE src) +target_compile_definitions(_core PRIVATE VERSION_INFO=${PROJECT_VERSION}) +install(TARGETS _core DESTINATION ${PROJECT_NAME}) diff --git a/Makefile b/Makefile index 38d0208..9ac046f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PROJECT_SOURCE_DIR ?= $(abspath ./) PROJECT_NAME ?= $(shell basename $(PROJECT_SOURCE_DIR)) +NUM_JOBS ?= 8 all: @echo nothing special @@ -14,11 +15,7 @@ lint: pre-commit run -a lint_install: pre-commit install - -build: - mkdir -p build && cd build && \ - cmake .. && make -.PHONY: build +.PHONY: lint lint_install docs_build: mkdocs build @@ -26,7 +23,7 @@ docs_serve: mkdocs serve -a 0.0.0.0:8088 DOCKER_TAG_WINDOWS ?= ghcr.io/cubao/build-env-windows-x64:v0.0.1 -DOCKER_TAG_LINUX ?= ghcr.io/cubao/build-env-manylinux2014-x64:v0.0.3 +DOCKER_TAG_LINUX ?= ghcr.io/cubao/build-env-manylinux2014-x64:v0.0.5 DOCKER_TAG_MACOS ?= ghcr.io/cubao/build-env-macos-arm64:v0.0.1 test_in_win: @@ -46,28 +43,31 @@ test_in_dev_container: -v `pwd`:`pwd` -w `pwd` -it $(DEV_CONTAINER_IMAG) bash PYTHON ?= python3 +build: + $(PYTHON) -m pip install scikit_build_core pyproject_metadata pathspec pybind11 + CMAKE_BUILD_PARALLEL_LEVEL=$(NUM_JOBS) $(PYTHON) -m pip install --no-build-isolation -Ceditable.rebuild=true -Cbuild-dir=build -ve. python_install: - $(PYTHON) setup.py install -python_build: - $(PYTHON) setup.py bdist_wheel + $(PYTHON) -m pip install . --verbose +python_wheel: + $(PYTHON) -m pip wheel . -w build --verbose python_sdist: - $(PYTHON) setup.py sdist - # tar -tvf dist/fast_crossing-*.tar.gz + $(PYTHON) -m pip sdist . --verbose python_test: pytest pytest: - pytest tests --capture=tee-sys -.PHONY: python_install python_build python_sdist python_test pytest + python3 -m pip install pytest + pytest tests/test_basic.py +.PHONY: build + +restub: + pybind11-stubgen fast_crossing._core -o stubs + cp -rf stubs/fast_crossing/_core src/fast_crossing -# conda create -y -n py36 python=3.6 -# conda create -y -n py37 python=3.7 # conda create -y -n py38 python=3.8 # conda create -y -n py39 python=3.9 # conda create -y -n py310 python=3.10 +# conda create -y -n py311 python=3.11 +# conda create -y -n py312 python=3.12 # conda env list -python_build_py36: - PYTHON=python conda run --no-capture-output -n py36 make python_build -python_build_py37: - PYTHON=python conda run --no-capture-output -n py37 make python_build python_build_py38: PYTHON=python conda run --no-capture-output -n py38 make python_build python_build_py39: @@ -76,11 +76,13 @@ python_build_py310: PYTHON=python conda run --no-capture-output -n py310 make python_build python_build_py311: PYTHON=python conda run --no-capture-output -n py311 make python_build -python_build_all: python_build_py36 python_build_py37 python_build_py38 python_build_py39 python_build_py310 python_build_py311 +python_build_py312: + PYTHON=python conda run --no-capture-output -n py312 make python_build +python_build_all: python_build_py38 python_build_py39 python_build_py310 python_build_py311 python_build_py312 python_build_all_in_linux: docker run --rm -w `pwd` -v `pwd`:`pwd` -v `pwd`/build/linux:`pwd`/build -it $(DOCKER_TAG_LINUX) make python_build_all make repair_wheels && rm -rf dist/*.whl && mv wheelhouse/*.whl dist && rm -rf wheelhouse -python_build_all_in_macos: python_build_py38 python_build_py39 python_build_py310 python_build_py311 +python_build_all_in_macos: python_build_py38 python_build_py39 python_build_py310 python_build_py311 python_build_py312 python_build_all_in_windows: python_build_all repair_wheels: diff --git a/benchmarks/benchmark_point_in_polygon.py b/benchmarks/benchmark_point_in_polygon.py index 9723ef5..c1a810f 100644 --- a/benchmarks/benchmark_point_in_polygon.py +++ b/benchmarks/benchmark_point_in_polygon.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import math import os import random import time -from typing import List, Tuple import numpy as np from loguru import logger @@ -44,8 +45,7 @@ def point_in_polygon_polygons(points: np.ndarray, polygon: np.ndarray) -> np.nda num_edges_children = 4 num_nodes_children = 4 tree = polygons.build_search_tree(polygon, num_edges_children, num_nodes_children) - mask = polygons.points_are_inside(tree, points).astype(np.int32) - return mask + return polygons.points_are_inside(tree, points).astype(np.int32) def point_in_polygon_shapely(points: np.ndarray, polygon: np.ndarray) -> np.ndarray: @@ -68,8 +68,7 @@ def load_points(path: str): def load_polygon(path: str): if path.endswith((".npy", ".pcd")): - return load_points(path) - pass + load_points(path) def write_mask(mask: np.ndarray, path: str): @@ -90,12 +89,12 @@ def wrapped_fn(input_points: str, input_polygon: str, output_path: str): # https://stackoverflow.com/questions/8997099/algorithm-to-generate-random-2d-polygon def generate_polygon( - center: Tuple[float, float], + center: tuple[float, float], avg_radius: float, irregularity: float, spikiness: float, num_vertices: int, -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: """ Start with the center of the polygon at center, then creates the polygon by sampling points on a circle around the center. @@ -147,7 +146,7 @@ def generate_polygon( return points -def random_angle_steps(steps: int, irregularity: float) -> List[float]: +def random_angle_steps(steps: int, irregularity: float) -> list[float]: """Generates the division of a circumference in random angles. Args: diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index bd846b7..b60f423 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -10,6 +10,10 @@ To upgrade `fast-crossing` to the latest version, use pip: pip install -U fast-crossing ``` +## Version 0.1.0 (2024-10-03) + +* Add typing stubs + ## Version 0.0.9 (2024-09-07) * Update pybind11 (for python 3.11, 3.12), ditch python 3.6, 3.7 diff --git a/fast_crossing/__init__.py b/fast_crossing/__init__.py deleted file mode 100644 index 31f4c37..0000000 --- a/fast_crossing/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from _pybind11_fast_crossing import * # noqa -from _pybind11_fast_crossing import __version__ # noqa diff --git a/headers b/headers index 03fbb5a..33d192a 160000 --- a/headers +++ b/headers @@ -1 +1 @@ -Subproject commit 03fbb5a8178a223f8e48600558c8fcea408f68e1 +Subproject commit 33d192a459e9b620a2cdcec0ad146ea7989aae77 diff --git a/pybind11 b/pybind11 deleted file mode 160000 index 8a801bd..0000000 --- a/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a801bdc32b40dc54f62e982c6e36577af4b12bb diff --git a/pyproject.toml b/pyproject.toml index 7b3b581..0bc84dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,98 @@ [build-system] -requires = [ - "setuptools>=42", - "wheel", - "ninja", - "cmake>=3.12", +requires = ["scikit-build-core>=0.3.3", "pybind11"] +build-backend = "scikit_build_core.build" + + +[project] +name = "fast_crossing" +version = "0.1.0" +url = "https://fast-crossing.readthedocs.io" +description="fast crossing" +readme = "README.md" +authors = [ + { name = "district10", email = "dvorak4tzx@gmail.com" }, ] -build-backend = "setuptools.build_meta" +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.optional-dependencies] +test = ["pytest", "scipy"] + + +[tool.scikit-build] +wheel.expand-macos-universal-tags = true -[tool.isort] -profile = "black" [tool.pytest.ini_options] minversion = "6.0" addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true -filterwarnings = ["error"] +log_cli_level = "INFO" +filterwarnings = [ + "error", +] testpaths = ["tests"] + [tool.cibuildwheel] test-command = "pytest {project}/tests" test-extras = ["test"] test-skip = ["*universal2:arm64"] -# Setuptools bug causes collision between pypy and cpython artifacts -before-build = "rm -rf {project}/build" +build-verbosity = 1 + + +[tool.ruff] +src = ["src"] + +[tool.ruff.lint] +exclude = ["*.pyi", "scripts/*.py"] +extend-select = [ + "B", # flake8-bugbear + "I", # isort + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 + "EXE", # flake8-executable + "NPY", # NumPy specific rules + "PD", # pandas-vet +] +ignore = [ + "ARG002", + "EM101", + "NPY002", + "PLR09", # Too many X + "PLR2004", # Magic comparison + "PT018", + "PTH100", + "PTH103", + "PTH119", + "PTH120", + "RUF013", +] +isort.required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["T20"] diff --git a/scripts/debug_polyline_in_polygon.py b/scripts/debug_polyline_in_polygon.py index c162234..6b9554c 100644 --- a/scripts/debug_polyline_in_polygon.py +++ b/scripts/debug_polyline_in_polygon.py @@ -2,10 +2,10 @@ # recorded video: # - youtube: https://www.youtube.com/watch?v=1dPJ3P84FxE # - bilibil: https://www.bilibili.com/video/BV1D24y1u7uB +from __future__ import annotations import math import random -from typing import List, Tuple import numpy as np from vedo import Circle, show # noqa @@ -19,13 +19,13 @@ # https://stackoverflow.com/questions/8997099/algorithm-to-generate-random-2d-polygon def generate_polygon( *, - center: Tuple[float, float] = (0.0, 0.0), + center: tuple[float, float] = (0.0, 0.0), avg_radius: float = 100.0, irregularity: float = 2.0, spikiness: float = 0.3, num_vertices: int = 100, close: bool = True, -) -> List[Tuple[float, float]]: +) -> list[tuple[float, float]]: if irregularity < 0 or irregularity > 1: raise ValueError("Irregularity must be between 0 and 1.") if spikiness < 0 or spikiness > 1: @@ -35,7 +35,7 @@ def generate_polygon( irregularity *= 2 * math.pi / num_vertices spikiness *= avg_radius - def random_angle_steps(steps: int, irregularity: float) -> List[float]: + def random_angle_steps(steps: int, irregularity: float) -> list[float]: angles = [] lower = (2 * math.pi / steps) - irregularity upper = (2 * math.pi / steps) + irregularity diff --git a/scripts/spline_draw.py b/scripts/spline_draw.py index abd83ab..17e1d92 100644 --- a/scripts/spline_draw.py +++ b/scripts/spline_draw.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from vedo import Picture, dataurl from vedo.applications import SplinePlotter # ready to use class! diff --git a/setup.py b/setup.py deleted file mode 100644 index 93dcc95..0000000 --- a/setup.py +++ /dev/null @@ -1,140 +0,0 @@ -import os -import re -import subprocess -import sys - -from setuptools import Extension, find_packages, setup -from setuptools.command.build_ext import build_ext - -# Convert distutils Windows platform specifiers to CMake -A arguments -PLAT_TO_CMAKE = { - "win32": "Win32", - "win-amd64": "x64", - "win-arm32": "ARM", - "win-arm64": "ARM64", -} - - -# A CMakeExtension needs a sourcedir instead of a file list. -# The name must be the _single_ output extension from the CMake build. -# If you need multiple extensions, see scikit-build. -class CMakeExtension(Extension): - def __init__(self, name, sourcedir=""): - Extension.__init__(self, name, sources=[]) - self.sourcedir = os.path.abspath(sourcedir) - - -class CMakeBuild(build_ext): - def build_extension(self, ext): - extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) - - # required for auto-detection & inclusion of auxiliary "native" libs - if not extdir.endswith(os.path.sep): - extdir += os.path.sep - - debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug - cfg = "Debug" if debug else "Release" - - # CMake lets you override the generator - we need to check this. - # Can be set with Conda-Build, for example. - cmake_generator = os.environ.get("CMAKE_GENERATOR", "") - - # Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON - # FAST_CROSSING_VERSION_INFO shows you how to pass a value into the C++ code - # from Python. - cmake_args = [ - f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}", - f"-DPYTHON_EXECUTABLE={sys.executable}", - f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm - ] - build_args = [] - # Adding CMake arguments set as environment variable - # (needed e.g. to build for ARM OSx on conda-forge) - if "CMAKE_ARGS" in os.environ: - cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] - - # In this example, we pass in the version to C++. You might not need to. - cmake_args += [ - f"-DFAST_CROSSING_VERSION_INFO={self.distribution.get_version()}" - ] - - if self.compiler.compiler_type != "msvc": - # Using Ninja-build since it a) is available as a wheel and b) - # multithreads automatically. MSVC would require all variables be - # exported for Ninja to pick it up, which is a little tricky to do. - # Users can override the generator with CMAKE_GENERATOR in CMake - # 3.15+. - if not cmake_generator or cmake_generator == "Ninja": - try: - import ninja # noqa: F401 - - ninja_executable_path = os.path.join(ninja.BIN_DIR, "ninja") - cmake_args += [ - "-GNinja", - f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}", - ] - except ImportError: - pass - - else: - - # Single config generators are handled "normally" - single_config = any(x in cmake_generator for x in {"NMake", "Ninja"}) - - # CMake allows an arch-in-generator style for backward compatibility - contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"}) - - # Specify the arch if using MSVC generator, but only if it doesn't - # contain a backward-compatibility arch spec already in the - # generator name. - if not single_config and not contains_arch: - cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] - - # Multi-config generators have a different way to specify configs - if not single_config: - cmake_args += [ - f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}" - ] - build_args += ["--config", cfg] - - if sys.platform.startswith("darwin"): - # Cross-compile support for macOS - respect ARCHFLAGS if set - archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", "")) - if archs: - cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] - - # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level - # across all generators. - if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: - # self.parallel is a Python 3 only way to set parallel jobs by hand - # using -j in the build_ext call, not supported by pip or PyPA-build. - if hasattr(self, "parallel") and self.parallel: - # CMake 3.12+ only. - build_args += [f"-j{self.parallel}"] - - build_temp = os.path.join(self.build_temp, ext.name) - if not os.path.exists(build_temp): - os.makedirs(build_temp) - - subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp) - subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=build_temp) - - -# The information here can also be placed in setup.cfg - better separation of -# logic and declaration, and simpler if you include description/version in a file. -setup( - name="fast_crossing", - version="0.0.9", - author="tzx", - author_email="dvorak4tzx@gmail.com", - url="https://fast-crossing.readthedocs.io", - description="fast crossing", - long_description=open("README.md", encoding="utf-8").read(), - long_description_content_type="text/markdown", - packages=find_packages(), - ext_modules=[CMakeExtension("fast_crossing")], - cmdclass={"build_ext": CMakeBuild}, - zip_safe=False, - install_requires=["numpy"], - extras_require={"test": ["pytest>=6.0", "scipy"]}, -) diff --git a/src/fast_crossing/__init__.py b/src/fast_crossing/__init__.py new file mode 100644 index 0000000..337c4f9 --- /dev/null +++ b/src/fast_crossing/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from ._core import * # noqa: F403 +from ._core import __version__ # noqa: F401 diff --git a/src/fast_crossing/_core/__init__.pyi b/src/fast_crossing/_core/__init__.pyi new file mode 100644 index 0000000..a80cded --- /dev/null +++ b/src/fast_crossing/_core/__init__.pyi @@ -0,0 +1,1665 @@ +from __future__ import annotations +import numpy +import typing +from . import tf + +__all__ = [ + "Arrow", + "FastCrossing", + "FlatBush", + "KdQuiver", + "KdTree", + "LineSegment", + "PolylineRuler", + "Quiver", + "densify_polyline", + "douglas_simplify", + "douglas_simplify_indexes", + "douglas_simplify_mask", + "intersect_segments", + "point_in_polygon", + "polyline_in_polygon", + "snap_onto_2d", + "tf", +] + +class Arrow: + @staticmethod + def _angle( + vec: numpy.ndarray[numpy.float64[3, 1]], + *, + ref: numpy.ndarray[numpy.float64[3, 1]], + ) -> float: + """ + Calculate angle between two vectors + """ + @staticmethod + @typing.overload + def _heading(heading: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Convert heading to unit vector + """ + @staticmethod + @typing.overload + def _heading(east: float, north: float) -> float: + """ + Convert east and north components to heading + """ + @staticmethod + def _unit_vector( + vector: numpy.ndarray[numpy.float64[3, 1]], with_eps: bool = True + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Normalize a vector to unit length + """ + def Frenet(self) -> numpy.ndarray[numpy.float64[3, 3]]: + """ + Get the Frenet frame of the Arrow + """ + def __copy__(self, arg0: dict) -> Arrow: + """ + Create a copy of the Arrow + """ + @typing.overload + def __init__(self) -> None: + """ + Default constructor for Arrow + """ + @typing.overload + def __init__(self, position: numpy.ndarray[numpy.float64[3, 1]]) -> None: + """ + Constructor for Arrow with position + """ + @typing.overload + def __init__( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + direction: numpy.ndarray[numpy.float64[3, 1]], + ) -> None: + """ + Constructor for Arrow with position and direction + """ + def __repr__(self) -> str: ... + def copy(self) -> Arrow: + """ + Create a copy of the Arrow + """ + @typing.overload + def direction(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the direction of the Arrow + """ + @typing.overload + def direction(self, arg0: numpy.ndarray[numpy.float64[3, 1]]) -> Arrow: + """ + Set the direction of the Arrow + """ + def forward(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the forward direction of the Arrow + """ + def has_index(self, check_range: bool = True) -> bool: + """ + Check if the Arrow has a valid index + """ + @typing.overload + def heading(self) -> float: + """ + Get the heading of the Arrow + """ + @typing.overload + def heading(self, new_value: float) -> Arrow: + """ + Set the heading of the Arrow + """ + @typing.overload + def label(self) -> numpy.ndarray[numpy.int32[2, 1]]: + """ + Get the label of the Arrow + """ + @typing.overload + def label(self, new_value: numpy.ndarray[numpy.int32[2, 1]]) -> Arrow: + """ + Set the label of the Arrow + """ + @typing.overload + def label( + self, + polyline_index: int, + segment_index: int, + *, + t: float | None = None, + range: float | None = None, + ) -> Arrow: + """ + Set the label of the Arrow with polyline and segment indices + """ + def leftward(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the leftward direction of the Arrow + """ + @typing.overload + def polyline_index(self) -> int: + """ + Get the polyline index of the Arrow + """ + @typing.overload + def polyline_index(self, new_value: int) -> Arrow: + """ + Set the polyline index of the Arrow + """ + @typing.overload + def position(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the position of the Arrow + """ + @typing.overload + def position(self, new_value: numpy.ndarray[numpy.float64[3, 1]]) -> Arrow: + """ + Set the position of the Arrow + """ + @typing.overload + def range(self) -> float: + """ + Get the range of the Arrow + """ + @typing.overload + def range(self, new_value: float) -> Arrow: + """ + Set the range of the Arrow + """ + def reset_index(self) -> None: + """ + Reset the index of the Arrow + """ + @typing.overload + def segment_index(self) -> int: + """ + Get the segment index of the Arrow + """ + @typing.overload + def segment_index(self, new_value: int) -> Arrow: + """ + Set the segment index of the Arrow + """ + @typing.overload + def t(self) -> float: + """ + Get the t parameter of the Arrow + """ + @typing.overload + def t(self, new_value: float) -> Arrow: + """ + Set the t parameter of the Arrow + """ + def upward(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the upward direction of the Arrow + """ + +class FastCrossing: + def __init__(self, *, is_wgs84: bool = False) -> None: + """ + Initialize FastCrossing object. + + :param is_wgs84: Whether coordinates are in WGS84 format, defaults to false + """ + @typing.overload + def add_polyline( + self, polyline: numpy.ndarray[numpy.float64[m, 3]], *, index: int = -1 + ) -> int: + """ + Add polyline to the tree. + + :param polyline: The polyline to add + :param index: Custom polyline index, defaults to -1 + :return: The index of the added polyline + """ + @typing.overload + def add_polyline( + self, + polyline: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + index: int = -1, + ) -> int: + """ + Add polyline to the tree (alternative format). + + :param polyline: The polyline to add + :param index: Custom polyline index, defaults to -1 + :return: The index of the added polyline + """ + def arrow( + self, *, polyline_index: int, point_index: int + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], numpy.ndarray[numpy.float64[3, 1]]]: + """ + Get an arrow (position and direction) at a specific point on a polyline. + + :param polyline_index: Index of the polyline + :param point_index: Index of the point within the polyline + :return: Arrow (position and direction) + """ + def bush(self, autobuild: bool = True) -> ...: + """ + Export the internal FlatBush index. + + :param autobuild: Whether to automatically build the index if not already built, defaults to true + :return: FlatBush index + """ + @typing.overload + def coordinates( + self, polyline_index: int, segment_index: int, ratio: float + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get coordinates at a specific position on a polyline. + + :param polyline_index: Index of the polyline + :param segment_index: Index of the segment within the polyline + :param ratio: Ratio along the segment (0 to 1) + :return: Coordinates at the specified position + """ + @typing.overload + def coordinates( + self, index: numpy.ndarray[numpy.int32[2, 1]], ratio: float + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get coordinates at a specific position on a polyline (alternative format). + + :param index: Combined index of polyline and segment + :param ratio: Ratio along the segment (0 to 1) + :return: Coordinates at the specified position + """ + @typing.overload + def coordinates( + self, + intersection: tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ], + second: bool = True, + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get coordinates of an intersection. + + :param intersection: The intersection object + :param second: Whether to use the second polyline of the intersection, defaults to true + :return: Coordinates of the intersection + """ + def finish(self) -> None: + """ + Finalize indexing after adding all polylines + """ + @typing.overload + def intersections( + self, + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get all segment intersections in the tree + """ + @typing.overload + def intersections( + self, *, z_offset_range: tuple[float, float], self_intersection: int = 2 + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get segment intersections in the tree, filtered by conditions. + + :param z_offset_range: Z-offset range for filtering + :param self_intersection: Self-intersection parameter, defaults to 2 + """ + @typing.overload + def intersections( + self, + start: numpy.ndarray[numpy.float64[2, 1]], + to: numpy.ndarray[numpy.float64[2, 1]], + *, + dedup: bool = True, + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get crossing intersections with [start, to] segment. + + :param from: Start point of the segment + :param to: End point of the segment + :param dedup: Whether to remove duplicates, defaults to true + :return: Sorted intersections by t ratio + """ + @typing.overload + def intersections( + self, polyline: numpy.ndarray[numpy.float64[m, 3]], *, dedup: bool = True + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get crossing intersections with a polyline. + + :param polyline: The polyline to check intersections with + :param dedup: Whether to remove duplicates, defaults to true + :return: Sorted intersections by t ratio + """ + @typing.overload + def intersections( + self, + polyline: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + dedup: bool = True, + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get crossing intersections with a polyline (alternative format). + + :param polyline: The polyline to check intersections with + :param dedup: Whether to remove duplicates, defaults to true + :return: Sorted intersections by t ratio + """ + @typing.overload + def intersections( + self, + polyline: numpy.ndarray[numpy.float64[m, 3]], + *, + z_min: float, + z_max: float, + dedup: bool = True, + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get crossing intersections with a polyline, filtered by Z range. + + :param polyline: The polyline to check intersections with + :param z_min: Minimum Z value for filtering + :param z_max: Maximum Z value for filtering + :param dedup: Whether to remove duplicates, defaults to true + :return: Sorted intersections by t ratio + """ + @typing.overload + def intersections( + self, + polyline: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + z_min: float, + z_max: float, + dedup: bool = True, + ) -> list[ + tuple[ + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.float64[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + numpy.ndarray[numpy.int32[2, 1]], + ] + ]: + """ + Get crossing intersections with a polyline, filtered by Z range (alternative format). + + :param polyline: The polyline to check intersections with + :param z_min: Minimum Z value for filtering + :param z_max: Maximum Z value for filtering + :param dedup: Whether to remove duplicates, defaults to true + :return: Sorted intersections by t ratio + """ + def is_wgs84(self) -> bool: + """ + Check if the coordinates are in WGS84 format. + + :return: True if coordinates are in WGS84 format, False otherwise + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[2, 1]], float]: + """ + Find the nearest point to a given position. + + :param position: The query position + :param return_squared_l2: Whether to return squared L2 distance, defaults to false + :return: Nearest point information + """ + @typing.overload + def nearest( + self, + index: numpy.ndarray[numpy.int32[2, 1]], + *, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[2, 1]], float]: + """ + Find the nearest point to a given index. + + :param index: The query index + :param return_squared_l2: Whether to return squared L2 distance, defaults to false + :return: Nearest point information + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + k: int | None = None, + radius: float | None = None, + sort: bool = True, + return_squared_l2: bool = False, + filter: tuple[numpy.ndarray[numpy.float64[3, 1]], ...] | None = None, + ) -> tuple[numpy.ndarray[numpy.int32[m, 2]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Find k nearest points to a given position with optional filtering. + + :param position: The query position + :param k: Number of nearest neighbors to find (optional) + :param radius: Search radius (optional) + :param sort: Whether to sort the results, defaults to true + :param return_squared_l2: Whether to return squared L2 distance, defaults to false + :param filter: Optional filter parameters + :return: Nearest points information + """ + def num_poylines(self) -> int: + """ + Get the number of polylines in the FastCrossing object. + + :return: Number of polylines + """ + @typing.overload + def point_index(self, index: int) -> numpy.ndarray[numpy.int32[2, 1]]: + """ + Get point index for a given index. + + :param index: The index to query + :return: The point index + """ + @typing.overload + def point_index( + self, indexes: numpy.ndarray[numpy.int32[m, 1]] + ) -> list[numpy.ndarray[numpy.int32[2, 1]]]: + """ + Get point indexes for given indexes. + + :param indexes: The indexes to query + :return: The point indexes + """ + def polyline_ruler(self, index: int) -> ...: + """ + Get a specific polyline ruler. + + :param index: Index of the polyline + :return: Polyline ruler for the specified index + """ + def polyline_rulers(self) -> dict[int, ...]: + """ + Get all polyline rulers. + + :return: Dictionary of polyline rulers + """ + def quiver(self) -> ...: + """ + Export the internal Quiver object. + + :return: Quiver object + """ + @typing.overload + def segment_index(self, index: int) -> numpy.ndarray[numpy.int32[2, 1]]: + """ + Get segment index for a given index. + + :param index: The index to query + :return: The segment index + """ + @typing.overload + def segment_index( + self, indexes: numpy.ndarray[numpy.int32[m, 1]] + ) -> list[numpy.ndarray[numpy.int32[2, 1]]]: + """ + Get segment indexes for given indexes. + + :param indexes: The indexes to query + :return: The segment indexes + """ + @typing.overload + def within( + self, + *, + min: numpy.ndarray[numpy.float64[2, 1]], + max: numpy.ndarray[numpy.float64[2, 1]], + segment_wise: bool = True, + sort: bool = True, + ) -> list[numpy.ndarray[numpy.int32[2, 1]]]: + """ + Find polylines within a bounding box. + + :param min: Minimum corner of the bounding box + :param max: Maximum corner of the bounding box + :param segment_wise: Whether to return segment-wise results, defaults to true + :param sort: Whether to sort the results, defaults to true + :return: Polylines within the bounding box + """ + @typing.overload + def within( + self, + *, + polygon: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + segment_wise: bool = True, + sort: bool = True, + ) -> list[numpy.ndarray[numpy.int32[2, 1]]]: + """ + Find polylines within a polygon. + + :param polygon: The polygon to check against + :param segment_wise: Whether to return segment-wise results, defaults to true + :param sort: Whether to sort the results, defaults to true + :return: Polylines within the polygon + """ + @typing.overload + def within( + self, + *, + center: numpy.ndarray[numpy.float64[2, 1]], + width: float, + height: float, + heading: float = 0.0, + segment_wise: bool = True, + sort: bool = True, + ) -> list[numpy.ndarray[numpy.int32[2, 1]]]: + """ + Find polylines within a rotated rectangle. + + :param center: Center of the rectangle + :param width: Width of the rectangle + :param height: Height of the rectangle + :param heading: Heading angle of the rectangle, defaults to 0.0 + :param segment_wise: Whether to return segment-wise results, defaults to true + :param sort: Whether to sort the results, defaults to true + :return: Polylines within the rotated rectangle + """ + +class FlatBush: + @typing.overload + def __init__(self) -> None: + """ + Initialize an empty FlatBush index. + """ + @typing.overload + def __init__(self, reserve: int) -> None: + """ + Initialize a FlatBush index with a reserved capacity. + + :param reserve: Number of items to reserve space for + """ + @typing.overload + def add( + self, + minX: float, + minY: float, + maxX: float, + maxY: float, + *, + label0: int = -1, + label1: int = -1, + ) -> int: + """ + Add a bounding box to the index. + + :param minX: Minimum X coordinate of the bounding box + :param minY: Minimum Y coordinate of the bounding box + :param maxX: Maximum X coordinate of the bounding box + :param maxY: Maximum Y coordinate of the bounding box + :param label0: First label (optional) + :param label1: Second label (optional) + :return: Index of the added item + """ + @typing.overload + def add( + self, + polyline: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + label0: int = -1, + ) -> int: + """ + Add a polyline to the index. + + :param polyline: Polyline coordinates + :param label0: Label for the polyline (optional) + :return: Index of the added item + """ + @typing.overload + def add( + self, + box: numpy.ndarray[numpy.float64[4, 1]], + *, + label0: int = -1, + label1: int = -1, + ) -> int: + """ + Add a bounding box to the index using a vector. + + :param box: Vector of [minX, minY, maxX, maxY] + :param label0: First label (optional) + :param label1: Second label (optional) + :return: Index of the added item + """ + def box(self, index: int) -> numpy.ndarray[numpy.float64[4, 1]]: + """ + Get the bounding box for a specific index. + + :param index: Index of the item + :return: Bounding box of the item + """ + def boxes( + self, + ) -> numpy.ndarray[numpy.float64[m, 4], numpy.ndarray.flags.c_contiguous]: + """ + Get all bounding boxes in the index. + + :return: Reference to the vector of bounding boxes + """ + def finish(self) -> None: + """ + Finish the index construction. + """ + def label(self, index: int) -> numpy.ndarray[numpy.int32[2, 1]]: + """ + Get the label for a specific index. + + :param index: Index of the item + :return: Label of the item + """ + def labels( + self, + ) -> numpy.ndarray[numpy.int32[m, 2], numpy.ndarray.flags.c_contiguous]: + """ + Get all labels in the index. + + :return: Reference to the vector of labels + """ + def reserve(self, arg0: int) -> None: + """ + Reserve space for a number of items. + + :param n: Number of items to reserve space for + """ + @typing.overload + def search(self, minX: float, minY: float, maxX: float, maxY: float) -> list[int]: + """ + Search for items within a bounding box. + + :param minX: Minimum X coordinate of the search box + :param minY: Minimum Y coordinate of the search box + :param maxX: Maximum X coordinate of the search box + :param maxY: Maximum Y coordinate of the search box + :return: Vector of indices of items within the search box + """ + @typing.overload + def search(self, bbox: numpy.ndarray[numpy.float64[4, 1]]) -> list[int]: + """ + Search for items within a bounding box using a vector. + + :param bbox: Vector of [minX, minY, maxX, maxY] + :return: Vector of indices of items within the search box + """ + @typing.overload + def search( + self, + min: numpy.ndarray[numpy.float64[2, 1]], + max: numpy.ndarray[numpy.float64[2, 1]], + ) -> list[int]: + """ + Search for items within a bounding box using min and max vectors. + + :param min: Vector of [minX, minY] + :param max: Vector of [maxX, maxY] + :return: Vector of indices of items within the search box + """ + def size(self) -> int: + """ + Get the number of items in the index. + + :return: Number of items in the index + """ + +class KdQuiver(Quiver): + @staticmethod + def _filter( + *, + arrows: list[Arrow], + arrow: Arrow, + params: Quiver.FilterParams, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Filter arrows based on the given parameters + """ + @typing.overload + def __init__(self) -> None: + """ + Default constructor for KdQuiver + """ + @typing.overload + def __init__(self, anchor_lla: numpy.ndarray[numpy.float64[3, 1]]) -> None: + """ + Constructor for KdQuiver with anchor LLA coordinates + """ + @typing.overload + def add(self, polyline: numpy.ndarray[numpy.float64[m, 3]], index: int = -1) -> int: + """ + Add a polyline to the KdQuiver + """ + @typing.overload + def add( + self, + polyline: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + index: int = -1, + ) -> int: + """ + Add a 2D polyline to the KdQuiver + """ + @typing.overload + def arrow(self, point_index: int) -> Arrow: + """ + Get the arrow at the given point index + """ + @typing.overload + def arrow(self, polyline_index: int, segment_index: int) -> Arrow: + """ + Get the arrow at the given polyline and segment indices + """ + @typing.overload + def arrow(self, polyline_index: int, segment_index: int, *, t: float) -> Arrow: + """ + Get the arrow at the given polyline, segment indices, and t parameter + """ + @typing.overload + def arrow(self, polyline_index: int, *, range: float) -> Arrow: + """ + Get the arrow at the given polyline index and range + """ + def arrows(self, indexes: numpy.ndarray[numpy.int32[m, 1]]) -> list[Arrow]: + """ + Get arrows for the given indexes + """ + def directions( + self, indexes: numpy.ndarray[numpy.int32[m, 1]] + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Get directions for the given indexes + """ + @typing.overload + def filter( + self, + *, + hits: numpy.ndarray[numpy.int32[m, 1]], + arrow: Arrow, + params: Quiver.FilterParams, + ) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Filter hits based on the given parameters + """ + @typing.overload + def filter( + self, + *, + hits: numpy.ndarray[numpy.int32[m, 1]], + norms: numpy.ndarray[numpy.float64[m, 1]], + arrow: Arrow, + params: Quiver.FilterParams, + ) -> tuple[numpy.ndarray[numpy.int32[m, 1]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Filter hits and norms based on the given parameters + """ + @typing.overload + def index(self, point_index: int) -> numpy.ndarray[numpy.int32[2, 1]]: + """ + Get the index for the given point index + """ + @typing.overload + def index(self, polyline_index: int, segment_index: int) -> int: + """ + Get the index for the given polyline and segment indices + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + return_squared_l2: bool = False, + ) -> tuple[int, float]: + """ + Find the nearest point to the given position + """ + @typing.overload + def nearest( + self, index: int, *, return_squared_l2: bool = False + ) -> tuple[int, float]: + """ + Find the nearest point to the point at the given index + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + k: int, + sort: bool = True, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[m, 1]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Find k nearest points to the given position + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + radius: float, + sort: bool = True, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[m, 1]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Find all points within a given radius of the query position + """ + @typing.overload + def positions( + self, + ) -> numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous]: + """ + Get all positions in the KdQuiver + """ + @typing.overload + def positions( + self, indexes: numpy.ndarray[numpy.int32[m, 1]] + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Get positions for the given indexes + """ + def reset(self) -> None: + """ + Reset the KdQuiver + """ + +class KdTree: + @typing.overload + def __init__(self, leafsize: int = 10) -> None: + """ + Initialize KdTree with specified leaf size. + + :param leafsize: Maximum number of points in leaf node, defaults to 10 + """ + @typing.overload + def __init__(self, points: numpy.ndarray[numpy.float64[m, 3]]) -> None: + """ + Initialize KdTree with 3D points. + + :param points: 3D points to initialize the tree + """ + @typing.overload + def __init__( + self, + points: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + ) -> None: + """ + Initialize KdTree with 2D points. + + :param points: 2D points to initialize the tree + """ + @typing.overload + def add(self, points: numpy.ndarray[numpy.float64[m, 3]]) -> None: + """ + Add 3D points to the KdTree. + + :param points: 3D points to add + """ + @typing.overload + def add( + self, + points: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + ) -> None: + """ + Add 2D points to the KdTree. + + :param points: 2D points to add + """ + def build_index(self, force_rebuild: bool = False) -> None: + """ + Build the KdTree index. + + :param force_rebuild: Force rebuilding the index even if already built, defaults to false + """ + def leafsize(self) -> int: + """ + Get the current leaf size of the KdTree. + + :return: Current leaf size + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + return_squared_l2: bool = False, + ) -> tuple[int, float]: + """ + Find the nearest point to the given position. + + :param position: Query position + :param return_squared_l2: If true, return squared L2 distance, defaults to false + :return: Tuple of (index, distance) + """ + @typing.overload + def nearest( + self, index: int, *, return_squared_l2: bool = False + ) -> tuple[int, float]: + """ + Find the nearest point to the point at the given index. + + :param index: Index of the query point + :param return_squared_l2: If true, return squared L2 distance, defaults to false + :return: Tuple of (index, distance) + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + k: int, + sort: bool = True, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[m, 1]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Find k nearest points to the given position. + + :param position: Query position + :param k: Number of nearest neighbors to find + :param sort: If true, sort results by distance, defaults to true + :param return_squared_l2: If true, return squared L2 distances, defaults to false + :return: Tuple of (indices, distances) + """ + @typing.overload + def nearest( + self, + position: numpy.ndarray[numpy.float64[3, 1]], + *, + radius: float, + sort: bool = True, + return_squared_l2: bool = False, + ) -> tuple[numpy.ndarray[numpy.int32[m, 1]], numpy.ndarray[numpy.float64[m, 1]]]: + """ + Find all points within a given radius of the query position. + + :param position: Query position + :param radius: Search radius + :param sort: If true, sort results by distance, defaults to true + :param return_squared_l2: If true, return squared L2 distances, defaults to false + :return: Tuple of (indices, distances) + """ + def points( + self, + ) -> numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous]: + """ + Get the points in the KdTree. + + :return: Reference to the points in the tree + """ + def reset(self) -> None: + """ + Reset the KdTree, clearing all points. + """ + def reset_index(self) -> None: + """ + Reset the index of the KdTree. + """ + def set_leafsize(self, value: int) -> None: + """ + Set the leaf size of the KdTree. + + :param value: New leaf size value + """ + +class LineSegment: + def __init__( + self, + A: numpy.ndarray[numpy.float64[3, 1]], + B: numpy.ndarray[numpy.float64[3, 1]], + ) -> None: + """ + Initialize a LineSegment with two 3D points. + """ + def distance(self, P: numpy.ndarray[numpy.float64[3, 1]]) -> float: + """ + Calculate the distance from a point to the line segment. + """ + def distance2(self, P: numpy.ndarray[numpy.float64[3, 1]]) -> float: + """ + Calculate the squared distance from a point to the line segment. + """ + def intersects( + self, other: LineSegment + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], float, float, float] | None: + """ + Check if this line segment intersects with another. + """ + @property + def A(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the start point of the line segment. + """ + @property + def AB(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the vector from A to B. + """ + @property + def B(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the end point of the line segment. + """ + @property + def length(self) -> float: + """ + Get the length of the line segment. + """ + @property + def length2(self) -> float: + """ + Get the squared length of the line segment. + """ + +class PolylineRuler: + @staticmethod + def _along( + line: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + dist: float, + *, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Find a point at a specified distance along a polyline. + """ + @staticmethod + def _dirs( + polyline: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Calculate direction vectors for each segment of a polyline. + """ + @staticmethod + def _distance( + a: numpy.ndarray[numpy.float64[3, 1]], + b: numpy.ndarray[numpy.float64[3, 1]], + *, + is_wgs84: bool = False, + ) -> float: + """ + Calculate the distance between two points. + """ + @staticmethod + def _interpolate( + A: numpy.ndarray[numpy.float64[3, 1]], + B: numpy.ndarray[numpy.float64[3, 1]], + *, + t: float, + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Interpolate between two points. + """ + @staticmethod + def _lineDistance( + line: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> float: + """ + Calculate the total length of a polyline. + """ + @staticmethod + def _lineSlice( + start: numpy.ndarray[numpy.float64[3, 1]], + stop: numpy.ndarray[numpy.float64[3, 1]], + line: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Extract a portion of a polyline between two points. + """ + @staticmethod + def _lineSliceAlong( + start: float, + stop: float, + line: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Extract a portion of a polyline between two distances along it. + """ + @staticmethod + def _pointOnLine( + line: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + P: numpy.ndarray[numpy.float64[3, 1]], + *, + is_wgs84: bool = False, + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], int, float]: + """ + Find the closest point on a polyline to a given point. + """ + @staticmethod + def _pointToSegmentDistance( + P: numpy.ndarray[numpy.float64[3, 1]], + A: numpy.ndarray[numpy.float64[3, 1]], + B: numpy.ndarray[numpy.float64[3, 1]], + *, + is_wgs84: bool = False, + ) -> float: + """ + Calculate the distance from a point to a line segment. + """ + @staticmethod + def _ranges( + polyline: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> numpy.ndarray[numpy.float64[m, 1]]: + """ + Calculate cumulative distances along a polyline. + """ + @staticmethod + def _squareDistance( + a: numpy.ndarray[numpy.float64[3, 1]], + b: numpy.ndarray[numpy.float64[3, 1]], + *, + is_wgs84: bool = False, + ) -> float: + """ + Calculate the squared distance between two points. + """ + def N(self) -> int: + """ + Get the number of points in the polyline. + """ + def __init__( + self, + coords: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, + ) -> None: + """ + Initialize a PolylineRuler with coordinates and coordinate system. + """ + def along(self, dist: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Find a point at a specified distance along the polyline. + """ + @typing.overload + def arrow( + self, *, index: int, t: float + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], numpy.ndarray[numpy.float64[3, 1]]]: + """ + Get the arrow (point and direction) at a specific segment index and interpolation factor. + """ + @typing.overload + def arrow( + self, range: float, *, smooth_joint: bool = True + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], numpy.ndarray[numpy.float64[3, 1]]]: + """ + Get the arrow (point and direction) at a specific cumulative distance. + """ + @typing.overload + def arrows( + self, ranges: numpy.ndarray[numpy.float64[m, 1]], *, smooth_joint: bool = True + ) -> tuple[ + numpy.ndarray[numpy.float64[m, 1]], + numpy.ndarray[numpy.float64[m, 3]], + numpy.ndarray[numpy.float64[m, 3]], + ]: + """ + Get arrows (points and directions) at multiple cumulative distances. + """ + @typing.overload + def arrows( + self, step: float, *, with_last: bool = True, smooth_joint: bool = True + ) -> tuple[ + numpy.ndarray[numpy.float64[m, 1]], + numpy.ndarray[numpy.float64[m, 3]], + numpy.ndarray[numpy.float64[m, 3]], + ]: + """ + Get arrows (points and directions) at regular intervals along the polyline. + """ + @typing.overload + def at(self, *, range: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the point on the polyline at a specific cumulative distance. + """ + @typing.overload + def at(self, *, segment_index: int) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the point on the polyline at a specific segment index. + """ + @typing.overload + def at(self, *, segment_index: int, t: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the point on the polyline at a specific segment index and interpolation factor. + """ + @typing.overload + def dir(self, *, point_index: int) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the direction vector at a specific point index. + """ + @typing.overload + def dir( + self, *, range: float, smooth_joint: bool = True + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the direction vector at a specific cumulative distance. + """ + def dirs(self) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Get direction vectors for each segment of the polyline. + """ + def extended_along(self, range: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the extended cumulative distance along the polyline. + """ + def is_wgs84(self) -> bool: + """ + Check if the coordinate system is WGS84. + """ + def k(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the scale factor for distance calculations. + """ + def length(self) -> float: + """ + Get the total length of the polyline. + """ + def lineDistance(self) -> float: + """ + Get the total length of the polyline. + """ + def lineSlice( + self, + start: numpy.ndarray[numpy.float64[3, 1]], + stop: numpy.ndarray[numpy.float64[3, 1]], + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Extract a portion of the polyline between two points. + """ + def lineSliceAlong( + self, start: float, stop: float + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Extract a portion of the polyline between two distances along it. + """ + def local_frame( + self, range: float, *, smooth_joint: bool = True + ) -> numpy.ndarray[numpy.float64[4, 4]]: + """ + Get the local coordinate frame at a specific cumulative distance. + """ + def pointOnLine( + self, P: numpy.ndarray[numpy.float64[3, 1]] + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], int, float]: + """ + Find the closest point on the polyline to a given point. + """ + def polyline(self) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Get the polyline coordinates. + """ + @typing.overload + def range(self, segment_index: int) -> float: + """ + Get the cumulative distance at a specific segment index. + """ + @typing.overload + def range(self, *, segment_index: int, t: float) -> float: + """ + Get the cumulative distance at a specific segment index and interpolation factor. + """ + def ranges(self) -> numpy.ndarray[numpy.float64[m, 1]]: + """ + Get cumulative distances along the polyline. + """ + def scanline( + self, range: float, *, min: float, max: float, smooth_joint: bool = True + ) -> tuple[numpy.ndarray[numpy.float64[3, 1]], numpy.ndarray[numpy.float64[3, 1]]]: + """ + Generate a scanline perpendicular to the polyline at a specific cumulative distance. + """ + def segment_index(self, range: float) -> int: + """ + Get the segment index for a given cumulative distance. + """ + def segment_index_t(self, range: float) -> tuple[int, float]: + """ + Get the segment index and interpolation factor for a given cumulative distance. + """ + +class Quiver: + class FilterParams: + def __init__(self) -> None: + """ + Default constructor for FilterParams + """ + @typing.overload + def angle_slots(self) -> numpy.ndarray[numpy.float64[m, 1]] | None: + """ + Get the angle slots of the FilterParams + """ + @typing.overload + def angle_slots( + self, arg0: numpy.ndarray[numpy.float64[m, 1]] | None + ) -> Quiver.FilterParams: + """ + Set the angle slots of the FilterParams + """ + def is_trivial(self) -> bool: + """ + Check if the FilterParams is trivial + """ + @typing.overload + def x_slots(self) -> numpy.ndarray[numpy.float64[m, 1]] | None: + """ + Get the x slots of the FilterParams + """ + @typing.overload + def x_slots( + self, arg0: numpy.ndarray[numpy.float64[m, 1]] | None + ) -> Quiver.FilterParams: + """ + Set the x slots of the FilterParams + """ + @typing.overload + def y_slots(self) -> numpy.ndarray[numpy.float64[m, 1]] | None: + """ + Get the y slots of the FilterParams + """ + @typing.overload + def y_slots( + self, arg0: numpy.ndarray[numpy.float64[m, 1]] | None + ) -> Quiver.FilterParams: + """ + Set the y slots of the FilterParams + """ + @typing.overload + def z_slots(self) -> numpy.ndarray[numpy.float64[m, 1]] | None: + """ + Get the z slots of the FilterParams + """ + @typing.overload + def z_slots( + self, arg0: numpy.ndarray[numpy.float64[m, 1]] | None + ) -> Quiver.FilterParams: + """ + Set the z slots of the FilterParams + """ + + @staticmethod + def _k(arg0: float) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the constant k + """ + @typing.overload + def __init__(self) -> None: + """ + Default constructor for Quiver + """ + @typing.overload + def __init__(self, anchor_lla: numpy.ndarray[numpy.float64[3, 1]]) -> None: + """ + Constructor for Quiver with anchor LLA coordinates + """ + def anchor(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the anchor point of the Quiver + """ + @typing.overload + def enu2lla( + self, coords: numpy.ndarray[numpy.float64[3, 1]] + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Convert ENU coordinates to LLA + """ + @typing.overload + def enu2lla( + self, coords: numpy.ndarray[numpy.float64[m, 3]] + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Convert multiple ENU coordinates to LLA + """ + def forwards(self, arrow: Arrow, delta_x: float) -> Arrow: + """ + Move the Arrow forward by delta_x + """ + def inv_k(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the inverse k value of the Quiver + """ + def is_wgs84(self) -> bool: + """ + Check if the Quiver is using WGS84 coordinates + """ + def k(self) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Get the k value of the Quiver + """ + def leftwards(self, arrow: Arrow, delta_y: float) -> Arrow: + """ + Move the Arrow leftward by delta_y + """ + @typing.overload + def lla2enu( + self, coords: numpy.ndarray[numpy.float64[3, 1]] + ) -> numpy.ndarray[numpy.float64[3, 1]]: + """ + Convert LLA coordinates to ENU + """ + @typing.overload + def lla2enu( + self, coords: numpy.ndarray[numpy.float64[m, 3]] + ) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Convert multiple LLA coordinates to ENU + """ + def towards( + self, + arrow: Arrow, + delta_frenet: numpy.ndarray[numpy.float64[3, 1]], + *, + update_direction: bool = True, + ) -> Arrow: + """ + Move the Arrow in Frenet coordinates + """ + def update( + self, + arrow: Arrow, + delta_enu: numpy.ndarray[numpy.float64[3, 1]], + *, + update_direction: bool = True, + ) -> Arrow: + """ + Update the Arrow's position and optionally direction + """ + def upwards(self, arrow: Arrow, delta_z: float) -> Arrow: + """ + Move the Arrow upward by delta_z + """ + +def densify_polyline( + polyline: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + max_gap: float, +) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + densify polyline, interpolate to satisfy max_gap + """ + +@typing.overload +def douglas_simplify( + coords: numpy.ndarray[numpy.float64[m, 3]], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Simplify a polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def douglas_simplify( + coords: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.float64[m, 2]]: + """ + Simplify a 2D polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def douglas_simplify_indexes( + coords: numpy.ndarray[numpy.float64[m, 3]], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Get indexes of points to keep when simplifying a polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def douglas_simplify_indexes( + coords: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Get indexes of points to keep when simplifying a 2D polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def douglas_simplify_mask( + coords: numpy.ndarray[numpy.float64[m, 3]], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Get a mask of points to keep when simplifying a polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def douglas_simplify_mask( + coords: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + epsilon: float, + *, + is_wgs84: bool = False, + recursive: bool = True, +) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + Get a mask of points to keep when simplifying a 2D polyline using the Douglas-Peucker algorithm. + """ + +@typing.overload +def intersect_segments( + a1: numpy.ndarray[numpy.float64[2, 1]], + a2: numpy.ndarray[numpy.float64[2, 1]], + b1: numpy.ndarray[numpy.float64[2, 1]], + b2: numpy.ndarray[numpy.float64[2, 1]], +) -> tuple[numpy.ndarray[numpy.float64[2, 1]], float, float] | None: + """ + Intersect two 2D line segments. + """ + +@typing.overload +def intersect_segments( + a1: numpy.ndarray[numpy.float64[3, 1]], + a2: numpy.ndarray[numpy.float64[3, 1]], + b1: numpy.ndarray[numpy.float64[3, 1]], + b2: numpy.ndarray[numpy.float64[3, 1]], +) -> tuple[numpy.ndarray[numpy.float64[3, 1]], float, float, float] | None: + """ + Intersect two 3D line segments. + """ + +def point_in_polygon( + *, + points: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + polygon: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], +) -> numpy.ndarray[numpy.int32[m, 1]]: + """ + point-in-polygon test, returns 0-1 mask + """ + +@typing.overload +def polyline_in_polygon( + polyline: numpy.ndarray[numpy.float64[m, 3]], + polygon: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + fc: FastCrossing, +) -> dict[ + tuple[int, float, float, int, float, float], numpy.ndarray[numpy.float64[m, 3]] +]: ... +@typing.overload +def polyline_in_polygon( + polyline: numpy.ndarray[numpy.float64[m, 3]], + polygon: numpy.ndarray[numpy.float64[m, 2], numpy.ndarray.flags.c_contiguous], + *, + is_wgs84: bool = False, +) -> dict[ + tuple[int, float, float, int, float, float], numpy.ndarray[numpy.float64[m, 3]] +]: ... +def snap_onto_2d( + P: numpy.ndarray[numpy.float64[2, 1]], + A: numpy.ndarray[numpy.float64[2, 1]], + B: numpy.ndarray[numpy.float64[2, 1]], +) -> tuple[numpy.ndarray[numpy.float64[2, 1]], float, float]: + """ + Snap P onto line segment AB + """ + +__version__: str = "0.1.0" diff --git a/src/fast_crossing/_core/tf.pyi b/src/fast_crossing/_core/tf.pyi new file mode 100644 index 0000000..2442a6d --- /dev/null +++ b/src/fast_crossing/_core/tf.pyi @@ -0,0 +1,133 @@ +from __future__ import annotations +import numpy +import typing + +__all__ = [ + "R_ecef_enu", + "T_ecef_enu", + "apply_transform", + "apply_transform_inplace", + "cheap_ruler_k", + "ecef2enu", + "ecef2lla", + "enu2ecef", + "enu2lla", + "lla2ecef", + "lla2enu", +] + +def R_ecef_enu(lon: float, lat: float) -> numpy.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]]: + """ + 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]]: + """ + 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]]: + """ + 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, + ], + *, + batch_size: int = 1000, +) -> None: + """ + Apply transformation matrix to coordinates in-place. + """ + +def cheap_ruler_k(latitude: float) -> numpy.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], + *, + anchor_lla: numpy.ndarray[numpy.float64[3, 1]] | None = None, + cheap_ruler: bool = False, +) -> numpy.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]]: + """ + 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]]: + """ + Convert multiple ECEF coordinates to LLA (Longitude, Latitude, Altitude). + """ + +def enu2ecef( + enus: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + anchor_lla: numpy.ndarray[numpy.float64[3, 1]], + cheap_ruler: bool = False, +) -> numpy.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], + *, + anchor_lla: numpy.ndarray[numpy.float64[3, 1]], + cheap_ruler: bool = True, +) -> numpy.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]]: + """ + 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]]: + """ + Convert multiple LLA (Longitude, Latitude, Altitude) to ECEF coordinates. + """ + +def lla2enu( + llas: numpy.ndarray[numpy.float64[m, 3], numpy.ndarray.flags.c_contiguous], + *, + anchor_lla: numpy.ndarray[numpy.float64[3, 1]] | None = None, + cheap_ruler: bool = True, +) -> numpy.ndarray[numpy.float64[m, 3]]: + """ + Convert LLA (Longitude, Latitude, Altitude) to ENU (East, North, Up) coordinates. + """ diff --git a/fast_crossing/cli/__init__.py b/src/fast_crossing/cli/__init__.py similarity index 100% rename from fast_crossing/cli/__init__.py rename to src/fast_crossing/cli/__init__.py diff --git a/src/fast_crossing/py.typed b/src/fast_crossing/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/fast_crossing/spatial.py b/src/fast_crossing/spatial.py similarity index 91% rename from fast_crossing/spatial.py rename to src/fast_crossing/spatial.py index b40d4f8..1b69b43 100644 --- a/fast_crossing/spatial.py +++ b/src/fast_crossing/spatial.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import numpy as np -from _pybind11_fast_crossing import KdTree as _KdTree + +from ._core import KdTree as _KdTree class KDTree: @@ -24,7 +27,7 @@ def query(self, x, k=1, *args, **kwargs): if isinstance(k, (int, np.integer)): ret_ii, ret_dd = [], [] for xyz in x: - xyz = self.vec3(xyz) + xyz = self.vec3(xyz) # noqa: PLW2901 if k == 1: ii, dd = self.tree.nearest(xyz) else: @@ -37,7 +40,7 @@ def query(self, x, k=1, *args, **kwargs): K = max(k) ret_ii, ret_dd = [], [] for xyz in x: - xyz = self.vec3(xyz) + xyz = self.vec3(xyz) # noqa: PLW2901 ii, dd = self.tree.nearest(xyz, k=K) ii = [ii[kk - 1] for kk in k] dd = [dd[kk - 1] for kk in k] @@ -68,7 +71,7 @@ def query_ball_point( return_sorted = x.ndim != 1 if x.ndim == 1: xyz = self.vec3(x) - ii, dd = self.tree.nearest( + ii, _ = self.tree.nearest( xyz, radius=r, sort=return_sorted, @@ -82,9 +85,9 @@ def query_ball_point( if isinstance(r, (int, float, np.number)): r = [r] * len(x) ret_ii = [] - for pp, rr in zip(x, r): # noqa + for pp, rr in zip(x, r): xyz = self.vec3(pp) - ii, dd = self.tree.nearest( + ii, _ = self.tree.nearest( xyz, radius=rr, sort=return_sorted, diff --git a/src/main.cpp b/src/main.cpp index 67e0ed3..d9c60bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,7 +36,7 @@ namespace py = pybind11; using namespace pybind11::literals; -PYBIND11_MODULE(_pybind11_fast_crossing, m) +PYBIND11_MODULE(_core, m) { cubao::bind_fast_crossing(m); cubao::bind_flatbush(m); diff --git a/src/polyline_in_polygon.hpp b/src/polyline_in_polygon.hpp index f0b6b03..943238c 100644 --- a/src/polyline_in_polygon.hpp +++ b/src/polyline_in_polygon.hpp @@ -26,44 +26,50 @@ polyline_in_polygon(const RowVectors &polyline, // const FastCrossing &fc) { auto intersections = fc.intersections(polyline); - // pt, (t, s), cur_label=(poly1, seg1), tree_label=(poly2, seg2) auto ruler = PolylineRuler(polyline, fc.is_wgs84()); - // 0.0, [r1, r2, ..., length] - const int N = intersections.size() + 1; - Eigen::VectorXd ranges(N); - int idx = -1; - for (auto &inter : intersections) { - int seg_idx = std::get<2>(inter)[1]; - double t = std::get<1>(inter)[0]; - double r = ruler.range(seg_idx, t); - ranges[++idx] = r; + if (intersections.empty()) { + int inside = point_in_polygon(polyline.block(0, 0, 1, 2), polygon)[0]; + if (!inside) { + return {}; + } + return PolylineChunks{ + {{0, 0.0, 0.0, ruler.N() - 2, 1.0, ruler.length()}, polyline}}; } - ranges[++idx] = ruler.length(); - RowVectorsNx2 midpoints(N, 3); + // pt, (t, s), cur_label=(poly1, seg1), tree_label=(poly2, seg2) + const int N = intersections.size() + 2; + // init ranges + Eigen::VectorXd ranges(N); { - idx = 0; - double r = 0.0; - while (idx < N) { - double rr = ranges[idx]; - midpoints.row(idx) = ruler.at((r + rr) / 2.0).head(2); - r = rr; - ++idx; + int idx = -1; + ranges[++idx] = 0.0; + for (auto &inter : intersections) { + int seg_idx = std::get<2>(inter)[1]; + double t = std::get<1>(inter)[0]; + double r = ruler.range(seg_idx, t); + ranges[++idx] = r; } + ranges[++idx] = ruler.length(); + } + // ranges o------o--------o-----------------o + // midpts ^ ^ ^ + RowVectorsNx2 midpoints(N - 1, 2); + for (int i = 0; i < N - 1; ++i) { + double rr = (ranges[i] + ranges[i + 1]) / 2.0; + midpoints.row(i) = ruler.along(rr).head(2); } auto mask = point_in_polygon(midpoints, polygon); PolylineChunks ret; { - idx = 0; - double r = 0.0; - while (idx < N) { - if (mask[idx] && ranges[idx] > r) { - auto [seg1, t1] = ruler.segment_index_t(r); - auto [seg2, t2] = ruler.segment_index_t(ranges[idx]); - ret.emplace(std::make_tuple(seg1, t1, r, seg2, t2, ranges[idx]), - ruler.lineSliceAlong(r, ranges[idx])); + for (int i = 0; i < N - 1; ++i) { + double r1 = ranges[i]; + double r2 = ranges[i + 1]; + if (r2 <= r1 || mask[i] == 0) { + continue; } - r = ranges[idx]; - ++idx; + auto [seg1, t1] = ruler.segment_index_t(r1); + auto [seg2, t2] = ruler.segment_index_t(r2); + ret.emplace(std::make_tuple(seg1, t1, r1, seg2, t2, r2), + ruler.lineSliceAlong(r1, r2)); } } return ret; diff --git a/src/pybind11_fast_crossing.hpp b/src/pybind11_fast_crossing.hpp index 4ae77c4..9944310 100644 --- a/src/pybind11_fast_crossing.hpp +++ b/src/pybind11_fast_crossing.hpp @@ -24,86 +24,139 @@ using rvp = py::return_value_policy; CUBAO_INLINE void bind_fast_crossing(py::module &m) { py::class_(m, "FastCrossing", py::module_local()) - .def(py::init(), py::kw_only(), "is_wgs84"_a = false) + .def(py::init(), py::kw_only(), "is_wgs84"_a = false, + "Initialize FastCrossing object.\n\n" + ":param is_wgs84: Whether coordinates are in WGS84 format, " + "defaults to false") // add polyline .def("add_polyline", - py::overload_cast( &FastCrossing::add_polyline), "polyline"_a, py::kw_only(), "index"_a = -1, - "add polyline to tree, you can " - "provide your own polyline index, " - "default: -1") + "Add polyline to the tree.\n\n" + ":param polyline: The polyline to add\n" + ":param index: Custom polyline index, defaults to -1\n" + ":return: The index of the added polyline") .def("add_polyline", - py::overload_cast< const Eigen::Ref &, int>(&FastCrossing::add_polyline), "polyline"_a, py::kw_only(), "index"_a = -1, - "add polyline to tree, you can " - "provide your own polyline index, " - "default: -1") - // finish + "Add polyline to the tree (alternative format).\n\n" + ":param polyline: The polyline to add\n" + ":param index: Custom polyline index, defaults to -1\n" + ":return: The index of the added polyline") + + .def("finish", &FastCrossing::finish, + "Finalize indexing after adding all polylines") - .def("finish", &FastCrossing::finish, "finish to finalize indexing") // intersections .def("intersections", py::overload_cast<>(&FastCrossing::intersections, py::const_), - "all segment intersections in tree") - .def("intersections", - py::overload_cast &, int>( - &FastCrossing::intersections, py::const_), - py::kw_only(), "z_offset_range"_a, "self_intersection"_a = 2, - "segment intersections in tree, filter by some condition") + "Get all segment intersections in the tree") + + .def( + "intersections", + py::overload_cast &, int>( + &FastCrossing::intersections, py::const_), + py::kw_only(), "z_offset_range"_a, "self_intersection"_a = 2, + "Get segment intersections in the tree, filtered by conditions.\n\n" + ":param z_offset_range: Z-offset range for filtering\n" + ":param self_intersection: Self-intersection parameter, defaults " + "to 2") + .def("intersections", py::overload_cast(&FastCrossing::intersections, py::const_), - "from"_a, "to"_a, py::kw_only(), "dedup"_a = true, - "crossing intersections with [from, to] segment " - "(sorted by t ratio)") + "start"_a, "to"_a, py::kw_only(), "dedup"_a = true, + "Get crossing intersections with [start, to] segment.\n\n" + ":param from: Start point of the segment\n" + ":param to: End point of the segment\n" + ":param dedup: Whether to remove duplicates, defaults to true\n" + ":return: Sorted intersections by t ratio") + .def("intersections", py::overload_cast( &FastCrossing::intersections, py::const_), "polyline"_a, py::kw_only(), "dedup"_a = true, - "crossing intersections with polyline (sorted by t ratio)") + "Get crossing intersections with a polyline.\n\n" + ":param polyline: The polyline to check intersections with\n" + ":param dedup: Whether to remove duplicates, defaults to true\n" + ":return: Sorted intersections by t ratio") + .def("intersections", py::overload_cast< const Eigen::Ref &, bool>(&FastCrossing::intersections, py::const_), "polyline"_a, py::kw_only(), "dedup"_a = true, - "crossing intersections with polyline (sorted by t ratio)") + "Get crossing intersections with a polyline (alternative " + "format).\n\n" + ":param polyline: The polyline to check intersections with\n" + ":param dedup: Whether to remove duplicates, defaults to true\n" + ":return: Sorted intersections by t ratio") + .def("intersections", py::overload_cast(&FastCrossing::intersections, py::const_), "polyline"_a, py::kw_only(), "z_min"_a, "z_max"_a, "dedup"_a = true, - "crossing intersections with polyline (sorted by t ratio)") + "Get crossing intersections with a polyline, filtered by Z " + "range.\n\n" + ":param polyline: The polyline to check intersections with\n" + ":param z_min: Minimum Z value for filtering\n" + ":param z_max: Maximum Z value for filtering\n" + ":param dedup: Whether to remove duplicates, defaults to true\n" + ":return: Sorted intersections by t ratio") + .def( "intersections", py::overload_cast< const Eigen::Ref &, double, double, bool>(&FastCrossing::intersections, py::const_), "polyline"_a, py::kw_only(), "z_min"_a, "z_max"_a, "dedup"_a = true, - "crossing intersections with polyline (sorted by t ratio)") + "Get crossing intersections with a polyline, filtered by Z range " + "(alternative format).\n\n" + ":param polyline: The polyline to check intersections with\n" + ":param z_min: Minimum Z value for filtering\n" + ":param z_max: Maximum Z value for filtering\n" + ":param dedup: Whether to remove duplicates, defaults to true\n" + ":return: Sorted intersections by t ratio") + // segment_index .def("segment_index", py::overload_cast(&FastCrossing::segment_index, py::const_), - "index"_a) + "index"_a, + "Get segment index for a given index.\n\n" + ":param index: The index to query\n" + ":return: The segment index") + .def("segment_index", py::overload_cast( &FastCrossing::segment_index, py::const_), - "indexes"_a) + "indexes"_a, + "Get segment indexes for given indexes.\n\n" + ":param indexes: The indexes to query\n" + ":return: The segment indexes") + // point_index .def("point_index", py::overload_cast(&FastCrossing::point_index, py::const_), - "index"_a) + "index"_a, + "Get point index for a given index.\n\n" + ":param index: The index to query\n" + ":return: The point index") + .def("point_index", py::overload_cast( &FastCrossing::point_index, py::const_), - "indexes"_a) + "indexes"_a, + "Get point indexes for given indexes.\n\n" + ":param indexes: The indexes to query\n" + ":return: The point indexes") + // within .def("within", py::overload_cast &, bool, bool>(&FastCrossing::within, py::const_), py::kw_only(), // "polygon"_a, // "segment_wise"_a = true, // - "sort"_a = true) + "sort"_a = true, + "Find polylines within a polygon.\n\n" + ":param polygon: The polygon to check against\n" + ":param segment_wise: Whether to return segment-wise results, " + "defaults to true\n" + ":param sort: Whether to sort the results, defaults to true\n" + ":return: Polylines within the polygon") + .def("within", py::overload_cast(&FastCrossing::within, py::const_), @@ -129,18 +197,40 @@ CUBAO_INLINE void bind_fast_crossing(py::module &m) "height"_a, // "heading"_a = 0.0, // "segment_wise"_a = true, // - "sort"_a = true) + "sort"_a = true, + "Find polylines within a rotated rectangle.\n\n" + ":param center: Center of the rectangle\n" + ":param width: Width of the rectangle\n" + ":param height: Height of the rectangle\n" + ":param heading: Heading angle of the rectangle, defaults to 0.0\n" + ":param segment_wise: Whether to return segment-wise results, " + "defaults to true\n" + ":param sort: Whether to sort the results, defaults to true\n" + ":return: Polylines within the rotated rectangle") + // nearest .def("nearest", py::overload_cast( &FastCrossing::nearest, py::const_), "position"_a, py::kw_only(), // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find the nearest point to a given position.\n\n" + ":param position: The query position\n" + ":param return_squared_l2: Whether to return squared L2 distance, " + "defaults to false\n" + ":return: Nearest point information") + .def("nearest", py::overload_cast( &FastCrossing::nearest, py::const_), "index"_a, py::kw_only(), // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find the nearest point to a given index.\n\n" + ":param index: The query index\n" + ":param return_squared_l2: Whether to return squared L2 distance, " + "defaults to false\n" + ":return: Nearest point information") + .def("nearest", py::overload_cast, // k @@ -156,36 +246,90 @@ CUBAO_INLINE void bind_fast_crossing(py::module &m) "radius"_a = std::nullopt, // "sort"_a = true, // "return_squared_l2"_a = false, // - "filter"_a = std::nullopt) + "filter"_a = std::nullopt, + "Find k nearest points to a given position with optional " + "filtering.\n\n" + ":param position: The query position\n" + ":param k: Number of nearest neighbors to find (optional)\n" + ":param radius: Search radius (optional)\n" + ":param sort: Whether to sort the results, defaults to true\n" + ":param return_squared_l2: Whether to return squared L2 distance, " + "defaults to false\n" + ":param filter: Optional filter parameters\n" + ":return: Nearest points information") + // coordinates .def("coordinates", py::overload_cast(&FastCrossing::coordinates, py::const_), - "polyline_index"_a, "segment_index"_a, "ratio"_a) + "polyline_index"_a, "segment_index"_a, "ratio"_a, + "Get coordinates at a specific position on a polyline.\n\n" + ":param polyline_index: Index of the polyline\n" + ":param segment_index: Index of the segment within the polyline\n" + ":param ratio: Ratio along the segment (0 to 1)\n" + ":return: Coordinates at the specified position") + .def("coordinates", py::overload_cast( &FastCrossing::coordinates, py::const_), - "index"_a, "ratio"_a) + "index"_a, "ratio"_a, + "Get coordinates at a specific position on a polyline " + "(alternative format).\n\n" + ":param index: Combined index of polyline and segment\n" + ":param ratio: Ratio along the segment (0 to 1)\n" + ":return: Coordinates at the specified position") + .def("coordinates", py::overload_cast( &FastCrossing::coordinates, py::const_), - "intersection"_a, "second"_a = true) + "intersection"_a, "second"_a = true, + "Get coordinates of an intersection.\n\n" + ":param intersection: The intersection object\n" + ":param second: Whether to use the second polyline of the " + "intersection, defaults to true\n" + ":return: Coordinates of the intersection") + // arrow .def("arrow", py::overload_cast(&FastCrossing::arrow, py::const_), - py::kw_only(), "polyline_index"_a, "point_index"_a) + py::kw_only(), "polyline_index"_a, "point_index"_a, + "Get an arrow (position and direction) at a specific point on a " + "polyline.\n\n" + ":param polyline_index: Index of the polyline\n" + ":param point_index: Index of the point within the polyline\n" + ":return: Arrow (position and direction)") + // - .def("is_wgs84", &FastCrossing::is_wgs84) - .def("num_poylines", &FastCrossing::num_poylines) + .def( + "is_wgs84", &FastCrossing::is_wgs84, + "Check if the coordinates are in WGS84 format.\n\n" + ":return: True if coordinates are in WGS84 format, False otherwise") + + .def("num_poylines", &FastCrossing::num_poylines, + "Get the number of polylines in the FastCrossing object.\n\n" + ":return: Number of polylines") + .def("polyline_rulers", &FastCrossing::polyline_rulers, - rvp::reference_internal) + rvp::reference_internal, + "Get all polyline rulers.\n\n" + ":return: Dictionary of polyline rulers") + .def("polyline_ruler", &FastCrossing::polyline_ruler, "index"_a, - rvp::reference_internal) + rvp::reference_internal, + "Get a specific polyline ruler.\n\n" + ":param index: Index of the polyline\n" + ":return: Polyline ruler for the specified index") + // export .def("bush", &FastCrossing::export_bush, "autobuild"_a = true, - rvp::reference_internal) - .def("quiver", &FastCrossing::export_quiver, rvp::reference_internal) - // - ; + rvp::reference_internal, + "Export the internal FlatBush index.\n\n" + ":param autobuild: Whether to automatically build the index if " + "not already built, defaults to true\n" + ":return: FlatBush index") + + .def("quiver", &FastCrossing::export_quiver, rvp::reference_internal, + "Export the internal Quiver object.\n\n" + ":return: Quiver object"); } } // namespace cubao diff --git a/src/pybind11_flatbush.hpp b/src/pybind11_flatbush.hpp index 24c1667..c0725c5 100644 --- a/src/pybind11_flatbush.hpp +++ b/src/pybind11_flatbush.hpp @@ -25,9 +25,13 @@ CUBAO_INLINE void bind_flatbush(py::module &m) { using FlatBush = flatbush::FlatBush; py::class_(m, "FlatBush", py::module_local()) - .def(py::init<>()) - .def(py::init(), "reserve"_a) - .def("reserve", &FlatBush::Reserve) + .def(py::init<>(), "Initialize an empty FlatBush index.") + .def(py::init(), "reserve"_a, + "Initialize a FlatBush index with a reserved capacity.\n\n" + ":param reserve: Number of items to reserve space for") + .def("reserve", &FlatBush::Reserve, + "Reserve space for a number of items.\n\n" + ":param n: Number of items to reserve space for") .def("add", py::overload_cast &, int>(&FlatBush::Add), "polyline"_a, // - py::kw_only(), "label0"_a = -1) - + py::kw_only(), "label0"_a = -1, + "Add a polyline to the index.\n\n" + ":param polyline: Polyline coordinates\n" + ":param label0: Label for the polyline (optional)\n" + ":return: Index of the added item") .def( "add", [](FlatBush &self, // @@ -51,14 +66,27 @@ CUBAO_INLINE void bind_flatbush(py::module &m) label0, label1); }, "box"_a, py::kw_only(), // - "label0"_a = -1, "label1"_a = -1) - .def("finish", &FlatBush::Finish) - // - .def("boxes", &FlatBush::boxes, rvp::reference_internal) - .def("labels", &FlatBush::labels, rvp::reference_internal) - .def("box", &FlatBush::box, "index"_a) - .def("label", &FlatBush::label, "index"_a) - // + "label0"_a = -1, "label1"_a = -1, + "Add a bounding box to the index using a vector.\n\n" + ":param box: Vector of [minX, minY, maxX, maxY]\n" + ":param label0: First label (optional)\n" + ":param label1: Second label (optional)\n" + ":return: Index of the added item") + .def("finish", &FlatBush::Finish, "Finish the index construction.") + .def("boxes", &FlatBush::boxes, rvp::reference_internal, + "Get all bounding boxes in the index.\n\n" + ":return: Reference to the vector of bounding boxes") + .def("labels", &FlatBush::labels, rvp::reference_internal, + "Get all labels in the index.\n\n" + ":return: Reference to the vector of labels") + .def("box", &FlatBush::box, "index"_a, + "Get the bounding box for a specific index.\n\n" + ":param index: Index of the item\n" + ":return: Bounding box of the item") + .def("label", &FlatBush::label, "index"_a, + "Get the label for a specific index.\n\n" + ":param index: Index of the item\n" + ":return: Label of the item") .def( "search", [](const FlatBush &self, // @@ -67,13 +95,22 @@ CUBAO_INLINE void bind_flatbush(py::module &m) return self.Search(minX, minY, maxX, maxY); }, "minX"_a, "minY"_a, // - "maxX"_a, "maxY"_a) + "maxX"_a, "maxY"_a, + "Search for items within a bounding box.\n\n" + ":param minX: Minimum X coordinate of the search box\n" + ":param minY: Minimum Y coordinate of the search box\n" + ":param maxX: Maximum X coordinate of the search box\n" + ":param maxY: Maximum Y coordinate of the search box\n" + ":return: Vector of indices of items within the search box") .def( "search", [](const FlatBush &self, const Eigen::Vector4d &bbox) { return self.Search(bbox[0], bbox[1], bbox[2], bbox[3]); }, - "bbox"_a) + "bbox"_a, + "Search for items within a bounding box using a vector.\n\n" + ":param bbox: Vector of [minX, minY, maxX, maxY]\n" + ":return: Vector of indices of items within the search box") .def( "search", [](const FlatBush &self, // @@ -81,9 +118,14 @@ CUBAO_INLINE void bind_flatbush(py::module &m) const Eigen::Vector2d &max) { return self.Search(min[0], min[1], max[0], max[1]); }, - "min"_a, "max"_a) - .def("size", &FlatBush::Size) - // - ; + "min"_a, "max"_a, + "Search for items within a bounding box using min and max " + "vectors.\n\n" + ":param min: Vector of [minX, minY]\n" + ":param max: Vector of [maxX, maxY]\n" + ":return: Vector of indices of items within the search box") + .def("size", &FlatBush::Size, + "Get the number of items in the index.\n\n" + ":return: Number of items in the index"); } } // namespace cubao diff --git a/src/pybind11_nanoflann_kdtree.hpp b/src/pybind11_nanoflann_kdtree.hpp index 0b700f0..1c1edc1 100644 --- a/src/pybind11_nanoflann_kdtree.hpp +++ b/src/pybind11_nanoflann_kdtree.hpp @@ -25,48 +25,95 @@ CUBAO_INLINE void bind_nanoflann_kdtree(py::module &m) { using KdTree = cubao::KdTree; py::class_(m, "KdTree", py::module_local()) - .def(py::init(), "leafsize"_a = 10) - .def(py::init(), "points"_a) - .def(py::init &>(), "points"_a) + .def(py::init(), "leafsize"_a = 10, + "Initialize KdTree with specified leaf size.\n\n" + ":param leafsize: Maximum number of points in leaf node, defaults " + "to 10") + .def(py::init(), "points"_a, + "Initialize KdTree with 3D points.\n\n" + ":param points: 3D points to initialize the tree") + .def(py::init &>(), "points"_a, + "Initialize KdTree with 2D points.\n\n" + ":param points: 2D points to initialize the tree") // - .def("points", &KdTree::points, rvp::reference_internal) + .def("points", &KdTree::points, rvp::reference_internal, + "Get the points in the KdTree.\n\n" + ":return: Reference to the points in the tree") // .def("add", py::overload_cast(&KdTree::add), - "points"_a) + "points"_a, + "Add 3D points to the KdTree.\n\n" + ":param points: 3D points to add") .def("add", py::overload_cast &>( &KdTree::add), - "points"_a) + "points"_a, + "Add 2D points to the KdTree.\n\n" + ":param points: 2D points to add") // - .def("reset", &KdTree::reset) - .def("reset_index", &KdTree::reset_index) - .def("build_index", &KdTree::build_index, "force_rebuild"_a = false) + .def("reset", &KdTree::reset, "Reset the KdTree, clearing all points.") + .def("reset_index", &KdTree::reset_index, + "Reset the index of the KdTree.") + .def("build_index", &KdTree::build_index, "force_rebuild"_a = false, + "Build the KdTree index.\n\n" + ":param force_rebuild: Force rebuilding the index even if already " + "built, defaults to false") // - .def("leafsize", &KdTree::leafsize) - .def("set_leafsize", &KdTree::set_leafsize, "value"_a) + .def("leafsize", &KdTree::leafsize, + "Get the current leaf size of the KdTree.\n\n" + ":return: Current leaf size") + .def("set_leafsize", &KdTree::set_leafsize, "value"_a, + "Set the leaf size of the KdTree.\n\n" + ":param value: New leaf size value") // .def("nearest", py::overload_cast(&KdTree::nearest, py::const_), - "position"_a, py::kw_only(), "return_squared_l2"_a = false) + "position"_a, py::kw_only(), "return_squared_l2"_a = false, + "Find the nearest point to the given position.\n\n" + ":param position: Query position\n" + ":param return_squared_l2: If true, return squared L2 distance, " + "defaults to false\n" + ":return: Tuple of (index, distance)") .def("nearest", py::overload_cast(&KdTree::nearest, py::const_), - "index"_a, py::kw_only(), "return_squared_l2"_a = false) + "index"_a, py::kw_only(), "return_squared_l2"_a = false, + "Find the nearest point to the point at the given index.\n\n" + ":param index: Index of the query point\n" + ":param return_squared_l2: If true, return squared L2 distance, " + "defaults to false\n" + ":return: Tuple of (index, distance)") // - .def("nearest", - py::overload_cast( - &KdTree::nearest, py::const_), - "position"_a, py::kw_only(), - "k"_a, // - "sort"_a = true, // - "return_squared_l2"_a = false) - .def("nearest", - py::overload_cast( - &KdTree::nearest, py::const_), - "position"_a, py::kw_only(), - "radius"_a, // - "sort"_a = true, // - "return_squared_l2"_a = false) + .def( + "nearest", + py::overload_cast( + &KdTree::nearest, py::const_), + "position"_a, py::kw_only(), + "k"_a, // + "sort"_a = true, // + "return_squared_l2"_a = false, + "Find k nearest points to the given position.\n\n" + ":param position: Query position\n" + ":param k: Number of nearest neighbors to find\n" + ":param sort: If true, sort results by distance, defaults to true\n" + ":param return_squared_l2: If true, return squared L2 distances, " + "defaults to false\n" + ":return: Tuple of (indices, distances)") + .def( + "nearest", + py::overload_cast( + &KdTree::nearest, py::const_), + "position"_a, py::kw_only(), + "radius"_a, // + "sort"_a = true, // + "return_squared_l2"_a = false, + "Find all points within a given radius of the query position.\n\n" + ":param position: Query position\n" + ":param radius: Search radius\n" + ":param sort: If true, sort results by distance, defaults to true\n" + ":param return_squared_l2: If true, return squared L2 distances, " + "defaults to false\n" + ":return: Tuple of (indices, distances)") // ; } diff --git a/src/pybind11_quiver.hpp b/src/pybind11_quiver.hpp index 4da9d67..6b68516 100644 --- a/src/pybind11_quiver.hpp +++ b/src/pybind11_quiver.hpp @@ -29,14 +29,18 @@ CUBAO_INLINE void bind_quiver(py::module &m) using Arrow = cubao::Arrow; py::class_(m, "Arrow", py::module_local()) // - .def(py::init<>()) - .def(py::init(), "position"_a) + .def(py::init<>(), "Default constructor for Arrow") + .def(py::init(), "position"_a, + "Constructor for Arrow with position") .def(py::init(), - "position"_a, "direction"_a) + "position"_a, "direction"_a, + "Constructor for Arrow with position and direction") // - .def("label", py::overload_cast<>(&Arrow::label, py::const_)) + .def("label", py::overload_cast<>(&Arrow::label, py::const_), + "Get the label of the Arrow") .def("label", py::overload_cast(&Arrow::label), - "new_value"_a, rvp::reference_internal) + "new_value"_a, rvp::reference_internal, + "Set the label of the Arrow") .def("label", py::overload_cast(&Arrow::polyline_index, py::const_)) + py::overload_cast<>(&Arrow::polyline_index, py::const_), + "Get the polyline index of the Arrow") .def("polyline_index", py::overload_cast(&Arrow::polyline_index), - "new_value"_a, rvp::reference_internal) + "new_value"_a, rvp::reference_internal, + "Set the polyline index of the Arrow") .def("segment_index", - py::overload_cast<>(&Arrow::segment_index, py::const_)) + py::overload_cast<>(&Arrow::segment_index, py::const_), + "Get the segment index of the Arrow") .def("segment_index", py::overload_cast(&Arrow::segment_index), - "new_value"_a, rvp::reference_internal) + "new_value"_a, rvp::reference_internal, + "Set the segment index of the Arrow") // - .def("t", py::overload_cast<>(&Arrow::t, py::const_)) + .def("t", py::overload_cast<>(&Arrow::t, py::const_), + "Get the t parameter of the Arrow") .def("t", py::overload_cast(&Arrow::t), "new_value"_a, - rvp::reference_internal) - .def("range", py::overload_cast<>(&Arrow::range, py::const_)) + rvp::reference_internal, "Set the t parameter of the Arrow") + .def("range", py::overload_cast<>(&Arrow::range, py::const_), + "Get the range of the Arrow") .def("range", py::overload_cast(&Arrow::range), "new_value"_a, - rvp::reference_internal) + rvp::reference_internal, "Set the range of the Arrow") // - .def("reset_index", py::overload_cast<>(&Arrow::reset_index)) + .def("reset_index", py::overload_cast<>(&Arrow::reset_index), + "Reset the index of the Arrow") .def("has_index", py::overload_cast(&Arrow::has_index, py::const_), - "check_range"_a = true) + "check_range"_a = true, "Check if the Arrow has a valid index") // - .def("position", py::overload_cast<>(&Arrow::position, py::const_)) + .def("position", py::overload_cast<>(&Arrow::position, py::const_), + "Get the position of the Arrow") .def("position", py::overload_cast(&Arrow::position), - "new_value"_a, rvp::reference_internal) - .def("direction", py::overload_cast<>(&Arrow::direction, py::const_)) + "new_value"_a, rvp::reference_internal, + "Set the position of the Arrow") + .def("direction", py::overload_cast<>(&Arrow::direction, py::const_), + "Get the direction of the Arrow") .def("direction", py::overload_cast(&Arrow::direction), - rvp::reference_internal) - .def("forward", py::overload_cast<>(&Arrow::direction, py::const_)) - .def("leftward", py::overload_cast<>(&Arrow::leftward, py::const_)) - .def("upward", py::overload_cast<>(&Arrow::upward, py::const_)) - .def("Frenet", py::overload_cast<>(&Arrow::Frenet, py::const_)) + rvp::reference_internal, "Set the direction of the Arrow") + .def("forward", py::overload_cast<>(&Arrow::direction, py::const_), + "Get the forward direction of the Arrow") + .def("leftward", py::overload_cast<>(&Arrow::leftward, py::const_), + "Get the leftward direction of the Arrow") + .def("upward", py::overload_cast<>(&Arrow::upward, py::const_), + "Get the upward direction of the Arrow") + .def("Frenet", py::overload_cast<>(&Arrow::Frenet, py::const_), + "Get the Frenet frame of the Arrow") // - .def("heading", py::overload_cast<>(&Arrow::heading, py::const_)) + .def("heading", py::overload_cast<>(&Arrow::heading, py::const_), + "Get the heading of the Arrow") .def("heading", py::overload_cast(&Arrow::heading), - "new_value"_a, rvp::reference_internal) + "new_value"_a, rvp::reference_internal, + "Set the heading of the Arrow") .def_static("_heading", py::overload_cast(&Arrow::_heading), - "heading"_a) - .def_static("_heading", - py::overload_cast(&Arrow::_heading), - "east"_a, "north"_a) + "heading"_a, "Convert heading to unit vector") + .def_static( + "_heading", py::overload_cast(&Arrow::_heading), + "east"_a, "north"_a, "Convert east and north components to heading") // .def_static("_unit_vector", &Arrow::_unit_vector, "vector"_a, - "with_eps"_a = true) - .def_static("_angle", &Arrow::_angle, "vec"_a, py::kw_only(), "ref"_a) + "with_eps"_a = true, "Normalize a vector to unit length") + .def_static("_angle", &Arrow::_angle, "vec"_a, py::kw_only(), "ref"_a, + "Calculate angle between two vectors") // .def("__repr__", [](const Arrow &self) { @@ -109,9 +131,13 @@ CUBAO_INLINE void bind_quiver(py::module &m) dir[0], dir[1], dir[2], // self.heading()); }) - .def("copy", [](const Arrow &self) -> Arrow { return self; }) - .def("__copy__", - [](const Arrow &self, py::dict) -> Arrow { return self; }) + .def( + "copy", [](const Arrow &self) -> Arrow { return self; }, + "Create a copy of the Arrow") + .def( + "__copy__", + [](const Arrow &self, py::dict) -> Arrow { return self; }, + "Create a copy of the Arrow") // ; @@ -119,128 +145,159 @@ CUBAO_INLINE void bind_quiver(py::module &m) auto pyQuiver = py::class_(m, "Quiver", py::module_local()) // - .def(py::init<>()) - .def(py::init(), "anchor_lla"_a) + .def(py::init<>(), "Default constructor for Quiver") + .def(py::init(), "anchor_lla"_a, + "Constructor for Quiver with anchor LLA coordinates") // - .def_static("_k", &Quiver::k) - .def("k", [](const Quiver &self) { return self.k_; }) - .def("inv_k", [](const Quiver &self) { return self.inv_k_; }) - .def("anchor", [](const Quiver &self) { return self.anchor_; }) - .def("is_wgs84", [](const Quiver &self) { return self.is_wgs84_; }) + .def_static("_k", &Quiver::k, "Get the constant k") + .def( + "k", [](const Quiver &self) { return self.k_; }, + "Get the k value of the Quiver") + .def( + "inv_k", [](const Quiver &self) { return self.inv_k_; }, + "Get the inverse k value of the Quiver") + .def( + "anchor", [](const Quiver &self) { return self.anchor_; }, + "Get the anchor point of the Quiver") + .def( + "is_wgs84", [](const Quiver &self) { return self.is_wgs84_; }, + "Check if the Quiver is using WGS84 coordinates") // - .def("forwards", &Quiver::forwards, "arrow"_a, "delta_x"_a) - .def("leftwards", &Quiver::leftwards, "arrow"_a, "delta_y"_a) - .def("upwards", &Quiver::upwards, "arrow"_a, "delta_z"_a) + .def("forwards", &Quiver::forwards, "arrow"_a, "delta_x"_a, + "Move the Arrow forward by delta_x") + .def("leftwards", &Quiver::leftwards, "arrow"_a, "delta_y"_a, + "Move the Arrow leftward by delta_y") + .def("upwards", &Quiver::upwards, "arrow"_a, "delta_z"_a, + "Move the Arrow upward by delta_z") .def("towards", &Quiver::towards, "arrow"_a, "delta_frenet"_a, - py::kw_only(), "update_direction"_a = true) + py::kw_only(), "update_direction"_a = true, + "Move the Arrow in Frenet coordinates") // .def("update", &Quiver::update, "arrow"_a, "delta_enu"_a, - py::kw_only(), "update_direction"_a = true) + py::kw_only(), "update_direction"_a = true, + "Update the Arrow's position and optionally direction") // .def("enu2lla", py::overload_cast(&Quiver::enu2lla, py::const_), - "coords"_a) + "coords"_a, "Convert ENU coordinates to LLA") .def("enu2lla", py::overload_cast(&Quiver::enu2lla, py::const_), - "coords"_a) + "coords"_a, "Convert multiple ENU coordinates to LLA") .def("lla2enu", py::overload_cast(&Quiver::lla2enu, py::const_), - "coords"_a) + "coords"_a, "Convert LLA coordinates to ENU") .def("lla2enu", py::overload_cast(&Quiver::lla2enu, py::const_), - "coords"_a) + "coords"_a, "Convert multiple LLA coordinates to ENU") // ; // FilterParams using FilterParams = Quiver::FilterParams; py::class_(pyQuiver, "FilterParams", py::module_local()) - .def(py::init<>()) - .def("x_slots", py::overload_cast<>(&FilterParams::x_slots, py::const_)) + .def(py::init<>(), "Default constructor for FilterParams") + .def("x_slots", py::overload_cast<>(&FilterParams::x_slots, py::const_), + "Get the x slots of the FilterParams") .def("x_slots", py::overload_cast &>( &FilterParams::x_slots), - rvp::reference_internal) - .def("y_slots", py::overload_cast<>(&FilterParams::y_slots, py::const_)) + rvp::reference_internal, "Set the x slots of the FilterParams") + .def("y_slots", py::overload_cast<>(&FilterParams::y_slots, py::const_), + "Get the y slots of the FilterParams") .def("y_slots", py::overload_cast &>( &FilterParams::y_slots), - rvp::reference_internal) - .def("z_slots", py::overload_cast<>(&FilterParams::z_slots, py::const_)) + rvp::reference_internal, "Set the y slots of the FilterParams") + .def("z_slots", py::overload_cast<>(&FilterParams::z_slots, py::const_), + "Get the z slots of the FilterParams") .def("z_slots", py::overload_cast &>( &FilterParams::z_slots), - rvp::reference_internal) + rvp::reference_internal, "Set the z slots of the FilterParams") .def("angle_slots", - py::overload_cast<>(&FilterParams::angle_slots, py::const_)) + py::overload_cast<>(&FilterParams::angle_slots, py::const_), + "Get the angle slots of the FilterParams") .def("angle_slots", py::overload_cast &>( &FilterParams::angle_slots), - rvp::reference_internal) - .def("is_trivial", &FilterParams::is_trivial) + rvp::reference_internal, "Set the angle slots of the FilterParams") + .def("is_trivial", &FilterParams::is_trivial, + "Check if the FilterParams is trivial") // ; using KdQuiver = cubao::KdQuiver; py::class_(m, "KdQuiver", py::module_local()) - .def(py::init<>()) - .def(py::init(), "anchor_lla"_a) + .def(py::init<>(), "Default constructor for KdQuiver") + .def(py::init(), "anchor_lla"_a, + "Constructor for KdQuiver with anchor LLA coordinates") // add .def("add", py::overload_cast(&KdQuiver::add), - "polyline"_a, "index"_a = -1) + "polyline"_a, "index"_a = -1, "Add a polyline to the KdQuiver") .def("add", py::overload_cast &, int>( &KdQuiver::add), - "polyline"_a, "index"_a = -1) + "polyline"_a, "index"_a = -1, "Add a 2D polyline to the KdQuiver") // nearest .def("nearest", py::overload_cast( &KdQuiver::nearest, py::const_), "position"_a, py::kw_only(), // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find the nearest point to the given position") .def("nearest", py::overload_cast(&KdQuiver::nearest, py::const_), "index"_a, py::kw_only(), // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find the nearest point to the point at the given index") .def("nearest", py::overload_cast( &KdQuiver::nearest, py::const_), "position"_a, py::kw_only(), // "k"_a, // "sort"_a = true, // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find k nearest points to the given position") .def("nearest", py::overload_cast( &KdQuiver::nearest, py::const_), "position"_a, py::kw_only(), // "radius"_a, // "sort"_a = true, // - "return_squared_l2"_a = false) + "return_squared_l2"_a = false, + "Find all points within a given radius of the query position") // positions - .def("positions", py::overload_cast<>(&KdQuiver::positions, py::const_)) + .def("positions", py::overload_cast<>(&KdQuiver::positions, py::const_), + "Get all positions in the KdQuiver") .def("positions", py::overload_cast(&KdQuiver::positions, py::const_), - "indexes"_a) + "indexes"_a, "Get positions for the given indexes") // directions - .def("directions", &KdQuiver::directions, "indexes"_a) + .def("directions", &KdQuiver::directions, "indexes"_a, + "Get directions for the given indexes") // arrows - .def("arrows", &KdQuiver::arrows, "indexes"_a) + .def("arrows", &KdQuiver::arrows, "indexes"_a, + "Get arrows for the given indexes") // arrow .def("arrow", py::overload_cast(&KdQuiver::arrow, py::const_), - "point_index"_a) + "point_index"_a, "Get the arrow at the given point index") .def("arrow", py::overload_cast(&KdQuiver::arrow, py::const_), - "polyline_index"_a, "segment_index"_a) + "polyline_index"_a, "segment_index"_a, + "Get the arrow at the given polyline and segment indices") .def("arrow", py::overload_cast(&KdQuiver::arrow, py::const_), - "polyline_index"_a, "segment_index"_a, py::kw_only(), "t"_a) + "polyline_index"_a, "segment_index"_a, py::kw_only(), "t"_a, + "Get the arrow at the given polyline, segment indices, and t " + "parameter") .def("arrow", py::overload_cast(&KdQuiver::arrow, py::const_), - "polyline_index"_a, py::kw_only(), "range"_a) + "polyline_index"_a, py::kw_only(), "range"_a, + "Get the arrow at the given polyline index and range") // filter .def_static("_filter", py::overload_cast &, // @@ -252,7 +309,8 @@ CUBAO_INLINE void bind_quiver(py::module &m) "arrows"_a, // "arrow"_a, // "params"_a, // - "is_wgs84"_a = false) + "is_wgs84"_a = false, + "Filter arrows based on the given parameters") .def("filter", py::overload_cast(&KdQuiver::index, py::const_), - "point_index"_a) + "point_index"_a, "Get the index for the given point index") .def("index", py::overload_cast(&KdQuiver::index, py::const_), - "polyline_index"_a, "segment_index"_a); + "polyline_index"_a, "segment_index"_a, + "Get the index for the given polyline and segment indices"); } } // namespace cubao diff --git a/tests/test_basic.py b/tests/test_basic.py index 0d1f720..59e6ea5 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys import time @@ -11,6 +13,7 @@ FlatBush, KdQuiver, KdTree, + PolylineRuler, Quiver, densify_polyline, point_in_polygon, @@ -40,7 +43,7 @@ def test_fast_crossing(): fc.finish() # num_poylines - assert 2 == fc.num_poylines() + assert fc.num_poylines() == 2 rulers = fc.polyline_rulers() assert len(rulers) == 2 ruler0 = fc.polyline_ruler(0) @@ -167,7 +170,7 @@ def test_fast_crossing_filter_by_z(): assert fc.coordinates(ret[0])[2] == 10 assert fc.coordinates(ret[1])[2] == 20 - assert 3 == len(fc.intersections()) # 3 == C(3,2): choose 2 from 3 + assert len(fc.intersections()) == 3 # 3 == C(3,2): choose 2 from 3 # for row in fc.intersections(): # print(row) @@ -229,8 +232,8 @@ def test_densify(): assert len(dense) == 4 dense = densify_polyline(coords, max_gap=3.0) assert len(dense) == 3 - assert 3 == len(densify_polyline(coords, max_gap=3.0 + 1e-3)) - assert 4 == len(densify_polyline(coords, max_gap=3.0 - 1e-3)) + assert len(densify_polyline(coords, max_gap=3.0 + 1e-3)) == 3 + assert len(densify_polyline(coords, max_gap=3.0 - 1e-3)) == 4 def test_point_in_polygon(): @@ -264,7 +267,7 @@ def _test_cKDTree_query(KDTree): expected_k1 = [[2.0, 0.2236068], [0, 13]] expected_k2 = [[[2.0, 2.23606798], [0.2236068, 0.80622577]], [[0, 6], [13, 19]]] - for k, expected in zip([1, 2], [expected_k1, expected_k2]): # noqa + for k, expected in zip([1, 2], [expected_k1, expected_k2]): dd, ii = tree.query([[0, 0], [2.2, 2.9]], k=k) DD, II = expected np.testing.assert_allclose(dd, DD, atol=1e-6) @@ -282,7 +285,7 @@ def _test_cKDTree_query(KDTree): [[2.0, 2.23606797749979], [0.22360679774997916, 0.8062257748298548]], [[0, 6], [13, 19]], ] - for k, expected in zip( # noqa + for k, expected in zip( [[1], [2], [1, 2]], [expected_k1, expected_k2, expected_k1_k2] ): dd, ii = tree.query([[0, 0], [2.2, 2.9]], k=k) @@ -294,14 +297,14 @@ def _test_cKDTree_query(KDTree): points = np.c_[x.ravel(), y.ravel()] tree = KDTree(points) ret = sorted(tree.query_ball_point([2 + 1e-15, 1e-15], 1 + 1e-9)) - assert np.all([4, 8, 9, 12] == np.array(ret)) + assert np.all(np.array(ret) == [4, 8, 9, 12]) ret = tree.query_ball_point([[2, 0], [3, 0]], 1 + 1e-9) - assert np.all([4, 8, 9, 12] == np.array(sorted(ret[0]))) - assert np.all([8, 12, 13] == np.array(sorted(ret[1]))) + assert np.all(np.array(sorted(ret[0])) == [4, 8, 9, 12]) + assert np.all(np.array(sorted(ret[1])) == [8, 12, 13]) ret = tree.query_ball_point([[2, 0], [3, 0]], 1 + 1e-9, return_length=True) - assert np.all([4, 3] == np.array(ret)) + assert np.all(np.array(ret) == [4, 3]) def test_scipy_cKDTree(): @@ -413,7 +416,6 @@ def test_quiver(): # update (delta in EUN, x->east, y->north, z->up) updated = quiver.update(arrow, [2, 0, 0]) np.testing.assert_allclose(updated.position(), [2, 1, 0], atol=1e-8) - # arrow = Arrow([0, 1, 0], direction=Arrow._unit_vector([1, 1, 0])) updated = quiver.towards(arrow, [3, 3, 0]) sqrt2 = np.sqrt(2) @@ -516,7 +518,6 @@ def test_kdquiver(): def test_kdquiver_filter_by_angle(): - quiver = KdQuiver() vecs = {} headings = [0, 30, 60, 90, 120] @@ -755,6 +756,15 @@ def test_nearst_wgs84(): # assert len(idx) == 5 +def __print_chunks(chunks): + for cidx, ((seg1, t1, r1, seg2, t2, r2), polyline) in enumerate(chunks.items()): + print(f"\nchunk index: {cidx}", sep=", ") + print(f"length: {r2 - r1:.3f}") + print(f"seg={seg1},t={t1:.3f}, r={r1:.2f}", sep="; ") + print(f"seg={seg2},t={t2:.3f}, r={r2:.2f}") + print(polyline.round(2).tolist()) + + def test_polyline_in_polygon(): """ 2 4 @@ -786,17 +796,34 @@ def test_polyline_in_polygon(): [8.0, -7.0, 4.0], # 5 ] ) + ruler = PolylineRuler(polyline_12345, is_wgs84=False) chunks = polyline_in_polygon(polyline_12345, polygon_ABCD) ranges = [] - for (seg1, t1, r1, seg2, t2, r2), polyline in chunks.items(): + for _, _, r1, _, _, r2 in chunks: ranges.append(r2 - r1) - print(f"\n#length: {r2 - r1:.3f}") - print(seg1, t1, r1) - print(seg2, t2, r2) - print(polyline) expected_ranges = [2.72883, 14.4676666, 7.01783] np.testing.assert_allclose(ranges, expected_ranges, atol=1e-4) + # test inside + polyline = next(iter(chunks.values())) + polyline_updated = np.copy(polyline_12345) + polyline_updated[0] = polyline.mean(axis=0) + chunks2 = polyline_in_polygon(polyline_updated, polygon_ABCD) + __print_chunks(chunks) + __print_chunks(chunks2) + + chunks1 = list(chunks.items()) + chunks2 = list(chunks2.items()) + assert len(chunks1) == len(chunks2) + assert np.fabs(chunks1[0][1][-1] - chunks2[0][1][-1]).max() == 0.0 + assert np.fabs(chunks1[1][1] - chunks2[1][1]).sum() < 1e-6 + assert np.fabs(chunks1[2][1] - chunks2[2][1]).sum() < 1e-6 + + chunks3 = polyline_in_polygon(ruler.lineSliceAlong(1, 2), polygon_ABCD) + assert len(chunks3) == 0 + chunks3 = polyline_in_polygon(ruler.lineSliceAlong(7, 8), polygon_ABCD) + assert len(chunks3) == 1 + # test fc fc = FastCrossing() fc.add_polyline(polygon_ABCD) @@ -823,7 +850,7 @@ def test_polyline_in_polygon(): is_wgs84=True, ) ranges = [] - for _, _, r1, _, _, r2 in chunks.keys(): + for _, _, r1, _, _, r2 in chunks: ranges.append(r2 - r1) np.testing.assert_allclose(ranges, expected_ranges, atol=1e-4) @@ -834,7 +861,6 @@ def test_polyline_in_polygon(): def pytest_main(dir: str, *, test_file: str = None): - os.chdir(dir) sys.exit( pytest.main(