Skip to content

Repository files navigation

boost_geometry

CIPublishdocs.rscodecovcrates.ioMSRVlicense

A Rust port of Boost.Geometry, carrying over its design philosophy: dimension-agnostic, coordinate-system-agnostic, bring-your-own-type, strategy-pluggable.

The library's algorithms are written against concept traits (Point, Ring, Polygon, …), not concrete structs — so your own domain types participate directly, exactly like BOOST_GEOMETRY_REGISTER_* in C++. There is no mandatory point or polygon type to convert into: you register the types you already have and call the algorithms on them. That makes boost_geometry a complement to whatever geometry types your stack already uses, rather than a container you must migrate onto.

  • Edition: Rust 2024, MSRV 1.85
  • Safety:unsafe_code = "forbid" across the whole workspace
  • Docs: see docs/ for the architecture, the tag-dispatch pattern, and the overlay engine

Quick start — your own polygon type, buffered and validated

Add the dependency:

cargo add boost_geometry
Coupled — with #[derive(Point)]

Derive Point on your own coordinate struct, register your own ring and polygon types with one macro declaration each, run is_valid_polygon on them directly, and buffer (runnable as cargo run --example parcel_buffer):

use boost_geometry::Point;use boost_geometry::adapt::{register_polygon, register_ring};use boost_geometry::overlay::{JoinStrategy, buffer_convex_polygon, is_valid_polygon};use boost_geometry::prelude::*;// Your own geometry types — no wrapper, no conversion trait, and no// library point type: the derive turns your struct into a Point.#[derive(Clone,Copy,Default,Point)]structCoord{x:f64,y:f64,}structBoundary{points:Vec<Coord>,}structParcel{outer:Boundary,holes:Vec<Boundary>,}// One declaration each and the library's algorithms accept them.register_ring!(Boundary,Coord, |s| s.points.iter());register_polygon!(Parcel,Coord,
ring = Boundary,
|s| outer = &s.outer,
inners = s.holes.iter());fnmain(){let c = |x, y| Coord{ x, y };// A 2×2 square parcel (clockwise, closed — the default ring convention).let parcel = Parcel{outer:Boundary{points:vec![
c(0.0,0.0),
c(0.0,2.0),
c(2.0,2.0),
c(2.0,0.0),
c(0.0,0.0),],},holes:vec![],};// Validate the user-owned type directly.println!("parcel valid: {:?}", is_valid_polygon(&parcel));// Buffer it outward by 1.0 with round joins — directly on the// user-owned type.letmut grown = buffer_convex_polygon(&parcel,1.0,JoinStrategy::Round{points_per_circle:360,},);// Normalise ring orientation, then validate the result.correct(&mut grown);println!("buffered valid: {:?}", is_valid_polygon(&grown));println!("area {:.3} -> {:.3}", area(&parcel), area(&grown));}

Output:

parcel valid: Ok(())
buffered valid: Ok(())
area 4.000 -> 15.141
De-coupled — without the derive

The derive is pure sugar: it emits the Geometry + Point (+ PointMut) impls below. Writing them by hand is the escape hatch for computed coordinates, packed storage, or FFI structs — same flow, same output (runnable as cargo run --example parcel_buffer_manual):

use boost_geometry::adapt::{register_polygon, register_ring};use boost_geometry::cs::Cartesian;use boost_geometry::overlay::{JoinStrategy, buffer_convex_polygon, is_valid_polygon};use boost_geometry::prelude::*;use boost_geometry::tag::PointTag;use boost_geometry::trait_::{Geometry,Point,PointMut};// Your own point type, registered by hand — the escape hatch for// computed coordinates, packed storage, or FFI structs the derive// cannot express.#[derive(Clone,Copy,Default)]structCoord{x:f64,y:f64,}implGeometryforCoord{typeKind = PointTag;typePoint = Coord;}implPointforCoord{typeScalar = f64;typeCs = Cartesian;constDIM:usize = 2;fnget<constD:usize>(&self) -> f64{matchD{0 => self.x,1 => self.y,
_ => unreachable!(),}}}// Only needed by algorithms that construct points (buffer, correct).implPointMutforCoord{fnset<constD:usize>(&mutself,v:f64){matchD{0 => self.x = v,1 => self.y = v,
_ => unreachable!(),}}}structBoundary{points:Vec<Coord>,}structParcel{outer:Boundary,holes:Vec<Boundary>,}register_ring!(Boundary,Coord, |s| s.points.iter());register_polygon!(Parcel,Coord,
ring = Boundary,
|s| outer = &s.outer,
inners = s.holes.iter());fnmain(){let c = |x, y| Coord{ x, y };// A 2×2 square parcel (clockwise, closed — the default ring convention).let parcel = Parcel{outer:Boundary{points:vec![
c(0.0,0.0),
c(0.0,2.0),
c(2.0,2.0),
c(2.0,0.0),
c(0.0,0.0),],},holes:vec![],};println!("parcel valid: {:?}", is_valid_polygon(&parcel));letmut grown = buffer_convex_polygon(&parcel,1.0,JoinStrategy::Round{points_per_circle:360,},);correct(&mut grown);println!("buffered valid: {:?}", is_valid_polygon(&grown));println!("area {:.3} -> {:.3}", area(&parcel), area(&grown));}

The buffered area matches the closed form for a square grown by distance d with round corners: s² + 4·s·d + π·d² = 4 + 8 + π ≈ 15.14.

What the example shows (identical for both paths above):

  • register_ring! / register_polygon! implement the concept traits for your structs (Rust's orphan rule forbids a blanket impl, so the macros mint it per type — the same coherence workaround the BOOST_GEOMETRY_REGISTER_* macros perform in C++). Defaults are closed, clockwise rings; both are overridable per type.
  • is_valid_polygon checks the OGC simple-feature rules — point count, closure, finite coordinates, spikes, self-intersections, ring orientation, hole containment — and reports the first failure as a ValidityFailure variant (Err(SelfIntersection), Err(WrongOrientation), …) rather than a bare false.
  • buffer_convex_polygon grows the polygon outward, rounding each corner with a circular arc (JoinStrategy::Miter gives sharp corners instead). v1 buffers points and convex polygons with positive distances.
  • correct fixes ring closure and orientation in place — the Boost bg::correct counterpart.

How it stays type-agnostic

The library never sees your Coord, Boundary, or Parcel as a concrete type. It only ever sees "some G that satisfies the Point (or Ring, or Polygon) concept trait", and reads it through that trait's methods. Your struct keeps its own fields, layout, and ownership; the register_*! macros just teach the trait how to read it. That is the whole trick — the same one BOOST_GEOMETRY_REGISTER_* performs in C++, rebuilt from ordinary Rust generics instead of template specialisation.

The techniques below are worth stealing for any bring-your-own-type library.

The Rust features that make it work

1. A concept is a trait; the data stays yours. A geometry is anything implementing the concept trait — the library owns behaviour, you own the bytes. The read surface is tiny:

pubtraitPoint:Geometry<Kind = PointTag,Point = Self>{typeScalar:CoordinateScalar;// associated type — your f64, i32, fixed-point…typeCs:CoordinateSystem;// associated type — Cartesian / Spherical / GeographicconstDIM:usize;// dimensions, known at compile timefnget<constD:usize>(&self) -> Self::Scalar;// read axis D}

Because Scalar and Cs are associated types (not generic parameters), a function written fn distance<P: Point>(a: &P, b: &P) carries the scalar and coordinate system along for free — no <P, S, Cs> soup at every call site.

2. Const generics move dimension checks to compile time.get::<const D> takes the axis as a const generic, so p.get::<2>() on a 2-D point is a compile error, not a runtime panic — the bound check happens once, in the type system.

3. Zero-sized marker tags + supertraits give free category dispatch. Each kind has a ZST tag (PointTag, RingTag, …) named by Geometry::Kind, and the tags form a hierarchy with plain supertraits:

pubtraitPolylinear:Linear{}// reproduces C++'s `polylinear_tag : linear_tag`

So fn f<T: Linear>() accepts segments, linestrings, and multi-linestrings in one signature — category dispatch with no macro, no enum, no dyn.

4. The orphan rule is sidestepped with per-type macros. Rust forbids a blanket impl<T> Point for T, and you can't impl Point for Vec<YourCoord> from this crate either (neither is yours). So register_ring! / #[derive(Point)]mint one concrete impl per type at your call site, where the coherence rules allow it — exactly the role the BOOST_GEOMETRY_REGISTER_* macros play.

5. Dispatch resolves statically, then vanishes. "One function, many kinds" (within on a ring vs. a polygon) is solved by a Kind → zero-sized-strategy type-level picker; every layer is a ZST and a static trait resolution, so at -O the whole chain collapses to a single direct call — no vtable, no branch. This is the codebase's one recurring idiom; the full mechanism (and why the obvious impl<G: Ring> + impl<G: Polygon> approach hits E0119) is written up in docs/02-tag-dispatch-pattern.md.

The payoff: your types participate with no wrapper, no conversion, and no runtime cost — the generic code monomorphises straight onto your struct's own accessors.

Features

Every capability below is a free function you call on your own registered types — no conversion step. Adding the single boost_geometry crate brings in all of the ✅ rows; the I/O and reprojection formats are separate crates so a default build stays lean. The Docs link opens the rustdoc for that item; the no_std column is the status of the crate the function lives in (see the full matrix below).

Naming mirrors Boost.Geometry: a strategy-driven algorithm exposes a strategy-less default (picks the right strategy for the coordinate system) plus a _with companion that takes an explicit strategy.

Auto-generated from the source; run python3 .github/scripts/feature_table.py after adding an export.

Functionno_stdDocs
Measures — Scalar quantities of a geometry
area / area_with / box_area / multi_polygon_area / ring_area
area_dyn
azimuth / azimuth_with
centroid / centroid_with
closest_points / closest_points_with
comparable_distance / comparable_distance_with / distance / distance_with
discrete_frechet_distance / discrete_frechet_distance_with
discrete_hausdorff_distance / discrete_hausdorff_distance_with
distance_dyn
length / length_with / perimeter / perimeter_with / ring_perimeter / ring_perimeter_with
length_dyn
Spatial predicates — Boolean relationships between geometries
contains_properly / crosses / overlaps / relate_matrix / relation / relate / touches
coordinate_position
covered_by / within
disjoint / disjoint_box_box
equals
intersects / intersects_reversed
within_dyn
Boolean operations — Overlay and offset of areal geometries
buffer / buffer_convex_polygon / buffer_point / buffer_with / buffer_with_strategy
difference / intersection / sym_difference / union / union_poly
line_intersection
point_on_surface
Construction & transformation — Derive a new geometry from an existing one
chaikin_smoothing
concave_hull / concave_hull_with / k_nearest_concave_hull
convex_hull
densify
destination / destination_with
envelope
envelope_dyn
expand / expand_with
line_interpolate
line_locate_point
linestring_segmentize / linestring_segmentize_with
map_coords / map_coords_in_place
minimum_rotated_rect
monotone_subdivision
rhumb_azimuth / rhumb_azimuth_with / rhumb_destination / rhumb_destination_with / rhumb_distance / rhumb_distance_with / rhumb_length / rhumb_length_with
simplify / simplify_with
transform
triangulate_earcut
Inspection — Query a geometry's shape or membership
for_each_point / for_each_segment
is_convex
is_empty
is_simple
is_valid / is_valid_polygon / is_valid_polygon_with / is_valid_ring / is_valid_ring_with / is_valid_with / validity_reason / validity_reason_with
num_geometries
num_interior_rings
num_points
num_segments
Mutation & assembly — Build up or normalise a geometry in place
append / append_to_ring
assign_values
clear
convert
correct / correct_closure
make_box / make_point / make_segment
merge_elements / merge_multipolygon / merge_polygons / stitch_triangles
remove_spikes
reverse
unique
Spatial index — Bulk-loadable R-tree with nearest-neighbour and predicate queries
and / not / satisfies
Rtree
I/O — Well-Known Text — Parse and write the OGC WKT format
from_wkt / parse_linestring / parse_multi_linestring / parse_multi_point / parse_multi_polygon / parse_point / parse_polygon
to_wkt / to_wkt_polygon / write_wkt
I/O — Well-Known Binary — Parse and write the OGC WKB format
from_wkb
to_wkb / to_wkb_polygon
I/O — GeoJSON — Parse and write GeoJSON (RFC 7946)
from_geojson
to_geojson / to_geojson_polygon
I/O — SVG — Render geometries to SVG (debugging)
SvgMapper
Reprojection — CRS-to-CRS point reprojection (standalone crate)
reproject

Ecosystem adapters register the types of other crates so the algorithms above accept them directly: geo-types (GeoPoint, GeoPolygon, …) and nalgebra (NaPoint2, NaVector3, …). Both are no_std.

Everything the boost_geometry facade re-exports is browsable from one place: docs.rs/boost_geometry. The I/O, projection, and adapter crates are separate dependencies — their columns link to their own docs.rs pages.

Workspace layout

Nineteen crates form a dependency spine from foundational tag/coords crates up through traits, models, strategies, and algorithms to the boost_geometry facade — plus adapters (nalgebra, geo-types), IO (WKT, WKB, GeoJSON, SVG), an R-tree, overlay operations, and projections. boost_geometry re-exports everything; depend on it alone unless you need a slimmer build.

See docs/01-architecture.md for the full map, and the per-crate no_std status just below.

no_std support

Generated by .github/scripts/no_std_support.py, which builds every crate with --no-default-features (falling back to --features libm for crates that need a libm-backed Float impl) and is checked in CI:

Crateno_std
boost_geometry
geometry-adapt
geometry-adapt-geo-types
geometry-adapt-nalgebra
geometry-algorithm
geometry-coords
geometry-cs
geometry-derive
geometry-io-geojson
geometry-io-svg
geometry-io-wkb
geometry-io-wkt
geometry-model
geometry-overlay
geometry-proj
geometry-rtree
geometry-strategy
geometry-tag
geometry-trait

License

BSL-1.0 — see LICENSE.

About

Rust port of Boost.Geometry — dimension-agnostic, bring-your-own-type, strategy-pluggable computational geometry

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages