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}