diff --git a/.github/workflows/osrm-backend-docker.yml b/.github/workflows-disabled/osrm-backend-docker.yml similarity index 100% rename from .github/workflows/osrm-backend-docker.yml rename to .github/workflows-disabled/osrm-backend-docker.yml diff --git a/.github/workflows-disabled/python-bindings.yml b/.github/workflows-disabled/python-bindings.yml new file mode 100644 index 0000000000..1cb46f9844 --- /dev/null +++ b/.github/workflows-disabled/python-bindings.yml @@ -0,0 +1,137 @@ +name: Python Bindings + +on: + push: + branches: + - nn-py-bindings + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build_sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build sdist + run: | + python -m pip install build + python -m build --sdist + echo "=== sdist produced ===" + ls -la dist/ + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + build_wheels: + name: Build - cp312, ${{ matrix.name }} + needs: [build_sdist] + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x86_64 + runner: ubuntu-latest + - name: Linux aarch64 + runner: ubuntu-24.04-arm + - name: macOS arm64 + runner: macos-14 + - name: Windows amd64 + runner: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Download sdist + if: runner.os == 'Linux' + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Locate sdist + if: runner.os == 'Linux' + id: sdist + shell: bash + run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" + + - name: Restore Conan cache + id: conan-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v5 + with: + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + v4-conan-${{ runner.os }}- + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3.4.0 + with: + # for linux build the wheel from the sdist + package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} + env: + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + + - name: Save Conan cache + uses: actions/cache/save@v5 + if: "!cancelled() && runner.os == 'Windows' && steps.conan-cache.outputs.cache-hit != 'true'" + with: + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.name }} + path: wheelhouse/*.whl + + publish_test_pypi: + name: Publish to TestPyPI + needs: [build_wheels] + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Download wheels + uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheels-* + merge-multiple: true + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true diff --git a/.github/workflows/stale.yml b/.github/workflows-disabled/stale.yml similarity index 100% rename from .github/workflows/stale.yml rename to .github/workflows-disabled/stale.yml diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 281dad242b..5554417bcf 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -1,13 +1,11 @@ name: osrm-backend CI on: push: - branches: - - master tags: - v* - pull_request: - branches: - - master + # pull_request: + # branches: + # - master workflow_dispatch: env: @@ -24,7 +22,10 @@ concurrency: jobs: conan-windows-release-node: needs: format-taginfo-docs - runs-on: windows-2025 + strategy: + matrix: + os: [windows-2025] + runs-on: ${{ matrix.os }} continue-on-error: false env: BUILD_TYPE: Release @@ -43,7 +44,16 @@ jobs: echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV - run: npm install --ignore-scripts - run: npm link --ignore-scripts + - name: Restore Conan cache + id: conan-restore + uses: actions/cache/restore@v5 + with: + path: ~/.conan2 + key: v11-conan-${{ matrix.os }}-${{ hashFiles('conanfile.py') }} + restore-keys: | + v11-conan-${{ matrix.os }}- - name: Build + id: build shell: bash run: | mkdir build @@ -56,6 +66,12 @@ jobs: cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. cmake --build . --config Release + - name: Save Conan cache + if: steps.build.outcome == 'success' && steps.conan-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/.conan2 + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} # TODO: MSVC goes out of memory when building our tests # - name: Run tests @@ -75,22 +91,36 @@ jobs: # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm # node test/nodejs/index.js - - name: Build Node package - shell: bash - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 + # - name: Build Node package + # shell: bash + # run: ./scripts/ci/node_package.sh + # - name: Publish Node package + # if: ${{ env.PUBLISH == 'On' }} + # uses: ncipollo/release-action@v1 + # with: + # allowUpdates: true + # artifactErrorsFailBuild: true + # artifacts: build/stage/**/*.tar.gz + # omitBody: true + # omitBodyDuringUpdate: true + # omitName: true + # omitNameDuringUpdate: true + # replacesArtifacts: true + # token: ${{ secrets.GITHUB_TOKEN }} + - name: Run CIBuildWheel + if: github.event_name != 'pull_request' + uses: pypa/cibuildwheel@v3.4.0 with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + output-dir: dist + env: + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + - name: Upload Windows wheel + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: wheels-windows + path: dist/*.whl + if-no-files-found: error format-taginfo-docs: runs-on: ubuntu-slim @@ -128,192 +158,192 @@ jobs: npm run docs && ./scripts/error_on_dirty.sh "Run 'npm run docs' locally and commit the changes." npm audit --production - docker-image-matrix: - strategy: - matrix: - docker-base-image: ['debian', 'alpine'] - needs: format-taginfo-docs - runs-on: ubuntu-22.04 - continue-on-error: false - steps: - - name: Check out the repo - uses: actions/checkout@v6 - - name: Enable osm.pbf cache - uses: actions/cache@v5 - with: - path: berlin-latest.osm.pbf - key: v1-berlin-osm-pbf - restore-keys: | - v1-berlin-osm-pbf - - name: Docker build - run: | - docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . - - name: Test Docker image - run: | - if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then - wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf - fi - TAG=osrm-backend-local - # when `--memory-swap` value equals `--memory` it means container won't use swap - # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details - MEMORY_ARGS="--memory=1g --memory-swap=1g" - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson - if [ ! -s "${PWD}/berlin-latest.geojson" ] - then - >&2 echo "No berlin-latest.geojson found" - exit 1 - fi - # removing `.osrm.nbg` to check that whole pipeline works without it - rm -rf "${PWD}/berlin-latest.osrm.nbg" - - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm - docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & - curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" - docker stop osrm-container + # docker-image-matrix: + # strategy: + # matrix: + # docker-base-image: ['debian', 'alpine'] + # needs: format-taginfo-docs + # runs-on: ubuntu-22.04 + # continue-on-error: false + # steps: + # - name: Check out the repo + # uses: actions/checkout@v6 + # - name: Enable osm.pbf cache + # uses: actions/cache@v5 + # with: + # path: berlin-latest.osm.pbf + # key: v1-berlin-osm-pbf + # restore-keys: | + # v1-berlin-osm-pbf + # - name: Docker build + # run: | + # docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . + # - name: Test Docker image + # run: | + # if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then + # wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf + # fi + # TAG=osrm-backend-local + # # when `--memory-swap` value equals `--memory` it means container won't use swap + # # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details + # MEMORY_ARGS="--memory=1g --memory-swap=1g" + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson + # if [ ! -s "${PWD}/berlin-latest.geojson" ] + # then + # >&2 echo "No berlin-latest.geojson found" + # exit 1 + # fi + # # removing `.osrm.nbg` to check that whole pipeline works without it + # rm -rf "${PWD}/berlin-latest.osrm.nbg" + + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm + # docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & + # curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" + # docker stop osrm-container build-matrix: needs: format-taginfo-docs strategy: matrix: include: - - name: clang-20-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-20 - CXXCOMPILER: clang++-20 - ENABLE_LTO: OFF - - - name: clang-19-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-19 - CXXCOMPILER: clang++-19 - ENABLE_LTO: OFF - - - name: clang-18-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_LTO: OFF - - - name: clang-18-debug - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_LTO: OFF - - - name: clang-18-debug-clang-tidy - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CLANG_TIDY: ON - NODE_PACKAGE_TESTS_ONLY: ON - ENABLE_LTO: OFF - - - name: clang-18-debug-asan-ubsan - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_SANITIZER: ON - TARGET_ARCH: x86_64-asan-ubsan - OSRM_CONNECTION_RETRIES: 10 - OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 - - - name: clang-17-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-17 - CXXCOMPILER: clang++-17 - ENABLE_LTO: OFF - - - name: clang-16-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-16 - CXXCOMPILER: clang++-16 - ENABLE_LTO: OFF - - - name: gcc-14-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: gcc-14 - CXXCOMPILER: g++-14 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: gcc-13-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: gcc-13 - CXXCOMPILER: g++-13 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: gcc-13-debug-cov - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: gcc-13 - CXXCOMPILER: g++-13 - ENABLE_COVERAGE: ON - - - name: gcc-12-release - continue-on-error: false - node: 24 - runs-on: ubuntu-22.04 - BUILD_TYPE: Release - CCOMPILER: gcc-12 - CXXCOMPILER: g++-12 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: conan-linux-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_LTO: OFF - - - name: conan-linux-debug-asan-ubsan - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_SANITIZER: ON - ENABLE_LTO: OFF + # - name: clang-20-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-20 + # CXXCOMPILER: clang++-20 + # ENABLE_LTO: OFF + + # - name: clang-19-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-19 + # CXXCOMPILER: clang++-19 + # ENABLE_LTO: OFF + + # - name: clang-18-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_LTO: OFF + + # - name: clang-18-debug + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_LTO: OFF + + # - name: clang-18-debug-clang-tidy + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CLANG_TIDY: ON + # NODE_PACKAGE_TESTS_ONLY: ON + # ENABLE_LTO: OFF + + # - name: clang-18-debug-asan-ubsan + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_SANITIZER: ON + # TARGET_ARCH: x86_64-asan-ubsan + # OSRM_CONNECTION_RETRIES: 10 + # OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 + + # - name: clang-17-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-17 + # CXXCOMPILER: clang++-17 + # ENABLE_LTO: OFF + + # - name: clang-16-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-16 + # CXXCOMPILER: clang++-16 + # ENABLE_LTO: OFF + + # - name: gcc-14-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-14 + # CXXCOMPILER: g++-14 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: gcc-13-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-13 + # CXXCOMPILER: g++-13 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: gcc-13-debug-cov + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: gcc-13 + # CXXCOMPILER: g++-13 + # ENABLE_COVERAGE: ON + + # - name: gcc-12-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-22.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-12 + # CXXCOMPILER: g++-12 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: conan-linux-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_LTO: OFF + + # - name: conan-linux-debug-asan-ubsan + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_SANITIZER: ON + # ENABLE_LTO: OFF - name: conan-linux-release-node - build_node_package: true + build_bindings: true continue-on-error: false node: 24 runs-on: ubuntu-24.04 @@ -323,48 +353,47 @@ jobs: ENABLE_CONAN: ON NODE_PACKAGE_TESTS_ONLY: ON - - name: conan-linux-debug-node - build_node_package: true - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-16 - CXXCOMPILER: clang++-16 - ENABLE_CONAN: ON - NODE_PACKAGE_TESTS_ONLY: ON - - - name: conan-linux-arm64-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04-arm - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_LTO: OFF + # - name: conan-linux-debug-node + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-16 + # CXXCOMPILER: clang++-16 + # ENABLE_CONAN: ON + # NODE_PACKAGE_TESTS_ONLY: ON + + # - name: conan-linux-arm64-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04-arm + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_LTO: OFF - name: conan-macos-x64-release-node - build_node_package: true + build_bindings: true continue-on-error: true node: 24 - runs-on: macos-26-intel # x86_64 + runs-on: macos-15-intel # x86_64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ ENABLE_ASSERTIONS: ON ENABLE_CONAN: ON - - name: conan-macos-arm64-release-node - build_node_package: true - continue-on-error: true - node: 24 - runs-on: macos-26 # arm64 - BUILD_TYPE: Release - CCOMPILER: clang - CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON - ENABLE_CONAN: ON + # - name: conan-macos-arm64-release-node + # build_bindings: true + # continue-on-error: true + # node: 24 + # runs-on: macos-15 # arm64 + # BUILD_TYPE: Release + # CCOMPILER: clang + # CXXCOMPILER: clang++ + # ENABLE_ASSERTIONS: ON + # ENABLE_CONAN: ON name: ${{ matrix.name}} continue-on-error: ${{ matrix.continue-on-error }} @@ -386,8 +415,12 @@ jobs: OSRM_CONNECTION_RETRIES: ${{ matrix.OSRM_CONNECTION_RETRIES }} OSRM_CONNECTION_EXP_BACKOFF_COEF: ${{ matrix.OSRM_CONNECTION_EXP_BACKOFF_COEF }} ENABLE_LTO: ${{ matrix.ENABLE_LTO }} + BUILD_BINDINGS: ${{ matrix.build_bindings }} steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true - name: Build machine architecture run: uname -m - name: Use Node.js @@ -401,20 +434,22 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - name: Enable compiler cache - uses: actions/cache@v5 + - name: Restore compiler cache + id: ccache-restore + uses: actions/cache/restore@v5 with: path: ~/.ccache key: ccache-${{ matrix.name }}-${{ github.sha }} restore-keys: | ccache-${{ matrix.name }}- - - name: Enable Conan cache - uses: actions/cache@v5 + - name: Restore Conan cache + id: conan-restore + uses: actions/cache/restore@v5 with: path: ~/.conan2 - key: v10-conan-${{ matrix.name }}-${{ github.sha }} + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} restore-keys: | - v10-conan-${{ matrix.name }}- + v11-conan-${{ matrix.name }}- - name: Enable test cache uses: actions/cache@v5 with: @@ -484,17 +519,15 @@ jobs: fi fi - # TBB - TBB_VERSION=2021.12.0 + # TBB (Linux only; macOS gets it from conan or brew) if [[ "${RUNNER_OS}" == "Linux" ]]; then + TBB_VERSION=2021.12.0 TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-lin.tgz" - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-mac.tgz" + wget --tries 5 ${TBB_URL} -O onetbb.tgz + tar zxvf onetbb.tgz + sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ + sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ fi - wget --tries 5 ${TBB_URL} -O onetbb.tgz - tar zxvf onetbb.tgz - sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ - sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ - name: Prepare build run: | mkdir ${OSRM_BUILD_DIR} @@ -505,14 +538,9 @@ jobs: fi echo "CC=${CCOMPILER}" >> $GITHUB_ENV echo "CXX=${CXXCOMPILER}" >> $GITHUB_ENV - if [[ "${RUNNER_OS}" == "macOS" ]]; then - # missing from GCC path, needed for conan builds of libiconv, for example. - sudo xcode-select --switch /Library/Developer/CommandLineTools - echo "LIBRARY_PATH=${LIBRARY_PATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" >> $GITHUB_ENV - echo "CPATH=${CPATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" >> $GITHUB_ENV - fi - name: Build and install OSRM + id: build run: | echo "Using ${JOBS} jobs" pushd ${OSRM_BUILD_DIR} @@ -556,77 +584,134 @@ jobs: popd env: Boost_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - - name: Run all tests - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} - run: | - make -C test/data benchmark - - # macOS SIP strips the linker path. Reset this inside the running shell - export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} - - # All tests assume to be run from the build directory - pushd ${OSRM_BUILD_DIR} - for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done - if [ -z "${ENABLE_SANITIZER}" ]; then - npm run nodejs-tests - fi - popd - npm test -- --parallel $JOBS - - - name: Use Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 + - name: Save compiler cache + if: steps.build.outcome == 'success' + uses: actions/cache/save@v5 with: - node-version: 22 - - name: Run Node package tests on Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node 24 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 + path: ~/.ccache + key: ccache-${{ matrix.name }}-${{ github.sha }} + - name: Save Conan cache + if: steps.build.outcome == 'success' && steps.conan-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 with: - node-version: 24 - - name: Run Node package tests on Node 24 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 + path: ~/.conan2 + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} + # - name: Run all tests + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} + # run: | + # make -C test/data benchmark + + # # macOS SIP strips the linker path. Reset this inside the running shell + # export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} + + # # All tests assume to be run from the build directory + # pushd ${OSRM_BUILD_DIR} + # for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done + # if [ -z "${ENABLE_SANITIZER}" ]; then + # npm run nodejs-tests + # fi + # popd + # npm test -- --parallel $JOBS + + # - name: Use Node 22 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: 22 + # - name: Run Node package tests on Node 22 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests + # - name: Use Node 24 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: 24 + # - name: Run Node package tests on Node 24 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests + # - name: Use Node latest + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: latest + # - name: Run Node package tests on Node-latest + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests + + # - name: Upload test logs + # uses: actions/upload-artifact@v6 + # if: failure() + # with: + # name: logs + # path: test/logs/ + + # - name: Build Node package + # if: ${{ env.BUILD_BINDINGS }} + # run: ./scripts/ci/node_package.sh + # - name: Publish Node package + # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + # uses: ncipollo/release-action@v1 + # with: + # allowUpdates: true + # artifactErrorsFailBuild: true + # artifacts: build/stage/**/*.tar.gz + # omitBody: true + # omitBodyDuringUpdate: true + # omitName: true + # omitNameDuringUpdate: true + # replacesArtifacts: true + # token: ${{ secrets.GITHUB_TOKEN }} + + # Python bindings + + - name: Set up Python + if: ${{ env.BUILD_BINDINGS }} + uses: actions/setup-python@v5 with: - node-version: latest - - name: Run Node package tests on Node-latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + python-version: "3.12" + + - name: Build sdist + if: ${{ env.BUILD_BINDINGS && runner.os == 'Linux' }} run: | - node --version - npm run nodejs-tests + python -m pip install build + python -m build --sdist - - name: Upload test logs - uses: actions/upload-artifact@v6 - if: failure() + - name: Locate sdist + if: ${{ env.BUILD_BINDINGS && runner.os == 'Linux' }} + id: sdist + shell: bash + run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3.4.1 + if: ${{ env.BUILD_BINDINGS }} with: - name: logs - path: test/logs/ - - - name: Build Node package - if: ${{ matrix.build_node_package }} - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ matrix.build_node_package && env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 + # for linux build the wheel from the sdist + package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} + output-dir: dist + env: + # Mount the workflow-cached ~/.ccache into the container so Linux + # wheel builds share the main-build ccache across runs. + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /home/runner/.ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" + + - name: Upload wheels and sdist + if: ${{ env.BUILD_BINDINGS && github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v4 with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + name: wheels-${{ matrix.name }} + path: | + dist/*.whl + dist/*.tar.gz + if-no-files-found: error + - name: Show CCache statistics run: | ccache -p @@ -634,6 +719,6 @@ jobs: ci-complete: runs-on: ubuntu-latest - needs: [build-matrix, conan-windows-release-node, docker-image-matrix] + needs: [build-matrix] steps: - run: echo "CI complete" diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index 12321d759b..6e95a53909 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -1,6 +1,9 @@ name: Monthly Release on: + push: + branches: + - nn-py-bindings schedule: # 1st of each month at 08:00 UTC - cron: '0 8 1 * *' @@ -11,10 +14,9 @@ on: required: false type: string branch: - description: 'Branch to release from (defaults to master)' + description: 'Branch to release from (defaults to the ref that triggered this workflow)' required: false type: string - default: 'master' concurrency: group: release-monthly-${{ github.ref }} @@ -30,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.branch || 'master' }} + ref: ${{ inputs.branch || github.ref_name }} fetch-depth: 0 - uses: actions/setup-node@v6 @@ -50,7 +52,7 @@ jobs: fi # Validate branch input (prevent shell injection) - BRANCH="${{ inputs.branch || 'master' }}" + BRANCH="${{ inputs.branch || github.ref_name }}" if ! echo "$BRANCH" | grep -E '^[a-zA-Z0-9._/-]+$' > /dev/null; then echo "Error: branch name contains invalid characters" exit 1 @@ -132,7 +134,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.BACKEND_RELEASE_TOKEN }} run: | - BRANCH="${{ inputs.branch || 'master' }}" + BRANCH="${{ inputs.branch || github.ref_name }}" git push origin "$BRANCH" git push origin "${{ steps.version.outputs.tag }}" @@ -177,6 +179,7 @@ jobs: prerelease: false - name: Wait for CI to complete + id: wait_ci env: GH_TOKEN: ${{ github.token }} run: | @@ -209,6 +212,7 @@ jobs: if [ "$STATUS" = "completed" ]; then if [ "$CONCLUSION" = "success" ]; then echo "✓ CI workflow completed successfully, proceeding with npm publish" + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" exit 0 else echo "✗ CI workflow completed with conclusion=$CONCLUSION (expected success)" @@ -228,9 +232,25 @@ jobs: echo "✗ Timed out waiting for CI workflow to complete for tag $TAG" exit 1 - - name: Install dependencies (skip native build scripts) - run: npm ci --ignore-scripts + - name: Download wheels from CI run + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + run-id: ${{ steps.wait_ci.outputs.run_id }} + github-token: ${{ github.token }} + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + skip-existing: true + verbose: true + repository-url: https://test.pypi.org/legacy/ + + # - name: Install dependencies (skip native build scripts) + # run: npm ci --ignore-scripts - - name: Publish to npm - run: npm publish + # - name: Publish to npm + # run: npm publish diff --git a/.gitignore b/.gitignore index f72889eae4..680ceab6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,11 @@ debug.lua ########################## lib/binding_napi_v8 +# Python +########################## +__pycache__/ +*.whl + # documentation related files ########################## .vitepress/cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..0bd24ab61a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + # matches (more or less) the current clang-format on OSRM CI + # TODO(nils): we should change to pypi's clang tools for reproducibility + rev: v18.1.8 + hooks: + - id: clang-format + types_or: [c, c++] + files: ^src/python/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.5 + hooks: + - id: ruff + args: [--fix] + files: ^(src/python/|test/python/) + - id: ruff-format + files: ^(src/python/|test/python/) diff --git a/AGENT.md b/AGENT.md index dc4fa5113e..fa6ff0179c 100644 --- a/AGENT.md +++ b/AGENT.md @@ -93,6 +93,37 @@ npx cucumber-js -p home -p mld -p mmap ``` +## Python Bindings + +Python bindings live under `src/python/` using nanobind + scikit-build-core. + +### Layout + +- `src/python/CMakeLists.txt` — nanobind module definition, installs everything under `osrm/` namespace +- `src/python/osrm/` — Python package (`__init__.py`, `__main__.py`, `.pyi` stubs) +- `src/python/src/` — C++ nanobind source files (16 files) +- `src/python/include/python/` — C++ binding headers (`.hpp`) + +### Key details + +- `ENABLE_PYTHON_BINDINGS=ON` triggers `add_subdirectory(src/python)` in root CMakeLists.txt +- Links against the in-tree `osrm` target directly (no FetchContent) +- Binding headers use `#include "python/..."` prefix; OSRM headers use `engine/...` paths (not the `osrm/` forwarding headers), except `osrm/osrm.hpp` +- LTO disabled for `osrm_ext` target (nanobind `NB_MAKE_OPAQUE` + GCC LTO + `-Werror` = ODR violation) +- Wheel installs executables to `osrm/bin/`, profiles to `osrm/share/profiles/`; `wheel.exclude` filters out root CMake install artifacts (`lib/`, `include/`, `bin/`, `share/`) +- `__main__.py` finds executables: `osrm/bin/` (wheel) → `build/*/` (editable) → PATH +- Type stubs (`osrm_ext.pyi`) auto-generated by `nanobind_add_stub()`; rebuild + commit after C++ changes +- C++ formatted by project clang-format; Python by ruff (both via `.pre-commit-config.yaml`) + +### Building & testing + +```bash +pip install -e ".[dev]" # editable install +cd test/data && make # build test data +python -m osrm datastore test/data/ch/monaco +pytest test/python/ +``` + ## Contributing diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d48c24da6..5bf1f8c9ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ option(ENABLE_DEBUG_LOGGING "Use debug logging in release mode" OFF) option(ENABLE_FUZZING "Fuzz testing using LLVM's libFuzzer" OFF) option(ENABLE_LTO "Use Link Time Optimisation" ON) option(ENABLE_NODE_BINDINGS "Build NodeJs bindings" OFF) +option(ENABLE_PYTHON_BINDINGS "Build Python bindings" OFF) option(ENABLE_SANITIZER "Use memory sanitizer for Debug build" OFF) if (ENABLE_CONAN) @@ -150,6 +151,22 @@ include(CheckCXXCompilerFlag) include(FindPackageHandleStandardArgs) include(GNUInstallDirs) +# On macOS, brew-installed headers land in prefixes that clang may already +# search implicitly (/usr/local on Intel, /opt/homebrew on ARM in some +# configurations). When a path is implicit, CMake filters it out of +# include_directories(SYSTEM ...) — no -isystem is emitted — and clang's +# system-header warning suppression stops reliably catching template- +# instantiation diagnostics from headers under that prefix. Force -isystem +# via add_compile_options so brew-installed boost/lua/tbb headers are +# treated as system headers regardless of implicit status. +if(APPLE) + foreach(_brew_prefix /usr/local /opt/homebrew) + if(EXISTS "${_brew_prefix}/include") + add_compile_options(SHELL:-isystem ${_brew_prefix}/include) + endif() + endforeach() +endif() + include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/include/) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}/include/) include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/generated/include/) @@ -491,7 +508,6 @@ set(ZLIB_LIBRARY ${ZLIB_LIBRARIES}) add_definitions(${OSRM_DEFINES}) include_directories(SYSTEM ${DEPENDENCIES_INCLUDE_DIRS}) - # Binaries target_link_libraries(osrm-datastore osrm_store ${Boost_PROGRAM_OPTIONS_LIBRARY}) target_link_libraries(osrm-extract osrm_extract ${Boost_PROGRAM_OPTIONS_LIBRARY}) @@ -719,6 +735,10 @@ if (ENABLE_NODE_BINDINGS) add_subdirectory(src/nodejs) endif() +if (ENABLE_PYTHON_BINDINGS) + add_subdirectory(src/python) +endif() + if (ENABLE_FUZZING) # Requires libosrm being built with sanitizers; make configurable and default to ubsan set(FUZZ_SANITIZER "undefined" CACHE STRING "Sanitizer to be used for Fuzz testing") diff --git a/README.md b/README.md index 70f661502f..d468d646d8 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,24 @@ For usage details have a look [these API docs](docs/nodejs/api.md). An exemplary implementation by a 3rd party with Docker and Node.js can be found [here](https://github.com/door2door-io/osrm-express-server-demo). +### Using the Python Bindings + +The Python bindings provide read-only access to the routing engine via [nanobind](https://github.com/wjakob/nanobind). + +You can install the Python bindings from PyPI via + + pip install osrm-bindings + +We distribute `abi3` wheels for CPython 3.12+ on Linux (x86_64, aarch64), macOS (arm64) and Windows (x86_64). On other platforms `pip` will fall back to building from source, which requires CPython 3.10+ and the OSRM build dependencies. + +To build from source from this repository: + + pip install . + +#### Package docs + +For usage details and examples have a look at [the Python bindings README](src/python/README.md). + ## References in publications When using the code in a (scientific) publication, please cite diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 1c2a310a5f..ad96720fc2 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -14,7 +14,11 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, { text: 'HTTP API', link: '/http' }, - { text: 'Node.js API', link: '/nodejs/api' } + { text: 'Node.js API', link: '/nodejs/api' }, + { text: 'Python', items: [ + { text: 'API', link: '/python/api' }, + { text: 'Development', link: '/python/development' } + ]} ], sidebar: [ @@ -24,7 +28,8 @@ export default defineConfig({ { text: 'Tool options', link: '/tools'}, { text: 'Developing', link: '/developing' }, { text: 'Testing', link: '/testing' }, - { text: 'Releasing', link: '/releasing' } + { text: 'Releasing', link: '/releasing' }, + { text: 'Python Development', link: '/python/development' } ] }, { @@ -33,6 +38,7 @@ export default defineConfig({ { text: 'HTTP API', link: '/http' }, { text: 'Node.js API', link: '/nodejs/api' }, { text: 'libosrm C++ API', link: '/libosrm' }, + { text: 'Python API', link: '/python/api' }, ] }, { diff --git a/docs/index.md b/docs/index.md index f615efabd8..b7a029b28e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,9 @@ hero: - theme: alt text: Node.js API link: /nodejs/api + - theme: alt + text: Python API + link: /python/api - theme: alt text: View on GitHub link: https://github.com/Project-OSRM/osrm-backend @@ -46,6 +49,7 @@ OSRM provides powerful routing services through both HTTP and Node.js APIs: - **[HTTP API](./http.md)** - RESTful API for routing services - **[Node.js API](./nodejs/api.md)** - Native Node.js bindings for embedded use +- **[Python API](./python/api.md)** - Python bindings via nanobind ([Development Guide](./python/development.md)) ## Documentation diff --git a/docs/python/api.md b/docs/python/api.md new file mode 100644 index 0000000000..0346c51c5f --- /dev/null +++ b/docs/python/api.md @@ -0,0 +1,243 @@ +# Python API + +The Python bindings provide access to OSRM's routing services through the `osrm` package. Install with `pip install osrm-bindings`. + +## OSRM + +The `OSRM` class is the main entry point. It requires a `.osrm.*` dataset prepared by the OSRM toolchain. + +```python +import osrm + +# From file +engine = osrm.OSRM("path/to/data.osrm") + +# With keyword arguments +engine = osrm.OSRM( + storage_config="path/to/data.osrm", + algorithm="CH", # or "MLD" + use_shared_memory=False, + max_locations_trip=3, + max_locations_viaroute=3, + max_locations_distance_table=3, + max_locations_map_matching=3, + max_results_nearest=1, + max_alternatives=1, + default_radius="unlimited", +) + +# Using shared memory (requires osrm-datastore) +engine = osrm.OSRM(use_shared_memory=True) +``` + +### Parameters + +- **`storage_config`** `str` - Path to the `.osrm` dataset. +- **`algorithm`** `str` - Routing algorithm: `"CH"` or `"MLD"`. Default: `"CH"`. +- **`use_shared_memory`** `bool` - Connect to shared memory datastore. Default: `True`. +- **`dataset_name`** `str` - Named shared memory dataset (requires `osrm-datastore --dataset_name`). +- **`memory_file`** `str` - **Deprecated.** Equivalent to `use_mmap=True`. +- **`use_mmap`** `bool` - Memory-map files instead of loading into RAM. +- **`max_locations_trip`** `int` - Max locations in trip queries. +- **`max_locations_viaroute`** `int` - Max locations in route queries. +- **`max_locations_distance_table`** `int` - Max locations in table queries. +- **`max_locations_map_matching`** `int` - Max locations in match queries. +- **`max_results_nearest`** `int` - Max results in nearest queries. +- **`max_alternatives`** `int` - Max alternative routes. +- **`default_radius`** `float | "unlimited"` - Default search radius in meters. + +### Services + +All service methods take a parameters object and return a dict-like `Object`: + +```python +result = engine.Route(route_params) +print(result["routes"]) +print(result["waypoints"]) +``` + +## Route + +Finds the fastest route between two or more coordinates. + +```python +params = osrm.RouteParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077)], + steps=True, + alternatives=2, + annotations=["speed", "duration"], + geometries="geojson", + overview="full", +) +result = engine.Route(params) +``` + +### RouteParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`steps`** `bool` - Return route steps for each leg. Default: `False`. +- **`alternatives`** `int` - Number of alternative routes to search for. Default: `0`. +- **`annotations`** `list[str]` - Additional metadata: `"none"`, `"duration"`, `"nodes"`, `"distance"`, `"weight"`, `"datasources"`, `"speed"`, `"all"`. Default: `[]`. +- **`geometries`** `str` - Geometry format: `"polyline"`, `"polyline6"`, `"geojson"`. Default: `"polyline"`. +- **`overview`** `str` - Overview geometry: `"simplified"`, `"full"`, `"false"`. Default: `"simplified"`. +- **`continue_straight`** `bool | None` - Force route to continue straight at waypoints. +- **`waypoints`** `list[int]` - Indices of coordinates to treat as waypoints. Must include first and last. + +## Table + +Computes duration/distance matrices between coordinates. + +```python +params = osrm.TableParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + sources=[0], + destinations=[1, 2], + annotations=["duration", "distance"], +) +result = engine.Table(params) +``` + +### TableParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`sources`** `list[int]` - Indices of source coordinates. Default: all. +- **`destinations`** `list[int]` - Indices of destination coordinates. Default: all. +- **`annotations`** `list[str]` - `"duration"`, `"distance"`, `"all"`. Default: `["duration"]`. +- **`fallback_speed`** `float` - Speed for crow-flies fallback when no route found. +- **`fallback_coordinate_type`** `str` - `"input"` or `"snapped"`. +- **`scale_factor`** `float` - Scales duration values. Default: `1.0`. + +## Nearest + +Finds the nearest street segment for a coordinate. + +```python +params = osrm.NearestParameters( + coordinates=[(7.41337, 43.72956)], + number_of_results=3, +) +result = engine.Nearest(params) +``` + +### NearestParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`number_of_results`** `int` - Number of nearest segments to return. Default: `1`. + +## Match + +Snaps noisy GPS traces to the road network. + +```python +params = osrm.MatchParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[5.0, 5.0, 5.0], + annotations=["speed"], + geometries="geojson", +) +result = engine.Match(params) +``` + +### MatchParameters + +Inherits all [RouteParameters](#routeparameters) and [BaseParameters](#baseparameters). + +- **`timestamps`** `list[int]` - UNIX timestamps for each coordinate. +- **`gaps`** `str` - Gap handling: `"split"` or `"ignore"`. Default: `"split"`. +- **`tidy`** `bool` - Remove duplicates. Default: `False`. +- **`waypoints`** `list[int]` - Indices of coordinates to treat as waypoints. + +## Trip + +Solves the Traveling Salesman Problem for the given coordinates. + +```python +params = osrm.TripParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + source="first", + destination="last", + roundtrip=True, + annotations=["duration"], + geometries="geojson", +) +result = engine.Trip(params) +``` + +### TripParameters + +Inherits all [RouteParameters](#routeparameters) and [BaseParameters](#baseparameters). + +- **`source`** `str` - `"any"` or `"first"`. Default: `"any"`. +- **`destination`** `str` - `"any"` or `"last"`. Default: `"any"`. +- **`roundtrip`** `bool` - Return to first location. Default: `True`. + +## Tile + +Generates vector tiles with internal routing graph data. + +```python +params = osrm.TileParameters(x=17059, y=11948, z=15) +result = engine.Tile(params) # returns bytes +``` + +### TileParameters + +- **`x`** `int` - Tile x coordinate. +- **`y`** `int` - Tile y coordinate. +- **`z`** `int` - Tile zoom level. + +## BaseParameters + +Shared parameters inherited by Nearest, Table, Route, Match, and Trip. + +- **`coordinates`** `list[tuple[float, float]]` - List of `(longitude, latitude)` pairs. +- **`hints`** `list[str | None]` - Base64-encoded hints from previous requests. +- **`radiuses`** `list[float | None]` - Search radius per coordinate in meters. `None` for unlimited. +- **`bearings`** `list[tuple[int, int] | None]` - `(bearing, range)` pairs in degrees. `None` for unrestricted. +- **`approaches`** `list[str | None]` - `"curb"`, `"unrestricted"`, or `None`. +- **`generate_hints`** `bool` - Include hints in response. Default: `True`. +- **`exclude`** `list[str]` - Road classes to avoid (e.g. `["motorway"]`). +- **`snapping`** `str` - `"default"` or `"any"`. Default: `"default"`. + +## Types + +### Coordinate + +```python +coord = osrm.Coordinate((7.41337, 43.72956)) +print(coord.lon, coord.lat) +``` + +### Bearing + +```python +bearing = osrm.Bearing((200, 180)) +print(bearing.bearing, bearing.range) +``` + +### Object / Array + +Service results are returned as `Object` (dict-like) and `Array` (list-like) wrappers around OSRM's internal JSON types. They support `[]`, `len()`, `in`, and iteration. + +```python +result = engine.Route(params) +for route in result["routes"]: + print(route["distance"], route["duration"]) +``` + +## CLI + +The package also installs OSRM command-line tools, accessible via `python -m osrm`: + +```bash +python -m osrm extract data.osm.pbf -p profiles/car.lua +python -m osrm contract data.osrm +python -m osrm partition data.osrm +python -m osrm customize data.osrm +python -m osrm datastore data.osrm +python -m osrm routed data.osrm +``` diff --git a/docs/python/development.md b/docs/python/development.md new file mode 100644 index 0000000000..00226534a7 --- /dev/null +++ b/docs/python/development.md @@ -0,0 +1,261 @@ +# Python Bindings Development Guide + +## Installing for production + +Pre-built wheels are published to PyPI for Linux (x86\_64, aarch64), macOS (arm64), and Windows (amd64), requiring Python 3.12+: + +```bash +pip install osrm-bindings +``` + +To build from source (e.g. unsupported platform): + +```bash +pip install osrm-bindings --no-binary osrm-bindings +``` + +Source builds compile the full OSRM C++ library — this takes a long time. +See [platform-specific notes](#platform-specific-build-requirements) for prerequisites. + +## Installing for development + +Clone the repo and install in editable mode with dev dependencies: + +```bash +git clone https://github.com/Project-OSRM/osrm-backend +cd osrm-backend +pip install -e ".[dev]" +``` + +Install pre-commit hooks: + +```bash +pre-commit install +``` + +## Platform-specific build requirements + +### Linux + +No extra steps needed. Wheels are built inside a custom manylinux image +that has all OSRM dependencies baked in. A regular source install will pull +OSRM's dependencies via the system package manager or build them from source. + +### macOS + +Install OSRM's C++ dependencies via Homebrew: + +```bash +brew install lua tbb boost@1.90 +brew link boost@1.90 +``` + +### Windows + +Windows uses [Conan](https://conan.io/) for OSRM's C++ dependencies. Install +it and generate a default profile before building: + +```bash +pip install conan==2.27.0 +conan profile detect --force +``` + +Pass `ENABLE_CONAN=ON` to CMake at build time (see below). + +## Building locally + +### Editable install (recommended for development) + +A standard `pip install -e .` works, but by default pip uses PEP 517 isolated +builds — each invocation creates a temporary directory, compiles everything, +then discards it. This means OSRM is recompiled from scratch every time. + +Use `--no-build-isolation` to make scikit-build-core reuse the persistent +build directory (`build/{wheel_tag}/`) across runs: + +```bash +# Linux / macOS +pip install -e . --no-build-isolation + +# Windows +pip install -e . --no-build-isolation -C cmake.define.ENABLE_CONAN=ON +``` + +The first run is slow (full OSRM compile). Subsequent runs only recompile +changed binding files. + +::: warning Keep config flags identical across runs +scikit-build-core hashes its configuration to detect changes. If the flags +differ between runs, it wipes the build directory and starts from scratch. +::: + +::: warning Generator mismatch +CMake records the generator in `CMakeCache.txt`. If you ever see +`Does not match the generator used previously`, delete the build directory +and rebuild from scratch: +```powershell +Remove-Item -Recurse -Force build/cp312-abi3-win_amd64 +``` +::: + +### Building a wheel + +After the editable install has compiled everything, produce a wheel without +recompiling: + +```bash +# Linux / macOS +pip wheel . --no-build-isolation -w dist + +# Windows +pip wheel . --no-build-isolation -C cmake.define.ENABLE_CONAN=ON -w dist +``` + +CMake finds the existing artifacts in the build directory and skips +recompilation. The wheel lands in `dist/`. + +### Wheel repair + +Locally built wheels link against system shared libraries and are tagged +as `linux_x86_64` (not `manylinux`). To make them portable or to inspect +their dependencies, use the platform-specific repair tools: + +**Linux** — [auditwheel](https://github.com/pypa/auditwheel): + +```bash +pip install auditwheel +auditwheel show dist/*.whl # inspect shared library dependencies +auditwheel repair -w dist dist/*.whl # bundle libs and retag as manylinux +``` + +**macOS** — [delocate](https://github.com/matthew-brett/delocate): + +```bash +pip install delocate +delocate-listdeps dist/*.whl # inspect dependencies +delocate-wheel -w dist dist/*.whl # bundle dylibs +``` + +**Windows** — [delvewheel](https://github.com/adang1345/delvewheel): + +```bash +pip install delvewheel +delvewheel show dist/*.whl # inspect dependencies +delvewheel repair -w dist dist/*.whl +``` + +On Windows, Conan's shared DLLs (tbb, hwloc) must be on PATH for delvewheel +to find them. Activate the `conanrun.bat` generated in the build tree first. + +::: tip +cibuildwheel runs wheel repair automatically in CI. You only need these +commands when building wheels locally for distribution. +::: + +### Compiler cache + +On Linux and macOS, ccache is used automatically (pre-installed in the +manylinux image; installed via Homebrew for macOS CI). + +On Windows, scikit-build-core defaults to the **Visual Studio generator**, +which does not support `CMAKE_CXX_COMPILER_LAUNCHER`. The build dir reuse +from `--no-build-isolation` is the main speed optimisation for local Windows +development. + +## Running tests + +Build the test data (requires the package to be installed so the `osrm` +executables are available): + +```bash +# Linux / macOS +cd test/data && make + +# Windows +cd test\data && windows-build-test-data.bat +``` + +Load the shared memory datastore: + +```bash +python -m osrm datastore test/data/ch/monaco +``` + +Run the test suite: + +```bash +pytest test/python/ +``` + +## Running cibuildwheel locally + +[cibuildwheel](https://cibuildwheel.pypa.io/) builds wheels inside isolated +environments that closely match CI. Install it with: + +```bash +pip install cibuildwheel +``` + +Build for the current platform: + +```bash +cibuildwheel --platform linux # requires Docker on non-Linux hosts +cibuildwheel --platform macos +cibuildwheel --platform windows +``` + +Wheels land in `wheelhouse/`. + +**Windows note:** cibuildwheel's `config-settings` in pyproject.toml are +*replaced* (not merged) by `CIBW_CONFIG_SETTINGS_WINDOWS` if that env var is +set. Always include `ENABLE_CONAN=ON` explicitly when overriding via the env +var. + +**Linux note:** The manylinux container mounts the host ccache directory via a +Docker volume. Set `CCACHE_DIR` on the host so the mount path matches what CI +uses: + +```bash +CIBW_CONTAINER_ENGINE="docker; create_args: --volume /tmp/ccache:/ccache" \ +CIBW_ENVIRONMENT_LINUX="CCACHE_DIR=/ccache" \ +cibuildwheel --platform linux +``` + +## Type stubs + +`src/python/osrm/osrm_ext.pyi` is auto-generated by `nanobind_add_stub()` at +build time and committed to the repository so documentation tools can work +without compiling the extension. + +After changing C++ bindings, rebuild and commit the updated stub: + +```bash +pip install -e . --no-build-isolation # regenerates the .pyi +git add src/python/osrm/osrm_ext.pyi +``` + +To regenerate manually without a full rebuild: + +```bash +pip install nanobind ruff +python -m nanobind.stubgen -m osrm.osrm_ext -o src/python/osrm/osrm_ext.pyi +ruff format src/python/osrm/osrm_ext.pyi +``` + +## Releasing + +Releases are driven by git tags. `setuptools-scm` reads the tag to set the +package version — no manual version bumps needed. + +1. Ensure CI is green on `main`. +2. Create and push an annotated tag: + ```bash + git tag -a v1.2.3 -m "v1.2.3" + git push origin v1.2.3 + ``` +3. The publish workflow triggers on tag push, builds wheels for all platforms, + and uploads to PyPI via trusted publisher. +4. Verify the release at [pypi.org/project/osrm-bindings](https://pypi.org/project/osrm-bindings/). + +**Test release without tagging:** trigger the publish workflow manually via +`workflow_dispatch` with the `upload` input set to `true`. diff --git a/package-lock.json b/package-lock.json index e95a8f3cc4..2191dd965a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.1", + "version": "26.4.5", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 22f88fe929..81562f8e97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.1", + "version": "26.4.5", "private": false, "type": "module", "description": "The Open Source Routing Machine is a high performance routing engine written in C++ designed to run on OpenStreetMap data.", diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000000..e7178176b3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,103 @@ +[build-system] +requires = [ + "scikit-build-core >=0.12.0", + "nanobind >=2.12.0", + "setuptools-scm >=10", + "cmake", + "ninja", + "conan==2.27.0; sys_platform == 'win32'", +] +build-backend = "scikit_build_core.build" + +[project] +name = "osrm-bindings" +dynamic = ["version"] +description = "Python bindings for the osrm-backend project" +readme = "src/python/README.md" +requires-python = ">=3.10" +authors = [{name = "Doo Woong Chung"}, { name = "Nils Nolde" }] +license = { file = "LICENSE.TXT" } + +[project.optional-dependencies] +dev = ["pytest", "pre-commit", "ruff"] + +[project.urls] +repository = "https://github.com/Project-OSRM/osrm-backend" + +[tool.ruff] +exclude = [".venv", "build"] +extend-exclude = [ + "dist", + "wheelhouse", + "*build*", + "*.egg-info", +] +line-length = 105 +lint.ignore = ["E731", "F811", "E741"] + +[tool.scikit-build.metadata] +version.provider = "scikit_build_core.metadata.setuptools_scm" + +[tool.setuptools_scm] +version_scheme = "no-guess-dev" +local_scheme = "no-local-version" + +[tool.scikit-build] +minimum-version = "0.4" +build-dir = "build/{wheel_tag}" +cmake.build-type = "Release" +wheel.py-api = "cp312" +wheel.packages = ["src/python/osrm"] +wheel.exclude = ["include/**", "lib/**", "bin/**", "share/**"] +sdist.include = ["pyproject.toml"] +sdist.exclude = ["dist", "wheelhouse", "build", "build-*", ".venv", "**/.venv"] + +[tool.scikit-build.cmake.define] +ENABLE_PYTHON_BINDINGS = "ON" +CMAKE_BUILD_TYPE = "Release" +CMAKE_CXX_SCAN_FOR_MODULES = "OFF" + +[tool.cibuildwheel] +archs = ["native"] +build = "cp312-*" +build-verbosity = 1 +skip = "*musllinux*" +# TODO(nils): should we leave it here or push to osrm org? +manylinux-x86_64-image = "ghcr.io/nilsnolde/manylinux:2_28_osrm_python" +manylinux-aarch64-image = "ghcr.io/nilsnolde/manylinux:2_28_osrm_python" +test-requires = "pytest" +test-command = [ + "cd {project}/test/data && python -m osrm extract -p {project}/profiles/car.lua monaco.osm.pbf", + "mkdir -p {project}/test/data/ch && cp {project}/test/data/monaco.osrm* {project}/test/data/ch/", + "python -m osrm contract {project}/test/data/ch/monaco.osrm", + "mkdir -p {project}/test/data/mld && cp {project}/test/data/monaco.osrm* {project}/test/data/mld/", + "python -m osrm partition {project}/test/data/mld/monaco.osrm", + "python -m osrm customize {project}/test/data/mld/monaco.osrm", + "python -m osrm datastore {project}/test/data/ch/monaco", + "pytest {project}/test/python" +] + +[tool.cibuildwheel.linux] +environment = "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH}" +before-build = "ccache -s && ccache -M 500M" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=$HOME/.ccache" +before-build = "ccache -s && ccache -M 500M" +before-all = """ + export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 + brew install lua tbb boost@1.90 ccache + brew link boost@1.90 +""" + +[tool.cibuildwheel.windows] +before-build = "pip install conan==2.27.0 delvewheel && conan profile detect --force" +config-settings = "cmake.define.ENABLE_CONAN=ON" +repair-wheel-command = 'call build\cp312-abi3-win_amd64\conanrun.bat && delvewheel repair --analyze-existing-exes --add-dll hwloc.dll --no-mangle tbb12.dll --no-mangle hwloc.dll -w {dest_dir} {wheel}' +test-command = [ + "cd /d {project}/test/data", + "windows-build-test-data.bat", + "python -m osrm datastore {project}/test/data/ch/monaco", + "pytest {project}/test/python" +] diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt new file mode 100644 index 0000000000..4883df97bc --- /dev/null +++ b/src/python/CMakeLists.txt @@ -0,0 +1,106 @@ +message(STATUS "Building Python bindings") + +set(EXT_NAME "osrm_ext") + +find_package(Python + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule +) + +# Find nanobind's cmake dir via the Python module +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR +) +list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") +find_package(nanobind CONFIG REQUIRED) + +set(SRCS + src/osrm_nb.cpp + src/engineconfig_nb.cpp + src/utility/osrm_utility.cpp + src/utility/param_utility.cpp + + src/parameters/baseparameter_nb.cpp + src/parameters/routeparameter_nb.cpp + src/parameters/matchparameter_nb.cpp + src/parameters/nearestparameter_nb.cpp + src/parameters/tableparameter_nb.cpp + src/parameters/tileparameter_nb.cpp + src/parameters/tripparameter_nb.cpp + + src/types/optional_nb.cpp + src/types/coordinate_nb.cpp + src/types/jsoncontainer_nb.cpp + src/types/approach_nb.cpp + src/types/bearing_nb.cpp +) +nanobind_add_module( + ${EXT_NAME} + STABLE_ABI + NB_STATIC + ${SRCS} +) + +# OSRM's root CMakeLists.txt enables CMAKE_INTERPROCEDURAL_OPTIMIZATION globally, +# which conflicts with nanobind's NB_MAKE_OPAQUE macros (ODR violations across TUs +# flagged as errors during LTO). Disable IPO for the extension module — nanobind's +# own LTO flag (passed to nanobind_add_module above) handles LTO for the binding. +set_target_properties(${EXT_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE) +if(NOT MSVC) + # Suppress warnings from nanobind headers included by our binding sources. + target_compile_options(${EXT_NAME} PRIVATE + -Wno-suggest-destructor-override + -Wno-redundant-decls + ) + # Also suppress LTO warnings-as-errors at link time, since the osrm library + # is compiled with fat LTO objects and GCC's LTO linker plugin sees ODR + # differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. + target_link_options(${EXT_NAME} PRIVATE -Wno-error) + + # nanobind's internal sources trigger warnings under OSRM's strict -Werror flags + # (inherited via CMAKE_CXX_FLAGS). Suppress for the nanobind static library target. + set(_nb_suppress -Wno-error -Wno-suggest-destructor-override -Wno-redundant-decls) + if(TARGET nanobind-static-abi3) + target_compile_options(nanobind-static-abi3 PRIVATE ${_nb_suppress}) + elseif(TARGET nanobind-static) + target_compile_options(nanobind-static PRIVATE ${_nb_suppress}) + endif() +endif() + +# Binding headers live in src/python/include/python/ +target_include_directories(${EXT_NAME} + PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +# Link against the osrm library built by the parent project +target_link_libraries(${EXT_NAME} + PRIVATE + osrm + Python::SABIModule +) + +if (MSVC) + target_compile_definitions(${EXT_NAME} PRIVATE BOOST_ALL_NO_LIB) +endif() + +# Install the extension module into the osrm Python package +install(TARGETS ${EXT_NAME} LIBRARY DESTINATION osrm) + +# Install OSRM executables and profiles into the wheel +set(OSRM_EXECUTABLES osrm-extract osrm-contract osrm-customize osrm-partition osrm-datastore osrm-routed osrm-components) +install(TARGETS ${OSRM_EXECUTABLES} DESTINATION osrm/bin) +install(DIRECTORY ${PROJECT_SOURCE_DIR}/profiles DESTINATION osrm/share) + +# Generate Python type stubs (.pyi) from the compiled extension module. +# Stubs are written to the source tree so they can be committed and used by +# documentation tools without compiling the extension. +nanobind_add_stub( + ${EXT_NAME}_stub + MODULE osrm_ext + OUTPUT ${PROJECT_SOURCE_DIR}/src/python/osrm/osrm_ext.pyi + PYTHON_PATH $ + DEPENDS ${EXT_NAME} +) diff --git a/src/python/README.md b/src/python/README.md new file mode 100644 index 0000000000..f2ee4b4395 --- /dev/null +++ b/src/python/README.md @@ -0,0 +1,118 @@ +# osrm-bindings + +**Python bindings for [osrm-backend](https://github.com/Project-OSRM/osrm-backend) using [nanobind](https://github.com/wjakob/nanobind).** + +## PyPI + +``` +pip install osrm-bindings +``` + +> [!NOTE] +> On PyPI we only distribute `abi3` wheels for each platform, i.e. one needs at least Python 3.12 to install the wheels. Of course it'll fall back to attempt an installation from source. + +Platform | Arch +---|--- +Linux | x86_64 +Linux | aarch64 +MacOS | arm64 +Windows | x86_64 + +## From Source + +osrm-bindings requires **CPython 3.10+** and can be installed from source (e.g. from an sdist on PyPI) by running the following command in the repository root: + +``` +pip install . +``` + +The Python bindings are built alongside the OSRM C++ libraries. The version of the bindings matches the version of osrm-backend. + +### System dependencies + +A source/sdist build compiles the full OSRM C++ library, so the usual C++ toolchain (CMake ≥ 3.18, a C++20 compiler, Git) plus OSRM's native dependencies must be available. Also a development Python installation has to be in the PATH. + +**Debian / Ubuntu** + +``` +sudo apt-get install -y \ + cmake g++ git pkg-config \ + libboost-all-dev libbz2-dev liblua5.4-dev \ + libtbb-dev libxml2-dev libzip-dev +``` + +**Fedora / RHEL / Rocky / AlmaLinux** + +``` +sudo dnf install -y \ + cmake gcc-c++ git pkgconf-pkg-config \ + boost-devel bzip2-devel lua-devel \ + tbb-devel libxml2-devel libzip-devel +``` + +**Alpine** + +``` +apk add --no-cache \ + cmake clang make git pkgconf \ + boost-dev bzip2-dev lua5.4-dev \ + onetbb-dev libxml2-dev libzip-dev expat-dev +``` + +**macOS (Homebrew)** + +``` +brew install cmake lua tbb boost@1.90 +brew link boost@1.90 +``` + +**Windows** + +Windows uses [Conan](https://conan.io/) for OSRM's C++ dependencies. Install Conan 2.x and pass `ENABLE_CONAN=ON` to CMake: + +``` +pip install conan==2.27.0 +conan profile detect --force +pip install . -C cmake.define.ENABLE_CONAN=ON +``` + +A full Visual Studio 2022 toolchain (or the Build Tools equivalent with the C++ workload) is required. + +## Example + +The following example will showcase the process of calculating routes between two coordinates. + +First, import the `osrm` library, and instantiate an instance of OSRM: +```python +import osrm + +# Instantiate osrm_py instance +osrm_py = osrm.OSRM("./test/data/ch/monaco.osrm") +``` + +Then, declare `RouteParameters`, and then pass it into the `osrm_py` instance: +```python +# Declare Route Parameters +route_params = osrm.RouteParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)] +) + +# Pass it into the osrm_py instance +res = osrm_py.Route(route_params) + +# Print out result output +print(res["waypoints"]) +print(res["routes"]) +``` + +## Type Stubs + +The file `src/python/osrm/osrm_ext.pyi` contains auto-generated type stubs for the C++ extension module. These are used by IDEs for autocompletion and by documentation tools without compiling the extension. + +After changing C++ bindings, rebuild the project to regenerate the stubs: + +``` +pip install -e . +``` + +Then commit the updated `.pyi` file. diff --git a/src/python/include/python/engineconfig_nb.hpp b/src/python/include/python/engineconfig_nb.hpp new file mode 100644 index 0000000000..1f57d271c2 --- /dev/null +++ b/src/python/include/python/engineconfig_nb.hpp @@ -0,0 +1,19 @@ +#ifndef OSRM_NB_ENGINECONFIG_H +#define OSRM_NB_ENGINECONFIG_H + +#include "engine/engine_config.hpp" + +#include + +#include + +using osrm::engine::EngineConfig; + +void init_EngineConfig(nanobind::module_ &m); + +static const std::unordered_map algorithm_map{ + {"CH", EngineConfig::Algorithm::CH}, + {std::string(), EngineConfig::Algorithm::CH}, + {"MLD", EngineConfig::Algorithm::MLD}}; + +#endif // OSRM_NB_ENGINECONFIG_H diff --git a/src/python/include/python/parameters/baseparameter_nb.hpp b/src/python/include/python/parameters/baseparameter_nb.hpp new file mode 100644 index 0000000000..999b59203c --- /dev/null +++ b/src/python/include/python/parameters/baseparameter_nb.hpp @@ -0,0 +1,28 @@ +#ifndef OSRM_NB_BASEPARAMETER_H +#define OSRM_NB_BASEPARAMETER_H + +#include "engine/api/base_parameters.hpp" + +#include + +#include + +using osrm::engine::api::BaseParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::BaseParameters::SnappingType) +NB_MAKE_OPAQUE(osrm::engine::api::BaseParameters::OutputFormatType) + +void init_BaseParameters(nanobind::module_ &m); + +static const std::unordered_map snapping_map{ + {"default", BaseParameters::SnappingType::Default}, + {std::string(), BaseParameters::SnappingType::Default}, + {"any", BaseParameters::SnappingType::Any}}; + +static const std::unordered_map output_map{ + {"json", BaseParameters::OutputFormatType::JSON}, + {std::string(), BaseParameters::OutputFormatType::JSON}, + {"flatbuffers", BaseParameters::OutputFormatType::FLATBUFFERS}}; + +#endif // OSRM_NB_BASEPARAMETER_H diff --git a/src/python/include/python/parameters/matchparameter_nb.hpp b/src/python/include/python/parameters/matchparameter_nb.hpp new file mode 100644 index 0000000000..cf3a397d08 --- /dev/null +++ b/src/python/include/python/parameters/matchparameter_nb.hpp @@ -0,0 +1,23 @@ +#ifndef OSRM_NB_MATCHPARAMETER_H +#define OSRM_NB_MATCHPARAMETER_H + +#include "python/parameters/routeparameter_nb.hpp" +#include "engine/api/match_parameters.hpp" + +#include + +#include + +using osrm::engine::api::MatchParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::MatchParameters::GapsType) + +void init_MatchParameters(nanobind::module_ &m); + +static const std::unordered_map gaps_map{ + {"split", MatchParameters::GapsType::Split}, + {std::string(), MatchParameters::GapsType::Split}, + {"ignore", MatchParameters::GapsType::Ignore}}; + +#endif // OSRM_NB_MATCHPARAMETER_H diff --git a/src/python/include/python/parameters/nearestparameter_nb.hpp b/src/python/include/python/parameters/nearestparameter_nb.hpp new file mode 100644 index 0000000000..b8b8b704da --- /dev/null +++ b/src/python/include/python/parameters/nearestparameter_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_NEARESTPARAMETER_H +#define OSRM_NB_NEARESTPARAMETER_H + +#include + +void init_NearestParameters(nanobind::module_ &m); + +#endif // OSRM_NB_NEARESTPARAMETER_H diff --git a/src/python/include/python/parameters/routeparameter_nb.hpp b/src/python/include/python/parameters/routeparameter_nb.hpp new file mode 100644 index 0000000000..b1254ac528 --- /dev/null +++ b/src/python/include/python/parameters/routeparameter_nb.hpp @@ -0,0 +1,43 @@ +#ifndef OSRM_NB_ROUTEPARAMETER_H +#define OSRM_NB_ROUTEPARAMETER_H + +#include "python/parameters/baseparameter_nb.hpp" +#include "engine/api/route_parameters.hpp" + +#include + +#include + +using osrm::engine::api::RouteParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::GeometriesType) +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::OverviewType) +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::AnnotationsType) + +void init_RouteParameters(nanobind::module_ &m); + +static const std::unordered_map geometries_map{ + {"polyline", RouteParameters::GeometriesType::Polyline}, + {std::string(), RouteParameters::GeometriesType::Polyline}, + {"polyline6", RouteParameters::GeometriesType::Polyline6}, + {"geojson", RouteParameters::GeometriesType::GeoJSON}}; + +static const std::unordered_map overview_map{ + {"simplified", RouteParameters::OverviewType::Simplified}, + {std::string(), RouteParameters::OverviewType::Simplified}, + {"full", RouteParameters::OverviewType::Full}, + {"false", RouteParameters::OverviewType::False}}; + +static const std::unordered_map + route_annotations_map{{"none", RouteParameters::AnnotationsType::None}, + {std::string(), RouteParameters::AnnotationsType::None}, + {"duration", RouteParameters::AnnotationsType::Duration}, + {"nodes", RouteParameters::AnnotationsType::Nodes}, + {"distance", RouteParameters::AnnotationsType::Distance}, + {"weight", RouteParameters::AnnotationsType::Weight}, + {"datasources", RouteParameters::AnnotationsType::Datasources}, + {"speed", RouteParameters::AnnotationsType::Speed}, + {"all", RouteParameters::AnnotationsType::All}}; + +#endif // OSRM_NB_ROUTEPARAMETER_H diff --git a/src/python/include/python/parameters/tableparameter_nb.hpp b/src/python/include/python/parameters/tableparameter_nb.hpp new file mode 100644 index 0000000000..9f78534cf5 --- /dev/null +++ b/src/python/include/python/parameters/tableparameter_nb.hpp @@ -0,0 +1,31 @@ +#ifndef OSRM_NB_TABLEPARAMETER_H +#define OSRM_NB_TABLEPARAMETER_H + +#include "python/parameters/baseparameter_nb.hpp" +#include "engine/api/table_parameters.hpp" + +#include + +#include + +using osrm::engine::api::TableParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::TableParameters::FallbackCoordinateType) +NB_MAKE_OPAQUE(osrm::engine::api::TableParameters::AnnotationsType) + +void init_TableParameters(nanobind::module_ &m); + +static const std::unordered_map fallback_map{ + {"input", TableParameters::FallbackCoordinateType::Input}, + {std::string(), TableParameters::FallbackCoordinateType::Input}, + {"snapped", TableParameters::FallbackCoordinateType::Snapped}}; + +static const std::unordered_map + table_annotations_map{{"none", TableParameters::AnnotationsType::None}, + {std::string(), TableParameters::AnnotationsType::None}, + {"duration", TableParameters::AnnotationsType::Duration}, + {"distance", TableParameters::AnnotationsType::Distance}, + {"all", TableParameters::AnnotationsType::All}}; + +#endif // OSRM_NB_TABLEPARAMETER_H diff --git a/src/python/include/python/parameters/tileparameter_nb.hpp b/src/python/include/python/parameters/tileparameter_nb.hpp new file mode 100644 index 0000000000..b02742e111 --- /dev/null +++ b/src/python/include/python/parameters/tileparameter_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_TILEPARAMETER_H +#define OSRM_NB_TILEPARAMETER_H + +#include + +void init_TileParameters(nanobind::module_ &m); + +#endif // OSRM_NB_TILEPARAMETER_H diff --git a/src/python/include/python/parameters/tripparameter_nb.hpp b/src/python/include/python/parameters/tripparameter_nb.hpp new file mode 100644 index 0000000000..ad6bccb596 --- /dev/null +++ b/src/python/include/python/parameters/tripparameter_nb.hpp @@ -0,0 +1,29 @@ +#ifndef OSRM_NB_TRIPPARAMETER_H +#define OSRM_NB_TRIPPARAMETER_H + +#include "python/parameters/routeparameter_nb.hpp" +#include "engine/api/trip_parameters.hpp" + +#include + +#include + +using osrm::engine::api::TripParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::TripParameters::SourceType) +NB_MAKE_OPAQUE(osrm::engine::api::TripParameters::DestinationType) + +void init_TripParameters(nanobind::module_ &m); + +static const std::unordered_map source_map{ + {"any", TripParameters::SourceType::Any}, + {std::string(), TripParameters::SourceType::Any}, + {"first", TripParameters::SourceType::First}}; + +static const std::unordered_map dest_map{ + {"any", TripParameters::DestinationType::Any}, + {std::string(), TripParameters::DestinationType::Any}, + {"last", TripParameters::DestinationType::Last}}; + +#endif // OSRM_NB_TRIPPARAMETER_H diff --git a/src/python/include/python/types/approach_nb.hpp b/src/python/include/python/types/approach_nb.hpp new file mode 100644 index 0000000000..8299e5d854 --- /dev/null +++ b/src/python/include/python/types/approach_nb.hpp @@ -0,0 +1,20 @@ +#ifndef OSRM_NB_APPROACH_H +#define OSRM_NB_APPROACH_H + +#include "engine/approach.hpp" + +#include + +#include +#include + +using osrm::engine::Approach; + +void init_Approach(nanobind::module_ &m); + +static const std::unordered_map approach_map{ + {"curb", Approach::CURB}, + {std::string(), Approach::CURB}, + {"unrestricted", Approach::UNRESTRICTED}}; + +#endif // OSRM_NB_APPROACH_H diff --git a/src/python/include/python/types/bearing_nb.hpp b/src/python/include/python/types/bearing_nb.hpp new file mode 100644 index 0000000000..f4e5396331 --- /dev/null +++ b/src/python/include/python/types/bearing_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_BEARING_H +#define OSRM_NB_BEARING_H + +#include + +void init_Bearing(nanobind::module_ &m); + +#endif // OSRM_NB_BEARING_H diff --git a/src/python/include/python/types/coordinate_nb.hpp b/src/python/include/python/types/coordinate_nb.hpp new file mode 100644 index 0000000000..ebfa3deac3 --- /dev/null +++ b/src/python/include/python/types/coordinate_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_COORDINATE_H +#define OSRM_NB_COORDINATE_H + +#include + +void init_Coordinate(nanobind::module_ &m); + +#endif // OSRM_NB_COORDINATE_H diff --git a/src/python/include/python/types/jsoncontainer_nb.hpp b/src/python/include/python/types/jsoncontainer_nb.hpp new file mode 100644 index 0000000000..8e67768f85 --- /dev/null +++ b/src/python/include/python/types/jsoncontainer_nb.hpp @@ -0,0 +1,120 @@ +#ifndef OSRM_NB_JSONCONTAINER_H +#define OSRM_NB_JSONCONTAINER_H + +#include "util/json_container.hpp" + +#include + +#include +#include + +void init_JSONContainer(nanobind::module_ &m); + +namespace json = osrm::util::json; +using JSONValue = json::Value; + +// Custom Type Casters +namespace nanobind::detail +{ + +template <> struct type_caster +{ + static constexpr auto Name = const_name("JSONValue"); + + template using Caster = make_caster>; + + template + static handle from_cpp(T &&val, rv_policy policy, cleanup_list *cleanup) noexcept + { + return std::visit( + [&](auto &&v) { + return Caster::from_cpp(std::forward(v), policy, cleanup); + }, + std::forward(val)); + } +}; + +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::String &val, rv_policy, cleanup_list *) noexcept + { + return PyUnicode_FromStringAndSize(val.value.c_str(), val.value.size()); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::Number &val, rv_policy, cleanup_list *) noexcept + { + return PyFloat_FromDouble((double)val.value); + } +}; + +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::True &, rv_policy, cleanup_list *) noexcept + { + return handle(Py_True).inc_ref(); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::False &, rv_policy, cleanup_list *) noexcept + { + return handle(Py_False).inc_ref(); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::Null &, rv_policy, cleanup_list *) noexcept + { + return none().release(); + } +}; + +} // namespace nanobind::detail + +struct ValueStringifyVisitor +{ + std::string operator()(const json::String &str) { return "'" + str.value + "'"; } + std::string operator()(const json::Number &num) { return std::to_string(num.value); } + std::string operator()(const json::True &) { return "True"; } + std::string operator()(const json::False &) { return "False"; } + std::string operator()(const json::Null &) { return "None"; } + + std::string visitarray(const json::Array &arr) + { + std::string output = "["; + for (size_t i = 0; i < arr.values.size(); ++i) + { + if (i != 0) + { + output += ", "; + } + output += std::visit(*this, arr.values[i]); + } + return output + "]"; + } + std::string operator()(const json::Array &arr) { return visitarray(arr); } + + std::string visitobject(const json::Object &obj) + { + std::string output = "{"; + bool first = true; + for (const auto &itr : obj.values) + { + if (!first) + { + output += ", "; + } + output += '\''; + output += itr.first; + output += "': "; + output += std::visit(*this, itr.second); + first = false; + } + return output + "}"; + } + std::string operator()(const json::Object &obj) { return visitobject(obj); } +}; + +#endif // OSRM_NB_JSONCONTAINER_H diff --git a/src/python/include/python/types/optional_nb.hpp b/src/python/include/python/types/optional_nb.hpp new file mode 100644 index 0000000000..eed5b535cc --- /dev/null +++ b/src/python/include/python/types/optional_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_OPTIONAL_H +#define OSRM_NB_OPTIONAL_H + +#include + +void init_Optional(nanobind::module_ &m); + +#endif // OSRM_NB_OPTIONAL_H diff --git a/src/python/include/python/utility/osrm_utility.hpp b/src/python/include/python/utility/osrm_utility.hpp new file mode 100644 index 0000000000..1b59d9cfe0 --- /dev/null +++ b/src/python/include/python/utility/osrm_utility.hpp @@ -0,0 +1,19 @@ +#ifndef OSRM_NB_OSRM_UTIL_H +#define OSRM_NB_OSRM_UTIL_H + +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "util/json_container.hpp" + +#include + +namespace osrm_nb_util +{ + +void check_status(osrm::engine::Status status, osrm::util::json::Object &res); + +void populate_cfg_from_kwargs(const nanobind::kwargs &kwargs, osrm::engine::EngineConfig &config); + +} // namespace osrm_nb_util + +#endif // OSRM_NB_OSRM_UTIL_H diff --git a/src/python/include/python/utility/param_utility.hpp b/src/python/include/python/utility/param_utility.hpp new file mode 100644 index 0000000000..226968e496 --- /dev/null +++ b/src/python/include/python/utility/param_utility.hpp @@ -0,0 +1,101 @@ +#ifndef OSRM_NB_PARAM_UTIL_H +#define OSRM_NB_PARAM_UTIL_H + +#include "engine/api/base_parameters.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/approach.hpp" + +#include +#include +#include + +using osrm::engine::Approach; +using osrm::engine::api::BaseParameters; +using osrm::engine::api::MatchParameters; +using osrm::engine::api::RouteParameters; +using osrm::engine::api::TableParameters; +using osrm::engine::api::TripParameters; + +namespace osrm_nb_util +{ + +template +T str_to_enum(const std::string &str, + const std::string &type_name, + const std::unordered_map &enum_map) +{ + auto itr = enum_map.find(str); + + if (itr != enum_map.end()) + { + return itr->second; + } + + std::string valid_strs = "(Valid Options: "; + bool first = true; + + for (auto itr : enum_map) + { + if (itr.first.empty()) + { + continue; + } + if (!first) + { + valid_strs += ", "; + } + valid_strs += "'" + itr.first + "'"; + first = false; + } + valid_strs += ")"; + + throw std::invalid_argument("Invalid " + type_name + ": '" + str + "' " + valid_strs); +} + +template +std::string enum_to_str(T enum_type, + const std::string &type_name, + const std::unordered_map &enum_map) +{ + for (auto itr : enum_map) + { + if (itr.second == enum_type) + { + return itr.first; + } + } + + throw std::invalid_argument("Undefined " + type_name + " Enum"); +} + +void assign_baseparameters(BaseParameters *params, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping); + +void assign_routeparameters(RouteParameters *params, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints); + +RouteParameters::AnnotationsType +calculate_routeannotations_type(const std::vector &annotations); + +TableParameters::AnnotationsType +calculate_tableannotations_type(const std::vector &annotations); + +} // namespace osrm_nb_util + +#endif // OSRM_NB_PARAM_UTIL_H diff --git a/src/python/osrm/__init__.py b/src/python/osrm/__init__.py new file mode 100755 index 0000000000..28f43ef022 --- /dev/null +++ b/src/python/osrm/__init__.py @@ -0,0 +1,15 @@ +# ruff: noqa: F401 +from .osrm_ext import ( + OSRM, + EngineConfig, + Bearing, + Coordinate, + RouteParameters, + NearestParameters, + TableParameters, + TileParameters, + TripParameters, + MatchParameters, + Array, + Object, +) diff --git a/src/python/osrm/__main__.py b/src/python/osrm/__main__.py new file mode 100644 index 0000000000..493ca47e08 --- /dev/null +++ b/src/python/osrm/__main__.py @@ -0,0 +1,72 @@ +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +IS_WIN = platform.system().lower() == "windows" + +# Executables are installed under osrm/bin/ inside the package directory. +# For editable/dev installs, fall back to the CMake build directory or PATH. +_BIN_DIR = Path(__file__).parent / "bin" + +# delvewheel bundles shared DLLs (tbb12, hwloc) into osrm_bindings.libs/. +# Subprocess-launched executables can't benefit from the .pyd DLL path +# patching, so we pass PATH explicitly on Windows. +_LIBS_DIR = Path(__file__).parent.parent / "osrm_bindings.libs" + +COMMANDS = { + "components": "osrm-components", + "contract": "osrm-contract", + "customize": "osrm-customize", + "datastore": "osrm-datastore", + "extract": "osrm-extract", + "partition": "osrm-partition", + "routed": "osrm-routed", +} + +if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS: + print("Usage: python -m osrm [args...]") + print(f"Commands: {', '.join(COMMANDS)}") + sys.exit(1) + +exe_name = COMMANDS[sys.argv[1]] +if IS_WIN: + exe_name += ".exe" + + +def _find_executable(exe_name): + # 1. Installed wheel layout: osrm/bin/ + candidate = _BIN_DIR / exe_name + if candidate.is_file(): + return candidate + + # 2. Editable install: look in CMake build directories + project_root = Path(__file__).parent.parent.parent.parent + for build_dir in sorted(project_root.glob("build/*/"), reverse=True): + candidate = build_dir / exe_name + if candidate.is_file(): + return candidate + + # 3. System PATH + found = shutil.which(exe_name) + if found: + return Path(found) + + return None + + +executable = _find_executable(exe_name) +if not executable: + print(f"OSRM executable not found: {exe_name}") + sys.exit(1) + +cmd = [str(executable)] + sys.argv[2:] + +env = None +if IS_WIN and _LIBS_DIR.is_dir(): + env = {**os.environ, "PATH": str(_LIBS_DIR) + os.pathsep + os.environ.get("PATH", "")} + +proc = subprocess.run(cmd, env=env) +sys.exit(proc.returncode) diff --git a/src/python/osrm/osrm_ext.pyi b/src/python/osrm/osrm_ext.pyi new file mode 100644 index 0000000000..7809db9d38 --- /dev/null +++ b/src/python/osrm/osrm_ext.pyi @@ -0,0 +1,855 @@ +from collections.abc import Iterator, Sequence +from typing import overload + +class EngineConfig: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, **kwargs) -> None: ... + def IsValid(self) -> bool: ... + def SetStorageConfig(self, arg: str, /) -> None: ... + @property + def max_locations_trip(self) -> int: ... + @max_locations_trip.setter + def max_locations_trip(self, arg: int, /) -> None: ... + @property + def max_locations_viaroute(self) -> int: ... + @max_locations_viaroute.setter + def max_locations_viaroute(self, arg: int, /) -> None: ... + @property + def max_locations_distance_table(self) -> int: ... + @max_locations_distance_table.setter + def max_locations_distance_table(self, arg: int, /) -> None: ... + @property + def max_locations_map_matching(self) -> int: ... + @max_locations_map_matching.setter + def max_locations_map_matching(self, arg: int, /) -> None: ... + @property + def max_radius_map_matching(self) -> float: ... + @max_radius_map_matching.setter + def max_radius_map_matching(self, arg: float, /) -> None: ... + @property + def max_results_nearest(self) -> int: ... + @max_results_nearest.setter + def max_results_nearest(self, arg: int, /) -> None: ... + @property + def default_radius(self) -> float: ... + @default_radius.setter + def default_radius(self, arg: float, /) -> None: ... + @property + def max_alternatives(self) -> int: ... + @max_alternatives.setter + def max_alternatives(self, arg: int, /) -> None: ... + @property + def use_shared_memory(self) -> bool: ... + @use_shared_memory.setter + def use_shared_memory(self, arg: bool, /) -> None: ... + @property + def memory_file(self) -> str: ... + @memory_file.setter + def memory_file(self, arg: str, /) -> None: ... + @property + def use_mmap(self) -> bool: ... + @use_mmap.setter + def use_mmap(self, arg: bool, /) -> None: ... + @property + def algorithm(self) -> Algorithm: ... + @algorithm.setter + def algorithm(self, arg: Algorithm, /) -> None: ... + @property + def verbosity(self) -> str: ... + @verbosity.setter + def verbosity(self, arg: str, /) -> None: ... + @property + def dataset_name(self) -> str: ... + @dataset_name.setter + def dataset_name(self, arg: str, /) -> None: ... + +class Algorithm: + def __init__(self, arg: str, /) -> None: ... + def __repr__(self) -> str: ... + +class Approach: + def __init__(self, arg: str, /) -> None: ... + def __repr__(self) -> str: ... + +class Bearing: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, arg: tuple[int, int], /) -> None: ... + @property + def bearing(self) -> int: ... + @bearing.setter + def bearing(self, arg: int, /) -> None: ... + @property + def range(self) -> int: ... + @range.setter + def range(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + def __eq__(self, arg: Bearing, /) -> bool: ... + def __ne__(self, arg: Bearing, /) -> bool: ... + +class Coordinate: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, coordinate: Coordinate) -> None: ... + @overload + def __init__(self, arg: tuple[float, float], /) -> None: ... + @property + def lon(self) -> float: ... + @lon.setter + def lon(self, arg: float, /) -> None: ... + @property + def lat(self) -> float: ... + @lat.setter + def lat(self, arg: float, /) -> None: ... + def IsValid(self) -> bool: ... + def __repr__(self) -> str: ... + def __eq__(self, arg: Coordinate, /) -> bool: ... + def __ne__(self, arg: Coordinate, /) -> bool: ... + +class Object: + def __init__(self) -> None: ... + def __len__(self) -> int: ... + def __bool__(self) -> bool: ... + def __repr__(self) -> str: ... + def __getitem__(self, arg: str, /) -> object: ... + def __contains__(self, arg: str, /) -> bool: ... + def __iter__(self) -> Iterator[str]: ... + +class Array: + def __init__(self) -> None: ... + def __len__(self) -> int: ... + def __bool__(self) -> bool: ... + def __repr__(self) -> str: ... + def __getitem__(self, arg: int, /) -> object: ... + def __iter__(self) -> Iterator: ... + +class String: + def __init__(self, arg: str, /) -> None: ... + +class Number: + def __init__(self, arg: float, /) -> None: ... + +class BaseParameters: + def __init__(self) -> None: + r""" + Instantiates an instance of BaseParameters. + + Note: + This is the parent class to many parameter classes, and not intended to be used on its own. + + Args: + coordinates (list of floats pairs): Pairs of Longitude and Latitude Coordinates. (default []) + hints (list): Hint from previous request to derive position in street network. (default []) + radiuses (list of floats): Limits the search to given radius in meters. (default []) + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. (default []) + approaches (list): Keep waypoints on curb side. (default []) + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. (default True) + exclude (list of strings): Additive list of classes to avoid. (default []) + snapping (string 'default' | 'any'): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. (default \'\') + + Returns: + __init__ (osrm.osrm_ext.BaseParameters): A BaseParameter object, that is the parent object to many other Parameter objects. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + coordinates (list of floats pairs): Pairs of longitude & latitude coordinates. + hints (list): Hint from previous request to derive position in street network. + radiuses (list of floats): Limits the search to given radius in meters. + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. + approaches (list): Keep waypoints on curb side. + exclude (list of strings): Additive list of classes to avoid, order does not matter. + format (string): Specifies response type - currently only 'json' is supported. + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. + skip_waypoints (list): Removes waypoints from the response. + snapping (string): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. + """ + + @property + def coordinates(self) -> list[Coordinate]: ... + @coordinates.setter + def coordinates(self, arg: Sequence[Coordinate], /) -> None: ... + @property + def hints(self) -> list: ... + @hints.setter + def hints(self, arg: list, /) -> None: ... + @property + def radiuses(self) -> list[float | None]: ... + @radiuses.setter + def radiuses(self, arg: Sequence[float | None], /) -> None: ... + @property + def bearings(self) -> list[Bearing | None]: ... + @bearings.setter + def bearings(self, arg: Sequence[Bearing | None], /) -> None: ... + @property + def approaches(self) -> list[Approach | None]: ... + @approaches.setter + def approaches(self, arg: Sequence[Approach | None], /) -> None: ... + @property + def exclude(self) -> list[str]: ... + @exclude.setter + def exclude(self, arg: Sequence[str], /) -> None: ... + @property + def format(self) -> OutputFormatType | None: ... + @format.setter + def format(self, arg: OutputFormatType | None) -> None: ... + @property + def generate_hints(self) -> bool: ... + @generate_hints.setter + def generate_hints(self, arg: bool, /) -> None: ... + @property + def skip_waypoints(self) -> bool: ... + @skip_waypoints.setter + def skip_waypoints(self, arg: bool, /) -> None: ... + @property + def snapping(self) -> SnappingType: ... + @snapping.setter + def snapping(self, arg: SnappingType, /) -> None: ... + def IsValid(self) -> bool: ... + +class SnappingType: + def __init__(self, arg: str, /) -> None: + """Instantiates a SnappingType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on SnappingType value.""" + +class OutputFormatType: + def __init__(self, arg: str, /) -> None: + """Instantiates a OutputFormatType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on OutputFormatType value.""" + +class NearestParameters(BaseParameters): + @overload + def __init__(self) -> None: + """ + Instantiates an instance of NearestParameters. + + Examples: + >>> nearest_params = osrm.NearestParameters( + coordinates = [(7.41337, 43.72956)], + exclude = ['motorway'] + ) + >>> nearest_params.IsValid() + True + + Args: + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.NearestParameters): A NearestParameters object, for usage in osrm.OSRM.Nearest. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + number_of_results (unsigned int): Number of nearest segments that should be returned. + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def number_of_results(self) -> int: ... + @number_of_results.setter + def number_of_results(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + +class TableParameters(BaseParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of TableParameters. + + Examples: + >>> table_params = osrm.TableParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + sources = [0], + destinations = [1], + annotations = ['duration'], + fallback_speed = 1, + fallback_coordinate_type = 'input', + scale_factor = 0.9 + ) + >>> table_params.IsValid() + True + + Args: + sources (list of int): Use location with given index as source. (default []) + destinations (list of int): Use location with given index as destination. (default []) + annotations (list of 'none' | 'duration' | 'distance' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default []) + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, then use this speed to estimate duration. (default INVALID_FALLBACK_SPEED) + fallback_coordinate_type (string 'input' | 'snapped'): When using a fallback_speed, use the user-supplied coordinate (input), or the snapped location (snapped) for calculating distances. (default \'\') + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). (default 1.0) + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.TableParameters): A TableParameters object, for usage in Table. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + sources (list of int): Use location with given index as source. + destinations (list of int): Use location with given index as destination. + annotations (string): Returns additional metadata for each coordinate along the route geometry. + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, then use this speed to estimate duration. + fallback_coordinate_type (string): When using a fallback_speed, use the user-supplied coordinate (input), or the snapped location (snapped) for calculating distances. + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + sources: Sequence[int] = [], + destinations: Sequence[int] = [], + annotations: Sequence[TableAnnotationsType] = [], + fallback_speed: float = 3.4028234663852886e38, + fallback_coordinate_type: TableFallbackCoordinateType = "", + scale_factor: float = 1.0, + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def sources(self) -> list[int]: ... + @sources.setter + def sources(self, arg: Sequence[int], /) -> None: ... + @property + def destinations(self) -> list[int]: ... + @destinations.setter + def destinations(self, arg: Sequence[int], /) -> None: ... + @property + def fallback_speed(self) -> float: ... + @fallback_speed.setter + def fallback_speed(self, arg: float, /) -> None: ... + @property + def fallback_coordinate_type(self) -> TableFallbackCoordinateType: ... + @fallback_coordinate_type.setter + def fallback_coordinate_type(self, arg: TableFallbackCoordinateType, /) -> None: ... + @property + def annotations(self) -> TableAnnotationsType: ... + @annotations.setter + def annotations(self, arg: TableAnnotationsType, /) -> None: ... + @property + def scale_factor(self) -> float: ... + @scale_factor.setter + def scale_factor(self, arg: float, /) -> None: ... + def IsValid(self) -> bool: ... + +class TableFallbackCoordinateType: + def __init__(self, arg: str, /) -> None: + """Instantiates a FallbackCoordinateType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on FallbackCoordinateType value.""" + +class TableAnnotationsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a AnnotationsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on AnnotationsType value.""" + + def __and__(self, arg: TableAnnotationsType, /) -> bool: + """Return the bitwise AND result of two AnnotationsTypes.""" + + def __or__(self, arg: TableAnnotationsType, /) -> TableAnnotationsType: + """Return the bitwise OR result of two AnnotationsTypes.""" + + def __ior__(self, arg: TableAnnotationsType, /) -> TableAnnotationsType: + """Add the bitwise OR value of another AnnotationsType.""" + +class RouteParameters(BaseParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of RouteParameters. + + Examples: + >>> route_params = osrm.RouteParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + steps = True, + number_of_alternatives = 3, + annotations = ['speed'], + geometries = 'polyline', + overview = 'simplified', + continue_straight = False, + waypoints = [0, 1], + radiuses = [4.07, 4.07], + bearings = [(200, 180), (250, 180)], + # approaches = ['unrestricted', 'unrestricted'], + generate_hints = False, + exclude = ['motorway'], + snapping = 'any' + ) + >>> route_params.IsValid() + True + + Args: + steps (bool): Return route steps for each route leg. (default False) + number_of_alternatives (int): Search for n alternative routes. (default 0) + annotations (list of 'none' | 'duration' | 'nodes' | 'distance' | 'weight' | 'datasources' | 'speed' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default []) + geometries (string 'polyline' | 'polyline6' | 'geojson'): Returned route geometry format - influences overview and per step. (default ) + overview (string 'simplified' | 'full' | 'false'): Add overview geometry either full, simplified. (default \'\') + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. (default {}) + waypoints (list of int): Treats input coordinates indicated by given indices as waypoints in returned Match object. (default []) + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.RouteParameters): A RouteParameters object, for usage in Route. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + steps (bool): Return route steps for each route leg. + alternatives (bool): Search for alternative routes. + number_of_alternatives (int): Search for n alternative routes. + annotations_type (string): Returns additional metadata for each coordinate along the route geometry. + geometries (string): Returned route geometry format - influences overview and per step. + overview (string): Add overview geometry either full, simplified. + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + steps: bool = False, + number_of_alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def steps(self) -> bool: ... + @steps.setter + def steps(self, arg: bool, /) -> None: ... + @property + def alternatives(self) -> bool: ... + @alternatives.setter + def alternatives(self, arg: bool, /) -> None: ... + @property + def number_of_alternatives(self) -> int: ... + @number_of_alternatives.setter + def number_of_alternatives(self, arg: int, /) -> None: ... + @property + def annotations_type(self) -> RouteAnnotationsType: ... + @annotations_type.setter + def annotations_type(self, arg: RouteAnnotationsType, /) -> None: ... + @property + def geometries(self) -> RouteGeometriesType: ... + @geometries.setter + def geometries(self, arg: RouteGeometriesType, /) -> None: ... + @property + def overview(self) -> RouteOverviewType: ... + @overview.setter + def overview(self, arg: RouteOverviewType, /) -> None: ... + @property + def continue_straight(self) -> bool | None: ... + @continue_straight.setter + def continue_straight(self, arg: bool | None) -> None: ... + def IsValid(self) -> bool: ... + +class RouteGeometriesType: + def __init__(self, arg: str, /) -> None: + """Instantiates a GeometriesType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on GeometriesType value.""" + +class RouteOverviewType: + def __init__(self, arg: str, /) -> None: + """Instantiates a OverviewType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on OverviewType value.""" + +class RouteAnnotationsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a AnnotationsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on AnnotationsType value.""" + + def __and__(self, arg: RouteAnnotationsType, /) -> bool: + """Return the bitwise AND result of two AnnotationsTypes.""" + + def __or__(self, arg: RouteAnnotationsType, /) -> RouteAnnotationsType: + """Return the bitwise OR result of two AnnotationsTypes.""" + + def __ior__(self, arg: RouteAnnotationsType, /) -> RouteAnnotationsType: + """Add the bitwise OR value of another AnnotationsType.""" + +class MatchParameters(RouteParameters): + @overload + def __init__(self) -> None: + """ + Instantiates an instance of MatchParameters. + + Examples: + >>> match_params = osrm.MatchParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + timestamps = [1424684612, 1424684616, 1424684620], + gaps = 'split', + tidy = True + ) + >>> match_params.IsValid() + True + + Args: + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. (default []) + gaps (list of 'split' | 'ignore'): Allows the input track splitting based on huge timestamp gaps between points. (default []) + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. (default False) + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.MatchParameters): A MatchParameters object, for usage in Match. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. + gaps (string): Allows the input track splitting based on huge timestamp gaps between points. + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. + RouteParameters (osrm.RouteParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + timestamps: Sequence[int] = [], + gaps: MatchGapsType = "", + tidy: bool = False, + steps: bool = False, + number_of_alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def timestamps(self) -> list[int]: ... + @timestamps.setter + def timestamps(self, arg: Sequence[int], /) -> None: ... + @property + def gaps(self) -> MatchGapsType: ... + @gaps.setter + def gaps(self, arg: MatchGapsType, /) -> None: ... + @property + def tidy(self) -> bool: ... + @tidy.setter + def tidy(self, arg: bool, /) -> None: ... + def IsValid(self) -> bool: ... + +class MatchGapsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a GapsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on GapsType value.""" + +class TripParameters(RouteParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of TripParameters. + + Examples: + >>> trip_params = osrm.TripParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + source = 'any', + destination = 'last', + roundtrip = False + ) + >>> trip_params.IsValid() + True + + Args: + source (string 'any' | 'first'): Returned route starts at 'any' or 'first' coordinate. (default \'\') + destination (string 'any' | 'last'): Returned route ends at 'any' or 'last' coordinate. (default \'\') + roundtrip (bool): Returned route is a roundtrip (route returns to first location). (default True) + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.TripParameters): A TripParameters object, for usage in Trip. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + source (string): Returned route starts at 'any' or 'first' coordinate. + destination (string): Returned route ends at 'any' or 'last' coordinate. + roundtrip (bool): Returned route is a roundtrip (route returns to first location). + RouteParameters (osrm.RouteParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + source: TripSourceType = "", + destination: TripDestinationType = "", + roundtrip: bool = True, + steps: bool = False, + alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def source(self) -> TripSourceType: ... + @source.setter + def source(self, arg: TripSourceType, /) -> None: ... + @property + def destination(self) -> TripDestinationType: ... + @destination.setter + def destination(self, arg: TripDestinationType, /) -> None: ... + @property + def roundtrip(self) -> bool: ... + @roundtrip.setter + def roundtrip(self, arg: bool, /) -> None: ... + def IsValid(self) -> bool: ... + +class TripSourceType: + def __init__(self, arg: str, /) -> None: + """Instantiates a SourceType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on SourceType value.""" + +class TripDestinationType: + def __init__(self, arg: str, /) -> None: + """Instantiates a DestinationType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on DestinationType value.""" + +class TileParameters: + @overload + def __init__(self) -> None: + """ + Instantiates an instance of TileParameters. + + Examples: + >>> tile_params = osrm.TileParameters([17059, 11948, 15]) + >>> tile_params = osrm.TileParameters( + x = 17059, + y = 11948, + z = 15 + ) + >>> tile_params.IsValid() + True + + Args: + list (list of int): Instantiates an instance of TileParameters using an array [x, y, z]. + x (int): x value. + y (int): y value. + z (int): z value. + + Returns: + __init__ (osrm.TileParameters): A TileParameters object, for usage in Tile. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + x (int): x value. + y (int): y value. + z (int): z value. + """ + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: ... + @overload + def __init__(self, arg: Sequence[int], /) -> None: ... + @property + def x(self) -> int: ... + @x.setter + def x(self, arg: int, /) -> None: ... + @property + def y(self) -> int: ... + @y.setter + def y(self, arg: int, /) -> None: ... + @property + def z(self) -> int: ... + @z.setter + def z(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + +class OSRM: + @overload + def __init__(self, arg: EngineConfig, /) -> None: + """ + Instantiates an instance of OSRM. + + Examples: + >>> import osrm + >>> osrm_py = osrm.OSRM('.tests/test_data/ch/monaco.osrm') + >>> osrm_py = osrm.OSRM( + algorithm = 'CH', + storage_config = '.tests/test_data/ch/monaco.osrm', + max_locations_trip = 3, + max_locations_viaroute = 3, + max_locations_distance_table = 3, + max_locations_map_matching = 3, + max_results_nearest = 1, + max_alternatives = 1, + default_radius = 'unlimited' + ) + + Args: + storage_config (string): File path string to storage config. + EngineConfig (osrm.osrm_ext.EngineConfig): Keyword arguments from the EngineConfig class. + + Returns: + __init__ (osrm.OSRM): A OSRM object. + + Raises: + RuntimeError: On invalid OSRM EngineConfig parameters. + """ + + @overload + def __init__(self, arg: str, /) -> None: ... + @overload + def __init__(self, **kwargs) -> None: ... + def Match(self, arg: MatchParameters, /) -> Object: + """ + Matches/snaps given GPS points to the road network in the most plausible way. + + Examples: + >>> res = osrm_py.Match(match_params) + + Args: + match_params (osrm.MatchParameters): MatchParameters Object. + + Returns: + (json): [A Match JSON Response](https://project-osrm.org/docs/v5.24.0/api/#match-service). + + Raises: + RuntimeError: On invalid MatchParameters. + """ + + def Nearest(self, arg: NearestParameters, /) -> Object: + """ + Snaps a coordinate to the street network and returns the nearest matches. + + Examples: + >>> res = osrm_py.Nearest(nearest_params) + + Args: + nearest_params (osrm.NearestParameters): NearestParameters Object. + + Returns: + (json): [A Nearest JSON Response](https://project-osrm.org/docs/v5.24.0/api/#nearest-service). + + Raises: + RuntimeError: On invalid NearestParameters. + """ + + def Route(self, arg: RouteParameters, /) -> Object: + """ + Finds the fastest route between coordinates in the supplied order. + + Examples: + >>> res = osrm_py.Route(route_params) + + Args: + route_params (osrm.RouteParameters): RouteParameters Object. + + Returns: + (json): [A Route JSON Response](https://project-osrm.org/docs/v5.24.0/api/#route-service). + + Raises: + RuntimeError: On invalid RouteParameters. + """ + + def Table(self, arg: TableParameters, /) -> Object: + """ + Computes the duration of the fastest route between all pairs of supplied coordinates. + + Examples: + >>> res = osrm_py.Table(table_params) + + Args: + table_params (osrm.TableParameters): TableParameters Object. + + Returns: + (json): [A Table JSON Response](https://project-osrm.org/docs/v5.24.0/api/#table-service). + + Raises: + RuntimeError: On invalid TableParameters. + """ + + def Tile(self, arg: TileParameters, /) -> object: + """ + Computes the duration of the fastest route between all pairs of supplied coordinates. + + Examples: + >>> res = osrm_py.Tile(tile_params) + + Args: + tile_params (osrm.TileParameters): TileParameters Object. + + Returns: + (json): [A Tile JSON Response](https://project-osrm.org/docs/v5.24.0/api/#tile-service). + + Raises: + RuntimeError: On invalid TileParameters. + """ + + def Trip(self, arg: TripParameters, /) -> Object: + """ + Solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion algorithm). + + Examples: + >>> res = osrm_py.Trip(trip_params) + + Args: + trip_params (osrm.TripParameters): TripParameters Object. + + Returns: + (json): [A Trip JSON Response](https://project-osrm.org/docs/v5.24.0/api/#trip-service). + + Raises: + RuntimeError: On invalid TripParameters. + """ diff --git a/src/python/src/engineconfig_nb.cpp b/src/python/src/engineconfig_nb.cpp new file mode 100644 index 0000000000..8ca1776b81 --- /dev/null +++ b/src/python/src/engineconfig_nb.cpp @@ -0,0 +1,68 @@ +#include "python/engineconfig_nb.hpp" +#include "python/utility/osrm_utility.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/engine_config.hpp" + +#include +#include + +#include + +NB_MAKE_OPAQUE(osrm::engine::EngineConfig::Algorithm) + +namespace nb = nanobind; + +void init_EngineConfig(nb::module_ &m) +{ + using osrm::engine::EngineConfig; + + nb::class_(m, "EngineConfig", nb::is_final()) + .def(nb::init<>()) + .def("__init__", + [](EngineConfig *t, const nb::kwargs &kwargs) + { + new (t) EngineConfig(); + + osrm_nb_util::populate_cfg_from_kwargs(kwargs, *t); + + if (!t->IsValid()) + { + throw std::runtime_error("Config Parameters are Invalid"); + } + }) + .def("IsValid", &EngineConfig::IsValid) + .def("SetStorageConfig", + [](EngineConfig &self, const std::string &path) + { self.storage_config = osrm::storage::StorageConfig(path); }) + .def_rw("max_locations_trip", &EngineConfig::max_locations_trip) + .def_rw("max_locations_viaroute", &EngineConfig::max_locations_viaroute) + .def_rw("max_locations_distance_table", &EngineConfig::max_locations_distance_table) + .def_rw("max_locations_map_matching", &EngineConfig::max_locations_map_matching) + .def_rw("max_radius_map_matching", &EngineConfig::max_radius_map_matching) + .def_rw("max_results_nearest", &EngineConfig::max_results_nearest) + .def_rw("default_radius", &EngineConfig::default_radius) + .def_rw("max_alternatives", &EngineConfig::max_alternatives) + .def_rw("use_shared_memory", &EngineConfig::use_shared_memory) + .def_prop_rw( + "memory_file", + [](const EngineConfig &c) { return c.memory_file.string(); }, + [](EngineConfig &c, const std::string &val) + { c.memory_file = std::filesystem::path(val); }) + .def_rw("use_mmap", &EngineConfig::use_mmap) + .def_rw("algorithm", &EngineConfig::algorithm) + .def_rw("verbosity", &EngineConfig::verbosity) + .def_rw("dataset_name", &EngineConfig::dataset_name); + + nb::class_(m, "Algorithm") + .def("__init__", + [](EngineConfig::Algorithm *t, const std::string &str) + { + EngineConfig::Algorithm algorithm = + osrm_nb_util::str_to_enum(str, "Algorithm", algorithm_map); + new (t) EngineConfig::Algorithm(algorithm); + }) + .def("__repr__", + [](EngineConfig::Algorithm type) + { return osrm_nb_util::enum_to_str(type, "Algorithm", algorithm_map); }); + nb::implicitly_convertible(); +} diff --git a/src/python/src/osrm_nb.cpp b/src/python/src/osrm_nb.cpp new file mode 100644 index 0000000000..687af0480f --- /dev/null +++ b/src/python/src/osrm_nb.cpp @@ -0,0 +1,260 @@ +#include "python/engineconfig_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/parameters/matchparameter_nb.hpp" +#include "python/parameters/nearestparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/parameters/tableparameter_nb.hpp" +#include "python/parameters/tileparameter_nb.hpp" +#include "python/parameters/tripparameter_nb.hpp" +#include "python/types/approach_nb.hpp" +#include "python/types/bearing_nb.hpp" +#include "python/types/coordinate_nb.hpp" +#include "python/types/jsoncontainer_nb.hpp" +#include "python/types/optional_nb.hpp" +#include "python/utility/osrm_utility.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/nearest_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/tile_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "osrm/osrm.hpp" + +#include +#include + +#include + +namespace nb = nanobind; + +NB_MODULE(osrm_ext, m) +{ + namespace api = osrm::engine::api; + namespace json = osrm::util::json; + + using osrm::OSRM; + using osrm::engine::EngineConfig; + using osrm::engine::api::MatchParameters; + using osrm::engine::api::NearestParameters; + using osrm::engine::api::RouteParameters; + using osrm::engine::api::TableParameters; + using osrm::engine::api::TileParameters; + using osrm::engine::api::TripParameters; + + init_EngineConfig(m); + + init_Approach(m); + init_Bearing(m); + init_Coordinate(m); + init_JSONContainer(m); + init_Optional(m); + + init_BaseParameters(m); + init_NearestParameters(m); + init_TableParameters(m); + init_RouteParameters(m); + init_MatchParameters(m); + init_TripParameters(m); + init_TileParameters(m); + + nb::class_(m, "OSRM", nb::is_final()) + .def(nb::init(), + "Instantiates an instance of OSRM.\n\n" + "Examples:\n\ + >>> import osrm\n\ + >>> osrm_py = osrm.OSRM('.tests/test_data/ch/monaco.osrm')\n\ + >>> osrm_py = osrm.OSRM(\n\ + algorithm = 'CH',\n\ + storage_config = '.tests/test_data/ch/monaco.osrm',\n\ + max_locations_trip = 3,\n\ + max_locations_viaroute = 3,\n\ + max_locations_distance_table = 3,\n\ + max_locations_map_matching = 3,\n\ + max_results_nearest = 1,\n\ + max_alternatives = 1,\n\ + default_radius = 'unlimited'\n\ + )\n\n" + "Args:\n\ + storage_config (string): File path string to storage config.\n\ + EngineConfig (osrm.osrm_ext.EngineConfig): Keyword arguments from the EngineConfig class.\n\n" + "Returns:\n\ + __init__ (osrm.OSRM): A OSRM object.\n\n" + "Raises:\n\ + RuntimeError: On invalid OSRM EngineConfig parameters.") + .def("__init__", + [](OSRM *t, const std::string &storage_path) + { + EngineConfig config; + config.storage_config = osrm::storage::StorageConfig(storage_path); + config.use_shared_memory = false; + + if (!config.IsValid()) + { + throw std::runtime_error("Required files are missing"); + } + + new (t) OSRM(config); + }) + .def("__init__", + [](OSRM *t, const nb::kwargs &kwargs) + { + EngineConfig config; + osrm_nb_util::populate_cfg_from_kwargs(kwargs, config); + + if (!config.IsValid()) + { + throw std::runtime_error("Config Parameters are Invalid"); + } + + new (t) OSRM(config); + }) + .def( + "Match", + [](OSRM *t, const MatchParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Match Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Match(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Matches/snaps given GPS points to the road network in the most plausible way.\n\n" + "Examples:\n\ + >>> res = osrm_py.Match(match_params)\n\n" + "Args:\n\ + match_params (osrm.MatchParameters): MatchParameters Object.\n\n" + "Returns:\n\ + (json): [A Match JSON Response](https://project-osrm.org/docs/v5.24.0/api/#match-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid MatchParameters.") + .def( + "Nearest", + [](OSRM *t, const NearestParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Nearest Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Nearest(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Snaps a coordinate to the street network and returns the nearest matches.\n\n" + "Examples:\n\ + >>> res = osrm_py.Nearest(nearest_params)\n\n" + "Args:\n\ + nearest_params (osrm.NearestParameters): NearestParameters Object.\n\n" + "Returns:\n\ + (json): [A Nearest JSON Response](https://project-osrm.org/docs/v5.24.0/api/#nearest-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid NearestParameters.") + .def( + "Route", + [](OSRM *t, const RouteParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Route Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Route(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Finds the fastest route between coordinates in the supplied order.\n\n" + "Examples:\n\ + >>> res = osrm_py.Route(route_params)\n\n" + "Args:\n\ + route_params (osrm.RouteParameters): RouteParameters Object.\n\n" + "Returns:\n\ + (json): [A Route JSON Response](https://project-osrm.org/docs/v5.24.0/api/#route-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid RouteParameters.") + .def( + "Table", + [](OSRM *t, const TableParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Table Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Table(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Computes the duration of the fastest route between all pairs of supplied " + "coordinates.\n\n" + "Examples:\n\ + >>> res = osrm_py.Table(table_params)\n\n" + "Args:\n\ + table_params (osrm.TableParameters): TableParameters Object.\n\n" + "Returns:\n\ + (json): [A Table JSON Response](https://project-osrm.org/docs/v5.24.0/api/#table-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TableParameters.") + .def( + "Tile", + [](OSRM *t, const TileParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Tile Parameters"); + } + + std::string result; + t->Tile(params, result); + nb::object obj = nb::bytes(result.c_str(), result.size()); + + return obj; + }, + "Computes the duration of the fastest route between all pairs of supplied " + "coordinates.\n\n" + "Examples:\n\ + >>> res = osrm_py.Tile(tile_params)\n\n" + "Args:\n\ + tile_params (osrm.TileParameters): TileParameters Object.\n\n" + "Returns:\n\ + (json): [A Tile JSON Response](https://project-osrm.org/docs/v5.24.0/api/#tile-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TileParameters.") + .def( + "Trip", + [](OSRM *t, const TripParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Trip Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Trip(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion " + "algorithm).\n\n" + "Examples:\n\ + >>> res = osrm_py.Trip(trip_params)\n\n" + "Args:\n\ + trip_params (osrm.TripParameters): TripParameters Object.\n\n" + "Returns:\n\ + (json): [A Trip JSON Response](https://project-osrm.org/docs/v5.24.0/api/#trip-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TripParameters."); +} diff --git a/src/python/src/parameters/baseparameter_nb.cpp b/src/python/src/parameters/baseparameter_nb.cpp new file mode 100644 index 0000000000..ce717ea444 --- /dev/null +++ b/src/python/src/parameters/baseparameter_nb.cpp @@ -0,0 +1,124 @@ +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/base_parameters.hpp" +#include "engine/hint.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_BaseParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + + nb::class_(m, "BaseParameters") + .def(nb::init<>(), + "Instantiates an instance of BaseParameters.\n\n" + "Note:\n\ + This is the parent class to many parameter classes, and not intended to be used on its own.\n\n" + "Args:\n\ + coordinates (list of floats pairs): Pairs of Longitude and Latitude Coordinates. (default [])\n\ + hints (list): Hint from previous request to derive position in street network. (default [])\n\ + radiuses (list of floats): Limits the search to given radius in meters. (default [])\n\ + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. (default [])\n\ + approaches (list): Keep waypoints on curb side. (default [])\n\ + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. (default True)\n\ + exclude (list of strings): Additive list of classes to avoid. (default [])\n\ + snapping (string 'default' | 'any'): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. (default '')\n\n" + "Returns:\n\ + __init__ (osrm.osrm_ext.BaseParameters): A BaseParameter object, that is the parent object to many other Parameter objects.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + coordinates (list of floats pairs): Pairs of longitude & latitude coordinates.\n\ + hints (list): Hint from previous request to derive position in street network.\n\ + radiuses (list of floats): Limits the search to given radius in meters.\n\ + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction.\n\ + approaches (list): Keep waypoints on curb side.\n\ + exclude (list of strings): Additive list of classes to avoid, order does not matter.\n\ + format (string): Specifies response type - currently only 'json' is supported.\n\ + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests.\n\ + skip_waypoints (list): Removes waypoints from the response.\n\ + snapping (string): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph.") + .def_rw("coordinates", &BaseParameters::coordinates) + .def_prop_rw( + "hints", + [](const BaseParameters &p) + { + nb::list result; + for (const auto &h : p.hints) + { + if (h) + { + result.append(h->ToBase64()); + } + else + { + result.append(nb::none()); + } + } + return result; + }, + [](BaseParameters &p, const nb::list &hints) + { + p.hints.clear(); + for (auto item : hints) + { + if (item.is_none()) + { + p.hints.push_back(std::nullopt); + } + else + { + p.hints.push_back( + osrm::engine::Hint::FromBase64(nb::cast(item))); + } + } + }) + .def_rw("radiuses", &BaseParameters::radiuses) + .def_rw("bearings", &BaseParameters::bearings) + .def_rw("approaches", &BaseParameters::approaches) + .def_rw("exclude", &BaseParameters::exclude) + .def_rw("format", &BaseParameters::format) + .def_rw("generate_hints", &BaseParameters::generate_hints) + .def_rw("skip_waypoints", &BaseParameters::skip_waypoints) + .def_rw("snapping", &BaseParameters::snapping) + .def("IsValid", &BaseParameters::IsValid); + + nb::class_(m, "SnappingType") + .def( + "__init__", + [](BaseParameters::SnappingType *t, const std::string &str) + { + BaseParameters::SnappingType snapping = + osrm_nb_util::str_to_enum(str, "SnappingType", snapping_map); + new (t) BaseParameters::SnappingType(snapping); + }, + "Instantiates a SnappingType based on provided String value.") + .def( + "__repr__", + [](BaseParameters::SnappingType type) + { return osrm_nb_util::enum_to_str(type, "SnappingType", snapping_map); }, + "Return a String based on SnappingType value."); + nb::implicitly_convertible(); + + nb::class_(m, "OutputFormatType") + .def( + "__init__", + [](BaseParameters::OutputFormatType *t, const std::string &str) + { + BaseParameters::OutputFormatType output = + osrm_nb_util::str_to_enum(str, "OutputFormatType", output_map); + new (t) BaseParameters::OutputFormatType(output); + }, + "Instantiates a OutputFormatType based on provided String value.") + .def( + "__repr__", + [](BaseParameters::OutputFormatType type) + { return osrm_nb_util::enum_to_str(type, "OutputFormatType", output_map); }, + "Return a String based on OutputFormatType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/matchparameter_nb.cpp b/src/python/src/parameters/matchparameter_nb.cpp new file mode 100644 index 0000000000..7da22c7b3a --- /dev/null +++ b/src/python/src/parameters/matchparameter_nb.cpp @@ -0,0 +1,130 @@ +#include "python/parameters/matchparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/match_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_MatchParameters(nb::module_ &m) +{ + using osrm::engine::api::MatchParameters; + using osrm::engine::api::RouteParameters; + + nb::class_(m, "MatchParameters") + .def(nb::init<>(), + "Instantiates an instance of MatchParameters.\n\n" + "Examples:\n\ + >>> match_params = osrm.MatchParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)],\n\ + timestamps = [1424684612, 1424684616, 1424684620],\n\ + gaps = 'split',\n\ + tidy = True\n\ + )\n\ + >>> match_params.IsValid()\n\ + True\n\n" + "Args:\n\ + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. (default [])\n\ + gaps (list of 'split' | 'ignore'): Allows the input track splitting based on huge timestamp gaps between points. (default [])\n\ + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. (default False)\n\ + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.MatchParameters): A MatchParameters object, for usage in Match.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch.\n\ + gaps (string): Allows the input track splitting based on huge timestamp gaps between points.\n\ + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks.\n\ + RouteParameters (osrm.RouteParameters): Attributes from parent class.") + .def( + "__init__", + [](MatchParameters *t, + std::vector timestamps, + MatchParameters::GapsType gaps_type, + bool tidy, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) MatchParameters(); + + t->timestamps = std::move(timestamps); + t->gaps = gaps_type; + t->tidy = tidy; + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "timestamps"_a = std::vector(), + "gaps"_a = std::string(), + "tidy"_a = false, + "steps"_a = false, + "number_of_alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("timestamps", &MatchParameters::timestamps) + .def_rw("gaps", &MatchParameters::gaps) + .def_rw("tidy", &MatchParameters::tidy) + .def("IsValid", &MatchParameters::IsValid); + + nb::class_(m, "MatchGapsType") + .def( + "__init__", + [](MatchParameters::GapsType *t, const std::string &str) + { + MatchParameters::GapsType gaps = + osrm_nb_util::str_to_enum(str, "MatchGapsType", gaps_map); + new (t) MatchParameters::GapsType(gaps); + }, + "Instantiates a GapsType based on provided String value.") + .def( + "__repr__", + [](MatchParameters::GapsType type) + { return osrm_nb_util::enum_to_str(type, "MatchGapsType", gaps_map); }, + "Return a String based on GapsType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/nearestparameter_nb.cpp b/src/python/src/parameters/nearestparameter_nb.cpp new file mode 100644 index 0000000000..ff61ac98c4 --- /dev/null +++ b/src/python/src/parameters/nearestparameter_nb.cpp @@ -0,0 +1,71 @@ +#include "python/parameters/nearestparameter_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/nearest_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_NearestParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::NearestParameters; + + nb::class_(m, "NearestParameters") + .def(nb::init<>(), + "Instantiates an instance of NearestParameters.\n\n" + "Examples:\n\ + >>> nearest_params = osrm.NearestParameters(\n\ + coordinates = [(7.41337, 43.72956)],\n\ + exclude = ['motorway']\n\ + )\n\ + >>> nearest_params.IsValid()\n\ + True\n\n" + "Args:\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.NearestParameters): A NearestParameters object, for usage in osrm.OSRM.Nearest.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + number_of_results (unsigned int): Number of nearest segments that should be returned.\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](NearestParameters *t, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) NearestParameters(); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("number_of_results", &NearestParameters::number_of_results) + .def("IsValid", &NearestParameters::IsValid); +} diff --git a/src/python/src/parameters/routeparameter_nb.cpp b/src/python/src/parameters/routeparameter_nb.cpp new file mode 100644 index 0000000000..c0aef476d1 --- /dev/null +++ b/src/python/src/parameters/routeparameter_nb.cpp @@ -0,0 +1,195 @@ +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/route_parameters.hpp" + +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_RouteParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::RouteParameters; + + nb::class_(m, "RouteParameters") + .def(nb::init<>(), + "Instantiates an instance of RouteParameters.\n\n" + "Examples:\n\ + >>> route_params = osrm.RouteParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + steps = True,\n\ + number_of_alternatives = 3,\n\ + annotations = ['speed'],\n\ + geometries = 'polyline',\n\ + overview = 'simplified',\n\ + continue_straight = False,\n\ + waypoints = [0, 1],\n\ + radiuses = [4.07, 4.07],\n\ + bearings = [(200, 180), (250, 180)],\n\ + # approaches = ['unrestricted', 'unrestricted'],\n\ + generate_hints = False,\n\ + exclude = ['motorway'],\n\ + snapping = 'any'\n\ + )\n\ + >>> route_params.IsValid()\n\ + True\n\n" + "Args:\n\ + steps (bool): Return route steps for each route leg. (default False)\n\ + number_of_alternatives (int): Search for n alternative routes. (default 0)\n\ + annotations (list of 'none' | 'duration' | 'nodes' | 'distance' | 'weight' | 'datasources' \ + | 'speed' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default [])\n\ + geometries (string 'polyline' | 'polyline6' | 'geojson'): Returned route geometry format - influences overview and per step. (default " + ")\n\ + overview (string 'simplified' | 'full' | 'false'): Add overview geometry either full, simplified. (default '')\n\ + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. (default {})\n\ + waypoints (list of int): Treats input coordinates indicated by given indices as waypoints in returned Match object. (default [])\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.RouteParameters): A RouteParameters object, for usage in Route.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + steps (bool): Return route steps for each route leg.\n\ + alternatives (bool): Search for alternative routes.\n\ + number_of_alternatives (int): Search for n alternative routes.\n\ + annotations_type (string): Returns additional metadata for each coordinate along the route geometry.\n\ + geometries (string): Returned route geometry format - influences overview and per step.\n\ + overview (string): Add overview geometry either full, simplified.\n\ + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns.\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](RouteParameters *t, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) RouteParameters(); + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "steps"_a = false, + "number_of_alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("steps", &RouteParameters::steps) + .def_rw("alternatives", &RouteParameters::alternatives) + .def_rw("number_of_alternatives", &RouteParameters::number_of_alternatives) + .def_rw("annotations_type", &RouteParameters::annotations_type) + .def_rw("geometries", &RouteParameters::geometries) + .def_rw("overview", &RouteParameters::overview) + .def_rw("continue_straight", &RouteParameters::continue_straight) + .def("IsValid", &RouteParameters::IsValid); + + nb::class_(m, "RouteGeometriesType") + .def( + "__init__", + [](RouteParameters::GeometriesType *t, const std::string &str) + { + RouteParameters::GeometriesType geometries = + osrm_nb_util::str_to_enum(str, "RouteGeometriesType", geometries_map); + new (t) RouteParameters::GeometriesType(geometries); + }, + "Instantiates a GeometriesType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::GeometriesType type) + { return osrm_nb_util::enum_to_str(type, "RouteGeometriesType", geometries_map); }, + "Return a String based on GeometriesType value."); + nb::implicitly_convertible(); + + nb::class_(m, "RouteOverviewType") + .def( + "__init__", + [](RouteParameters::OverviewType *t, const std::string &str) + { + RouteParameters::OverviewType overview = + osrm_nb_util::str_to_enum(str, "RouteOverviewType", overview_map); + new (t) RouteParameters::OverviewType(overview); + }, + "Instantiates a OverviewType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::OverviewType type) + { return osrm_nb_util::enum_to_str(type, "RouteOverviewType", overview_map); }, + "Return a String based on OverviewType value."); + nb::implicitly_convertible(); + + nb::class_(m, "RouteAnnotationsType") + .def( + "__init__", + [](RouteParameters::AnnotationsType *t, const std::string &str) + { + RouteParameters::AnnotationsType annotation = + osrm_nb_util::str_to_enum(str, "RouteAnnotationsType", route_annotations_map); + new (t) RouteParameters::AnnotationsType(annotation); + }, + "Instantiates a AnnotationsType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::AnnotationsType type) { return std::to_string((int)type); }, + "Return a String based on AnnotationsType value.") + .def( + "__and__", + [](RouteParameters::AnnotationsType lhs, RouteParameters::AnnotationsType rhs) + { return lhs & rhs; }, + nb::is_operator(), + "Return the bitwise AND result of two AnnotationsTypes.") + .def( + "__or__", + [](RouteParameters::AnnotationsType lhs, RouteParameters::AnnotationsType rhs) + { return lhs | rhs; }, + nb::is_operator(), + "Return the bitwise OR result of two AnnotationsTypes.") + .def( + "__ior__", + [](RouteParameters::AnnotationsType &lhs, RouteParameters::AnnotationsType rhs) + { return lhs = lhs | rhs; }, + nb::is_operator(), + "Add the bitwise OR value of another AnnotationsType."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/tableparameter_nb.cpp b/src/python/src/parameters/tableparameter_nb.cpp new file mode 100644 index 0000000000..665e36cf57 --- /dev/null +++ b/src/python/src/parameters/tableparameter_nb.cpp @@ -0,0 +1,175 @@ +#include "python/parameters/tableparameter_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/table_parameters.hpp" + +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TableParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::TableParameters; + static const std::unordered_map + table_annotations_map{{"none", TableParameters::AnnotationsType::None}, + {std::string(), TableParameters::AnnotationsType::None}, + {"duration", TableParameters::AnnotationsType::Duration}, + {"distance", TableParameters::AnnotationsType::Distance}, + {"all", TableParameters::AnnotationsType::All}}; + + nb::class_(m, "TableParameters") + .def(nb::init<>(), + "Instantiates an instance of TableParameters.\n\n" + "Examples:\n\ + >>> table_params = osrm.TableParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + sources = [0],\n\ + destinations = [1],\n\ + annotations = ['duration'],\n\ + fallback_speed = 1,\n\ + fallback_coordinate_type = 'input',\n\ + scale_factor = 0.9\n\ + )\n\ + >>> table_params.IsValid()\n\ + True\n\n" + "Args:\n\ + sources (list of int): Use location with given index as source. (default [])\n\ + destinations (list of int): Use location with given index as destination. (default [])\n\ + annotations (list of 'none' | 'duration' | 'distance' | 'all'): \ + Returns additional metadata for each coordinate along the route geometry. (default [])\n\ + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, \ + then use this speed to estimate duration. (default INVALID_FALLBACK_SPEED)\n\ + fallback_coordinate_type (string 'input' | 'snapped'): When using a fallback_speed, use the user-supplied coordinate (input), \ + or the snapped location (snapped) for calculating distances. (default '')\n\ + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). (default 1.0)\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.TableParameters): A TableParameters object, for usage in Table.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + sources (list of int): Use location with given index as source.\n\ + destinations (list of int): Use location with given index as destination.\n\ + annotations (string): Returns additional metadata for each coordinate along the route geometry.\n\ + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, \ + then use this speed to estimate duration.\n\ + fallback_coordinate_type (string): When using a fallback_speed, use the user-supplied coordinate (input), \ + or the snapped location (snapped) for calculating distances.\n\ + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations).\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](TableParameters *t, + std::vector sources, + std::vector destinations, + const std::vector &annotations, + double fallback_speed, + TableParameters::FallbackCoordinateType fallback_coordinate_type, + double scale_factor, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) TableParameters(); + + t->sources = std::move(sources); + t->destinations = std::move(destinations); + t->annotations = osrm_nb_util::calculate_tableannotations_type(annotations); + t->fallback_speed = fallback_speed; + t->fallback_coordinate_type = fallback_coordinate_type; + t->scale_factor = scale_factor; + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "sources"_a = std::vector(), + "destinations"_a = std::vector(), + "annotations"_a = std::vector(), + "fallback_speed"_a = osrm::from_alias(INVALID_FALLBACK_SPEED), + "fallback_coordinate_type"_a = std::string(), + "scale_factor"_a = 1.0, + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("sources", &TableParameters::sources) + .def_rw("destinations", &TableParameters::destinations) + .def_rw("fallback_speed", &TableParameters::fallback_speed) + .def_rw("fallback_coordinate_type", &TableParameters::fallback_coordinate_type) + .def_rw("annotations", &TableParameters::annotations) + .def_rw("scale_factor", &TableParameters::scale_factor) + .def("IsValid", &TableParameters::IsValid); + + nb::class_(m, "TableFallbackCoordinateType") + .def( + "__init__", + [](TableParameters::FallbackCoordinateType *t, const std::string &str) + { + TableParameters::FallbackCoordinateType fallback = + osrm_nb_util::str_to_enum(str, "TableFallbackCoordinateType", fallback_map); + new (t) TableParameters::FallbackCoordinateType(fallback); + }, + "Instantiates a FallbackCoordinateType based on provided String value.") + .def( + "__repr__", + [](TableParameters::FallbackCoordinateType type) { + return osrm_nb_util::enum_to_str(type, "TableFallbackCoordinateType", fallback_map); + }, + "Return a String based on FallbackCoordinateType value."); + nb::implicitly_convertible(); + + nb::class_(m, "TableAnnotationsType") + .def( + "__init__", + [](TableParameters::AnnotationsType *t, const std::string &str) + { + TableParameters::AnnotationsType annotation = + osrm_nb_util::str_to_enum(str, "TableAnnotationsType", table_annotations_map); + new (t) TableParameters::AnnotationsType(annotation); + }, + "Instantiates a AnnotationsType based on provided String value.") + .def( + "__repr__", + [](TableParameters::AnnotationsType type) { return std::to_string((int)type); }, + "Return a String based on AnnotationsType value.") + .def( + "__and__", + [](TableParameters::AnnotationsType lhs, TableParameters::AnnotationsType rhs) + { return lhs & rhs; }, + nb::is_operator(), + "Return the bitwise AND result of two AnnotationsTypes.") + .def( + "__or__", + [](TableParameters::AnnotationsType lhs, TableParameters::AnnotationsType rhs) + { return lhs | rhs; }, + nb::is_operator(), + "Return the bitwise OR result of two AnnotationsTypes.") + .def( + "__ior__", + [](TableParameters::AnnotationsType &lhs, TableParameters::AnnotationsType rhs) + { return lhs = lhs | rhs; }, + nb::is_operator(), + "Add the bitwise OR value of another AnnotationsType."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/tileparameter_nb.cpp b/src/python/src/parameters/tileparameter_nb.cpp new file mode 100644 index 0000000000..ec7127b20e --- /dev/null +++ b/src/python/src/parameters/tileparameter_nb.cpp @@ -0,0 +1,57 @@ +#include "python/parameters/tileparameter_nb.hpp" +#include "engine/api/tile_parameters.hpp" + +#include +#include +#include + +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TileParameters(nb::module_ &m) +{ + using osrm::engine::api::TileParameters; + + nb::class_(m, "TileParameters", nb::is_final()) + .def(nb::init<>(), + "Instantiates an instance of TileParameters.\n\n" + "Examples:\n\ + >>> tile_params = osrm.TileParameters([17059, 11948, 15])\n\ + >>> tile_params = osrm.TileParameters(\n\ + x = 17059,\n\ + y = 11948,\n\ + z = 15\n\ + )\n\ + >>> tile_params.IsValid()\n\ + True\n\n" + "Args:\n\ + list (list of int): Instantiates an instance of TileParameters using an array [x, y, z].\n\ + x (int): x value.\n\ + y (int): y value.\n\ + z (int): z value.\n\n" + "Returns:\n\ + __init__ (osrm.TileParameters): A TileParameters object, for usage in Tile.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + x (int): x value.\n\ + y (int): y value.\n\ + z (int): z value.") + .def(nb::init()) + .def("__init__", + [](TileParameters *t, const std::vector &coord) + { + if (coord.size() != 3) + { + throw std::runtime_error("Parameter must be an array [x, y, z]"); + } + + new (t) TileParameters{coord[0], coord[1], coord[2]}; + }) + .def_rw("x", &TileParameters::x) + .def_rw("y", &TileParameters::y) + .def_rw("z", &TileParameters::z) + .def("IsValid", &TileParameters::IsValid); + nb::implicitly_convertible, TileParameters>(); +} diff --git a/src/python/src/parameters/tripparameter_nb.cpp b/src/python/src/parameters/tripparameter_nb.cpp new file mode 100644 index 0000000000..962d84c777 --- /dev/null +++ b/src/python/src/parameters/tripparameter_nb.cpp @@ -0,0 +1,147 @@ +#include "python/parameters/tripparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/trip_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TripParameters(nb::module_ &m) +{ + using osrm::engine::api::RouteParameters; + using osrm::engine::api::TripParameters; + + nb::class_(m, "TripParameters") + .def(nb::init<>(), + "Instantiates an instance of TripParameters.\n\n" + "Examples:\n\ + >>> trip_params = osrm.TripParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + source = 'any',\n\ + destination = 'last',\n\ + roundtrip = False\n\ + )\n\ + >>> trip_params.IsValid()\n\ + True\n\n" + "Args:\n\ + source (string 'any' | 'first'): Returned route starts at 'any' or 'first' coordinate. (default '')\n\ + destination (string 'any' | 'last'): Returned route ends at 'any' or 'last' coordinate. (default '')\n\ + roundtrip (bool): Returned route is a roundtrip (route returns to first location). (default True)\n\ + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.TripParameters): A TripParameters object, for usage in Trip.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + source (string): Returned route starts at 'any' or 'first' coordinate.\n\ + destination (string): Returned route ends at 'any' or 'last' coordinate.\n\ + roundtrip (bool): Returned route is a roundtrip (route returns to first location).\n\ + RouteParameters (osrm.RouteParameters): Attributes from parent class.") + .def( + "__init__", + [](TripParameters *t, + TripParameters::SourceType source, + TripParameters::DestinationType destination, + bool roundtrip, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) TripParameters(); + + t->source = source; + t->destination = destination; + t->roundtrip = roundtrip; + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "source"_a = std::string(), + "destination"_a = std::string(), + "roundtrip"_a = true, + "steps"_a = false, + "alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("source", &TripParameters::source) + .def_rw("destination", &TripParameters::destination) + .def_rw("roundtrip", &TripParameters::roundtrip) + .def("IsValid", &TripParameters::IsValid); + + nb::class_(m, "TripSourceType") + .def( + "__init__", + [](TripParameters::SourceType *t, const std::string &str) + { + TripParameters::SourceType source = + osrm_nb_util::str_to_enum(str, "TripSourceType", source_map); + new (t) TripParameters::SourceType(source); + }, + "Instantiates a SourceType based on provided String value.") + .def( + "__repr__", + [](TripParameters::SourceType type) + { return osrm_nb_util::enum_to_str(type, "TripSourceType", source_map); }, + "Return a String based on SourceType value."); + nb::implicitly_convertible(); + + nb::class_(m, "TripDestinationType") + .def( + "__init__", + [](TripParameters::DestinationType *t, const std::string &str) + { + TripParameters::DestinationType dest = + osrm_nb_util::str_to_enum(str, "TripDestinationType", dest_map); + new (t) TripParameters::DestinationType(dest); + }, + "Instantiates a DestinationType based on provided String value.") + .def( + "__repr__", + [](TripParameters::DestinationType type) + { return osrm_nb_util::enum_to_str(type, "TripDestinationType", dest_map); }, + "Return a String based on DestinationType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/types/approach_nb.cpp b/src/python/src/types/approach_nb.cpp new file mode 100644 index 0000000000..bfed97684d --- /dev/null +++ b/src/python/src/types/approach_nb.cpp @@ -0,0 +1,27 @@ +#include "python/types/approach_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/approach.hpp" + +#include +#include + +NB_MAKE_OPAQUE(osrm::engine::Approach) + +namespace nb = nanobind; + +void init_Approach(nb::module_ &m) +{ + using osrm::engine::Approach; + + nb::class_(m, "Approach") + .def("__init__", + [](Approach *t, const std::string &str) + { + Approach approach = osrm_nb_util::str_to_enum(str, "Approach", approach_map); + new (t) Approach(approach); + }) + .def("__repr__", + [](Approach type) + { return osrm_nb_util::enum_to_str(type, "Approach", approach_map); }); + nb::implicitly_convertible(); +} diff --git a/src/python/src/types/bearing_nb.cpp b/src/python/src/types/bearing_nb.cpp new file mode 100644 index 0000000000..5474673011 --- /dev/null +++ b/src/python/src/types/bearing_nb.cpp @@ -0,0 +1,25 @@ +#include "python/types/bearing_nb.hpp" +#include "engine/bearing.hpp" + +#include +#include +#include + +namespace nb = nanobind; + +void init_Bearing(nb::module_ &m) +{ + using osrm::engine::Bearing; + + nb::class_(m, "Bearing") + .def(nb::init<>()) + .def("__init__", + [](Bearing *t, std::pair pair) + { new (t) Bearing{pair.first, pair.second}; }) + .def_rw("bearing", &Bearing::bearing) + .def_rw("range", &Bearing::range) + .def("IsValid", &Bearing::IsValid) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + nb::implicitly_convertible, Bearing>(); +} diff --git a/src/python/src/types/coordinate_nb.cpp b/src/python/src/types/coordinate_nb.cpp new file mode 100644 index 0000000000..36d5e45af3 --- /dev/null +++ b/src/python/src/types/coordinate_nb.cpp @@ -0,0 +1,55 @@ +#include "python/types/coordinate_nb.hpp" +#include "util/coordinate.hpp" + +#include +#include +#include +#include + +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_Coordinate(nb::module_ &m) +{ + namespace tag = osrm::util::tag; + using FloatLongitude = osrm::Alias; + using FloatLatitude = osrm::Alias; + + using osrm::util::Coordinate; + + nb::class_(m, "Coordinate") + .def(nb::init<>()) + .def(nb::init(), "coordinate"_a) + .def("__init__", + [](Coordinate *t, std::pair coords) + { + const FloatLongitude lon_ = FloatLongitude{coords.first}; + const FloatLatitude lat_ = FloatLatitude{coords.second}; + + new (t) Coordinate(lon_, lat_); + }) + .def_prop_rw( + "lon", + [](const Coordinate &c) + { return static_cast(static_cast(c.lon)) / osrm::COORDINATE_PRECISION; }, + [](Coordinate &c, double val) { c.lon = osrm::util::toFixed(FloatLongitude{val}); }) + .def_prop_rw( + "lat", + [](const Coordinate &c) + { return static_cast(static_cast(c.lat)) / osrm::COORDINATE_PRECISION; }, + [](Coordinate &c, double val) { c.lat = osrm::util::toFixed(FloatLatitude{val}); }) + .def("IsValid", &Coordinate::IsValid) + .def("__repr__", + [](const Coordinate &coord) + { + int lon = static_cast(coord.lon); + int lat = static_cast(coord.lat); + + return '(' + std::to_string(lon) + ',' + std::to_string(lat) + ')'; + }) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + nb::implicitly_convertible, Coordinate>(); +} diff --git a/src/python/src/types/jsoncontainer_nb.cpp b/src/python/src/types/jsoncontainer_nb.cpp new file mode 100644 index 0000000000..a61aa8c1a8 --- /dev/null +++ b/src/python/src/types/jsoncontainer_nb.cpp @@ -0,0 +1,65 @@ +#include "python/types/jsoncontainer_nb.hpp" +#include "util/json_container.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +namespace json = osrm::util::json; + +void init_JSONContainer(nb::module_ &m) +{ + nb::class_(m, "Object") + .def(nb::init<>()) + .def("__len__", [](const json::Object &obj) { return obj.values.size(); }) + .def("__bool__", [](const json::Object &obj) { return !obj.values.empty(); }) + .def("__repr__", + [](const json::Object &obj) + { + ValueStringifyVisitor visitor; + return visitor.visitobject(obj); + }) + .def("__getitem__", + [](json::Object &obj, const std::string &key) -> nb::object + { return nb::cast(obj.values.at(key)); }) + .def("__contains__", + [](const json::Object &obj, const std::string &key) + { return obj.values.count(key) > 0; }) + .def( + "__iter__", + [m](const json::Object &obj) { + return nb::make_key_iterator( + m, "key_iterator", obj.values.begin(), obj.values.end()); + }, + nb::keep_alive<0, 1>()); + + nb::class_(m, "Array") + .def(nb::init<>()) + .def("__len__", [](const json::Array &arr) { return arr.values.size(); }) + .def("__bool__", [](const json::Array &arr) { return !arr.values.empty(); }) + .def("__repr__", + [](const json::Array &arr) + { + ValueStringifyVisitor visitor; + return visitor.visitarray(arr); + }) + .def("__getitem__", + [](json::Array &arr, int i) -> nb::object { return nb::cast(arr.values[i]); }) + .def("__iter__", + [](const json::Array &arr) + { + nb::list items; + for (const auto &v : arr.values) + { + items.append(nb::cast(v)); + } + return nb::iter(items); + }); + + nb::class_(m, "String").def(nb::init()); + nb::class_(m, "Number").def(nb::init()); + + // Not exposed: json::True, json::False, json::Null — they shadow Python builtins +} diff --git a/src/python/src/types/optional_nb.cpp b/src/python/src/types/optional_nb.cpp new file mode 100644 index 0000000000..f0f5f40b6d --- /dev/null +++ b/src/python/src/types/optional_nb.cpp @@ -0,0 +1,8 @@ +#include "python/types/optional_nb.hpp" + +#include +#include + +namespace nb = nanobind; + +void init_Optional(nb::module_ &) {} diff --git a/src/python/src/utility/osrm_utility.cpp b/src/python/src/utility/osrm_utility.cpp new file mode 100644 index 0000000000..9bdbb2555d --- /dev/null +++ b/src/python/src/utility/osrm_utility.cpp @@ -0,0 +1,142 @@ +#include "python/utility/osrm_utility.hpp" +#include "python/engineconfig_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "osrm/osrm.hpp" + +#include +#include + +#include +#include + +#define UNLIMITED -1 + +namespace nb = nanobind; + +using osrm::engine::EngineConfig; + +template void assign_val(T &to_assign, const std::pair &val) +{ + try + { + to_assign = nb::cast(val.second); + } + catch (const nb::cast_error &ex) + { + throw std::runtime_error("Invalid type passed for argument: " + + nb::cast(val.first)); + } +} + +namespace osrm_nb_util +{ + +void check_status(osrm::engine::Status status, osrm::util::json::Object &res) +{ + if (status == osrm::engine::Status::Ok) + { + return; + } + + const std::string code = std::get(res.values.at("code")).value; + const std::string msg = std::get(res.values.at("message")).value; + + throw std::runtime_error(code + " - " + msg); +} + +void populate_cfg_from_kwargs(const nb::kwargs &kwargs, EngineConfig &config) +{ + std::unordered_map &)>> + assign_map{{"storage_config", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.storage_config = osrm::storage::StorageConfig(str); + }}, + {"max_locations_trip", + [&config](const std::pair &val) + { assign_val(config.max_locations_trip, val); }}, + {"max_locations_viaroute", + [&config](const std::pair &val) + { assign_val(config.max_locations_viaroute, val); }}, + {"max_locations_distance_table", + [&config](const std::pair &val) + { assign_val(config.max_locations_distance_table, val); }}, + {"max_locations_map_matching", + [&config](const std::pair &val) + { assign_val(config.max_locations_map_matching, val); }}, + {"max_radius_map_matching", + [&config](const std::pair &val) + { assign_val(config.max_radius_map_matching, val); }}, + {"max_results_nearest", + [&config](const std::pair &val) + { assign_val(config.max_results_nearest, val); }}, + {"default_radius", + [&config](const std::pair &val) + { + try + { + const std::string rad_val = nb::cast(val.second); + + if (!(rad_val == "unlimited" || rad_val == "UNLIMITED")) + { + throw std::runtime_error( + "default_radius must be a float value or 'unlimited'"); + } + + config.default_radius = UNLIMITED; + } + catch (const nb::cast_error &) + { + assign_val(config.default_radius, val); + } + }}, + {"max_alternatives", + [&config](const std::pair &val) + { assign_val(config.max_alternatives, val); }}, + {"use_shared_memory", + [&config](const std::pair &val) + { assign_val(config.use_shared_memory, val); }}, + {"memory_file", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.memory_file = std::filesystem::path(str); + }}, + {"use_mmap", + [&config](const std::pair &val) + { assign_val(config.use_mmap, val); }}, + {"algorithm", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.algorithm = + osrm_nb_util::str_to_enum(str, "Algorithm", algorithm_map); + }}, + {"verbosity", + [&config](const std::pair &val) + { assign_val(config.verbosity, val); }}, + {"dataset_name", [&config](const std::pair &val) { + assign_val(config.dataset_name, val); + }}}; + + for (auto kwarg : kwargs) + { + const std::string arg_str = nb::cast(kwarg.first); + auto itr = assign_map.find(arg_str); + + if (itr == assign_map.end()) + { + throw std::invalid_argument(arg_str); + } + + itr->second(kwarg); + } +} + +} // namespace osrm_nb_util diff --git a/src/python/src/utility/param_utility.cpp b/src/python/src/utility/param_utility.cpp new file mode 100644 index 0000000000..0b950cee26 --- /dev/null +++ b/src/python/src/utility/param_utility.cpp @@ -0,0 +1,107 @@ +#include "python/utility/param_utility.hpp" +#include "engine/api/base_parameters.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/approach.hpp" +#include "engine/hint.hpp" + +#include +#include +#include +#include + +using osrm::engine::Approach; +using osrm::engine::api::BaseParameters; +using osrm::engine::api::MatchParameters; +using osrm::engine::api::RouteParameters; +using osrm::engine::api::TableParameters; +using osrm::engine::api::TripParameters; + +namespace osrm_nb_util +{ + +void assign_baseparameters(BaseParameters *params, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) +{ + params->coordinates = std::move(coordinates); + params->hints.clear(); + for (const auto &h : hints) + { + if (h) + { + params->hints.push_back(osrm::engine::Hint::FromBase64(*h)); + } + else + { + params->hints.push_back(std::nullopt); + } + } + params->radiuses = std::move(radiuses); + params->bearings = std::move(bearings); + params->approaches = approaches; + params->generate_hints = generate_hints; + params->exclude = std::move(exclude); + params->snapping = snapping; +} + +void assign_routeparameters(RouteParameters *params, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints) +{ + params->steps = steps; + params->alternatives = (bool)number_of_alternatives; + params->number_of_alternatives = number_of_alternatives; + params->annotations = !annotations.empty(); + params->annotations_type = calculate_routeannotations_type(annotations); + params->geometries = geometries; + params->overview = overview; + params->continue_straight = continue_straight; + params->waypoints = std::move(waypoints); +} + +RouteParameters::AnnotationsType +calculate_routeannotations_type(const std::vector &annotations) +{ + RouteParameters::AnnotationsType res = RouteParameters::AnnotationsType::None; + + for (size_t i = 0; i < annotations.size(); ++i) + { + res = res | annotations[i]; + } + + return res; +} + +TableParameters::AnnotationsType +calculate_tableannotations_type(const std::vector &annotations) +{ + if (annotations.empty()) + { + return TableParameters::AnnotationsType::Duration; + } + + TableParameters::AnnotationsType res = TableParameters::AnnotationsType::None; + + for (size_t i = 0; i < annotations.size(); ++i) + { + res = res | annotations[i]; + } + + return res; +} + +} // namespace osrm_nb_util diff --git a/test/data/windows-build-test-data.bat b/test/data/windows-build-test-data.bat new file mode 100644 index 0000000000..1c14d23f4f --- /dev/null +++ b/test/data/windows-build-test-data.bat @@ -0,0 +1,44 @@ +@ECHO OFF +SETLOCAL EnableDelayedExpansion + +SET DATA_DIR=%CD% + +SET test_region=monaco +SET test_region_ch=ch\monaco +SET test_region_mld=mld\monaco +SET test_osm=%test_region%.osm.pbf + +SET CMD=python -m osrm extract -p %DATA_DIR%\..\..\profiles\car.lua %DATA_DIR%\monaco.osm.pbf +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +MKDIR ch +XCOPY %test_region%.osrm.* ch\ +XCOPY %test_region%.osrm ch\ +MKDIR mld +XCOPY %test_region%.osrm.* mld\ +XCOPY %test_region%.osrm mld\ + +SET CMD=python -m osrm contract %test_region_ch%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +SET CMD=python -m osrm partition %test_region_mld%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +SET CMD=python -m osrm customize %test_region_mld%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +GOTO DONE + +:ERROR +ECHO ~~~~~~~~~~~~~~~~~~~~~~ ERROR %~f0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +ECHO Failed command: %CMD% +ECHO Exit code: %EL% + +:DONE +ECHO ~~~~~~~~~~~~~~~~~~~~~~ DONE %~f0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +EXIT /b %EL% diff --git a/test/python/constants.py b/test/python/constants.py new file mode 100755 index 0000000000..3173bf1e8c --- /dev/null +++ b/test/python/constants.py @@ -0,0 +1,18 @@ +from pathlib import Path + +path = Path(__file__).parent.parent.joinpath("data") + +# Constants and fixtures for Python tests on our Monaco dataset. + +# Somewhere in Monaco +# http://www.openstreetmap.org/#map=18/43.73185/7.41772 +three_test_coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)] + +two_test_coordinates = three_test_coordinates[0:2] + +test_tile = {"at": [17059, 11948, 15], "size": 159125} + +data_path = str(path.joinpath("ch", "monaco.osrm").absolute()) +mld_data_path = str(path.joinpath("mld", "monaco.osrm").absolute()) +corech_data_path = str(path.joinpath("corech", "monaco.osrm").absolute()) +test_memory_path = str(path.joinpath("test_memory").absolute()) diff --git a/test/python/test_index.py b/test/python/test_index.py new file mode 100755 index 0000000000..1acb068855 --- /dev/null +++ b/test/python/test_index.py @@ -0,0 +1,118 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +test_memory_path = constants.test_memory_path + + +class TestIndex: + def test_default_noparam(self): + osrm.OSRM() + + def test_throwifnecessarynotexist(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM("missing.osrm") + assert str(ex.value) == "Required files are missing" + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(storage_config="missing.osrm", algorithm="MLD") + assert str(ex.value) == "Config Parameters are Invalid" + + def test_shmemarg(self): + osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_memfile(self): + # memory_file is deprecated in OSRM v6 (equivalent to use_mmap=True); no datastore needed + osrm.OSRM( + storage_config=data_path, + use_shared_memory=False, + memory_file=test_memory_path, + ) + + def test_shmemfalsenopath(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(use_shared_memory=False) + assert str(ex.value) == "Config Parameters are Invalid" + + def test_nonstringarg(self): + with pytest.raises(TypeError): + osrm.OSRM(True) + + def test_unknownalgo(self): + with pytest.raises(ValueError) as ex: + osrm.OSRM(algorithm="Foo") + ex_str = str(ex.value) + assert "Invalid Algorithm: 'Foo'" in ex_str + assert "'CH'" in ex_str + assert "'MLD'" in ex_str + + def test_invalidalgo(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm=3) + assert str(ex.value) == "Invalid type passed for argument: algorithm" + + def test_validalgos(self): + osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + + osrm.OSRM(algorithm="CH", storage_config=data_path, use_shared_memory=False) + + def test_datamatchalgo(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm="CH", storage_config=mld_data_path, use_shared_memory=False) + assert "Could not find any metrics for CH in the data." in str(ex.value) + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm="MLD", storage_config=data_path, use_shared_memory=False) + assert "Could not find any metrics for MLD in the data." in str(ex.value) + + def test_datasetnamenotstring(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(dataset_name=1337) + assert str(ex.value) == "Invalid type passed for argument: dataset_name" + + # osrm.OSRM(dataset_name = "") requires osrm-datastore (uses shared memory) + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(dataset_name="unsued_name___", use_shared_memory=True) + assert "shared memory" in str(ex.value).lower() or "osrm-datastore" in str(ex.value) + + def test_defaultradius(self): + osrm.OSRM(storage_config=data_path, use_shared_memory=False, default_radius=1) + + def test_unlimitedradius(self): + osrm.OSRM( + storage_config=data_path, + use_shared_memory=False, + default_radius="unlimited", + ) + + def test_customlimits(self): + osrm.OSRM( + storage_config=mld_data_path, + algorithm="MLD", + use_shared_memory=False, + max_locations_trip=3, + max_locations_viaroute=3, + max_locations_distance_table=3, + max_locations_map_matching=3, + max_results_nearest=1, + max_alternatives=1, + default_radius=1, + ) + + def test_invalidlimits(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM( + storage_config=mld_data_path, + algorithm="MLD", + max_locations_trip=1, + max_locations_viaroute=True, + max_locations_distance_table=False, + max_locations_map_matching="a lot", + max_results_nearest=None, + max_alternatives="10", + default_radius="10", + ) + assert "Invalid type passed for argument" in str(ex.value) diff --git a/test/python/test_match.py b/test/python/test_match.py new file mode 100755 index 0000000000..41f5941050 --- /dev/null +++ b/test/python/test_match.py @@ -0,0 +1,244 @@ +import sys +import pytest +import osrm +import constants +import math + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +@pytest.mark.skipif( + sys.platform == "win32", reason="Map matching segfaults on Windows (STATUS_ACCESS_VIOLATION)" +) +class TestMatch: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_match(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + for m in res["matchings"]: + assert ( + m["distance"] + and m["duration"] + and isinstance(m["legs"], osrm.Array) + and m["geometry"] + and m["confidence"] > 0 + ) + assert len(res["tracepoints"]) == 3 + for t in res["tracepoints"]: + assert ( + t["hint"] + and not math.isnan(t["matchings_index"]) + and not math.isnan(t["waypoint_index"]) + and t["name"] + ) + + def test_match_no_timestamps(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + ) + res = self.osrm_py.Match(match_params) + assert len(res["tracepoints"]) == 3 + assert len(res["matchings"]) == 1 + + def test_match_no_geometrycompression(self): + match_params = osrm.MatchParameters(coordinates=three_test_coordinates, geometries="geojson") + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert isinstance(res["matchings"][0]["geometry"], osrm.Object) + assert isinstance(res["matchings"][0]["geometry"]["coordinates"], osrm.Array) + + def test_match_geometrycompression(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert isinstance(res["matchings"][0]["geometry"], str) + + def test_match_speedannotations(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["speed"], + overview="false", + geometries="geojson", + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + for l in res["matchings"][0]["legs"]: + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + assert "geometry" not in res["matchings"][0] + + def test_match_severalannotations(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["duration", "distance", "nodes"], + overview="false", + geometries="geojson", + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert ( + len(l["steps"]) > 0 + and l["annotation"] + and l["annotation"]["distance"] is not None + and l["annotation"]["duration"] is not None + and l["annotation"]["nodes"] is not None + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + assert "geometry" not in res["matchings"][0] + + def test_match_alloptions(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["all"], + overview="false", + geometries="geojson", + gaps="split", + tidy=False, + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert ( + len(l["steps"]) > 0 + and l["annotation"] + and l["annotation"]["distance"] is not None + and l["annotation"]["duration"] is not None + ) + assert "geometry" not in res["matchings"][0] + + def test_match_missing_arg(self): + with pytest.raises(Exception): + self.osrm_py.Match(osrm.MatchParameters()) + + def test_match_nonobj_arg(self): + with pytest.raises(TypeError): + osrm.MatchParameters(None) + + def test_match_invalidcoords(self): + match_params = osrm.MatchParameters(coordinates=[]) + with pytest.raises(Exception): + self.osrm_py.Match(match_params) + with pytest.raises(Exception): + match_params.coordinates = [three_test_coordinates[0]] + self.osrm_py.Match(match_params) + with pytest.raises(TypeError): + match_params.coordinates = three_test_coordinates[0] + with pytest.raises(TypeError): + match_params.coordinates = [ + three_test_coordinates[0][0], + three_test_coordinates[0][1], + ] + + def test_match_invalidtimestamps(self): + match_params = osrm.MatchParameters(coordinates=three_test_coordinates) + with pytest.raises(Exception): + match_params.timestamps = [1424684612, 1424684616] + self.osrm_py.Match(match_params) + + def test_match_without_motorways(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + match_params = osrm.MatchParameters(coordinates=three_test_coordinates, exclude=["motorway"]) + res = osrm_py.Match(match_params) + assert len(res["tracepoints"]) == 3 + assert len(res["matchings"]) == 1 + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_needtwo(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [0] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_needcoordindices(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [1, 2] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_ordermatters(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [2, 0] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + def test_match_invalidwaypoints_mustcorrespond(self): + match_params = osrm.MatchParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0, 3, 2] + ) + with pytest.raises(Exception): + self.osrm_py.Match(match_params) + + def test_match_error_on_splittrace(self): + match_params = osrm.MatchParameters( + steps=True, + coordinates=three_test_coordinates + [(7.41902, 43.73487)], + timestamps=[1700, 1750, 1424684616, 1424684620], + waypoints=[0, 3], + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Match(match_params) + assert "NoMatch" in str(ex.value) + + def test_match_waypoints(self): + match_params = osrm.MatchParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0, 2] + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert len(res["matchings"][0]["legs"]) == 1 + for m in res["matchings"]: + assert ( + m["distance"] + and m["duration"] + and isinstance(m["legs"], osrm.Array) + and m["geometry"] + and (m["confidence"] > 0) + ) + assert len(res["tracepoints"]) == 3 + for t in res["tracepoints"]: + assert t["hint"] and not math.isnan(t["matchings_index"]) and t["name"] diff --git a/test/python/test_nearest.py b/test/python/test_nearest.py new file mode 100755 index 0000000000..1675819f7c --- /dev/null +++ b/test/python/test_nearest.py @@ -0,0 +1,16 @@ +import osrm +import constants + +mld_data_path = constants.mld_data_path +two_test_coordinates = constants.two_test_coordinates + + +class TestNearest: + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + + def test_nearest(self): + nearest_params = osrm.NearestParameters( + coordinates=[two_test_coordinates[0]], exclude=["motorway"] + ) + res = self.osrm_py.Nearest(nearest_params) + assert len(res["waypoints"]) == 1 diff --git a/test/python/test_route.py b/test/python/test_route.py new file mode 100755 index 0000000000..eaffc98104 --- /dev/null +++ b/test/python/test_route.py @@ -0,0 +1,287 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestRoute: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_route(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert res["waypoints"] + assert res["routes"] + assert res["routes"][0]["geometry"] + + def test_route_mld(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + route_params = osrm.RouteParameters(coordinates=[(13.43864, 52.51993), (13.415852, 52.513191)]) + res = engine.Route(route_params) + assert res["waypoints"] + assert res["routes"] + assert res["routes"][0]["geometry"] + + def test_route_alternatives(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + + route_params.alternatives = True + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) >= 1 + + route_params.number_of_alternatives = 3 + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) >= 1 + + def test_route_badparams(self): + route_params = osrm.RouteParameters(coordinates=[]) + with pytest.raises(Exception): + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = None + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = [[three_test_coordinates[0], three_test_coordinates[1]]] + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = [ + (213.43864, 252.51993), + (413.415852, 552.513191), + ] + self.osrm_py.Route(route_params) + + def test_route_shmem(self): + # Use file-mode OSRM (shared memory requires osrm-datastore) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert isinstance(res["routes"][0]["geometry"], str) + + def test_route_geometrycompression(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, geometries="geojson") + res = self.osrm_py.Route(route_params) + assert isinstance(res["routes"][0]["geometry"]["coordinates"], osrm.Array) + assert res["routes"][0]["geometry"]["type"] == "LineString" + + def test_route_polyline6(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline6", + steps=True, + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + assert isinstance(res["routes"][0]["legs"][0]["steps"][0]["geometry"], str) + + def test_route_speedannotations(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["speed"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + def test_route_severalannotations(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["duration", "distance", "nodes"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + def test_route_options(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["all"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + and l["annotation"]["weight"] + and l["annotation"]["datasources"] + and l["annotation"]["speed"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + # def test_route_validbearings(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # bearings = [(200, 180), (250, 180)] + # ) + # res = self.osrm_py.Route(route_params) + # + # assert(res["routes"][0]) + + # route_params.bearings = [None, (200, 180)] + # res = self.osrm_py.Route(route_params) + # + # assert(res["routes"][0]) + + # def test_route_validradius(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # radiuses = [100, 100] + # ) + # res = self.osrm_py.Route(route_params) + # + + # route_params.radiuses = [None, None] + # res = self.osrm_py.Route(route_params) + # + + # route_params.radiuses = [100, None] + # res = self.osrm_py.Route(route_params) + # + + # def test_route_validapproaches(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # approaches = [None, osrm.Approach.CURB] + # ) + # res = self.osrm_py.Route(route_params) + # + + # route_params.approaches = [osrm.Approach.UNRESTRICTED, None] + # res = self.osrm_py.Route(route_params) + # + + def test_route_customlimitsmld(self): + engine = osrm.OSRM( + algorithm="MLD", + storage_config=mld_data_path, + max_alternatives=10, + use_shared_memory=False, + ) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, number_of_alternatives=10) + res = engine.Route(route_params) + assert isinstance(res["routes"], osrm.Array) + + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, number_of_alternatives=11) + with pytest.raises(RuntimeError) as ex: + res = engine.Route(route_params) + assert "TooBig" in str(ex.value) + + def test_route_nomotorways(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, exclude=["motorway"]) + res = engine.Route(route_params) + assert len(res["waypoints"]) == 2 + assert len(res["routes"]) == 1 + + def test_route_invalidwaypoints(self): + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[1, 2] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[2, 0] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + def test_route_snapping(self): + route_params = osrm.RouteParameters( + coordinates=[ + (7.448205209414596, 43.754001097311544), + (7.447122039202185, 43.75306156811368), + ], + snapping="any", + ) + res = self.osrm_py.Route(route_params) + assert round(res["routes"][0]["distance"] * 10) == 1315 diff --git a/test/python/test_table.py b/test/python/test_table.py new file mode 100755 index 0000000000..f122756b0f --- /dev/null +++ b/test/python/test_table.py @@ -0,0 +1,141 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestTable: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_table_annotations(self): + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["distance"], + ) + res = self.osrm_py.Table(table_params) + assert res["distances"] + assert "durations" not in res + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["duration"], + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert "distances" not in res + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["duration", "distance"], + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert res["distances"] + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]] + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert "distances" not in res + + def test_table_snapping(self): + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + snapping="any", + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + + # def test_table_annotation(self): + # tables = ["distance", "duration"] + + # for annotation in tables: + # table_params = osrm.TableParameters( + # coordinates = [three_test_coordinates[0], three_test_coordinates[1]], + # annotations = [annotation] + # ) + # res = self.osrm_py.Table(table_params) + # + # rows = res[annotation] + # for i, col in enumerate(res[annotation]): + # assert(len(rows) == len(col)) + # for j, row in enumerate(col): + # if(i == j): + # # check that diagonal is zero + # assert(col[j] == 0) + # else: + # # everything else is non-zero and finite + # assert(not col[j] == 0) + # assert(math.isfinite(col[j])) + # assert(len(table_params.coordinates) == len(rows)) + + # for annotation in tables: + # table_params = osrm.TableParameters( + # coordinates = [three_test_coordinates[0], three_test_coordinates[1]], + # sources = [0], + # destinations = [0,1], + # annotations = [annotation] + # ) + # res = self.osrm_py.Table(table_params) + # + # rows = res[annotation] + # for i, col in enumerate(res[annotation]): + # assert(len(rows) == len(col)) + # for j, row in enumerate(col): + # if(i == j): + # # check that diagonal is zero + # assert(col[j] == 0) + # else: + # # everything else is non-zero and finite + # assert(not col[j] == 0) + # assert(math.isfinite(col[j])) + # assert(len(table_params.sources) == len(rows)) + + def test_table_withoutwaypoints(self): + table_params = osrm.TableParameters(coordinates=two_test_coordinates, annotations=["duration"]) + table_params.skip_waypoints = True + res = self.osrm_py.Table(table_params) + assert "sources" not in res + assert "destinations" not in res + + def test_table_fallbackspeeds(self): + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, + annotations=["duration"], + fallback_speed=1, + fallback_coordinate_type="input", + ) + res = self.osrm_py.Table(table_params) + assert len(res["destinations"]) == 2 + assert len(res["fallback_speed_cells"]) == 0 + + def test_table_invalidfallbackspeeds(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, + annotations=["duration"], + fallback_speed=-1, + ) + with pytest.raises(RuntimeError) as ex: + osrm_py.Table(table_params) + assert str(ex.value) == "Invalid Table Parameters" + + table_params.fallback_speed = 10 + osrm_py.Table(table_params) + + def test_table_invalidscalefactor(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, annotations=["duration"], scale_factor=-1 + ) + with pytest.raises(RuntimeError) as ex: + osrm_py.Table(table_params) + assert str(ex.value) == "Invalid Table Parameters" + + table_params.scale_factor = 1 + osrm_py.Table(table_params) diff --git a/test/python/test_tile.py b/test/python/test_tile.py new file mode 100755 index 0000000000..4498ef0e50 --- /dev/null +++ b/test/python/test_tile.py @@ -0,0 +1,26 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +test_tile = constants.test_tile + + +class TestTile: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_tile(self): + tile_params = osrm.TileParameters(test_tile["at"]) + res = self.osrm_py.Tile(tile_params) + assert len(res) == test_tile["size"] + + def test_tile_preconditions(self): + with pytest.raises(Exception): + # Must be an array + tile_params = osrm.TileParameters(17059, 11948, -15) + with pytest.raises(Exception): + # Must be unsigned + tile_params = osrm.TileParameters([17059, 11948, -15]) + tile_params = osrm.TileParameters([17059, 11948, 15]) + self.osrm_py.Tile(tile_params) diff --git a/test/python/test_trip.py b/test/python/test_trip.py new file mode 100755 index 0000000000..149efdc7f8 --- /dev/null +++ b/test/python/test_trip.py @@ -0,0 +1,110 @@ +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestTrip: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_trip_manylocations(self): + trip_parameters = osrm.TripParameters(coordinates=three_test_coordinates[0:5]) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip["geometry"] + + def test_trip_invalidargs(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip["geometry"] + + def test_trip_geometrycompression(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]] + ) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert isinstance(trip["geometry"], str) + + def test_trip_nogeometrycompression(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters(coordinates=two_test_coordinates, geometries="geojson") + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert isinstance(trip["geometry"]["coordinates"], osrm.Array) + + def test_trip_speedannotations(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["speed"], + overview="false", + ) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + assert "geometry" not in l + + def test_trip_severalannotations(self): + trip_params = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["duration", "distance", "nodes"], + overview="false", + ) + res = self.osrm_py.Trip(trip_params) + assert len(res["trips"]) == 1 + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + assert "geometry" not in l + + def test_trip_options(self): + trip_params = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["all"], + overview="false", + ) + res = self.osrm_py.Trip(trip_params) + assert len(res["trips"]) == 1 + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] + assert "geometry" not in trip + + def test_trip_nomotorways(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + trip_params = osrm.TripParameters(coordinates=two_test_coordinates, exclude=["motorway"]) + res = engine.Trip(trip_params) + assert len(res["waypoints"]) == 2 + assert len(res["trips"]) == 1