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]
1427 pub fn as_maps_url(&self) -> String {
1428 format!(
1429 "https://maps.secondlife.com/secondlife/{}/{}/{}/{}",
1430 self.region_name.to_string().replace(' ', "%20"),
1431 self.x,
1432 self.y,
1433 self.z
1434 )
1435 }
1436}
1437
1438#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1443pub struct UnconstrainedLocation {
1444 pub region_name: RegionName,
1446 pub x: i16,
1448 pub y: i16,
1450 pub z: i32,
1452}
1453
1454impl UnconstrainedLocation {
1455 #[must_use]
1457 pub const fn new(region_name: RegionName, x: i16, y: i16, z: i32) -> Self {
1458 Self {
1459 region_name,
1460 x,
1461 y,
1462 z,
1463 }
1464 }
1465
1466 #[must_use]
1468 pub const fn region_name(&self) -> &RegionName {
1469 &self.region_name
1470 }
1471
1472 #[must_use]
1474 pub const fn x(&self) -> i16 {
1475 self.x
1476 }
1477
1478 #[must_use]
1480 pub const fn y(&self) -> i16 {
1481 self.y
1482 }
1483
1484 #[must_use]
1486 pub const fn z(&self) -> i32 {
1487 self.z
1488 }
1489}
1490
1491#[cfg(feature = "chumsky")]
1497#[must_use]
1498pub fn unconstrained_location_parser<'src>() -> impl Parser<
1499 'src,
1500 &'src str,
1501 UnconstrainedLocation,
1502 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1503> {
1504 region_name_parser()
1505 .then_ignore(just('/'))
1506 .then(i16_parser())
1507 .then_ignore(just('/'))
1508 .then(i16_parser())
1509 .then_ignore(just('/'))
1510 .then(i32_parser())
1511 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1512}
1513
1514#[cfg(feature = "chumsky")]
1521#[must_use]
1522pub fn url_unconstrained_location_parser<'src>() -> impl Parser<
1523 'src,
1524 &'src str,
1525 UnconstrainedLocation,
1526 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1527> {
1528 url_region_name_parser()
1529 .then_ignore(just('/'))
1530 .then(i16_parser())
1531 .then_ignore(just('/'))
1532 .then(i16_parser())
1533 .then_ignore(just('/'))
1534 .then(i32_parser())
1535 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1536}
1537
1538#[cfg(feature = "chumsky")]
1545#[must_use]
1546pub fn urlencoded_unconstrained_location_parser<'src>() -> impl Parser<
1547 'src,
1548 &'src str,
1549 UnconstrainedLocation,
1550 chumsky::extra::Err<chumsky::error::Rich<'src, char>>,
1551> {
1552 url_region_name_parser()
1553 .then_ignore(just('/'))
1554 .then(i16_parser())
1555 .then_ignore(just('/'))
1556 .then(i16_parser())
1557 .then_ignore(just('/'))
1558 .then(i32_parser())
1559 .map(|(((region_name, x), y), z)| UnconstrainedLocation::new(region_name, x, y, z))
1560}
1561
1562impl TryFrom<UnconstrainedLocation> for Location {
1563 type Error = std::num::TryFromIntError;
1564
1565 fn try_from(value: UnconstrainedLocation) -> Result<Self, Self::Error> {
1566 Ok(Self::new(
1567 value.region_name,
1568 value.x.try_into()?,
1569 value.y.try_into()?,
1570 value.z.try_into()?,
1571 ))
1572 }
1573}
1574
1575impl From<Location> for UnconstrainedLocation {
1576 fn from(value: Location) -> Self {
1577 Self {
1578 region_name: value.region_name,
1579 x: value.x.into(),
1580 y: value.y.into(),
1581 z: value.z.into(),
1582 }
1583 }
1584}
1585
1586#[nutype::nutype(
1588 validate(greater_or_equal = 1, less_or_equal = 8),
1589 derive(
1590 Debug,
1591 Clone,
1592 Copy,
1593 Display,
1594 FromStr,
1595 Hash,
1596 PartialEq,
1597 Eq,
1598 PartialOrd,
1599 Ord,
1600 Serialize,
1601 Deserialize
1602 )
1603)]
1604pub struct ZoomLevel(u8);
1605
1606#[derive(Debug, Clone, thiserror::Error, strum::EnumIs)]
1609pub enum ZoomFitError {
1610 #[error("region size in x direction can not be zero")]
1612 RegionSizeXZero,
1613
1614 #[error("region size in y direction can not be zero")]
1616 RegionSizeYZero,
1617
1618 #[error("output image size in x direction can not be zero")]
1620 OutputSizeXZero,
1621
1622 #[error("output image size in y direction can not be zero")]
1624 OutputSizeYZero,
1625
1626 #[error("error converting a logarithm value into a u8")]
1628 LogarithmConversionError(#[from] std::num::TryFromIntError),
1629
1630 #[error("error creating zoom level from calculated value")]
1633 ZoomLevelError(#[from] ZoomLevelError),
1634}
1635
1636impl ZoomLevel {
1637 #[must_use]
1642 pub fn tile_size(&self) -> u16 {
1643 let exponent: u32 = self.into_inner().into();
1644 let exponent = exponent.saturating_sub(1);
1645 2u16.pow(exponent)
1646 }
1647
1648 #[expect(
1653 clippy::arithmetic_side_effects,
1654 reason = "both values we multiply here are u16 originally so their product should never overflow an u32"
1655 )]
1656 #[must_use]
1657 pub fn tile_size_in_pixels(&self) -> u32 {
1658 let tile_size: u32 = self.tile_size().into();
1659 let region_size_in_map_tile_in_pixels: u32 = self.pixels_per_region().into();
1660 tile_size * region_size_in_map_tile_in_pixels
1661 }
1662
1663 #[must_use]
1670 pub fn map_tile_corner(&self, GridCoordinates { x, y }: &GridCoordinates) -> GridCoordinates {
1671 let tile_size = u32::from(self.tile_size());
1672 #[expect(
1673 clippy::arithmetic_side_effects,
1674 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)"
1675 )]
1676 GridCoordinates {
1677 x: x.saturating_sub(x % tile_size),
1678 y: y.saturating_sub(y % tile_size),
1679 }
1680 }
1681
1682 #[must_use]
1687 pub fn pixels_per_region(&self) -> u16 {
1688 let exponent: u32 = self.into_inner().into();
1689 let exponent = exponent.saturating_sub(1);
1690 let exponent = 8u32.saturating_sub(exponent);
1691 2u16.pow(exponent)
1692 }
1693
1694 #[must_use]
1696 pub fn pixels_per_meter(&self) -> f32 {
1697 f32::from(self.pixels_per_region()) / 256f32
1698 }
1699
1700 pub fn max_zoom_level_to_fit_regions_into_output_image(
1710 region_x: u32,
1711 region_y: u32,
1712 output_x: u32,
1713 output_y: u32,
1714 ) -> Result<Self, ZoomFitError> {
1715 if region_x == 0 {
1716 return Err(ZoomFitError::RegionSizeXZero);
1717 }
1718 if region_y == 0 {
1719 return Err(ZoomFitError::RegionSizeYZero);
1720 }
1721 if output_x == 0 {
1722 return Err(ZoomFitError::OutputSizeXZero);
1723 }
1724 if output_y == 0 {
1725 return Err(ZoomFitError::OutputSizeYZero);
1726 }
1727 let output_pixels_per_region_x: u32 = output_x.div_ceil(region_x);
1728 let output_pixels_per_region_y: u32 = output_y.div_ceil(region_y);
1729 let max_zoom_level_x: u8 = 9u8.saturating_sub(std::cmp::min(
1730 8,
1731 output_pixels_per_region_x
1732 .ilog2()
1733 .try_into()
1734 .map_err(ZoomFitError::LogarithmConversionError)?,
1735 ));
1736 let max_zoom_level_y: u8 = 9u8.saturating_sub(std::cmp::min(
1737 8,
1738 output_pixels_per_region_y
1739 .ilog2()
1740 .try_into()
1741 .map_err(ZoomFitError::LogarithmConversionError)?,
1742 ));
1743 Ok(Self::try_new(std::cmp::max(
1744 max_zoom_level_x,
1745 max_zoom_level_y,
1746 ))?)
1747 }
1748}
1749
1750#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1752#[expect(
1753 clippy::module_name_repetitions,
1754 reason = "the type is used outside this module"
1755)]
1756pub struct MapTileDescriptor {
1757 zoom_level: ZoomLevel,
1759 lower_left_corner: GridCoordinates,
1761}
1762
1763impl MapTileDescriptor {
1764 #[must_use]
1769 pub fn new(zoom_level: ZoomLevel, grid_coordinates: GridCoordinates) -> Self {
1770 let lower_left_corner = zoom_level.map_tile_corner(&grid_coordinates);
1771 Self {
1772 zoom_level,
1773 lower_left_corner,
1774 }
1775 }
1776
1777 #[must_use]
1779 pub const fn zoom_level(&self) -> &ZoomLevel {
1780 &self.zoom_level
1781 }
1782
1783 #[must_use]
1785 pub const fn lower_left_corner(&self) -> &GridCoordinates {
1786 &self.lower_left_corner
1787 }
1788
1789 #[must_use]
1791 pub fn tile_size(&self) -> u16 {
1792 self.zoom_level.tile_size()
1793 }
1794
1795 #[must_use]
1797 pub fn tile_size_in_pixels(&self) -> u32 {
1798 self.zoom_level.tile_size_in_pixels()
1799 }
1800
1801 #[must_use]
1803 pub fn grid_rectangle(&self) -> GridRectangle {
1804 GridRectangle::new(
1805 self.lower_left_corner,
1806 GridCoordinates::new(
1807 self.lower_left_corner
1808 .x()
1809 .saturating_add(u32::from(self.zoom_level.tile_size()))
1810 .saturating_sub(1),
1811 self.lower_left_corner
1812 .y()
1813 .saturating_add(u32::from(self.zoom_level.tile_size()))
1814 .saturating_sub(1),
1815 ),
1816 )
1817 }
1818}
1819
1820#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1822pub struct USBWaypoint {
1823 location: Location,
1825 comment: Option<String>,
1827}
1828
1829impl USBWaypoint {
1830 #[must_use]
1832 pub const fn new(location: Location, comment: Option<String>) -> Self {
1833 Self { location, comment }
1834 }
1835
1836 #[must_use]
1838 pub const fn location(&self) -> &Location {
1839 &self.location
1840 }
1841
1842 #[must_use]
1844 pub fn region_coordinates(&self) -> RegionCoordinates {
1845 RegionCoordinates::new(
1846 f32::from(self.location.x()),
1847 f32::from(self.location.y()),
1848 f32::from(self.location.z()),
1849 )
1850 }
1851
1852 #[must_use]
1854 pub const fn comment(&self) -> Option<&String> {
1855 self.comment.as_ref()
1856 }
1857}
1858
1859impl std::fmt::Display for USBWaypoint {
1860 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1861 write!(f, "{}", self.location.as_maps_url())?;
1862 if let Some(comment) = &self.comment {
1863 write!(f, ",{comment}")?;
1864 }
1865 Ok(())
1866 }
1867}
1868
1869impl std::str::FromStr for USBWaypoint {
1870 type Err = LocationParseError;
1871
1872 fn from_str(s: &str) -> Result<Self, Self::Err> {
1873 if let Some((location, comment)) = s.split_once(',') {
1874 Ok(Self {
1875 location: location.parse()?,
1876 comment: Some(comment.to_owned()),
1877 })
1878 } else {
1879 Ok(Self {
1880 location: s.parse()?,
1881 comment: None,
1882 })
1883 }
1884 }
1885}
1886
1887#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1889pub struct USBNotecard {
1890 waypoints: Vec<USBWaypoint>,
1892}
1893
1894#[derive(Debug, thiserror::Error, strum::EnumIs)]
1896pub enum USBNotecardLoadError {
1897 #[error("I/O error opening or reading the file: {0}")]
1899 Io(#[from] std::io::Error),
1900 #[error("parse error deserializing the USB notecard lines: {0}")]
1902 LocationParseError(#[from] LocationParseError),
1903}
1904
1905impl USBNotecard {
1906 #[must_use]
1908 pub const fn new(waypoints: Vec<USBWaypoint>) -> Self {
1909 Self { waypoints }
1910 }
1911
1912 #[must_use]
1914 pub fn waypoints(&self) -> &[USBWaypoint] {
1915 &self.waypoints
1916 }
1917
1918 pub fn load_from_file(filename: &std::path::Path) -> Result<Self, USBNotecardLoadError> {
1925 let contents = std::fs::read_to_string(filename)?;
1926 Ok(contents.parse()?)
1927 }
1928}
1929
1930impl std::fmt::Display for USBNotecard {
1931 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1932 for waypoint in &self.waypoints {
1933 writeln!(f, "{waypoint}")?;
1934 }
1935 Ok(())
1936 }
1937}
1938
1939impl std::str::FromStr for USBNotecard {
1940 type Err = LocationParseError;
1941
1942 fn from_str(s: &str) -> Result<Self, Self::Err> {
1943 s.lines()
1944 .map(|line| line.parse::<USBWaypoint>())
1945 .collect::<Result<Vec<_>, _>>()
1946 .map(|waypoints| Self { waypoints })
1947 }
1948}
1949
1950#[cfg(test)]
1951mod test {
1952 use super::*;
1953 use pretty_assertions::assert_eq;
1954
1955 #[test]
1956 fn test_parse_location_bare() -> Result<(), Box<dyn std::error::Error>> {
1957 assert_eq!(
1958 "Beach%20Valley/110/67/24".parse::<Location>(),
1959 Ok(Location {
1960 region_name: RegionName::try_new("Beach Valley")?,
1961 x: 110,
1962 y: 67,
1963 z: 24
1964 }),
1965 );
1966 Ok(())
1967 }
1968
1969 #[test]
1970 fn test_parse_location_url_maps() -> Result<(), Box<dyn std::error::Error>> {
1971 assert_eq!(
1972 "http://maps.secondlife.com/secondlife/Beach%20Valley/110/67/24".parse::<Location>(),
1973 Ok(Location {
1974 region_name: RegionName::try_new("Beach Valley")?,
1975 x: 110,
1976 y: 67,
1977 z: 24
1978 }),
1979 );
1980 Ok(())
1981 }
1982
1983 #[test]
1984 fn test_as_maps_url_percent_encodes_spaces() -> Result<(), Box<dyn std::error::Error>> {
1985 let location = Location::new(RegionName::try_new("Beach Valley")?, 110, 67, 24);
1986 let url = location.as_maps_url();
1987 assert_eq!(
1988 url,
1989 "https://maps.secondlife.com/secondlife/Beach%20Valley/110/67/24"
1990 );
1991 assert_eq!(url.parse::<Location>(), Ok(location));
1993 Ok(())
1994 }
1995
1996 #[test]
1997 fn test_parse_location_url_slurl() -> Result<(), Box<dyn std::error::Error>> {
1998 assert_eq!(
1999 "http://slurl.com/secondlife/Beach%20Valley/110/67/24".parse::<Location>(),
2000 Ok(Location {
2001 region_name: RegionName::try_new("Beach Valley")?,
2002 x: 110,
2003 y: 67,
2004 z: 24
2005 }),
2006 );
2007 Ok(())
2008 }
2009
2010 #[test]
2011 fn test_parse_location_bare_with_usb_comment() -> Result<(), Box<dyn std::error::Error>> {
2012 assert_eq!(
2013 "Beach%20Valley/110/67/24,MUSTER".parse::<Location>(),
2014 Ok(Location {
2015 region_name: RegionName::try_new("Beach Valley")?,
2016 x: 110,
2017 y: 67,
2018 z: 24
2019 }),
2020 );
2021 Ok(())
2022 }
2023
2024 #[test]
2025 fn test_grid_rectangle_intersection_upper_right_corner()
2026 -> Result<(), Box<dyn std::error::Error>> {
2027 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2028 let rect2 = GridRectangle::new(GridCoordinates::new(15, 15), GridCoordinates::new(25, 25));
2029 assert_eq!(
2030 rect1.intersect(&rect2),
2031 Some(GridRectangle::new(
2032 GridCoordinates::new(15, 15),
2033 GridCoordinates::new(20, 20),
2034 ))
2035 );
2036 Ok(())
2037 }
2038
2039 #[test]
2040 fn test_grid_rectangle_intersection_upper_left_corner() -> Result<(), Box<dyn std::error::Error>>
2041 {
2042 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2043 let rect2 = GridRectangle::new(GridCoordinates::new(5, 15), GridCoordinates::new(15, 25));
2044 assert_eq!(
2045 rect1.intersect(&rect2),
2046 Some(GridRectangle::new(
2047 GridCoordinates::new(10, 15),
2048 GridCoordinates::new(15, 20),
2049 ))
2050 );
2051 Ok(())
2052 }
2053
2054 #[test]
2055 fn test_grid_rectangle_intersection_lower_left_corner() -> Result<(), Box<dyn std::error::Error>>
2056 {
2057 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2058 let rect2 = GridRectangle::new(GridCoordinates::new(5, 5), GridCoordinates::new(15, 15));
2059 assert_eq!(
2060 rect1.intersect(&rect2),
2061 Some(GridRectangle::new(
2062 GridCoordinates::new(10, 10),
2063 GridCoordinates::new(15, 15),
2064 ))
2065 );
2066 Ok(())
2067 }
2068
2069 #[test]
2070 fn test_grid_rectangle_intersection_lower_right_corner()
2071 -> Result<(), Box<dyn std::error::Error>> {
2072 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2073 let rect2 = GridRectangle::new(GridCoordinates::new(15, 5), GridCoordinates::new(25, 15));
2074 assert_eq!(
2075 rect1.intersect(&rect2),
2076 Some(GridRectangle::new(
2077 GridCoordinates::new(15, 10),
2078 GridCoordinates::new(20, 15),
2079 ))
2080 );
2081 Ok(())
2082 }
2083
2084 #[test]
2085 fn test_grid_rectangle_intersection_no_overlap() -> Result<(), Box<dyn std::error::Error>> {
2086 let rect1 = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2087 let rect2 = GridRectangle::new(GridCoordinates::new(30, 30), GridCoordinates::new(40, 40));
2088 assert_eq!(rect1.intersect(&rect2), None);
2089 Ok(())
2090 }
2091
2092 #[test]
2093 fn test_grid_rectangle_expanded_west() {
2094 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2095 assert_eq!(rect.expanded_west(0), rect);
2096 assert_eq!(
2097 rect.expanded_west(3),
2098 GridRectangle::new(GridCoordinates::new(7, 10), GridCoordinates::new(20, 20)),
2099 );
2100 let near_edge =
2101 GridRectangle::new(GridCoordinates::new(2, 10), GridCoordinates::new(20, 20));
2102 assert_eq!(
2103 near_edge.expanded_west(5),
2104 GridRectangle::new(GridCoordinates::new(0, 10), GridCoordinates::new(20, 20)),
2105 );
2106 }
2107
2108 #[test]
2109 fn test_grid_rectangle_expanded_east() {
2110 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2111 assert_eq!(rect.expanded_east(0), rect);
2112 assert_eq!(
2113 rect.expanded_east(3),
2114 GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(23, 20)),
2115 );
2116 let near_edge = GridRectangle::new(
2117 GridCoordinates::new(10, 10),
2118 GridCoordinates::new(u32::MAX - 2, 20),
2119 );
2120 assert_eq!(
2121 near_edge.expanded_east(5),
2122 GridRectangle::new(
2123 GridCoordinates::new(10, 10),
2124 GridCoordinates::new(u32::MAX, 20),
2125 ),
2126 );
2127 }
2128
2129 #[test]
2130 fn test_grid_rectangle_expanded_south() {
2131 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2132 assert_eq!(rect.expanded_south(0), rect);
2133 assert_eq!(
2134 rect.expanded_south(3),
2135 GridRectangle::new(GridCoordinates::new(10, 7), GridCoordinates::new(20, 20)),
2136 );
2137 let near_edge =
2138 GridRectangle::new(GridCoordinates::new(10, 2), GridCoordinates::new(20, 20));
2139 assert_eq!(
2140 near_edge.expanded_south(5),
2141 GridRectangle::new(GridCoordinates::new(10, 0), GridCoordinates::new(20, 20)),
2142 );
2143 }
2144
2145 #[test]
2146 fn test_grid_rectangle_expanded_north() {
2147 let rect = GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 20));
2148 assert_eq!(rect.expanded_north(0), rect);
2149 assert_eq!(
2150 rect.expanded_north(3),
2151 GridRectangle::new(GridCoordinates::new(10, 10), GridCoordinates::new(20, 23)),
2152 );
2153 let near_edge = GridRectangle::new(
2154 GridCoordinates::new(10, 10),
2155 GridCoordinates::new(20, u32::MAX - 2),
2156 );
2157 assert_eq!(
2158 near_edge.expanded_north(5),
2159 GridRectangle::new(
2160 GridCoordinates::new(10, 10),
2161 GridCoordinates::new(20, u32::MAX),
2162 ),
2163 );
2164 }
2165
2166 #[cfg(feature = "chumsky")]
2167 #[test]
2168 fn test_url_region_name_parser_no_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2169 let region_name = "Viterbo";
2170 assert_eq!(
2171 url_region_name_parser().parse(region_name).into_result(),
2172 Ok(RegionName::try_new(region_name)?)
2173 );
2174 Ok(())
2175 }
2176
2177 #[cfg(feature = "chumsky")]
2178 #[test]
2179 fn test_url_region_name_parser_url_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2180 let region_name = "Da Boom";
2181 let input = region_name.replace(' ', "%20");
2182 assert_eq!(
2183 url_region_name_parser().parse(&input).into_result(),
2184 Ok(RegionName::try_new(region_name)?)
2185 );
2186 Ok(())
2187 }
2188
2189 #[cfg(feature = "chumsky")]
2190 #[test]
2191 fn test_region_name_parser_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2192 let region_name = "Da Boom";
2193 assert_eq!(
2194 region_name_parser().parse(region_name).into_result(),
2195 Ok(RegionName::try_new(region_name)?)
2196 );
2197 Ok(())
2198 }
2199
2200 #[cfg(feature = "chumsky")]
2201 #[test]
2202 fn test_url_location_parser_no_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2203 let region_name = "Viterbo";
2204 let input = format!("{region_name}/1/2/300");
2205 assert_eq!(
2206 url_location_parser().parse(&input).into_result(),
2207 Ok(Location {
2208 region_name: RegionName::try_new(region_name)?,
2209 x: 1,
2210 y: 2,
2211 z: 300
2212 })
2213 );
2214 Ok(())
2215 }
2216
2217 #[cfg(feature = "chumsky")]
2218 #[test]
2219 fn test_url_location_parser_url_whitespace() -> Result<(), Box<dyn std::error::Error>> {
2220 let region_name = "Da Boom";
2221 let input = format!("{}/1/2/300", region_name.replace(' ', "%20"));
2222 assert_eq!(
2223 url_location_parser().parse(&input).into_result(),
2224 Ok(Location {
2225 region_name: RegionName::try_new(region_name)?,
2226 x: 1,
2227 y: 2,
2228 z: 300
2229 })
2230 );
2231 Ok(())
2232 }
2233
2234 #[cfg(feature = "chumsky")]
2235 #[test]
2236 fn test_url_location_parser_url_whitespace_single_digit_after_space()
2237 -> Result<(), Box<dyn std::error::Error>> {
2238 let region_name = "Foo Bar 3";
2239 let input = format!("{}/1/2/300", region_name.replace(' ', "%20"));
2240 assert_eq!(
2241 url_location_parser().parse(&input).into_result(),
2242 Ok(Location {
2243 region_name: RegionName::try_new(region_name)?,
2244 x: 1,
2245 y: 2,
2246 z: 300
2247 })
2248 );
2249 Ok(())
2250 }
2251
2252 #[cfg(feature = "chumsky")]
2253 #[test]
2254 fn test_region_coordinates_parser() -> Result<(), Box<dyn std::error::Error>> {
2255 assert_eq!(
2256 region_coordinates_parser()
2257 .parse("{ 63.0486, 45.2515, 1501.08 }")
2258 .into_result(),
2259 Ok(RegionCoordinates {
2260 x: 63.0486,
2261 y: 45.2515,
2262 z: 1501.08,
2263 })
2264 );
2265 Ok(())
2266 }
2267}