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