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}