Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/benchmarks-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Smoke-build the dependency-free portion of the benchmarks. Triggered only
# on PRs/pushes that actually touch benchmark sources, the root CMake, or
# this workflow file — so doc-only PRs don't pay the CI cost. The benchmarks
# themselves aren't run here (that would require S2 / Boost / GeographicLib
# installs), only `bench_geo_utils` and `bench_naive` which have no external
# deps. The intent is to catch silent breakage in `benchmarks/CMakeLists.txt`
# or the shared `random_data.hpp` / `constants.hpp` headers.

name: Benchmarks smoke-build

on:
push:
branches: [master, main]
paths:
- 'benchmarks/**'
- 'CMakeLists.txt'
- '.github/workflows/benchmarks-smoke.yml'
pull_request:
branches: [master, main]
paths:
- 'benchmarks/**'
- 'CMakeLists.txt'
- '.github/workflows/benchmarks-smoke.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke:
name: smoke-build (ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Configure
run: cmake -S . -B build-bench -DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON -DGEO_UTILS_CPP_BUILD_TESTS=OFF -DGEO_UTILS_CPP_BUILD_EXAMPLES=OFF -DCMAKE_BUILD_TYPE=Release

- name: Build (dependency-free benches only)
run: cmake --build build-bench --config Release --target bench_geo_utils bench_naive
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build/
build-*/

# Compiled Object files
*.slo
Expand Down
10 changes: 8 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ else()
endif()

include(CMakeDependentOption)
option(GEO_UTILS_CPP_BUILD_TESTS "Build geo-utils-cpp tests" ${_geo_utils_cpp_is_top_level})
option(GEO_UTILS_CPP_BUILD_EXAMPLES "Build geo-utils-cpp examples" ${_geo_utils_cpp_is_top_level})
option(GEO_UTILS_CPP_BUILD_TESTS "Build geo-utils-cpp tests" ${_geo_utils_cpp_is_top_level})
option(GEO_UTILS_CPP_BUILD_EXAMPLES "Build geo-utils-cpp examples" ${_geo_utils_cpp_is_top_level})
option(GEO_UTILS_CPP_BUILD_BENCHMARKS "Build geo-utils-cpp benchmarks" OFF)
cmake_dependent_option(GEO_UTILS_CPP_ENABLE_COVERAGE
"Enable gcov coverage instrumentation (GCC/Clang only)" OFF
"GEO_UTILS_CPP_BUILD_TESTS" OFF)
Expand Down Expand Up @@ -98,6 +99,11 @@ if(GEO_UTILS_CPP_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()

# Benchmarks (opt-in: pulls Google Benchmark and optionally S2/Boost/GeographicLib)
if(GEO_UTILS_CPP_BUILD_BENCHMARKS)
add_subdirectory(benchmarks)
endif()

# Installation
include(GNUInstallDirs)

Expand Down
136 changes: 105 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,31 @@
</a>
</p>

Header-only C++17 library for geographic (lat/lng) geometry (no dependencies).
Practical latitude/longitude geometry for C++17 projects that need GPS math,
not a full geometry framework.

Provides utilities for distance, bearing, polygon area, point-in-polygon, and
path proximity checks on Earth coordinates.
Distance, heading, polygon area, point-in-polygon, and path proximity checks —
header-only, no dependencies, no build step.

API inspired by Google Maps geometry utilities.
Uses spherical Earth approximation (like Google Maps).
The API is inspired by Google Maps geometry utilities and uses the same spherical
Earth approximation model.

## Features

* **`geo::` spherical functions** — distance, bearing, area, interpolation
* **`geo::` polygon functions** — point-in-polygon, path proximity, distance to segments

## Why use this library?

- Lightweight and header-only (no dependencies)
- Simple API for common GPS/lat-lng calculations
- Suitable for backend, GIS, navigation and tracking systems

## When not to use

- If you need high-precision geodesic calculations on an ellipsoid
- If you need advanced spatial indexing (use S2 / CGAL instead)
- **Lat/lng-native API** — pass latitude/longitude coordinates directly, no
framework-specific point types to convert through.
- **Header-only, dependency-free** — about 36 KB across 4 headers; nothing
to build or link.
- **Spherical math** — distance, heading, offset, interpolation, area.
- **Polygon utilities** — point-in-polygon and path proximity checks.
- **Fast** — matches hand-written haversine on `distance`; especially strong
on polygon `area` (see [benchmarks](docs/benchmarks.md)).
- **Focused scope** — intentionally small API for GPS, navigation, tracking,
backend, and GIS workflows.

## Installation

### FetchContent (recommended)
### FetchContent

```cmake
include(FetchContent)
Expand All @@ -85,13 +83,6 @@ target_link_libraries(your_target PRIVATE geo::utils)
vcpkg install geo-utils-cpp
```

Then in your `CMakeLists.txt`:

```cmake
find_package(GeoUtilsCpp 1.0.1 REQUIRED)
target_link_libraries(your_target PRIVATE geo::utils)
```

### xrepo

```sh
Expand All @@ -107,21 +98,35 @@ target("your_target")
add_packages("geo-utils-cpp")
```

### find_package
### Conan

```cmake
find_package(GeoUtilsCpp 1.0.1 REQUIRED)
target_link_libraries(your_target PRIVATE geo::utils)
```sh
conan install --requires=geo-utils-cpp/1.0.1 --build=missing
```

Conan Center support is pending
[conan-io/conan-center-index#30152](https://github.com/conan-io/conan-center-index/pull/30152)

### Manual

Copy the `include/` directory into your project and add it to your include path.

For more details see [docs/getting-started.md](docs/getting-started.md).
### Using it from CMake

With any of the above methods (vcpkg, xrepo, Conan, FetchContent, or a
system `find_package`), wire it into your build with:

```cmake
find_package(GeoUtilsCpp 1.0.1 REQUIRED)
target_link_libraries(your_target PRIVATE geo::utils)
```

For more details, see [docs/getting-started.md](docs/getting-started.md).

## Usage

Distance and heading between two points:

```cpp
#include <iostream>

Expand All @@ -139,6 +144,75 @@ int main() {
}
```

Polygon area, point-in-polygon, path length, and path proximity:

```cpp
#include <iostream>
#include <vector>

#include <geo/poly.hpp>

int main() {
// A small box around midtown Manhattan (vertices in CCW order).
std::vector<geo::LatLng> midtown = {
{40.74, -74.01}, {40.74, -73.96}, {40.78, -73.96}, {40.78, -74.01},
};
geo::LatLng timesSquare{40.7580, -73.9855};

std::cout << "Times Square inside: "
<< (geo::contains(timesSquare, midtown) ? "true" : "false") << "\n";
std::cout << "Polygon area: "
<< geo::area(midtown) / 1e6 << " km^2\n";

// A short polyline along Broadway, and a point near it.
std::vector<geo::LatLng> route = {
{40.7580, -73.9855}, // Times Square
{40.7680, -73.9818}, // Columbus Circle
{40.7780, -73.9740}, // Lincoln Center
};
geo::LatLng nearby{40.7670, -73.9820};

std::cout << "Route length: "
<< geo::path_length(route) / 1000.0 << " km\n";
std::cout << "Point within 200 m of route: "
<< (geo::on_path(nearby, route, /*geodesic=*/true, /*tolerance=*/200.0)
? "true" : "false")
<< "\n";
}
```

## Benchmarks

`geo-utils-cpp` is header-only with no runtime dependencies. Throughput on
Apple M1 / clang 17 / `-O2 -DNDEBUG` (higher is better):

| Library | `distance_between` (M pairs/s) | `area` (poly N=100, M polys/s) |
| -------------------- | -----------------------------: | -----------------------------: |
| **geo-utils-cpp** | **40.5** | **67.2** |
| naive haversine | 38.3 | — |
| S2 Geometry | 82.9 | 14.0 |
| Boost.Geometry | 39.8 | 36.2 |
| GeographicLib | 1.2 | 2.0 |

Native types are pre-built outside the timed loop, so the table compares
algorithmic cost rather than object-construction overhead. `geo-utils-cpp`
matches hand-written haversine and Boost.Geometry on simple spherical operations,
is especially strong on `area`, while S2 is faster on several operations when
conversion from lat/lng is excluded.

See [docs/benchmarks.md](docs/benchmarks.md) for full methodology, all
operations, and when to use each library.

## When not to use

- If you need high-precision ellipsoidal geodesics or sub-meter accuracy, use
GeographicLib.
- If polygon containment is your main hot path, especially for larger polygons,
consider S2 Geometry.
- If you need many geometry types, coordinate systems, or generic geometry
algorithms, Boost.Geometry may be a better fit.
- If you need spatial indexing, use S2, CGAL, or another dedicated spatial index.

## API Reference

See [docs/api.md](docs/api.md) for the full API reference.
Expand Down
118 changes: 118 additions & 0 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Benchmarks for geo-utils-cpp.
#
# Build with:
# cmake -B build -DGEO_UTILS_CPP_BUILD_BENCHMARKS=ON
# cmake --build build --target bench_all
#
# Google Benchmark is fetched automatically. S2, Boost.Geometry, and
# GeographicLib are looked up via find_package — missing competitors are
# skipped with a status message instead of causing a hard error.

include(FetchContent)

# --- Google Benchmark -------------------------------------------------------

set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
set(BENCHMARK_INSTALL_DOCS OFF CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
google_benchmark
URL https://github.com/google/benchmark/archive/v1.8.4.tar.gz
URL_HASH SHA256=3e7059b6b11fb1bbe28e33e02519398ca94c1818874ebed18e504dc6f709be45
DOWNLOAD_EXTRACT_TIMESTAMP ON
)
FetchContent_MakeAvailable(google_benchmark)

# --- Shared infrastructure --------------------------------------------------

add_library(geo_utils_cpp_bench_common INTERFACE)
target_include_directories(geo_utils_cpp_bench_common INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/common
)
target_compile_features(geo_utils_cpp_bench_common INTERFACE cxx_std_17)

# Aggregator target: depend on every bench that ends up being built.
add_custom_target(bench_all)

function(_geo_utils_cpp_add_bench name source)
add_executable(bench_${name} ${source})
target_link_libraries(bench_${name} PRIVATE
geo::utils
geo_utils_cpp_bench_common
benchmark::benchmark_main
${ARGN}
)
add_dependencies(bench_all bench_${name})
endfunction()

# --- geo-utils-cpp itself + naive haversine baseline ------------------------

_geo_utils_cpp_add_bench(geo_utils speed/bench_geo_utils.cpp)
_geo_utils_cpp_add_bench(naive speed/bench_naive.cpp)

# --- Optional: S2 Geometry --------------------------------------------------
#
# vcpkg port name: `s2geometry`. Homebrew formula: `s2geometry`. The CMake
# package is conventionally exported as `s2`, but case-sensitive filesystems
# may also see `S2Config.cmake` from some installs — accept either.
find_package(s2 NAMES s2 S2 QUIET)
if(s2_FOUND)
_geo_utils_cpp_add_bench(s2 speed/bench_s2.cpp s2::s2)
message(STATUS "geo-utils-cpp benchmarks: S2 found — bench_s2 enabled.")
else()
message(STATUS "geo-utils-cpp benchmarks: S2 not found — bench_s2 skipped. "
"Install via 'vcpkg install s2geometry' or 'brew install s2geometry'.")
endif()

# --- Optional: Boost.Geometry -----------------------------------------------
#
# Boost.Geometry is header-only. CMake 3.30 deprecates the FindBoost module in
# favour of Boost's exported BoostConfig.cmake; we prefer CONFIG and fall back
# to the legacy module so this works across CMake 3.14+.
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()

find_package(Boost 1.71 QUIET CONFIG)
if(NOT Boost_FOUND)
find_package(Boost 1.71 QUIET)
endif()

if(Boost_FOUND)
# Different Boost versions expose the header set as Boost::headers (>=1.71),
# Boost::boost (older), or only via Boost_INCLUDE_DIRS.
if(TARGET Boost::headers)
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp Boost::headers)
elseif(TARGET Boost::boost)
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp Boost::boost)
else()
_geo_utils_cpp_add_bench(boost speed/bench_boost.cpp)
target_include_directories(bench_boost PRIVATE ${Boost_INCLUDE_DIRS})
endif()
message(STATUS "geo-utils-cpp benchmarks: Boost.Geometry found — bench_boost enabled.")
else()
message(STATUS "geo-utils-cpp benchmarks: Boost not found — bench_boost skipped. "
"Install via 'vcpkg install boost-geometry' or 'brew install boost'.")
endif()

# --- Optional: GeographicLib ------------------------------------------------
#
# Recent GeographicLib versions export the target `GeographicLib::GeographicLib`.
# Older versions exposed `${GeographicLib_LIBRARIES}` instead — we accept both.
find_package(GeographicLib QUIET)
if(GeographicLib_FOUND)
if(TARGET GeographicLib::GeographicLib)
_geo_utils_cpp_add_bench(geographiclib speed/bench_geographiclib.cpp
GeographicLib::GeographicLib)
else()
_geo_utils_cpp_add_bench(geographiclib speed/bench_geographiclib.cpp
${GeographicLib_LIBRARIES})
target_include_directories(bench_geographiclib PRIVATE ${GeographicLib_INCLUDE_DIRS})
endif()
message(STATUS "geo-utils-cpp benchmarks: GeographicLib found — bench_geographiclib enabled.")
else()
message(STATUS "geo-utils-cpp benchmarks: GeographicLib not found — bench_geographiclib skipped. "
"Install via 'vcpkg install geographiclib' or 'brew install geographiclib'.")
endif()
Loading
Loading