1#[cfg(feature = "chumsky")]
4use chumsky::{
5 IterParser as _, Parser,
6 prelude::{any, just},
7 text::whitespace,
8};
9
10#[cfg(feature = "chumsky")]
11use crate::utils::{
12 f32_parser, i16_parser, i32_parser, u8_parser, u16_parser, u32_parser,
13 url_text_component_parser,
14};
15
16#[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
18pub struct Distance(f64);
19
20impl Distance {
21 #[must_use]
23 pub const fn new(meters: f64) -> Self {
24 Self(meters)
25 }
26
27 #[must_use]
29 pub const fn meters(&self) -> f64 {
30 self.0
31 }
32}
33
34impl std::fmt::Display for Distance {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{} m", self.0)
37 }
38}
39
40impl std::ops::Add for Distance {
41 type Output = Self;
42
43 fn add(self, rhs: Self) -> Self::Output {
44 Self(self.0 + rhs.0)
45 }
46}
47
48impl std::ops::Sub for Distance {
49 type Output = Self;
50
51 fn sub(self, rhs: Self) -> Self::Output {
52 Self(self.0 - rhs.0)
53 }
54}
55
56impl std::ops::Mul<u8> for Distance {
57 type Output = Self;
58
59 fn mul(self, rhs: u8) -> Self::Output {
60 Self(self.0 * f64::from(rhs))
61 }
62}
63
64impl std::ops::Mul<u16> for Distance {
65 type Output = Self;
66
67 fn mul(self, rhs: u16) -> Self::Output {
68 Self(self.0 * f64::from(rhs))
69 }
70}
71
72impl std::ops::Mul<u32> for Distance {
73 type Output = Self;
74
75 fn mul(self, rhs: u32) -> Self::Output {
76 Self(self.0 * f64::from(rhs))
77 }
78}
79
80impl std::ops::Mul<f32> for Distance {
81 type Output = Self;
82
83 fn mul(self, rhs: f32) -> Self::Output {
84 Self(self.0 * f64::from(rhs))
85 }
86}
87
88impl std::ops::Mul<f64> for Distance {
89 type Output = Self;
90
91 fn mul(self, rhs: f64) -> Self::Output {
92 Self(self.0 * rhs)
93 }
94}
95
96impl std::ops::Div<u8> for Distance {
97 type Output = Self;
98
99 fn div(self, rhs: u8) -> Self::Output {
100 Self(self.0 / f64::from(rhs))
101 }
102}
103
104impl std::ops::Div<u16> for Distance {
105 type Output = Self;
106
107 fn div(self, rhs: u16) -> Self::Output {
108 Self(self.0 / f64::from(rhs))
109 }
110}
111
112impl std::ops::Div<u32> for Distance {
113 type Output = Self;
114
115 fn div(self, rhs: u32) -> Self::Output {
116 Self(self.0 / f64::from(rhs))
117 }
118}
119
120impl std::ops::Div<f32> for Distance {
121 type Output = Self;
122
123 fn div(self, rhs: f32) -> Self::Output {
124 Self(self.0 / f64::from(rhs))
125 }
126}
127
128impl std::ops::Div<f64> for Distance {
129 type Output = Self;
130
131 fn div(self, rhs: f64) -> Self::Output {
132 Self(self.0 / rhs)
133 }
134}
135
136impl std::ops::Div for Distance {
137 type Output = f64;
138
139 fn div(self, rhs: Self) -> Self::Output {
140 self.0 / rhs.0
141 }
142}
143
144impl std::ops::Rem<u8> for Distance {
145 type Output = Self;
146
147 fn rem(self, rhs: u8) -> Self::Output {
148 Self(self.0 % f64::from(rhs))
149 }
150}
151
152impl std::ops::Rem<u16> for Distance {
153 type Output = Self;
154
155 fn rem(self, rhs: u16) -> Self::Output {
156 Self(self.0 % f64::from(rhs))
157 }
158}
159
160impl std::ops::Rem<u32> for Distance {
161 type Output = Self;
162
163 fn rem(self, rhs: u32) -> Self::Output {
164 Self(self.0 % f64::from(rhs))
165 }
166}
167
168impl std::ops::Rem<f32> for Distance {
169 type Output = Self;
170
171 fn rem(self, rhs: f32) -> Self::Output {
172 Self(self.0 % f64::from(rhs))
173 }
174}
175
176impl std::ops::Rem<f64> for Distance {
177 type Output = Self;
178
179 fn rem(self, rhs: f64) -> Self::Output {
180 Self(self.0 % rhs)
181 }
182}
183
184#[cfg(feature = "chumsky")]
192#[must_use]
193pub fn distance_parser<'src>()
194-> impl Parser<'src, &'src str, Distance, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
195 crate::utils::unsigned_f64_parser()
196 .then_ignore(whitespace().or_not())
197 .then_ignore(just('m'))
198 .map(Distance)
199}
200
201#[derive(
211 Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
212)]
213pub struct LandArea(pub u32);
214
215impl LandArea {
216 pub const ZERO: Self = Self(0);
218
219 #[must_use]
221 pub const fn get(&self) -> u32 {
222 self.0
223 }
224}
225
226impl std::fmt::Display for LandArea {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 let Self(value) = self;
229 write!(f, "{value} m²")
230 }
231}
232
233impl std::ops::Add for LandArea {
234 type Output = Self;
235
236 fn add(self, rhs: Self) -> Self::Output {
237 let Self(lhs) = self;
238 let Self(rhs) = rhs;
239 #[expect(
240 clippy::arithmetic_side_effects,
241 reason = "the same overflow behaviour as the underlying integer addition, which is what a caller summing areas expects"
242 )]
243 Self(lhs + rhs)
244 }
245}
246
247impl std::ops::Sub for LandArea {
248 type Output = Self;
249
250 fn sub(self, rhs: Self) -> Self::Output {
251 let Self(lhs) = self;
252 let Self(rhs) = rhs;
253 #[expect(
254 clippy::arithmetic_side_effects,
255 reason = "the same underflow behaviour as the underlying integer subtraction, which is what a caller differencing areas expects"
256 )]
257 Self(lhs - rhs)
258 }
259}
260
261#[cfg(feature = "chumsky")]
269#[must_use]
270pub fn land_area_parser<'src>()
271-> impl Parser<'src, &'src str, LandArea, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
272 u32_parser()
273 .then_ignore(whitespace().or_not())
274 .then_ignore(just("m²"))
275 .map(LandArea)
276}
277
278#[derive(
282 Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
283)]
284pub struct GridCoordinates {
285 x: u32,
292 y: u32,
299}
300
301impl GridCoordinates {
302 #[must_use]
304 pub const fn new(x: u32, y: u32) -> Self {
305 Self { x, y }
306 }
307
308 #[must_use]
310 pub const fn x(&self) -> u32 {
311 self.x
312 }
313
314 #[must_use]
316 pub const fn y(&self) -> u32 {
317 self.y
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct GridCoordinateOffset {
324 x: i32,
326 y: i32,
328}
329
330impl GridCoordinateOffset {
331 #[must_use]
333 pub const fn new(x: i32, y: i32) -> Self {
334 Self { x, y }
335 }
336
337 #[must_use]
339 pub const fn x(&self) -> i32 {
340 self.x
341 }
342
343 #[must_use]
345 pub const fn y(&self) -> i32 {
346 self.y
347 }
348}
349
350impl std::ops::Add<GridCoordinateOffset> for GridCoordinates {
351 type Output = Self;
352
353 fn add(self, rhs: GridCoordinateOffset) -> Self::Output {
354 Self::new(
355 (i64::from(self.x).saturating_add(i64::from(rhs.x)))
356 .try_into()
357 .unwrap_or(if rhs.x > 0 { u32::MAX } else { u32::MIN }),
358 (i64::from(self.y).saturating_add(i64::from(rhs.y)))
359 .try_into()
360 .unwrap_or(if rhs.y > 0 { u32::MAX } else { u32::MIN }),
361 )
362 }
363}
364
365impl std::ops::Sub<Self> for GridCoordinates {
366 type Output = GridCoordinateOffset;
367
368 fn sub(self, rhs: Self) -> Self::Output {
369 fn saturate_to_i32(difference: i64) -> i32 {
373 difference
374 .try_into()
375 .unwrap_or(if difference > 0 { i32::MAX } else { i32::MIN })
376 }
377 GridCoordinateOffset::new(
378 saturate_to_i32(i64::from(self.x).saturating_sub(i64::from(rhs.x))),
379 saturate_to_i32(i64::from(self.y).saturating_sub(i64::from(rhs.y))),
380 )
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct GridRectangle {
388 lower_left_corner: GridCoordinates,
390 upper_right_corner: GridCoordinates,
392}
393
394impl GridRectangle {
395 #[must_use]
397 pub fn new(corner1: GridCoordinates, corner2: GridCoordinates) -> Self {
398 Self {
399 lower_left_corner: GridCoordinates::new(
400 corner1.x().min(corner2.x()),
401 corner1.y().min(corner2.y()),
402 ),
403 upper_right_corner: GridCoordinates::new(
404 corner1.x().max(corner2.x()),
405 corner1.y().max(corner2.y()),
406 ),
407 }
408 }
409
410 #[must_use]
414 #[expect(
415 clippy::arithmetic_side_effects,
416 reason = "GridCoordinates + GridCoordinateOffset saturates at u32::MIN/u32::MAX"
417 )]
418 pub fn expanded_west(&self, by: u16) -> Self {
419 Self::new(
420 self.lower_left_corner.to_owned() + GridCoordinateOffset::new(-i32::from(by), 0),
421 self.upper_right_corner.to_owned(),
422 )
423 }
424
425 #[must_use]
429 #[expect(
430 clippy::arithmetic_side_effects,
431 reason = "GridCoordinates + GridCoordinateOffset saturates at u32::MIN/u32::MAX"
432 )]
433 pub fn expanded_east(&self, by: u16) -> Self {
434 Self::new(
435 self.lower_left_corner.to_owned(),
436 self.upper_right_corner.to_owned() + GridCoordinateOffset::new(i32::from(by), 0),
437 )
438 }
439
440 #[must_use]
444 #[expect(
445 clippy::arithmetic_side_effects,
446 reason = "GridCoordinates + GridCoordinateOffset saturates at u32::MIN/u32::MAX"
447 )]
448 pub fn expanded_south(&self, by: u16) -> Self {
449 Self::new(
450 self.lower_left_corner.to_owned() + GridCoordinateOffset::new(0, -i32::from(by)),
451 self.upper_right_corner.to_owned(),
452 )
453 }
454
455 #[must_use]
459 #[expect(
460 clippy::arithmetic_side_effects,
461 reason = "GridCoordinates + GridCoordinateOffset saturates at u32::MIN/u32::MAX"
462 )]
463 pub fn expanded_north(&self, by: u16) -> Self {
464 Self::new(
465 self.lower_left_corner.to_owned(),
466 self.upper_right_corner.to_owned() + GridCoordinateOffset::new(0, i32::from(by)),
467 )
468 }
469}
470
471pub trait GridRectangleLike {
474 #[must_use]
476 fn grid_rectangle(&self) -> GridRectangle;
477
478 #[must_use]
480 fn lower_left_corner(&self) -> GridCoordinates {
481 self.grid_rectangle().lower_left_corner().to_owned()
482 }
483
484 #[must_use]
486 fn lower_right_corner(&self) -> GridCoordinates {
487 GridCoordinates::new(
488 self.grid_rectangle().upper_right_corner().x(),
489 self.grid_rectangle().lower_left_corner().y(),
490 )
491 }
492
493 #[must_use]
495 fn upper_left_corner(&self) -> GridCoordinates {
496 GridCoordinates::new(
497 self.grid_rectangle().lower_left_corner().x(),
498 self.grid_rectangle().upper_right_corner().y(),
499 )
500 }
501
502 #[must_use]
504 fn upper_right_corner(&self) -> GridCoordinates {
505 self.grid_rectangle().upper_right_corner().to_owned()
506 }
507
508 #[must_use]
510 fn size_x(&self) -> u32 {
511 self.grid_rectangle().size_x()
512 }
513
514 #[must_use]
516 fn size_y(&self) -> u32 {
517 self.grid_rectangle().size_y()
518 }
519
520 #[must_use]
522 fn x_range(&self) -> std::ops::RangeInclusive<u32> {
523 self.lower_left_corner().x()..=self.upper_right_corner().x()
524 }
525
526 #[must_use]
528 fn y_range(&self) -> std::ops::RangeInclusive<u32> {
529 self.lower_left_corner().y()..=self.upper_right_corner().y()
530 }
531
532 #[must_use]
534 fn contains(&self, grid_coordinates: &GridCoordinates) -> bool {
535 self.lower_left_corner().x() <= grid_coordinates.x()
536 && grid_coordinates.x() <= self.upper_right_corner().x()
537 && self.lower_left_corner().y() <= grid_coordinates.y()
538 && grid_coordinates.y() <= self.upper_right_corner().y()
539 }
540
541 #[must_use]
544 fn intersect<O>(&self, other: &O) -> Option<GridRectangle>
545 where
546 O: GridRectangleLike,
547 {
548 let self_x_range: ranges::GenericRange<u32> = self.x_range().into();
549 let self_y_range: ranges::GenericRange<u32> = self.y_range().into();
550 let other_x_range: ranges::GenericRange<u32> = other.x_range().into();
551 let other_y_range: ranges::GenericRange<u32> = other.y_range().into();
552 let x_intersection = self_x_range.intersect(other_x_range);
553 let y_intersection = self_y_range.intersect(other_y_range);
554 match (x_intersection, y_intersection) {
555 (
556 ranges::OperationResult::Single(x_range),
557 ranges::OperationResult::Single(y_range),
558 ) => {
559 use std::ops::Bound;
560 use std::ops::RangeBounds as _;
561 match (
562 x_range.start_bound(),
563 x_range.end_bound(),
564 y_range.start_bound(),
565 y_range.end_bound(),
566 ) {
567 (
568 Bound::Included(start_x),
569 Bound::Included(end_x),
570 Bound::Included(start_y),
571 Bound::Included(end_y),
572 ) => Some(GridRectangle::new(
573 GridCoordinates::new(*start_x, *start_y),
574 GridCoordinates::new(*end_x, *end_y),
575 )),
576 _ => None,
577 }
578 }
579 _ => None,
580 }
581 }
582
583 #[must_use]
594 fn pps_hud_config(&self) -> String {
595 let lower_left_corner = GlobalCoordinates::from_grid_corner(self.lower_left_corner());
598 format!(
603 "<{},{},0>/{}/{}/1",
604 lower_left_corner.x(),
605 lower_left_corner.y(),
606 f64::from(self.size_x()),
607 f64::from(self.size_y())
608 )
609 }
610}
611
612impl GridRectangleLike for GridRectangle {
613 fn grid_rectangle(&self) -> GridRectangle {
614 self.to_owned()
615 }
616
617 fn lower_left_corner(&self) -> GridCoordinates {
618 self.lower_left_corner.to_owned()
619 }
620
621 fn upper_right_corner(&self) -> GridCoordinates {
622 self.upper_right_corner.to_owned()
623 }
624
625 fn size_x(&self) -> u32 {
626 self.upper_right_corner
627 .x()
628 .saturating_sub(self.lower_left_corner().x())
629 .saturating_add(1)
630 }
631
632 fn size_y(&self) -> u32 {
633 self.upper_right_corner
634 .y()
635 .saturating_sub(self.lower_left_corner().y())
636 .saturating_add(1)
637 }
638
639 fn x_range(&self) -> std::ops::RangeInclusive<u32> {
640 self.lower_left_corner.x()..=self.upper_right_corner.x()
641 }
642
643 fn y_range(&self) -> std::ops::RangeInclusive<u32> {
644 self.lower_left_corner.y()..=self.upper_right_corner.y()
645 }
646}
647
648impl GridRectangleLike for MapTileDescriptor {
649 fn grid_rectangle(&self) -> GridRectangle {
650 GridRectangle::new(
651 self.lower_left_corner,
652 GridCoordinates::new(
653 self.lower_left_corner
654 .x()
655 .saturating_add(u32::from(self.zoom_level.tile_size()))
656 .saturating_sub(1),
657 self.lower_left_corner
658 .y()
659 .saturating_add(u32::from(self.zoom_level.tile_size()))
660 .saturating_sub(1),
661 ),
662 )
663 }
664}
665
666pub trait GridCoordinatesExt {
668 fn bounding_rectangle(&self) -> Option<GridRectangle>;
674}
675
676impl GridCoordinatesExt for Vec<GridCoordinates> {
677 fn bounding_rectangle(&self) -> Option<GridRectangle> {
678 if self.is_empty() {
679 return None;
680 }
681 let (xs, ys): (Vec<u32>, Vec<u32>) = self.iter().map(|gc| (gc.x(), gc.y())).unzip();
682 #[expect(
684 clippy::unwrap_used,
685 reason = "we checked above that the container is non-empty"
686 )]
687 let (min_x, max_x) = (xs.iter().min().unwrap(), xs.iter().max().unwrap());
688 #[expect(
689 clippy::unwrap_used,
690 reason = "we checked above that the container is non-empty"
691 )]
692 let (min_y, max_y) = (ys.iter().min().unwrap(), ys.iter().max().unwrap());
693 Some(GridRectangle {
694 lower_left_corner: GridCoordinates::new(*min_x, *min_y),
695 upper_right_corner: GridCoordinates::new(*max_x, *max_y),
696 })
697 }
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
708pub struct RegionCoordinates {
709 x: f32,
712 y: f32,
715 z: f32,
719}
720
721#[cfg(feature = "chumsky")]
729#[must_use]
730pub fn region_coordinates_parser<'src>()
731-> impl Parser<'src, &'src str, RegionCoordinates, chumsky::extra::Err<chumsky::error::Rich<'src, char>>>
732{
733 just('{')
734 .ignore_then(whitespace().or_not())
735 .ignore_then(f32_parser())
736 .then_ignore(just(','))
737 .then_ignore(whitespace().or_not())
738 .then(f32_parser())
739 .then_ignore(just(','))
740 .then_ignore(whitespace().or_not())
741 .then(f32_parser())
742 .then_ignore(whitespace().or_not())
743 .then_ignore(just('}'))
744 .map(|((x, y), z)| RegionCoordinates::new(x, y, z))
745}
746
747impl RegionCoordinates {
748 #[must_use]
750 pub const fn new(x: f32, y: f32, z: f32) -> Self {
751 Self { x, y, z }
752 }
753
754 #[must_use]
756 pub const fn x(&self) -> f32 {
757 self.x
758 }
759
760 #[must_use]
762 pub const fn y(&self) -> f32 {
763 self.y
764 }
765
766 #[must_use]
768 pub const fn z(&self) -> f32 {
769 self.z
770 }
771
772 #[must_use]
774 pub fn in_bounds(&self) -> bool {
775 self.x >= 0f32
776 && self.x < 256f32
777 && self.y >= 0f32
778 && self.y < 256f32
779 && self.z >= 0f32
780 && self.z < 4096f32
781 }
782}
783
784impl From<crate::lsl::Vector> for RegionCoordinates {
785 fn from(value: crate::lsl::Vector) -> Self {
786 Self {
787 x: value.x,
788 y: value.y,
789 z: value.z,
790 }
791 }
792}
793
794const REGION_SIZE_METERS: f64 = 256.0;
796
797#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
805pub struct Direction {
806 x: f32,
808 y: f32,
810 z: f32,
812}
813
814impl Direction {
815 pub const ZERO: Self = Self {
818 x: 0.0,
819 y: 0.0,
820 z: 0.0,
821 };
822
823 #[must_use]
825 pub const fn new(x: f32, y: f32, z: f32) -> Self {
826 Self { x, y, z }
827 }
828
829 #[must_use]
831 pub const fn x(&self) -> f32 {
832 self.x
833 }
834
835 #[must_use]
837 pub const fn y(&self) -> f32 {
838 self.y
839 }
840
841 #[must_use]
843 pub const fn z(&self) -> f32 {
844 self.z
845 }
846
847 #[must_use]
849 pub fn length(&self) -> f32 {
850 self.z
851 .mul_add(self.z, self.x.mul_add(self.x, self.y * self.y))
852 .sqrt()
853 }
854
855 #[must_use]
858 pub fn normalized(&self) -> Option<Self> {
859 let length = self.length();
860 if length > f32::EPSILON {
861 Some(Self::new(self.x / length, self.y / length, self.z / length))
862 } else {
863 None
864 }
865 }
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
878pub struct GlobalCoordinates {
879 x: f64,
881 y: f64,
883 z: f64,
885}
886
887impl GlobalCoordinates {
888 #[must_use]
890 pub const fn new(x: f64, y: f64, z: f64) -> Self {
891 Self { x, y, z }
892 }
893
894 #[must_use]
896 pub const fn x(&self) -> f64 {
897 self.x
898 }
899
900 #[must_use]
902 pub const fn y(&self) -> f64 {
903 self.y
904 }
905
906 #[must_use]
908 pub const fn z(&self) -> f64 {
909 self.z
910 }
911
912 #[must_use]
916 pub fn from_grid_and_region(grid: GridCoordinates, region: RegionCoordinates) -> Self {
917 Self {
918 x: f64::from(grid.x()).mul_add(REGION_SIZE_METERS, f64::from(region.x())),
919 y: f64::from(grid.y()).mul_add(REGION_SIZE_METERS, f64::from(region.y())),
920 z: f64::from(region.z()),
921 }
922 }
923
924 #[must_use]
930 pub fn from_grid_corner(grid: GridCoordinates) -> Self {
931 Self {
932 x: f64::from(grid.x()) * REGION_SIZE_METERS,
933 y: f64::from(grid.y()) * REGION_SIZE_METERS,
934 z: 0.0,
935 }
936 }
937
938 #[must_use]
946 pub fn split(&self) -> Option<(GridCoordinates, RegionCoordinates)> {
947 let grid_x = region_index(self.x)?;
948 let grid_y = region_index(self.y)?;
949 let local_x = f64::from(grid_x).mul_add(-REGION_SIZE_METERS, self.x);
950 let local_y = f64::from(grid_y).mul_add(-REGION_SIZE_METERS, self.y);
951 Some((
952 GridCoordinates::new(grid_x, grid_y),
953 RegionCoordinates::new(narrow(local_x), narrow(local_y), narrow(self.z)),
954 ))
955 }
956}
957
958impl From<(GridCoordinates, RegionCoordinates)> for GlobalCoordinates {
959 fn from((grid, region): (GridCoordinates, RegionCoordinates)) -> Self {
960 Self::from_grid_and_region(grid, region)
961 }
962}
963
964impl From<GridCoordinates> for GlobalCoordinates {
965 fn from(grid: GridCoordinates) -> Self {
968 Self::from_grid_corner(grid)
969 }
970}
971
972#[expect(
976 clippy::as_conversions,
977 clippy::cast_possible_truncation,
978 clippy::cast_sign_loss,
979 reason = "the floored index is checked finite and within u32 range before the cast"
980)]
981fn region_index(meters: f64) -> Option<u32> {
982 let index = (meters / REGION_SIZE_METERS).floor();
983 if index.is_finite() && index >= 0.0 && index <= f64::from(u32::MAX) {
984 Some(index as u32)
985 } else {
986 None
987 }
988}
989
990#[expect(
994 clippy::as_conversions,
995 clippy::cast_possible_truncation,
996 reason = "a region-local offset is a small (0..256) in-range metre value"
997)]
998const fn narrow(meters: f64) -> f32 {
999 meters as f32
1000}
1001
1002#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1009pub struct Scale {
1010 x: f32,
1012 y: f32,
1014 z: f32,
1016}
1017
1018impl Scale {
1019 #[must_use]
1021 pub const fn new(x: f32, y: f32, z: f32) -> Self {
1022 Self { x, y, z }
1023 }
1024
1025 #[must_use]
1027 pub const fn x(&self) -> f32 {
1028 self.x
1029 }
1030
1031 #[must_use]
1033 pub const fn y(&self) -> f32 {
1034 self.y
1035 }
1036
1037 #[must_use]
1039 pub const fn z(&self) -> f32 {
1040 self.z
1041 }
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1054pub struct TeleportFlags(pub u32);
1055
1056impl TeleportFlags {
1057 pub const SET_HOME_TO_TARGET: u32 = 1 << 0;
1060 pub const SET_LAST_TO_TARGET: u32 = 1 << 1;
1062 pub const VIA_LURE: u32 = 1 << 2;
1064 pub const VIA_LANDMARK: u32 = 1 << 3;
1066 pub const VIA_LOCATION: u32 = 1 << 4;
1068 pub const VIA_HOME: u32 = 1 << 5;
1070 pub const VIA_TELEHUB: u32 = 1 << 6;
1072 pub const VIA_LOGIN: u32 = 1 << 7;
1074 pub const VIA_GODLIKE_LURE: u32 = 1 << 8;
1076 pub const GODLIKE: u32 = 1 << 9;
1078 pub const NINE_ONE_ONE: u32 = 1 << 10;
1080 pub const DISABLE_CANCEL: u32 = 1 << 11;
1083 pub const VIA_REGION_ID: u32 = 1 << 12;
1085 pub const IS_FLYING: u32 = 1 << 13;
1087 pub const SHOW_RESET_HOME: u32 = 1 << 14;
1089 pub const FORCE_REDIRECT: u32 = 1 << 15;
1092 pub const VIA_GLOBAL_COORDS: u32 = 1 << 16;
1094 pub const WITHIN_REGION: u32 = 1 << 17;
1096
1097 #[must_use]
1099 pub const fn contains(self, mask: u32) -> bool {
1100 self.0 & mask == mask
1101 }
1102}
1103
1104#[nutype::nutype(
1106 sanitize(trim),
1107 validate(len_char_min = 2, len_char_max = 35),
1108 derive(
1109 Debug,
1110 Clone,
1111 Display,
1112 Hash,
1113 PartialEq,
1114 Eq,
1115 PartialOrd,
1116 Ord,
1117 Serialize,
1118 Deserialize,
1119 AsRef
1120 )
1121)]
1122pub struct RegionName(String);
1123
1124#[cfg(feature = "chumsky")]
1130#[must_use]
1131pub fn url_region_name_parser<'src>()
1132-> impl Parser<'src, &'src str, RegionName, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1133 url_text_component_parser().try_map(|region_name, span| {
1134 RegionName::try_new(®ion_name).map_err(|err| {
1135 chumsky::error::Rich::custom(
1136 span,
1137 format!("failed to parse url-encoded region name ({region_name}): {err:?}"),
1138 )
1139 })
1140 })
1141}
1142
1143#[cfg(feature = "chumsky")]
1149#[must_use]
1150pub fn region_name_parser<'src>()
1151-> impl Parser<'src, &'src str, RegionName, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1152 any()
1153 .filter(|c: &char| {
1154 c.is_alphabetic() || c.is_numeric() || *c == ' ' || *c == '\'' || *c == '-'
1155 })
1156 .repeated()
1157 .at_least(2)
1158 .collect::<String>()
1159 .try_map(|region_name, span| {
1160 RegionName::try_new(®ion_name).map_err(|err| {
1161 chumsky::error::Rich::custom(
1162 span,
1163 format!("failed to parse region name ({region_name}): {err:?}"),
1164 )
1165 })
1166 })
1167}
1168
1169#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1173pub struct Location {
1174 pub region_name: RegionName,
1176 pub x: u8,
1178 pub y: u8,
1180 pub z: u16,
1182}
1183
1184#[cfg(feature = "chumsky")]
1190#[must_use]
1191pub fn location_parser<'src>()
1192-> impl Parser<'src, &'src str, Location, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1193 region_name_parser()
1194 .then_ignore(just('/'))
1195 .then(u8_parser())
1196 .then_ignore(just('/'))
1197 .then(u8_parser())
1198 .then_ignore(just('/'))
1199 .then(u16_parser())
1200 .map(|(((region_name, x), y), z)| Location::new(region_name, x, y, z))
1201}
1202
1203#[cfg(feature = "chumsky")]
1210#[must_use]
1211pub fn url_location_parser<'src>()
1212-> impl Parser<'src, &'src str, Location, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1213 url_region_name_parser()
1214 .then_ignore(just('/'))
1215 .then(u8_parser())
1216 .then_ignore(just('/'))
1217 .then(u8_parser())
1218 .then_ignore(just('/'))
1219 .then(u16_parser())
1220 .map(|(((region_name, x), y), z)| Location::new(region_name, x, y, z))
1221}
1222
1223#[cfg(feature = "chumsky")]
1230#[must_use]
1231pub fn url_encoded_location_parser<'src>()
1232-> impl Parser<'src, &'src str, Location, chumsky::extra::Err<chumsky::error::Rich<'src, char>>> {
1233 url_text_component_parser().try_map(|s, span| {
1234 location_parser().parse(&s).into_result().map_err(|err| {
1235 chumsky::error::Rich::custom(
1236 span,
1237 format!("Parsing {s} as location failed with: {err:#?}"),
1238 )
1239 })
1240 })
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, strum::EnumIs)]
1245pub enum LocationParseError {
1246 #[error(
1248 "unexpected number of /-separated components in the location URL {0}, found {1} expected 4 (for a bare location) or 8 (for a URL)"
1249 )]
1250 UnexpectedComponentCount(String, usize),
1251 #[error("unexpected scheme in the location URL {0}, found {1}, expected http: or https:")]
1253 UnexpectedScheme(String, String),
1254 #[error(
1256 "unexpected non-empty second component in location URL {0}, found {1}, expected http or https"
1257 )]
1258 UnexpectedNonEmptySecondComponent(String, String),
1259 #[error(
1261 "unexpected host in the location URL {0}, found {1}, expected maps.secondlife.com or slurl.com"
1262 )]
1263 UnexpectedHost(String, String),
1264 #[error("unexpected path in the location URL {0}, found {1}, expected secondlife")]
1266 UnexpectedPath(String, String),
1267 #[error("error parsing the region name {0}: {1}")]
1269 RegionName(String, RegionNameError),
1270 #[error("error parsing the X coordinate {0}: {1}")]
1272 X(String, std::num::ParseIntError),
1273 #[error("error parsing the Y coordinate {0}: {1}")]
1275 Y(String, std::num::ParseIntError),
1276 #[error("error parsing the Z coordinate {0}: {1}")]
1278 Z(String, std::num::ParseIntError),
1279}
1280
1281impl std::str::FromStr for Location {
1282 type Err = LocationParseError;
1283
1284 fn from_str(s: &str) -> Result<Self, Self::Err> {
1285 let usb_location = s
1287 .split_once(',')
1288 .map_or(s, |(usb_location, _usb_comment)| usb_location);
1289 let parts = usb_location.split('/').collect::<Vec<_>>();
1290 if let [region_name, x, y, z] = parts.as_slice() {
1291 let region_name = RegionName::try_new(region_name.replace("%20", " "))
1292 .map_err(|err| LocationParseError::RegionName(s.to_owned(), err))?;
1293 let x = x
1294 .parse()
1295 .map_err(|err| LocationParseError::X(s.to_owned(), err))?;
1296 let y = y
1297 .parse()
1298 .map_err(|err| LocationParseError::Y(s.to_owned(), err))?;
1299 let z = z
1300 .parse()
1301 .map_err(|err| LocationParseError::Z(s.to_owned(), err))?;
1302 return Ok(Self {
1303 region_name,
1304 x,
1305 y,
1306 z,
1307 });
1308 }
1309 if let [scheme, second_component, host, path, region_name, x, y, z] = parts.as_slice() {
1310 if *scheme != "http:" && *scheme != "https:" {
1311 return Err(LocationParseError::UnexpectedScheme(
1312 s.to_owned(),
1313 scheme.to_string(),
1314 ));
1315 }
1316 if !second_component.is_empty() {
1317 return Err(LocationParseError::UnexpectedNonEmptySecondComponent(
1318 s.to_owned(),
1319 second_component.to_string(),
1320 ));
1321 }
1322 if *host != "maps.secondlife.com" && *host != "slurl.com" {
1323 return Err(LocationParseError::UnexpectedHost(
1324 s.to_owned(),
1325 host.to_string(),
1326 ));
1327 }
1328 if *path != "secondlife" {
1329 return Err(LocationParseError::UnexpectedPath(
1330 s.to_owned(),
1331 path.to_string(),
1332 ));
1333 }
1334 let region_name = RegionName::try_new(region_name.replace("%20", " "))
1335 .map_err(|err| LocationParseError::RegionName(s.to_owned(), err))?;
1336 let x = x
1337 .parse()
1338 .map_err(|err| LocationParseError::X(s.to_owned(), err))?;
1339 let y = y
1340 .parse()
1341 .map_err(|err| LocationParseError::Y(s.to_owned(), err))?;
1342 let z = z
1343 .parse()
1344 .map_err(|err| LocationParseError::Z(s.to_owned(), err))?;
1345 return Ok(Self {
1346 region_name,
1347 x,
1348 y,
1349 z,
1350 });
1351 }
1352 Err(LocationParseError::UnexpectedComponentCount(
1353 s.to_owned(),
1354 parts.len(),
1355 ))
1356 }
1357}
1358
1359impl Location {
1360 #[must_use]
1362 pub const fn new(region_name: RegionName, x: u8, y: u8, z: u16) -> Self {
1363 Self {
1364 region_name,
1365 x,
1366 y,
1367 z,
1368 }
1369 }
1370
1371 #[must_use]
1373 pub const fn region_name(&self) -> &RegionName {
1374 &self.region_name
1375 }
1376
1377 #[must_use]
1379 pub const fn x(&self) -> u8 {
1380 self.x
1381 }
1382
1383 #[must_use]
1385 pub const fn y(&self) -> u8 {
1386 self.y
1387 }
1388
1389 #[must_use]
1391 pub const fn z(&self) -> u16 {
1392 self.z
1393 }
1394
1395 #[must_use]
1397 pub fn as_maps_url(&self) -> String {
1398 format!(
1399 "https://maps.secondlife.com/secondlife/{}/{}/{}/{}",
1400 self.region_name, self.x, self.y, self.z
1401 )
1402 }
1403}
1404
1405#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1410pub struct UnconstrainedLocation {
1411 pub region_name: RegionName,
1413 pub x: i16,
1415 pub y: i16,
1417 pub z: i32,
1419}
1420
1421impl UnconstrainedLocation {
1422 #[must_use]
1424 pub const fn new(region_name: RegionName, x: i16, y: i16, z: i32) -> Self {
1425 Self {
1426 region_name,
1427 x,
1428 y,
1429 z,
1430 }
1431 }
1432
1433 #[must_use]
1435 pub const fn region_name(&self) -> &RegionName {
1436 &self.region_name
1437 }
1438
1439 #[must_use]
1441 pub const fn x(&self) -> i16 {
1442 self.x
1443 }
1444
1445 #[must_use]
1447 pub const fn y(&self) -> i16 {
1448 self.y
1449 }
1450
1451 #[must_use]
1453 pub const fn z(&self) -> i32 {
1454 self.z
1455 }
1456}
1457
1458#[cfg(feature = "chumsky")]
1464#[must_use]
1465pub fn unconstrained_location_parser<'src>() -> impl Parser<
1466 'src,
1467 &'src str,
1468 UnconstrainedLocation,
1469 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1470> {
1471 region_name_parser()
1472 .then_ignore(just('/'))
1473 .then(i16_parser())
1474 .then_ignore(just('/'))
1475 .then(i16_parser())
1476 .then_ignore(just('/'))
1477 .then(i32_parser())
1478 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1479}
1480
1481#[cfg(feature = "chumsky")]
1488#[must_use]
1489pub fn url_unconstrained_location_parser<'src>() -> impl Parser<
1490 'src,
1491 &'src str,
1492 UnconstrainedLocation,
1493 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1494> {
1495 url_region_name_parser()
1496 .then_ignore(just('/'))
1497 .then(i16_parser())
1498 .then_ignore(just('/'))
1499 .then(i16_parser())
1500 .then_ignore(just('/'))
1501 .then(i32_parser())
1502 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1503}
1504
1505#[cfg(feature = "chumsky")]
1512#[must_use]
1513pub fn urlencoded_unconstrained_location_parser<'src>() -> impl Parser<
1514 'src,
1515 &'src str,
1516 UnconstrainedLocation,
1517 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1518> {
1519 url_region_name_parser()
1520 .then_ignore(just('/'))
1521 .then(i16_parser())
1522 .then_ignore(just('/'))
1523 .then(i16_parser())
1524 .then_ignore(just('/'))
1525 .then(i32_parser())
1526 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1527}
1528
1529impl TryFrom<UnconstrainedLocation> for Location {
1530 type Error = std::num::TryFromIntError;
1531
1532 fn try_from(value: UnconstrainedLocation) -> Result<Self, Self::Error> {
1533 Ok(Self::new(
1534 value.region_name,
1535 value.x.try_into()?,
1536 value.y.try_into()?,
1537 value.z.try_into()?,
1538 ))
1539 }
1540}
1541
1542impl From<Location> for UnconstrainedLocation {
1543 fn from(value: Location) -> Self {
1544 Self {
1545 region_name: value.region_name,
1546 x: value.x.into(),
1547 y: value.y.into(),
1548 z: value.z.into(),
1549 }
1550 }
1551}
1552
1553#[nutype::nutype(
1555 validate(greater_or_equal = 1, less_or_equal = 8),
1556 derive(
1557 Debug,
1558 Clone,
1559 Copy,
1560 Display,
1561 FromStr,
1562 Hash,
1563 PartialEq,
1564 Eq,
1565 PartialOrd,
1566 Ord,
1567 Serialize,
1568 Deserialize
1569 )
1570)]
1571pub struct ZoomLevel(u8);
1572
1573#[derive(Debug, Clone, thiserror::Error, strum::EnumIs)]
1576pub enum ZoomFitError {
1577 #[error("region size in x direction can not be zero")]
1579 RegionSizeXZero,
1580
1581 #[error("region size in y direction can not be zero")]
1583 RegionSizeYZero,
1584
1585 #[error("output image size in x direction can not be zero")]
1587 OutputSizeXZero,
1588
1589 #[error("output image size in y direction can not be zero")]
1591 OutputSizeYZero,
1592
1593 #[error("error converting a logarithm value into a u8")]
1595 LogarithmConversionError(#[from] std::num::TryFromIntError),
1596
1597 #[error("error creating zoom level from calculated value")]
1600 ZoomLevelError(#[from] ZoomLevelError),
1601}
1602
1603impl ZoomLevel {
1604 #[must_use]
1609 pub fn tile_size(&self) -> u16 {
1610 let exponent: u32 = self.into_inner().into();
1611 let exponent = exponent.saturating_sub(1);
1612 2u16.pow(exponent)
1613 }
1614
1615 #[expect(
1620 clippy::arithmetic_side_effects,
1621 reason = "both values we multiply here are u16 originally so their product should never overflow an u32"
1622 )]
1623 #[must_use]
1624 pub fn tile_size_in_pixels(&self) -> u32 {
1625 let tile_size: u32 = self.tile_size().into();
1626 let region_size_in_map_tile_in_pixels: u32 = self.pixels_per_region().into();
1627 tile_size * region_size_in_map_tile_in_pixels
1628 }
1629
1630 #[must_use]
1637 pub fn map_tile_corner(&self, GridCoordinates { x, y }: &GridCoordinates) -> GridCoordinates {
1638 let tile_size = u32::from(self.tile_size());
1639 #[expect(
1640 clippy::arithmetic_side_effects,
1641 reason = "remainder should not have any side-effects since tile_size is never 0 (no division by zero issues) or negative (no issues with x or y being e.g. i16::MIN which overflows when the sign is flipped)"
1642 )]
1643 GridCoordinates {
1644 x: x.saturating_sub(x % tile_size),
1645 y: y.saturating_sub(y % tile_size),
1646 }
1647 }
1648
1649 #[must_use]
1654 pub fn pixels_per_region(&self) -> u16 {
1655 let exponent: u32 = self.into_inner().into();
1656 let exponent = exponent.saturating_sub(1);
1657 let exponent = 8u32.saturating_sub(exponent);
1658 2u16.pow(exponent)
1659 }
1660
1661 #[must_use]
1663 pub fn pixels_per_meter(&self) -> f32 {
1664 f32::from(self.pixels_per_region()) / 256f32
1665 }
1666
1667 pub fn max_zoom_level_to_fit_regions_into_output_image(
1677 region_x: u32,
1678 region_y: u32,
1679 output_x: u32,
1680 output_y: u32,
1681 ) -> Result<Self, ZoomFitError> {
1682 if region_x == 0 {
1683 return Err(ZoomFitError::RegionSizeXZero);
1684 }
1685 if region_y == 0 {
1686 return Err(ZoomFitError::RegionSizeYZero);
1687 }
1688 if output_x == 0 {
1689 return Err(ZoomFitError::OutputSizeXZero);
1690 }
1691 if output_y == 0 {
1692 return Err(ZoomFitError::OutputSizeYZero);
1693 }
1694 let output_pixels_per_region_x: u32 = output_x.div_ceil(region_x);
1695 let output_pixels_per_region_y: u32 = output_y.div_ceil(region_y);
1696 let max_zoom_level_x: u8 = 9u8.saturating_sub(std::cmp::min(
1697 8,
1698 output_pixels_per_region_x
1699 .ilog2()
1700 .try_into()
1701 .map_err(ZoomFitError::LogarithmConversionError)?,
1702 ));
1703 let max_zoom_level_y: u8 = 9u8.saturating_sub(std::cmp::min(
1704 8,
1705 output_pixels_per_region_y
1706 .ilog2()
1707 .try_into()
1708 .map_err(ZoomFitError::LogarithmConversionError)?,
1709 ));
1710 Ok(Self::try_new(std::cmp::max(
1711 max_zoom_level_x,
1712 max_zoom_level_y,
1713 ))?)
1714 }
1715}
1716
1717#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1719#[expect(
1720 clippy::module_name_repetitions,
1721 reason = "the type is used outside this module"
1722)]
1723pub struct MapTileDescriptor {
1724 zoom_level: ZoomLevel,
1726 lower_left_corner: GridCoordinates,
1728}
1729
1730impl MapTileDescriptor {
1731 #[must_use]
1736 pub fn new(zoom_level: ZoomLevel, grid_coordinates: GridCoordinates) -> Self {
1737 let lower_left_corner = zoom_level.map_tile_corner(&grid_coordinates);
1738 Self {
1739 zoom_level,
1740 lower_left_corner,
1741 }
1742 }
1743
1744 #[must_use]
1746 pub const fn zoom_level(&self) -> &ZoomLevel {
1747 &self.zoom_level
1748 }
1749
1750 #[must_use]
1752 pub const fn lower_left_corner(&self) -> &GridCoordinates {
1753 &self.lower_left_corner
1754 }
1755
1756 #[must_use]
1758 pub fn tile_size(&self) -> u16 {
1759 self.zoom_level.tile_size()
1760 }
1761
1762 #[must_use]
1764 pub fn tile_size_in_pixels(&self) -> u32 {
1765 self.zoom_level.tile_size_in_pixels()
1766 }
1767
1768 #[must_use]
1770 pub fn grid_rectangle(&self) -> GridRectangle {
1771 GridRectangle::new(
1772 self.lower_left_corner,
1773 GridCoordinates::new(
1774 self.lower_left_corner
1775 .x()
1776 .saturating_add(u32::from(self.zoom_level.tile_size()))
1777 .saturating_sub(1),
1778 self.lower_left_corner
1779 .y()
1780 .saturating_add(u32::from(self.zoom_level.tile_size()))
1781 .saturating_sub(1),
1782 ),
1783 )
1784 }
1785}
1786
1787#[derive(Debug, Clone)]
1789pub struct USBWaypoint {
1790 location: Location,
1792 comment: Option<String>,
1794}
1795
1796impl USBWaypoint {
1797 #[must_use]
1799 pub const fn new(location: Location, comment: Option<String>) -> Self {
1800 Self { location, comment }
1801 }
1802
1803 #[must_use]
1805 pub const fn location(&self) -> &Location {
1806 &self.location
1807 }
1808
1809 #[must_use]
1811 pub fn region_coordinates(&self) -> RegionCoordinates {
1812 RegionCoordinates::new(
1813 f32::from(self.location.x()),
1814 f32::from(self.location.y()),
1815 f32::from(self.location.z()),
1816 )
1817 }
1818
1819 #[must_use]
1821 pub const fn comment(&self) -> Option<&String> {
1822 self.comment.as_ref()
1823 }
1824}
1825
1826impl std::fmt::Display for USBWaypoint {
1827 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1828 write!(f, "{}", self.location.as_maps_url())?;
1829 if let Some(comment) = &self.comment {
1830 write!(f, ",{comment}")?;
1831 }
1832 Ok(())
1833 }
1834}
1835
1836impl std::str::FromStr for USBWaypoint {
1837 type Err = LocationParseError;
1838
1839 fn from_str(s: &str) -> Result<Self, Self::Err> {
1840 if let Some((location, comment)) = s.split_once(',') {
1841 Ok(Self {
1842 location: location.parse()?,
1843 comment: Some(comment.to_owned()),
1844 })
1845 } else {
1846 Ok(Self {
1847 location: s.parse()?,
1848 comment: None,
1849 })
1850 }
1851 }
1852}
1853
1854#[derive(Debug, Clone)]
1856pub struct USBNotecard {
1857 waypoints: Vec<USBWaypoint>,
1859}
1860
1861#[derive(Debug, thiserror::Error, strum::EnumIs)]
1863pub enum USBNotecardLoadError {
1864 #[error("I/O error opening or reading the file: {0}")]
1866 Io(#[from] std::io::Error),
1867 #[error("parse error deserializing the USB notecard lines: {0}")]
1869 LocationParseError(#[from] LocationParseError),
1870}
1871
1872impl USBNotecard {
1873 #[must_use]
1875 pub const fn new(waypoints: Vec<USBWaypoint>) -> Self {
1876 Self { waypoints }
1877 }
1878
1879 #[must_use]
1881 pub fn waypoints(&self) -> &[USBWaypoint] {
1882 &self.waypoints
1883 }
1884
1885 pub fn load_from_file(filename: &std::path::Path) -> Result<Self, USBNotecardLoadError> {
1892 let contents = std::fs::read_to_string(filename)?;
1893 Ok(contents.parse()?)
1894 }
1895}
1896
1897impl std::fmt::Display for USBNotecard {
1898 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1899 for waypoint in &self.waypoints {
1900 writeln!(f, "{waypoint}")?;
1901 }
1902 Ok(())
1903 }
1904}
1905
1906impl std::str::FromStr for USBNotecard {
1907 type Err = LocationParseError;
1908
1909 fn from_str(s: &str) -> Result<Self, Self::Err> {
1910 s.lines()
1911 .map(|line| line.parse::<USBWaypoint>())
1912 .collect::<Result<Vec<_>, _>>()
1913 .map(|waypoints| Self { waypoints })
1914 }
1915}
1916
1917#[cfg(test)]
1918mod test {
1919 use super::*;
1920 use pretty_assertions::assert_eq;
1921
1922 #[test]
1923 fn test_parse_location_bare() -> Result<(), Box<dyn std::error::Error>> {
1924 assert_eq!(
1925 "Beach%20Valley/110/67/24".parse::<Location>(),
1926 Ok(Location {
1927 region_name: RegionName::try_new("Beach Valley")?,
1928 x: 110,
1929 y: 67,
1930 z: 24
1931 }),
1932 );
1933 Ok(())
1934 }
1935
1936 #[test]
1937 fn test_parse_location_url_maps() -> Result<(), Box<dyn std::error::Error>> {
1938 assert_eq!(
1939 "http://maps.secondlife.com/secondlife/Beach%20Valley/110/67/24".parse::<Location>(),
1940 Ok(Location {
1941 region_name: RegionName::try_new("Beach Valley")?,
1942 x: 110,
1943 y: 67,
1944 z: 24
1945 }),
1946 );
1947 Ok(())
1948 }
1949
1950 #[test]
1951 fn test_parse_location_url_slurl() -> Result<(), Box<dyn std::error::Error>> {
1952 assert_eq!(
1953 "http://slurl.com/secondlife/Beach%20Valley/110/67/24".parse::<Location>(),
1954 Ok(Location {
1955 region_name: RegionName::try_new("Beach Valley")?,
1956 x: 110,
1957 y: 67,
1958 z: 24
1959 }),
1960 );
1961 Ok(())
1962 }
1963
1964 #[test]
1965 fn test_parse_location_bare_with_usb_comment() -> Result<(), Box<dyn std::error::Error>> {
1966 assert_eq!(
1967 "Beach%20Valley/110/67/24,MUSTER".parse::<Location>(),
1968 Ok(Location {
1969 region_name: RegionName::try_new("Beach Valley")?,
1970 x: 110,
1971 y: 67,
1972 z: 24
1973 }),
1974 );
1975 Ok(())
1976 }
1977
1978 #[test]
1979 fn test_grid_rectangle_intersection_upper_right_corner()
1980 -> Result<(), Box<dyn std::error::Error>> {
1981 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
1982 let rect2 = GridRectangle::new(GridCoordinates::new(15, 15), GridCoordinates::new(25, 25));
1983 assert_eq!(
1984 rect1.intersect(&rect2),
1985 Some(GridRectangle::new(
1986 GridCoordinates::new(15, 15),
1987 GridCoordinates::new(20, 20),
1988 ))
1989 );
1990 Ok(())
1991 }
1992
1993 #[test]
1994 fn test_grid_rectangle_intersection_upper_left_corner() -> Result<(), Box<dyn std::error::Error>>
1995 {
1996 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
1997 let rect2 = GridRectangle::new(GridCoordinates::new(5, 15), GridCoordinates::new(15, 25));
1998 assert_eq!(
1999 rect1.intersect(&rect2),
2000 Some(GridRectangle::new(
2001 GridCoordinates::new(10, 15),
2002 GridCoordinates::new(15, 20),
2003 ))
2004 );
2005 Ok(())
2006 }
2007
2008 #[test]
2009 fn test_grid_rectangle_intersection_lower_left_corner() -> Result<(), Box<dyn std::error::Error>>
2010 {
2011 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2012 let rect2 = GridRectangle::new(GridCoordinates::new(5, 5), GridCoordinates::new(15, 15));
2013 assert_eq!(
2014 rect1.intersect(&rect2),
2015 Some(GridRectangle::new(
2016 GridCoordinates::new(10, 10),
2017 GridCoordinates::new(15, 15),
2018 ))
2019 );
2020 Ok(())
2021 }
2022
2023 #[test]
2024 fn test_grid_rectangle_intersection_lower_right_corner()
2025 -> Result<(), Box<dyn std::error::Error>> {
2026 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2027 let rect2 = GridRectangle::new(GridCoordinates::new(15, 5), GridCoordinates::new(25, 15));
2028 assert_eq!(
2029 rect1.intersect(&rect2),
2030 Some(GridRectangle::new(
2031 GridCoordinates::new(15, 10),
2032 GridCoordinates::new(20, 15),
2033 ))
2034 );
2035 Ok(())
2036 }
2037
2038 #[test]
2039 fn test_grid_rectangle_intersection_no_overlap() -> Result<(), Box<dyn std::error::Error>> {
2040 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2041 let rect2 = GridRectangle::new(GridCoordinates::new(30, 30), GridCoordinates::new(40, 40));
2042 assert_eq!(rect1.intersect(&rect2), None);
2043 Ok(())
2044 }
2045
2046 #[test]
2047 fn test_grid_rectangle_expanded_west() {
2048 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2049 assert_eq!(rect.expanded_west(0), rect);
2050 assert_eq!(
2051 rect.expanded_west(3),
2052 GridRectangle::new(GridCoordinates::new(7, 10), GridCoordinates::new(20, 20)),
2053 );
2054 let near_edge =
2055 GridRectangle::new(GridCoordinates::new(2, 10), GridCoordinates::new(20, 20));
2056 assert_eq!(
2057 near_edge.expanded_west(5),
2058 GridRectangle::new(GridCoordinates::new(0, 10), GridCoordinates::new(20, 20)),
2059 );
2060 }
2061
2062 #[test]
2063 fn test_grid_rectangle_expanded_east() {
2064 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2065 assert_eq!(rect.expanded_east(0), rect);
2066 assert_eq!(
2067 rect.expanded_east(3),
2068 GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(23, 20)),
2069 );
2070 let near_edge = GridRectangle::new(
2071 GridCoordinates::new(10, 10),
2072 GridCoordinates::new(u32::MAX - 2, 20),
2073 );
2074 assert_eq!(
2075 near_edge.expanded_east(5),
2076 GridRectangle::new(
2077 GridCoordinates::new(10, 10),
2078 GridCoordinates::new(u32::MAX, 20),
2079 ),
2080 );
2081 }
2082
2083 #[test]
2084 fn test_grid_rectangle_expanded_south() {
2085 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2086 assert_eq!(rect.expanded_south(0), rect);
2087 assert_eq!(
2088 rect.expanded_south(3),
2089 GridRectangle::new(GridCoordinates::new(10, 7), GridCoordinates::new(20, 20)),
2090 );
2091 let near_edge =
2092 GridRectangle::new(GridCoordinates::new(10, 2), GridCoordinates::new(20, 20));
2093 assert_eq!(
2094 near_edge.expanded_south(5),
2095 GridRectangle::new(GridCoordinates::new(10, 0), GridCoordinates::new(20, 20)),
2096 );
2097 }
2098
2099 #[test]
2100 fn test_grid_rectangle_expanded_north() {
2101 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2102 assert_eq!(rect.expanded_north(0), rect);
2103 assert_eq!(
2104 rect.expanded_north(3),
2105 GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 23)),
2106 );
2107 let near_edge = GridRectangle::new(
2108 GridCoordinates::new(10, 10),
2109 GridCoordinates::new(20, u32::MAX - 2),
2110 );
2111 assert_eq!(
2112 near_edge.expanded_north(5),
2113 GridRectangle::new(
2114 GridCoordinates::new(10, 10),
2115 GridCoordinates::new(20, u32::MAX),
2116 ),
2117 );
2118 }
2119
2120 #[cfg(feature = "chumsky")]
2121 #[test]
2122 fn test_url_region_name_parser_no_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2123 let region_name = "Viterbo";
2124 assert_eq!(
2125 url_region_name_parser().parse(region_name).into_result(),
2126 Ok(RegionName::try_new(region_name)?)
2127 );
2128 Ok(())
2129 }
2130
2131 #[cfg(feature = "chumsky")]
2132 #[test]
2133 fn test_url_region_name_parser_url_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2134 let region_name = "Da Boom";
2135 let input = region_name.replace(' ', "%20");
2136 assert_eq!(
2137 url_region_name_parser().parse(&input).into_result(),
2138 Ok(RegionName::try_new(region_name)?)
2139 );
2140 Ok(())
2141 }
2142
2143 #[cfg(feature = "chumsky")]
2144 #[test]
2145 fn test_region_name_parser_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2146 let region_name = "Da Boom";
2147 assert_eq!(
2148 region_name_parser().parse(region_name).into_result(),
2149 Ok(RegionName::try_new(region_name)?)
2150 );
2151 Ok(())
2152 }
2153
2154 #[cfg(feature = "chumsky")]
2155 #[test]
2156 fn test_url_location_parser_no_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2157 let region_name = "Viterbo";
2158 let input = format!("{region_name}/1/2/300");
2159 assert_eq!(
2160 url_location_parser().parse(&input).into_result(),
2161 Ok(Location {
2162 region_name: RegionName::try_new(region_name)?,
2163 x: 1,
2164 y: 2,
2165 z: 300
2166 })
2167 );
2168 Ok(())
2169 }
2170
2171 #[cfg(feature = "chumsky")]
2172 #[test]
2173 fn test_url_location_parser_url_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2174 let region_name = "Da Boom";
2175 let input = format!("{}/1/2/300", region_name.replace(' ', "%20"));
2176 assert_eq!(
2177 url_location_parser().parse(&input).into_result(),
2178 Ok(Location {
2179 region_name: RegionName::try_new(region_name)?,
2180 x: 1,
2181 y: 2,
2182 z: 300
2183 })
2184 );
2185 Ok(())
2186 }
2187
2188 #[cfg(feature = "chumsky")]
2189 #[test]
2190 fn test_url_location_parser_url_whitespace_single_digit_after_space()
2191 -> Result<(), Box<dyn std::error::Error>> {
2192 let region_name = "Foo Bar 3";
2193 let input = format!("{}/1/2/300", region_name.replace(' ', "%20"));
2194 assert_eq!(
2195 url_location_parser().parse(&input).into_result(),
2196 Ok(Location {
2197 region_name: RegionName::try_new(region_name)?,
2198 x: 1,
2199 y: 2,
2200 z: 300
2201 })
2202 );
2203 Ok(())
2204 }
2205
2206 #[cfg(feature = "chumsky")]
2207 #[test]
2208 fn test_region_coordinates_parser() -> Result<(), Box<dyn std::error::Error>> {
2209 assert_eq!(
2210 region_coordinates_parser()
2211 .parse("{ 63.0486, 45.2515, 1501.08 }")
2212 .into_result(),
2213 Ok(RegionCoordinates {
2214 x: 63.0486,
2215 y: 45.2515,
2216 z: 1501.08,
2217 })
2218 );
2219 Ok(())
2220 }
2221}