geometry_model/dyn_geometry.rs
1//! A runtime-tagged variant geometry — one variant per OGC kind.
2//!
3//! Mirrors `boost::geometry::dynamic_geometry_tag` and the
4//! variant adapters in
5//! `boost/geometry/geometries/adapted/boost_variant.hpp` (and the
6//! `boost_variant2` / `std_variant` siblings). Where the C++ side
7//! uses `boost::variant<...>` and a `dynamic_geometry_tag` to dispatch,
8//! the Rust port uses a plain enum and lets `match` do the dispatch —
9//! one arm per OGC kind, every arm parameterised by the same
10//! `(Scalar, Cs)` so the static algorithms inside each arm can still
11//! be monomorphised.
12//!
13//! Phase positioning:
14//! * KC2.T1 (this file) — the enum + `kind()` accessor.
15//! * KC2.T2 — `length_dyn`, `area_dyn`, … in `geometry-algorithm`.
16//! * KC2.T3 — `impl GeometryCollection for Vec<DynGeometry<...>>` so
17//! a heterogeneous vector is itself a collection.
18//! * KC4 — `length`/`area` of "wrong" kinds returns 0 — depends on
19//! the variant existing so the dispatch arm has somewhere to land.
20
21use alloc::vec::Vec;
22
23use geometry_coords::CoordinateScalar;
24use geometry_cs::{Cartesian, CoordinateSystem};
25use geometry_tag::DynamicGeometryTag;
26use geometry_trait::Geometry;
27
28use crate::{Linestring, MultiLinestring, MultiPoint, MultiPolygon, Point, Polygon};
29
30/// A geometry whose OGC kind is decided at runtime.
31///
32/// Mirrors Boost's `boost::geometry::dynamic_geometry_tag` family
33/// (`core/tags.hpp:124-125`,
34/// `geometries/adapted/boost_variant.hpp`). One variant per OGC kind;
35/// every variant carries the matching `model::*` struct parameterised
36/// by the supplied `Scalar` and `Cs`.
37///
38/// The parameterisation deliberately fixes a single
39/// `(Scalar, Cs)` across all variants — heterogeneous *scalar* or
40/// *coordinate-system* mixing inside one collection is out of scope
41/// for v1 (it would require runtime CS conversion, which is a
42/// `phase_07` projections concern).
43///
44/// # Layout
45///
46/// `GeometryCollection` wraps a `Vec<DynGeometry<Scalar, Cs>>` —
47/// nested collections are allowed. The recursive variant is required
48/// because OGC `GeometryCollection` itself is heterogeneous.
49///
50/// # Example
51///
52/// ```
53/// use geometry_cs::Cartesian;
54/// use geometry_model::{DynGeometry, DynKind, Point2D};
55///
56/// let p = DynGeometry::<f64, Cartesian>::Point(Point2D::new(1.0, 2.0));
57/// assert_eq!(p.kind(), DynKind::Point);
58/// ```
59#[derive(Debug, Clone, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(
62 feature = "serde",
63 serde(bound(
64 serialize = "Scalar: serde::Serialize",
65 deserialize = "Scalar: serde::Deserialize<'de>"
66 ))
67)]
68pub enum DynGeometry<Scalar: CoordinateScalar, Cs: CoordinateSystem = Cartesian> {
69 /// OGC `Point`. The dimension is fixed at 2D; 3D points go through
70 /// `PolyhedralSurface` (or wait for the v1.1 `DynPoint3` variant —
71 /// not in this task).
72 Point(Point<Scalar, 2, Cs>),
73
74 /// OGC `LineString`. Wraps [`Linestring<Point<Scalar, 2, Cs>>`].
75 LineString(Linestring<Point<Scalar, 2, Cs>>),
76
77 /// OGC `Polygon`. The two const-generic bools default to Boost's
78 /// `(ClockWise = true, Closed = true)`.
79 Polygon(Polygon<Point<Scalar, 2, Cs>>),
80
81 /// OGC `MultiPoint`.
82 MultiPoint(MultiPoint<Point<Scalar, 2, Cs>>),
83
84 /// OGC `MultiLineString`.
85 MultiLineString(MultiLinestring<Linestring<Point<Scalar, 2, Cs>>>),
86
87 /// OGC `MultiPolygon`.
88 MultiPolygon(MultiPolygon<Polygon<Point<Scalar, 2, Cs>>>),
89
90 /// OGC `GeometryCollection` — a `Vec` of dyn-geometries. Nested
91 /// collections are permitted.
92 GeometryCollection(Vec<DynGeometry<Scalar, Cs>>),
93 // OGC `PolyhedralSurface`. Reuses `crate::PolyhedralSurface` once a
94 // concrete model type exists (it is currently trait-only per v1
95 // T14); until then there is no variant to land here.
96 // PolyhedralSurface(PolyhedralSurface<...>)
97}
98
99/// Discriminant of [`DynGeometry`]. Stable across releases.
100///
101/// Mirrors the result of the `geometry::detail::single_tag_of` family
102/// plus Boost's `dynamic_geometry_concept`'s tag inspection
103/// (`geometries/concepts/dynamic_geometry_concept.hpp`).
104#[non_exhaustive]
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum DynKind {
107 Point,
108 LineString,
109 Polygon,
110 MultiPoint,
111 MultiLineString,
112 MultiPolygon,
113 GeometryCollection,
114 /// Reserved for the `PolyhedralSurface` variant once the
115 /// matching concrete model type lands. v1's `PolyhedralSurface`
116 /// is trait-only.
117 PolyhedralSurface,
118}
119
120impl<Scalar: CoordinateScalar, Cs: CoordinateSystem> Geometry for DynGeometry<Scalar, Cs> {
121 type Kind = DynamicGeometryTag;
122 // A runtime-tagged geometry has no single static point type; the
123 // representative choice is the 2D point every variant is built on.
124 // Mirrors Boost treating a `dynamic_geometry` through its
125 // `point_type` of the underlying variant elements.
126 type Point = Point<Scalar, 2, Cs>;
127}
128
129impl<Scalar: CoordinateScalar, Cs: CoordinateSystem> DynGeometry<Scalar, Cs> {
130 /// Discriminant of this value.
131 ///
132 /// Mirrors `tag<G>::type` on `dynamic_geometry_tag` Boost types
133 /// (`core/tag.hpp` + the variant adapters). Pure run-time
134 /// inspection — algorithms should not branch on `kind()` directly;
135 /// the `_dyn` algorithm wrappers in KC2.T2 match each arm and
136 /// forward to the static algorithm, keeping dispatch monomorphic
137 /// per arm.
138 #[must_use]
139 pub fn kind(&self) -> DynKind {
140 match self {
141 DynGeometry::Point(_) => DynKind::Point,
142 DynGeometry::LineString(_) => DynKind::LineString,
143 DynGeometry::Polygon(_) => DynKind::Polygon,
144 DynGeometry::MultiPoint(_) => DynKind::MultiPoint,
145 DynGeometry::MultiLineString(_) => DynKind::MultiLineString,
146 DynGeometry::MultiPolygon(_) => DynKind::MultiPolygon,
147 DynGeometry::GeometryCollection(_) => DynKind::GeometryCollection,
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 //! Round-trip each variant through `kind()`. Mirrors the per-tag
155 //! check pattern in `boost/geometry/test/core/tag.cpp`, but on the
156 //! runtime discriminant instead of the compile-time tag type.
157
158 use super::*;
159 use crate::{Point2D, linestring, polygon};
160 use alloc::vec;
161 use geometry_cs::Cartesian;
162
163 type S = f64;
164 type Pt = Point2D<S, Cartesian>;
165
166 #[test]
167 fn kind_round_trip_every_variant() {
168 let cases: Vec<(DynGeometry<S, Cartesian>, DynKind)> = vec![
169 (DynGeometry::Point(Pt::new(1.0, 2.0)), DynKind::Point),
170 (
171 DynGeometry::LineString(linestring![(0.0, 0.0), (1.0, 1.0)]),
172 DynKind::LineString,
173 ),
174 (
175 DynGeometry::Polygon(polygon![[
176 (0.0, 0.0),
177 (1.0, 0.0),
178 (1.0, 1.0),
179 (0.0, 1.0),
180 (0.0, 0.0)
181 ]]),
182 DynKind::Polygon,
183 ),
184 (
185 DynGeometry::MultiPoint(crate::MultiPoint(vec![Pt::new(0.0, 0.0)])),
186 DynKind::MultiPoint,
187 ),
188 (
189 DynGeometry::GeometryCollection(vec![]),
190 DynKind::GeometryCollection,
191 ),
192 ];
193 for (g, expected) in cases {
194 assert_eq!(g.kind(), expected);
195 }
196 }
197
198 #[test]
199 fn geometry_collection_can_nest() {
200 let nested = DynGeometry::<S, Cartesian>::GeometryCollection(vec![
201 DynGeometry::Point(Pt::new(1.0, 2.0)),
202 DynGeometry::GeometryCollection(vec![DynGeometry::Point(Pt::new(3.0, 4.0))]),
203 ]);
204 assert_eq!(
205 nested,
206 DynGeometry::GeometryCollection(vec![
207 DynGeometry::Point(Pt::new(1.0, 2.0)),
208 DynGeometry::GeometryCollection(vec![DynGeometry::Point(Pt::new(3.0, 4.0))]),
209 ])
210 );
211 }
212}