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