1use core::cmp::Ordering;
14
15use geometry_coords::{CoordinateScalar, Rational, RationalInteger};
16use geometry_cs::{
17 AngleUnit, CartesianFamily, CoordinateSystem, Geographic, GeographicFamily, Spherical,
18 SphericalFamily,
19};
20use geometry_tag::SameAs;
21use geometry_trait::Point;
22
23pub const ALL_DIMENSIONS: i8 = -1;
25
26#[derive(Debug, Default, Clone, Copy)]
30pub struct Less<const DIMENSION: i8 = ALL_DIMENSIONS>;
31
32#[derive(Debug, Default, Clone, Copy)]
37pub struct LessExact<const DIMENSION: i8 = ALL_DIMENSIONS>;
38
39#[derive(Debug, Default, Clone, Copy)]
44pub struct Greater<const DIMENSION: i8 = ALL_DIMENSIONS>;
45
46#[derive(Debug, Default, Clone, Copy)]
51pub struct EqualTo<const DIMENSION: i8 = ALL_DIMENSIONS>;
52
53impl<const DIMENSION: i8> Less<DIMENSION> {
54 #[inline]
61 #[must_use]
62 #[allow(
63 clippy::unused_self,
64 reason = "value-method policy objects are directly usable as sorting functors"
65 )]
66 pub fn apply<P1, P2>(self, left: &P1, right: &P2) -> bool
67 where
68 P1: Point,
69 P2: Point,
70 <P1::Cs as CoordinateSystem>::Family: ComparisonFamily<P1, P2>,
71 {
72 <<P1::Cs as CoordinateSystem>::Family as ComparisonFamily<P1, P2>>::less(
73 left, right, DIMENSION, false,
74 )
75 }
76}
77
78impl<const DIMENSION: i8> LessExact<DIMENSION> {
79 #[inline]
86 #[must_use]
87 #[allow(
88 clippy::unused_self,
89 reason = "value-method policy objects are directly usable as sorting functors"
90 )]
91 pub fn apply<P1, P2>(self, left: &P1, right: &P2) -> bool
92 where
93 P1: Point,
94 P2: Point,
95 <P1::Cs as CoordinateSystem>::Family: ComparisonFamily<P1, P2>,
96 {
97 <<P1::Cs as CoordinateSystem>::Family as ComparisonFamily<P1, P2>>::less(
98 left, right, DIMENSION, true,
99 )
100 }
101}
102
103impl<const DIMENSION: i8> Greater<DIMENSION> {
104 #[inline]
111 #[must_use]
112 #[allow(
113 clippy::unused_self,
114 reason = "value-method policy objects are directly usable as sorting functors"
115 )]
116 pub fn apply<P1, P2>(self, left: &P1, right: &P2) -> bool
117 where
118 P1: Point,
119 P2: Point,
120 <P1::Cs as CoordinateSystem>::Family: ComparisonFamily<P1, P2>,
121 {
122 <<P1::Cs as CoordinateSystem>::Family as ComparisonFamily<P1, P2>>::greater(
123 left, right, DIMENSION,
124 )
125 }
126}
127
128impl<const DIMENSION: i8> EqualTo<DIMENSION> {
129 #[inline]
136 #[must_use]
137 #[allow(
138 clippy::unused_self,
139 reason = "value-method policy objects are directly usable as sorting functors"
140 )]
141 pub fn apply<P1, P2>(self, left: &P1, right: &P2) -> bool
142 where
143 P1: Point,
144 P2: Point,
145 <P1::Cs as CoordinateSystem>::Family: ComparisonFamily<P1, P2>,
146 {
147 <<P1::Cs as CoordinateSystem>::Family as ComparisonFamily<P1, P2>>::equal(
148 left, right, DIMENSION,
149 )
150 }
151}
152
153#[doc(hidden)]
158pub trait ComparisonFamily<P1: Point, P2: Point>: sealed::ComparisonFamily {
159 #[doc(hidden)]
160 fn less(left: &P1, right: &P2, dimension: i8, exact: bool) -> bool;
161
162 #[doc(hidden)]
163 fn greater(left: &P1, right: &P2, dimension: i8) -> bool;
164
165 #[doc(hidden)]
166 fn equal(left: &P1, right: &P2, dimension: i8) -> bool;
167}
168
169mod sealed {
170 pub trait ComparisonFamily {}
171
172 impl ComparisonFamily for geometry_cs::CartesianFamily {}
173 impl ComparisonFamily for geometry_cs::SphericalFamily {}
174 impl ComparisonFamily for geometry_cs::GeographicFamily {}
175}
176
177impl<P1, P2> ComparisonFamily<P1, P2> for CartesianFamily
178where
179 P1: Point,
180 P2: Point,
181 <P1::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
182 <P2::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
183 P1::Scalar: CoordinateComparison<P2::Scalar>,
184{
185 #[inline]
186 fn less(left: &P1, right: &P2, dimension: i8, exact: bool) -> bool {
187 cartesian_compare(left, right, dimension, Relation::Less, exact)
188 }
189
190 #[inline]
191 fn greater(left: &P1, right: &P2, dimension: i8) -> bool {
192 cartesian_compare(left, right, dimension, Relation::Greater, false)
193 }
194
195 #[inline]
196 fn equal(left: &P1, right: &P2, dimension: i8) -> bool {
197 cartesian_compare(left, right, dimension, Relation::Equal, false)
198 }
199}
200
201impl<P1, P2> ComparisonFamily<P1, P2> for SphericalFamily
202where
203 P1: Point,
204 P2: Point,
205 P1::Cs: AngularCoordinateSystem,
206 P2::Cs: AngularCoordinateSystem,
207 <P1::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
208 <P2::Cs as CoordinateSystem>::Family: SameAs<SphericalFamily>,
209 P1::Scalar: AngularScalar + CoordinateComparison<P2::Scalar>,
210 P2::Scalar: AngularScalar,
211{
212 #[inline]
213 fn less(left: &P1, right: &P2, dimension: i8, exact: bool) -> bool {
214 angular_compare(left, right, dimension, Relation::Less, exact)
215 }
216
217 #[inline]
218 fn greater(left: &P1, right: &P2, dimension: i8) -> bool {
219 angular_compare(left, right, dimension, Relation::Greater, false)
220 }
221
222 #[inline]
223 fn equal(left: &P1, right: &P2, dimension: i8) -> bool {
224 angular_compare(left, right, dimension, Relation::Equal, false)
225 }
226}
227
228impl<P1, P2> ComparisonFamily<P1, P2> for GeographicFamily
229where
230 P1: Point,
231 P2: Point,
232 P1::Cs: AngularCoordinateSystem,
233 P2::Cs: AngularCoordinateSystem,
234 <P1::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
235 <P2::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
236 P1::Scalar: AngularScalar + CoordinateComparison<P2::Scalar>,
237 P2::Scalar: AngularScalar,
238{
239 #[inline]
240 fn less(left: &P1, right: &P2, dimension: i8, exact: bool) -> bool {
241 angular_compare(left, right, dimension, Relation::Less, exact)
242 }
243
244 #[inline]
245 fn greater(left: &P1, right: &P2, dimension: i8) -> bool {
246 angular_compare(left, right, dimension, Relation::Greater, false)
247 }
248
249 #[inline]
250 fn equal(left: &P1, right: &P2, dimension: i8) -> bool {
251 angular_compare(left, right, dimension, Relation::Equal, false)
252 }
253}
254
255#[derive(Clone, Copy)]
256enum Relation {
257 Less,
258 Greater,
259 Equal,
260}
261
262fn cartesian_compare<P1, P2>(
263 left: &P1,
264 right: &P2,
265 dimension: i8,
266 relation: Relation,
267 exact: bool,
268) -> bool
269where
270 P1: Point,
271 P2: Point,
272 P1::Scalar: CoordinateComparison<P2::Scalar>,
273{
274 let shared_dimensions = P1::DIM.min(P2::DIM);
275 validate_dimension(dimension, shared_dimensions);
276
277 if dimension == ALL_DIMENSIONS {
278 for index in 0..shared_dimensions {
279 let result = compare_ordinate(left, right, index, relation, exact);
280 if let Some(result) = result {
281 return result;
282 }
283 }
284 matches!(relation, Relation::Equal)
285 } else {
286 compare_ordinate(
287 left,
288 right,
289 usize::from(dimension.unsigned_abs()),
290 relation,
291 exact,
292 )
293 .unwrap_or(matches!(relation, Relation::Equal))
294 }
295}
296
297fn angular_compare<P1, P2>(
298 left: &P1,
299 right: &P2,
300 dimension: i8,
301 relation: Relation,
302 exact: bool,
303) -> bool
304where
305 P1: Point,
306 P2: Point,
307 P1::Cs: AngularCoordinateSystem,
308 P2::Cs: AngularCoordinateSystem,
309 P1::Scalar: AngularScalar + CoordinateComparison<P2::Scalar>,
310 P2::Scalar: AngularScalar,
311{
312 let shared_dimensions = P1::DIM.min(P2::DIM);
313 validate_dimension(dimension, shared_dimensions);
314
315 if dimension >= 2 {
316 return compare_ordinate(
317 left,
318 right,
319 usize::from(dimension.unsigned_abs()),
320 relation,
321 exact,
322 )
323 .unwrap_or(matches!(relation, Relation::Equal));
324 }
325
326 if dimension == 1 {
327 return angular_latitude(left, right, relation, exact);
328 }
329
330 let longitude = angular_longitude(left, right, relation, exact);
331 if let Some(result) = longitude {
332 return result;
333 }
334 if dimension == 0 {
335 return matches!(relation, Relation::Equal);
336 }
337
338 let latitude = angular_latitude_result(left, right, relation, exact);
339 if let Some(result) = latitude {
340 return result;
341 }
342 for index in 2..shared_dimensions {
343 if let Some(result) = compare_ordinate(left, right, index, relation, exact) {
344 return result;
345 }
346 }
347 matches!(relation, Relation::Equal)
348}
349
350fn angular_longitude<P1, P2>(left: &P1, right: &P2, relation: Relation, exact: bool) -> Option<bool>
351where
352 P1: Point,
353 P2: Point,
354 P1::Cs: AngularCoordinateSystem,
355 P2::Cs: AngularCoordinateSystem,
356 P1::Scalar: AngularScalar,
357 P2::Scalar: AngularScalar,
358{
359 let left_longitude = P1::Cs::to_radians(left.get::<0>());
360 let right_longitude = P2::Cs::to_radians(right.get::<0>());
361 let epsilon = angular_epsilon::<P1::Scalar, P2::Scalar>();
362 let coordinates_equal = values_equal(left_longitude, right_longitude, epsilon, exact);
363 let left_antimeridian = is_antimeridian(left_longitude, epsilon, exact);
364 let right_antimeridian = is_antimeridian(right_longitude, epsilon, exact);
365
366 let left_latitude = P1::Cs::to_radians(left.get::<1>());
367 let right_latitude = P2::Cs::to_radians(right.get::<1>());
368 let same_latitude = values_equal(left_latitude, right_latitude, epsilon, exact);
369 let shared_pole = same_latitude && is_pole(left_latitude, epsilon, exact);
370
371 if coordinates_equal || (left_antimeridian && right_antimeridian) || shared_pole {
372 None
373 } else if left_antimeridian {
374 Some(apply_ordering(Ordering::Greater, relation))
375 } else if right_antimeridian {
376 Some(apply_ordering(Ordering::Less, relation))
377 } else {
378 Some(apply_partial_order(
379 left_longitude.partial_cmp(&right_longitude),
380 relation,
381 ))
382 }
383}
384
385fn angular_latitude<P1, P2>(left: &P1, right: &P2, relation: Relation, exact: bool) -> bool
386where
387 P1: Point,
388 P2: Point,
389 P1::Cs: AngularCoordinateSystem,
390 P2::Cs: AngularCoordinateSystem,
391 P1::Scalar: AngularScalar,
392 P2::Scalar: AngularScalar,
393{
394 angular_latitude_result(left, right, relation, exact)
395 .unwrap_or(matches!(relation, Relation::Equal))
396}
397
398fn angular_latitude_result<P1, P2>(
399 left: &P1,
400 right: &P2,
401 relation: Relation,
402 exact: bool,
403) -> Option<bool>
404where
405 P1: Point,
406 P2: Point,
407 P1::Cs: AngularCoordinateSystem,
408 P2::Cs: AngularCoordinateSystem,
409 P1::Scalar: AngularScalar,
410 P2::Scalar: AngularScalar,
411{
412 let left_latitude = P1::Cs::to_radians(left.get::<1>());
413 let right_latitude = P2::Cs::to_radians(right.get::<1>());
414 let epsilon = angular_epsilon::<P1::Scalar, P2::Scalar>();
415 if values_equal(left_latitude, right_latitude, epsilon, exact) {
416 None
417 } else {
418 Some(apply_partial_order(
419 left_latitude.partial_cmp(&right_latitude),
420 relation,
421 ))
422 }
423}
424
425fn compare_ordinate<P1, P2>(
426 left: &P1,
427 right: &P2,
428 dimension: usize,
429 relation: Relation,
430 exact: bool,
431) -> Option<bool>
432where
433 P1: Point,
434 P2: Point,
435 P1::Scalar: CoordinateComparison<P2::Scalar>,
436{
437 let (ordering, epsilon_equal) = match dimension {
438 0 => left.get::<0>().compare_coordinate(right.get::<0>()),
439 1 => left.get::<1>().compare_coordinate(right.get::<1>()),
440 2 => left.get::<2>().compare_coordinate(right.get::<2>()),
441 3 => left.get::<3>().compare_coordinate(right.get::<3>()),
442 _ => unreachable!("Point dimensions above four are not supported"),
443 };
444 let equal = if exact {
445 ordering == Some(Ordering::Equal)
446 } else {
447 epsilon_equal
448 };
449 if equal {
450 None
451 } else {
452 Some(apply_partial_order(ordering, relation))
453 }
454}
455
456fn apply_partial_order(ordering: Option<Ordering>, relation: Relation) -> bool {
457 ordering.is_some_and(|ordering| apply_ordering(ordering, relation))
458}
459
460fn apply_ordering(ordering: Ordering, relation: Relation) -> bool {
461 match relation {
462 Relation::Less => ordering == Ordering::Less,
463 Relation::Greater => ordering == Ordering::Greater,
464 Relation::Equal => false,
465 }
466}
467
468fn validate_dimension(dimension: i8, shared_dimensions: usize) {
469 assert!(
470 shared_dimensions <= 4,
471 "Point dimensions above four are not supported"
472 );
473 assert!(
474 dimension == ALL_DIMENSIONS
475 || (dimension >= 0 && (dimension.unsigned_abs() as usize) < shared_dimensions),
476 "comparison dimension must be present in both points"
477 );
478}
479
480#[allow(clippy::float_cmp, reason = "exact comparison is a selectable policy")]
481fn values_equal(left: f64, right: f64, epsilon: f64, exact: bool) -> bool {
482 if exact {
483 left == right
484 } else {
485 scaled_equal(left, right, epsilon)
486 }
487}
488
489#[allow(
490 clippy::float_cmp,
491 reason = "exact equality is the required fast path before epsilon scaling"
492)]
493fn scaled_equal(left: f64, right: f64, epsilon: f64) -> bool {
494 left == right
495 || (left.is_finite()
496 && right.is_finite()
497 && (left - right).abs() <= epsilon * left.abs().max(right.abs()).max(1.0))
498}
499
500fn is_antimeridian(value: f64, epsilon: f64, exact: bool) -> bool {
501 values_equal(value.abs(), core::f64::consts::PI, epsilon, exact)
502}
503
504fn is_pole(value: f64, epsilon: f64, exact: bool) -> bool {
505 values_equal(value.abs(), core::f64::consts::FRAC_PI_2, epsilon, exact)
506}
507
508fn angular_epsilon<L: AngularScalar, R: AngularScalar>() -> f64 {
509 match (L::EPSILON, R::EPSILON) {
510 (0.0, right) => right,
511 (left, 0.0) => left,
512 (left, right) => left.min(right),
513 }
514}
515
516#[doc(hidden)]
519pub trait AngularScalar: CoordinateScalar {
520 #[doc(hidden)]
521 const EPSILON: f64;
522
523 #[doc(hidden)]
524 fn to_f64(self) -> f64;
525}
526
527impl AngularScalar for f32 {
528 const EPSILON: f64 = f32::EPSILON as f64;
529
530 #[inline]
531 fn to_f64(self) -> f64 {
532 f64::from(self)
533 }
534}
535
536impl AngularScalar for f64 {
537 const EPSILON: f64 = f64::EPSILON;
538
539 #[inline]
540 fn to_f64(self) -> f64 {
541 self
542 }
543}
544
545impl AngularScalar for i32 {
546 const EPSILON: f64 = 0.0;
547
548 #[inline]
549 fn to_f64(self) -> f64 {
550 f64::from(self)
551 }
552}
553
554impl AngularScalar for i64 {
555 const EPSILON: f64 = 0.0;
556
557 #[inline]
558 #[allow(
559 clippy::cast_precision_loss,
560 reason = "angular normalization uses f64 just like Boost calculation promotion"
561 )]
562 fn to_f64(self) -> f64 {
563 self as f64
564 }
565}
566
567impl<I: RationalInteger> AngularScalar for Rational<I> {
568 const EPSILON: f64 = 0.0;
569
570 #[inline]
571 fn to_f64(self) -> f64 {
572 self.to_f64()
573 }
574}
575
576#[doc(hidden)]
578pub trait AngularCoordinateSystem: CoordinateSystem {
579 #[doc(hidden)]
580 fn to_radians<T: AngularScalar>(value: T) -> f64;
581}
582
583impl<U: AngleUnit> AngularCoordinateSystem for Spherical<U> {
584 #[inline]
585 fn to_radians<T: AngularScalar>(value: T) -> f64 {
586 U::to_radians(value.to_f64())
587 }
588}
589
590impl<U: AngleUnit> AngularCoordinateSystem for Geographic<U> {
591 #[inline]
592 fn to_radians<T: AngularScalar>(value: T) -> f64 {
593 U::to_radians(value.to_f64())
594 }
595}
596
597#[doc(hidden)]
599pub trait CoordinateComparison<Rhs: CoordinateScalar>: CoordinateScalar {
600 #[doc(hidden)]
601 fn compare_coordinate(self, right: Rhs) -> (Option<Ordering>, bool);
602}
603
604macro_rules! impl_same_float_comparison {
605 ($type:ty) => {
606 impl CoordinateComparison<$type> for $type {
607 #[inline]
608 fn compare_coordinate(self, right: $type) -> (Option<Ordering>, bool) {
609 (
610 self.partial_cmp(&right),
611 scaled_equal(
612 f64::from(self),
613 f64::from(right),
614 f64::from(<$type>::EPSILON),
615 ),
616 )
617 }
618 }
619 };
620}
621
622impl_same_float_comparison!(f32);
623impl_same_float_comparison!(f64);
624
625macro_rules! impl_same_integer_comparison {
626 ($type:ty) => {
627 impl CoordinateComparison<$type> for $type {
628 #[inline]
629 fn compare_coordinate(self, right: $type) -> (Option<Ordering>, bool) {
630 (Some(self.cmp(&right)), self == right)
631 }
632 }
633 };
634}
635
636impl_same_integer_comparison!(i32);
637impl_same_integer_comparison!(i64);
638
639macro_rules! impl_mixed_float_comparison {
640 ($left:ty, $right:ty, $epsilon:expr) => {
641 impl CoordinateComparison<$right> for $left {
642 #[inline]
643 fn compare_coordinate(self, right: $right) -> (Option<Ordering>, bool) {
644 let left = <$left as AngularScalar>::to_f64(self);
645 let right = <$right as AngularScalar>::to_f64(right);
646 (
647 left.partial_cmp(&right),
648 scaled_equal(left, right, $epsilon),
649 )
650 }
651 }
652 };
653}
654
655impl_mixed_float_comparison!(f32, f64, f64::EPSILON);
656impl_mixed_float_comparison!(f64, f32, f64::EPSILON);
657impl_mixed_float_comparison!(i32, f32, f64::EPSILON);
658impl_mixed_float_comparison!(f32, i32, f64::EPSILON);
659impl_mixed_float_comparison!(i32, f64, f64::EPSILON);
660impl_mixed_float_comparison!(f64, i32, f64::EPSILON);
661impl_mixed_float_comparison!(i64, f32, f64::EPSILON);
662impl_mixed_float_comparison!(f32, i64, f64::EPSILON);
663impl_mixed_float_comparison!(i64, f64, f64::EPSILON);
664impl_mixed_float_comparison!(f64, i64, f64::EPSILON);
665
666impl CoordinateComparison<i64> for i32 {
667 #[inline]
668 fn compare_coordinate(self, right: i64) -> (Option<Ordering>, bool) {
669 let left = i64::from(self);
670 (Some(left.cmp(&right)), left == right)
671 }
672}
673
674impl CoordinateComparison<i32> for i64 {
675 #[inline]
676 fn compare_coordinate(self, right: i32) -> (Option<Ordering>, bool) {
677 let right = i64::from(right);
678 (Some(self.cmp(&right)), self == right)
679 }
680}
681
682impl<I, J> CoordinateComparison<Rational<J>> for Rational<I>
683where
684 I: RationalInteger,
685 J: RationalInteger,
686{
687 #[inline]
688 fn compare_coordinate(self, right: Rational<J>) -> (Option<Ordering>, bool) {
689 let left_cross = self.numerator().to_i128() * right.denominator().to_i128();
690 let right_cross = right.numerator().to_i128() * self.denominator().to_i128();
691 let ordering = left_cross.cmp(&right_cross);
692 (Some(ordering), ordering == Ordering::Equal)
693 }
694}
695
696macro_rules! impl_rational_integer_comparison {
697 ($integer:ty) => {
698 impl<I: RationalInteger> CoordinateComparison<$integer> for Rational<I> {
699 #[inline]
700 fn compare_coordinate(self, right: $integer) -> (Option<Ordering>, bool) {
701 let left = self.numerator().to_i128();
702 let right = i128::from(right) * self.denominator().to_i128();
703 let ordering = left.cmp(&right);
704 (Some(ordering), ordering == Ordering::Equal)
705 }
706 }
707
708 impl<I: RationalInteger> CoordinateComparison<Rational<I>> for $integer {
709 #[inline]
710 fn compare_coordinate(self, right: Rational<I>) -> (Option<Ordering>, bool) {
711 let left = i128::from(self) * right.denominator().to_i128();
712 let right = right.numerator().to_i128();
713 let ordering = left.cmp(&right);
714 (Some(ordering), ordering == Ordering::Equal)
715 }
716 }
717 };
718}
719
720impl_rational_integer_comparison!(i32);
721impl_rational_integer_comparison!(i64);
722
723macro_rules! impl_rational_float_comparison {
724 ($float:ty) => {
725 impl<I: RationalInteger> CoordinateComparison<$float> for Rational<I> {
726 #[inline]
727 fn compare_coordinate(self, right: $float) -> (Option<Ordering>, bool) {
728 let left = self.to_f64();
729 let right = f64::from(right);
730 (
731 left.partial_cmp(&right),
732 scaled_equal(left, right, f64::from(<$float>::EPSILON)),
733 )
734 }
735 }
736
737 impl<I: RationalInteger> CoordinateComparison<Rational<I>> for $float {
738 #[inline]
739 fn compare_coordinate(self, right: Rational<I>) -> (Option<Ordering>, bool) {
740 let left = f64::from(self);
741 let right = right.to_f64();
742 (
743 left.partial_cmp(&right),
744 scaled_equal(left, right, f64::from(<$float>::EPSILON)),
745 )
746 }
747 }
748 };
749}
750
751impl_rational_float_comparison!(f32);
752impl_rational_float_comparison!(f64);