Skip to main content

geometry_strategy/
envelope.rs

1//! Per-CS strategy for the axis-aligned bounding box (envelope).
2//!
3//! Mirrors three pieces of Boost.Geometry that collaborate to make
4//! `boost::geometry::envelope(g, mbr)` work for any geometry in any
5//! coordinate system:
6//!
7//! * `boost/geometry/strategies/envelope/services.hpp` — the
8//!   `services::default_strategy<G>` metafunction that picks the
9//!   per-CS envelope strategy.
10//! * `boost/geometry/strategies/envelope/cartesian.hpp` —
11//!   `strategies::envelope::cartesian<>`, whose per-tag dispatch
12//!   selects `envelope::cartesian_point` / `cartesian_box` /
13//!   `cartesian_segment` / `expand::point` / `expand::box_` and so
14//!   on. The Cartesian case for non-trivial geometries is a plain
15//!   per-coordinate min/max walk — no great-circle bookkeeping,
16//!   unlike the spherical / geographic strategies.
17//! * `boost/geometry/algorithms/detail/envelope/range.hpp:33-66` —
18//!   `envelope_range_of_boxes` and `envelope_range_of_points`, which
19//!   walk a point sequence and grow a `Box` by component-wise
20//!   `min` / `max`. The Rust port collapses the C++ template
21//!   recursion `dimension_one` / `dimension_two` of
22//!   `algorithms/detail/envelope/initialize.hpp` into the
23//!   [`seed_from_point`] / [`grow_from_point`] helpers below, written
24//!   against [`fold_dims`].
25//!
26//! ## Coherence note
27//!
28//! Boost dispatches on the geometry's tag via partial template
29//! specialisation — `dispatch::envelope<G, point_tag>` and
30//! `dispatch::envelope<G, linestring_tag>` are mutually exclusive
31//! because the C++ side can ask the compiler to prove tags are
32//! distinct. Rust's trait system cannot prove that a downstream type
33//! does *not* implement both [`geometry_trait::Point`] and (say)
34//! [`geometry_trait::Linestring`] simultaneously, so two open blankets
35//! on one strategy struct would collide (E0119). The port reproduces
36//! Boost's tag dispatch instead: one **per-kind strategy struct**
37//! ([`EnvelopePoint`], [`EnvelopeLinestring`], …) carries a single
38//! concept-bounded `EnvelopeStrategy` impl — distinct `Self`, so no
39//! overlap — and the tag-keyed [`EnvelopeStrategyForKind`] picker routes
40//! `G::Kind` to the right struct (disjoint on the tag). Because the
41//! picker keys on the tag [`geometry_trait::Geometry::Kind`] already
42//! carries, any concept-adapted foreign type resolves through the same
43//! path as the equivalent `geometry-model` value (see
44//! `specs/open-tag-dispatch/`).
45//!
46//! T35 lands the Cartesian implementation only — Boost's
47//! Spherical/Geographic envelope strategies arrive alongside the
48//! Haversine / Andoyer / Vincenty distance strategies in later
49//! tasks (T40+).
50
51use geometry_coords::CoordinateScalar;
52use geometry_cs::{CartesianFamily, CoordinateSystem};
53use geometry_model::Box as ModelBox;
54use geometry_tag::{
55    BoxTag, LinestringTag, MultiLinestringTag, MultiPointTag, MultiPolygonTag, PointTag,
56    PolygonTag, RingTag, SameAs, SegmentTag,
57};
58use geometry_trait::{
59    Box as BoxTrait, IndexedAccess, Linestring as LinestringTrait,
60    MultiLinestring as MultiLinestringTrait, MultiPoint as MultiPointTrait,
61    MultiPolygon as MultiPolygonTrait, Point as PointTrait, PointMut, Polygon as PolygonTrait,
62    Ring as RingTrait, Segment as SegmentTrait, fold_dims,
63};
64
65/// A strategy for computing the axis-aligned bounding box of a
66/// geometry.
67///
68/// Mirrors the per-CS envelope-strategy concept declared in
69/// `boost/geometry/strategies/envelope/services.hpp` and refined per
70/// coordinate system in `strategies/envelope/{cartesian,spherical,
71/// geographic}.hpp`. The Boost concept exposes a family of per-tag
72/// sub-strategies (`envelope::cartesian_point`,
73/// `envelope::cartesian_box`, …) keyed off the geometry's tag; the
74/// Rust analogue keeps that family as one per-kind strategy struct each
75/// ([`EnvelopePoint`], [`EnvelopeBox`], …), selected by the tag-keyed
76/// [`EnvelopeStrategyForKind`] picker.
77///
78/// # Associated items
79///
80/// * [`Self::Output`] — the bounding-box type. For the
81///   Cartesian implementation this is always
82///   `geometry_model::Box<G::Point>`, matching Boost's
83///   `default_envelope_result<Geometry>::type`
84///   (`strategies/default_envelope_result.hpp`).
85pub trait EnvelopeStrategy<G> {
86    /// The output box type. Mirrors
87    /// `boost::geometry::default_envelope_result<G>::type` from
88    /// `strategies/default_envelope_result.hpp`.
89    type Output;
90
91    /// Compute the axis-aligned bounding box of `g`.
92    ///
93    /// Mirrors `boost::geometry::dispatch::envelope<G, Tag>::apply`
94    /// from `algorithms/detail/envelope/interface.hpp`, with the
95    /// `Box` returned by value instead of mutated by reference.
96    fn envelope(&self, g: &G) -> Self::Output;
97}
98
99/// Cartesian envelope, per geometry kind: component-wise min / max across
100/// every coordinate of every point in the geometry.
101///
102/// Mirrors the per-tag sub-strategies of
103/// `boost::geometry::strategies::envelope::cartesian<>`
104/// (`strategies/envelope/cartesian.hpp`). Each struct is a stateless ZST
105/// carrying exactly one concept-bounded [`EnvelopeStrategy`] impl, so
106/// distinct kinds never overlap; the [`EnvelopeStrategyForKind`] picker
107/// routes a tag to its struct.
108#[derive(Debug, Default, Clone, Copy)]
109pub struct EnvelopePoint;
110/// Cartesian envelope of a [`geometry_trait::Linestring`]. See
111/// [`EnvelopePoint`].
112#[derive(Debug, Default, Clone, Copy)]
113pub struct EnvelopeLinestring;
114/// Cartesian envelope of a [`geometry_trait::Ring`]. See [`EnvelopePoint`].
115#[derive(Debug, Default, Clone, Copy)]
116pub struct EnvelopeRing;
117/// Cartesian envelope of a [`geometry_trait::Polygon`]. See
118/// [`EnvelopePoint`].
119#[derive(Debug, Default, Clone, Copy)]
120pub struct EnvelopePolygon;
121/// Cartesian envelope of a [`geometry_trait::Segment`]. See
122/// [`EnvelopePoint`].
123#[derive(Debug, Default, Clone, Copy)]
124pub struct EnvelopeSegment;
125/// Cartesian envelope of a [`geometry_trait::Box`]. See [`EnvelopePoint`].
126#[derive(Debug, Default, Clone, Copy)]
127pub struct EnvelopeBox;
128/// Cartesian envelope of a [`geometry_trait::MultiPoint`]. See
129/// [`EnvelopePoint`].
130#[derive(Debug, Default, Clone, Copy)]
131pub struct EnvelopeMultiPoint;
132/// Cartesian envelope of a [`geometry_trait::MultiLinestring`]. See
133/// [`EnvelopePoint`].
134#[derive(Debug, Default, Clone, Copy)]
135pub struct EnvelopeMultiLinestring;
136/// Cartesian envelope of a [`geometry_trait::MultiPolygon`]. See
137/// [`EnvelopePoint`].
138#[derive(Debug, Default, Clone, Copy)]
139pub struct EnvelopeMultiPolygon;
140
141// ---- Helpers ---------------------------------------------------------
142//
143// The shared shape of the per-tag dispatch is "seed the box from the
144// first point, then grow it by each remaining point". `seed_from_point`
145// writes both corners equal to `p`; `grow_from_point` widens each
146// corner per-dimension by `min` / `max` against `p`. Both helpers are
147// written against [`fold_dims`] so the per-dimension recursion is
148// shared with Pythagoras and the indexed-access materialiser.
149
150/// Initialise `out` so both its corners equal `p`.
151///
152/// Mirrors `detail::envelope::initialize<dimension_one, …>::apply` at
153/// `algorithms/detail/envelope/initialize.hpp:36-51` — the per-tag
154/// "envelope of a single point" base case.
155#[inline]
156pub fn seed_from_point<P>(out: &mut ModelBox<P>, p: &P)
157where
158    P: PointMut,
159{
160    fold_dims((), p, |(), p, d| {
161        // `fold_dims` hands us `d` as a `usize`; the four arms map to
162        // the const-generic `D` that `Point::get` / `Box::set_indexed`
163        // require. Dimensions past `MAX_DIM` are rejected by
164        // `fold_dims` itself.
165        match d {
166            0 => write_both::<P, 0>(out, p),
167            1 => write_both::<P, 1>(out, p),
168            2 => write_both::<P, 2>(out, p),
169            3 => write_both::<P, 3>(out, p),
170            _ => unreachable!("fold_dims: dimension out of MAX_DIM range"),
171        }
172    });
173}
174
175#[inline]
176fn write_both<P, const D: usize>(out: &mut ModelBox<P>, p: &P)
177where
178    P: PointMut,
179{
180    let v = p.get::<D>();
181    out.set_indexed::<0, D>(v);
182    out.set_indexed::<1, D>(v);
183}
184
185/// Widen `out` per-dimension by `p` — `min` into the min corner,
186/// `max` into the max corner.
187///
188/// Mirrors `detail::expand::point::apply` at
189/// `algorithms/detail/expand/point.hpp:34-55`, the per-point growth
190/// step used by every range envelope.
191#[inline]
192pub fn grow_from_point<P>(out: &mut ModelBox<P>, p: &P)
193where
194    P: PointMut,
195{
196    fold_dims((), p, |(), p, d| match d {
197        0 => grow_one::<P, 0>(out, p),
198        1 => grow_one::<P, 1>(out, p),
199        2 => grow_one::<P, 2>(out, p),
200        3 => grow_one::<P, 3>(out, p),
201        _ => unreachable!("fold_dims: dimension out of MAX_DIM range"),
202    });
203}
204
205#[inline]
206fn grow_one<P, const D: usize>(out: &mut ModelBox<P>, p: &P)
207where
208    P: PointMut,
209{
210    let v = p.get::<D>();
211    let lo = out.get_indexed::<0, D>();
212    let hi = out.get_indexed::<1, D>();
213    // `PartialOrd` is the only ordering bound `CoordinateScalar`
214    // guarantees, so we compare with `<` rather than calling
215    // `Ord::min` / `Ord::max` — the latter would force a total order
216    // we deliberately do not require (NaN coordinates remain caller
217    // error, exactly as on the Boost side).
218    if v < lo {
219        out.set_indexed::<0, D>(v);
220    }
221    if v > hi {
222        out.set_indexed::<1, D>(v);
223    }
224}
225
226/// Walk an iterator of points seeding the box from the first and
227/// growing by the rest.
228///
229/// For a **non-empty** range the box is seeded from the first real point
230/// (not from zero), so all-negative coordinates envelope correctly. For
231/// an **empty** range it returns `Box::default()` = `((0,0),(0,0))` — a
232/// degenerate box at the origin. Note this differs from Boost, which
233/// leaves an empty envelope *inverted* (`min = +∞`, `max = −∞`) so
234/// emptiness is detectable (`algorithms/detail/envelope/initialize.hpp`);
235/// callers that must distinguish "empty" from "a point at the origin"
236/// should check the source geometry for emptiness first. Representing an
237/// inverted box needs an infinity sentinel the v1 `Box` does not carry;
238/// deferred.
239#[inline]
240pub fn envelope_of_points<'a, P, I>(it: I) -> ModelBox<P>
241where
242    P: PointMut + Default + 'a,
243    P::Scalar: CoordinateScalar,
244    I: IntoIterator<Item = &'a P>,
245{
246    let mut out = ModelBox::<P>::default();
247    let mut it = it.into_iter();
248    if let Some(first) = it.next() {
249        seed_from_point(&mut out, first);
250        for p in it {
251            grow_from_point(&mut out, p);
252        }
253    }
254    out
255}
256
257// ---- Per-kind concept-bounded impls ---------------------------------
258//
259// Each impl below carries a single open concept bound (`G: Point`,
260// `G: Linestring`, …) on a distinct struct, so coherence stays trivial —
261// distinct `Self` types never overlap. The shape mirrors the Boost
262// per-tag dispatch in `algorithms/detail/envelope/`:
263//
264// * `point.hpp:33-58`             → `EnvelopePoint`
265// * `range.hpp:46-66`             → `EnvelopeLinestring` / `EnvelopeRing`
266// * `areal.hpp` (polygon arm)     → `EnvelopePolygon`
267// * `segment.hpp`                 → `EnvelopeSegment`
268// * `box.hpp`                     → `EnvelopeBox`
269// * `multipoint.hpp`              → `EnvelopeMultiPoint`
270// * `multilinestring.hpp`         → `EnvelopeMultiLinestring`
271// * `multipolygon.hpp`            → `EnvelopeMultiPolygon`
272//
273// The output box is always `ModelBox<G::Point>` — Boost's
274// `default_envelope_result<G>::type`.
275
276// ---- Point ----------------------------------------------------------
277
278impl<G> EnvelopeStrategy<G> for EnvelopePoint
279where
280    G: PointTrait + PointMut + Default,
281    <G::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
282{
283    type Output = ModelBox<G>;
284
285    #[inline]
286    fn envelope(&self, g: &G) -> Self::Output {
287        let mut out = ModelBox::<G>::default();
288        seed_from_point(&mut out, g);
289        out
290    }
291}
292
293// ---- Linestring -----------------------------------------------------
294
295impl<G> EnvelopeStrategy<G> for EnvelopeLinestring
296where
297    G: LinestringTrait,
298    G::Point: PointMut + Default,
299    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
300{
301    type Output = ModelBox<G::Point>;
302
303    #[inline]
304    fn envelope(&self, g: &G) -> Self::Output {
305        envelope_of_points::<G::Point, _>(g.points())
306    }
307}
308
309// ---- Ring -----------------------------------------------------------
310//
311// The four `(ClockWise, Closed)` combinations all reduce to "walk every
312// vertex, take componentwise extremes" — the closing edge does not
313// add new extremes, and orientation doesn't affect bounds. The C++
314// test `envelope.cpp:48-51` exercises exactly this insensitivity by
315// running the same `(4 1)(0 7)(7 9)` triangle in all four
316// combinations and expecting the same box.
317
318impl<G> EnvelopeStrategy<G> for EnvelopeRing
319where
320    G: RingTrait,
321    G::Point: PointMut + Default,
322    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
323{
324    type Output = ModelBox<G::Point>;
325
326    #[inline]
327    fn envelope(&self, g: &G) -> Self::Output {
328        envelope_of_points::<G::Point, _>(g.points())
329    }
330}
331
332// ---- Polygon --------------------------------------------------------
333//
334// Only the exterior ring contributes — interior rings live strictly
335// inside, so they can only ever narrow the box, never widen it.
336
337impl<G> EnvelopeStrategy<G> for EnvelopePolygon
338where
339    G: PolygonTrait,
340    G::Point: PointMut + Default,
341    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
342{
343    type Output = ModelBox<G::Point>;
344
345    #[inline]
346    fn envelope(&self, g: &G) -> Self::Output {
347        envelope_of_points::<G::Point, _>(g.exterior().points())
348    }
349}
350
351// ---- Segment --------------------------------------------------------
352//
353// Cartesian segment envelope is per-coordinate min/max of the two
354// endpoints — no great-circle handling, unlike the spherical /
355// geographic segment strategies.
356
357impl<G> EnvelopeStrategy<G> for EnvelopeSegment
358where
359    G: SegmentTrait,
360    G::Point: PointMut + Default,
361    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
362{
363    type Output = ModelBox<G::Point>;
364
365    #[inline]
366    fn envelope(&self, g: &G) -> Self::Output {
367        let mut out = ModelBox::<G::Point>::default();
368        seed_from_point(&mut out, &geometry_trait::segment_start(g));
369        grow_from_point(&mut out, &geometry_trait::segment_end(g));
370        out
371    }
372}
373
374// ---- Box ------------------------------------------------------------
375//
376// Envelope of a box is the box itself — produced by a fresh
377// allocation (rather than `Clone`) so the Cartesian path stays
378// uniform with the other geometry kinds, where the output is always a
379// freshly seeded box.
380
381impl<G> EnvelopeStrategy<G> for EnvelopeBox
382where
383    G: BoxTrait,
384    G::Point: PointMut + Default,
385    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
386{
387    type Output = ModelBox<G::Point>;
388
389    #[inline]
390    fn envelope(&self, g: &G) -> Self::Output {
391        let mut out = ModelBox::<G::Point>::default();
392        seed_from_point(&mut out, &geometry_trait::box_min(g));
393        grow_from_point(&mut out, &geometry_trait::box_max(g));
394        out
395    }
396}
397
398// ---- MultiPoint -----------------------------------------------------
399
400impl<G> EnvelopeStrategy<G> for EnvelopeMultiPoint
401where
402    G: MultiPointTrait,
403    G::ItemPoint: PointMut + Default,
404    <<G::ItemPoint as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
405{
406    type Output = ModelBox<G::ItemPoint>;
407
408    #[inline]
409    fn envelope(&self, g: &G) -> Self::Output {
410        envelope_of_points::<G::ItemPoint, _>(g.points())
411    }
412}
413
414// ---- MultiLinestring ------------------------------------------------
415//
416// The member concept is open (any `Linestring` whose point matches),
417// widening past the pinned element type of the earlier model-bound impl —
418// the intended BYO widening. Body walks every member point unchanged.
419
420impl<G> EnvelopeStrategy<G> for EnvelopeMultiLinestring
421where
422    G: MultiLinestringTrait,
423    G::Point: PointMut + Default,
424    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
425{
426    type Output = ModelBox<G::Point>;
427
428    #[inline]
429    fn envelope(&self, g: &G) -> Self::Output {
430        let mut out = ModelBox::<G::Point>::default();
431        let mut seeded = false;
432        for ls in g.linestrings() {
433            for p in ls.points() {
434                if seeded {
435                    grow_from_point(&mut out, p);
436                } else {
437                    seed_from_point(&mut out, p);
438                    seeded = true;
439                }
440            }
441        }
442        out
443    }
444}
445
446// ---- MultiPolygon ---------------------------------------------------
447//
448// The member concept is open (any `Polygon` whose point matches) — the
449// same BYO widening as `EnvelopeMultiLinestring`.
450
451impl<G> EnvelopeStrategy<G> for EnvelopeMultiPolygon
452where
453    G: MultiPolygonTrait,
454    G::Point: PointMut + Default,
455    <<G::Point as PointTrait>::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
456{
457    type Output = ModelBox<G::Point>;
458
459    #[inline]
460    fn envelope(&self, g: &G) -> Self::Output {
461        let mut out = ModelBox::<G::Point>::default();
462        let mut seeded = false;
463        for poly in g.polygons() {
464            for p in poly.exterior().points() {
465                if seeded {
466                    grow_from_point(&mut out, p);
467                } else {
468                    seed_from_point(&mut out, p);
469                    seeded = true;
470                }
471            }
472        }
473        out
474    }
475}
476
477/// Type-level "which `EnvelopeStrategy` struct does this geometry *kind*
478/// use". One impl per [`geometry_tag`] kind tag, keyed on the tag (never a
479/// concept blanket — that would overlap, E0119), so any concept-adapted
480/// foreign type resolves to the same struct as the equivalent model value.
481/// The [`crate::envelope`] free function routes `G → G::Kind → S` through
482/// this trait.
483#[doc(hidden)]
484pub trait EnvelopeStrategyForKind {
485    /// The per-kind [`EnvelopeStrategy`] struct this tag is computed with.
486    type S: Default;
487}
488
489impl EnvelopeStrategyForKind for PointTag {
490    type S = EnvelopePoint;
491}
492impl EnvelopeStrategyForKind for LinestringTag {
493    type S = EnvelopeLinestring;
494}
495impl EnvelopeStrategyForKind for RingTag {
496    type S = EnvelopeRing;
497}
498impl EnvelopeStrategyForKind for PolygonTag {
499    type S = EnvelopePolygon;
500}
501impl EnvelopeStrategyForKind for SegmentTag {
502    type S = EnvelopeSegment;
503}
504impl EnvelopeStrategyForKind for BoxTag {
505    type S = EnvelopeBox;
506}
507impl EnvelopeStrategyForKind for MultiPointTag {
508    type S = EnvelopeMultiPoint;
509}
510impl EnvelopeStrategyForKind for MultiLinestringTag {
511    type S = EnvelopeMultiLinestring;
512}
513impl EnvelopeStrategyForKind for MultiPolygonTag {
514    type S = EnvelopeMultiPolygon;
515}
516
517#[cfg(test)]
518mod tests {
519    //! Reference values come from
520    //! `geometry/test/algorithms/envelope_expand/envelope.cpp:38-54`
521    //! (the `test_2d` arm). Each test cites the line it mirrors.
522
523    use super::{
524        EnvelopeBox, EnvelopeLinestring, EnvelopePoint, EnvelopePolygon, EnvelopeSegment,
525        EnvelopeStrategy,
526    };
527    use geometry_cs::Cartesian;
528    use geometry_model::{Box, Linestring, Point2D, Polygon, Segment, linestring, polygon};
529    use geometry_trait::IndexedAccess as _;
530
531    type P = Point2D<f64, Cartesian>;
532
533    fn assert_2d(b: &Box<P>, xmin: f64, xmax: f64, ymin: f64, ymax: f64) {
534        assert_eq!(b.get_indexed::<0, 0>().to_bits(), xmin.to_bits());
535        assert_eq!(b.get_indexed::<0, 1>().to_bits(), ymin.to_bits());
536        assert_eq!(b.get_indexed::<1, 0>().to_bits(), xmax.to_bits());
537        assert_eq!(b.get_indexed::<1, 1>().to_bits(), ymax.to_bits());
538    }
539
540    /// `envelope.cpp:38` — `POINT(1 1)` → `(1,1) (1,1)`.
541    #[test]
542    fn point_envelope_collapses() {
543        let p = Point2D::<f64, Cartesian>::new(1.0, 1.0);
544        assert_2d(&EnvelopePoint.envelope(&p), 1.0, 1.0, 1.0, 1.0);
545    }
546
547    /// `envelope.cpp:39` — `LINESTRING(1 1,2 2)` → `(1,1) (2,2)`.
548    #[test]
549    fn linestring_two_points() {
550        let ls: Linestring<P> = linestring![(1.0, 1.0), (2.0, 2.0)];
551        assert_2d(&EnvelopeLinestring.envelope(&ls), 1.0, 2.0, 1.0, 2.0);
552    }
553
554    /// `envelope.cpp:40` — square polygon, envelope = the polygon.
555    #[test]
556    fn polygon_axis_aligned_square() {
557        let p: Polygon<P> = polygon![[(1.0, 1.0), (1.0, 3.0), (3.0, 3.0), (3.0, 1.0), (1.0, 1.0),]];
558        assert_2d(&EnvelopePolygon.envelope(&p), 1.0, 3.0, 1.0, 3.0);
559    }
560
561    /// `envelope.cpp:43` — `BOX(1 1,3 3)` — envelope is the box.
562    #[test]
563    fn box_envelope_is_self() {
564        let b = Box::from_corners(
565            Point2D::<f64, Cartesian>::new(1.0, 1.0),
566            Point2D::<f64, Cartesian>::new(3.0, 3.0),
567        );
568        assert_2d(&EnvelopeBox.envelope(&b), 1.0, 3.0, 1.0, 3.0);
569    }
570
571    /// `envelope.cpp:48` — non-convex closed CW ring; envelope
572    /// tightens to the extremes `(0,1)-(7,9)`.
573    #[test]
574    fn ring_non_convex() {
575        let p: Polygon<P> = polygon![[(4.0, 1.0), (0.0, 7.0), (7.0, 9.0), (4.0, 1.0)]];
576        assert_2d(&EnvelopePolygon.envelope(&p), 0.0, 7.0, 1.0, 9.0);
577    }
578
579    /// `envelope.cpp:54` — `SEGMENT(1 1,3 3)`.
580    #[test]
581    fn segment_envelope() {
582        let s = Segment::new(
583            Point2D::<f64, Cartesian>::new(1.0, 1.0),
584            Point2D::<f64, Cartesian>::new(3.0, 3.0),
585        );
586        assert_2d(&EnvelopeSegment.envelope(&s), 1.0, 3.0, 1.0, 3.0);
587    }
588
589    /// Segment with crossed coordinates — the start corner has to
590    /// be `(min_x, min_y)`, not "the first endpoint".
591    #[test]
592    fn segment_envelope_crossed() {
593        let s = Segment::new(
594            Point2D::<f64, Cartesian>::new(3.0, 1.0),
595            Point2D::<f64, Cartesian>::new(1.0, 3.0),
596        );
597        assert_2d(&EnvelopeSegment.envelope(&s), 1.0, 3.0, 1.0, 3.0);
598    }
599}