1use crate::error::ParseError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct EventId(u32);
26
27impl EventId {
28 #[must_use]
30 pub const fn new(id: u32) -> Self {
31 Self(id)
32 }
33 #[must_use]
35 pub const fn get(self) -> u32 {
36 self.0
37 }
38}
39
40impl std::fmt::Display for EventId {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46impl serde::Serialize for EventId {
47 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
48 serializer.serialize_u32(self.0)
49 }
50}
51
52impl<'de> serde::Deserialize<'de> for EventId {
53 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
54 Ok(Self(u32::deserialize(deserializer)?))
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct GlwEventKey(String);
66
67impl GlwEventKey {
68 #[must_use]
70 pub fn new<S: Into<String>>(key: S) -> Self {
71 Self(key.into())
72 }
73 #[must_use]
75 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78}
79
80impl std::fmt::Display for GlwEventKey {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.write_str(&self.0)
83 }
84}
85
86impl serde::Serialize for GlwEventKey {
87 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88 serializer.serialize_str(&self.0)
89 }
90}
91
92impl<'de> serde::Deserialize<'de> for GlwEventKey {
93 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
94 Ok(Self(String::deserialize(deserializer)?))
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct WindDirection(u16);
108
109impl WindDirection {
110 pub fn try_new(degrees: u16) -> Result<Self, ParseError> {
116 if degrees > 359 {
117 return Err(ParseError::OutOfRange {
118 field: "wind_direction",
119 value: degrees.to_string(),
120 allowed: "0..=359",
121 });
122 }
123 Ok(Self(degrees))
124 }
125 #[must_use]
127 pub const fn degrees(self) -> u16 {
128 self.0
129 }
130}
131
132impl serde::Serialize for WindDirection {
133 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
134 serializer.serialize_u16(self.0)
135 }
136}
137
138impl<'de> serde::Deserialize<'de> for WindDirection {
139 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
140 let v = u16::deserialize(deserializer)?;
141 Self::try_new(v).map_err(serde::de::Error::custom)
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq)]
152pub struct KnotSpeed(f32);
153
154impl KnotSpeed {
155 #[must_use]
157 pub const fn new(knots: f32) -> Self {
158 Self(knots)
159 }
160 #[must_use]
162 pub const fn knots(self) -> f32 {
163 self.0
164 }
165}
166
167impl serde::Serialize for KnotSpeed {
168 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169 serializer.serialize_f32(self.0)
170 }
171}
172
173impl<'de> serde::Deserialize<'de> for KnotSpeed {
174 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
175 Ok(Self(f32::deserialize(deserializer)?))
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct GustsPercent(u8);
182
183impl GustsPercent {
184 pub fn try_new(percent: u8) -> Result<Self, ParseError> {
190 if percent > 100 {
191 return Err(ParseError::OutOfRange {
192 field: "gusts_percent",
193 value: percent.to_string(),
194 allowed: "0..=100",
195 });
196 }
197 Ok(Self(percent))
198 }
199 #[must_use]
201 pub const fn percent(self) -> u8 {
202 self.0
203 }
204}
205
206impl serde::Serialize for GustsPercent {
207 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
208 serializer.serialize_u8(self.0)
209 }
210}
211
212impl<'de> serde::Deserialize<'de> for GustsPercent {
213 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
214 let v = u8::deserialize(deserializer)?;
215 Self::try_new(v).map_err(serde::de::Error::custom)
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub struct ShiftsDegrees(u16);
222
223impl ShiftsDegrees {
224 pub fn try_new(degrees: u16) -> Result<Self, ParseError> {
230 if degrees > 180 {
231 return Err(ParseError::OutOfRange {
232 field: "shifts_degrees",
233 value: degrees.to_string(),
234 allowed: "0..=180",
235 });
236 }
237 Ok(Self(degrees))
238 }
239 #[must_use]
241 pub const fn degrees(self) -> u16 {
242 self.0
243 }
244}
245
246impl serde::Serialize for ShiftsDegrees {
247 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
248 serializer.serialize_u16(self.0)
249 }
250}
251
252impl<'de> serde::Deserialize<'de> for ShiftsDegrees {
253 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
254 let v = u16::deserialize(deserializer)?;
255 Self::try_new(v).map_err(serde::de::Error::custom)
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub struct Period(u32);
262
263impl Period {
264 #[must_use]
266 pub const fn new(seconds: u32) -> Self {
267 Self(seconds)
268 }
269 #[must_use]
271 pub const fn seconds(self) -> u32 {
272 self.0
273 }
274}
275
276impl serde::Serialize for Period {
277 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
278 serializer.serialize_u32(self.0)
279 }
280}
281
282impl<'de> serde::Deserialize<'de> for Period {
283 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
284 Ok(Self(u32::deserialize(deserializer)?))
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq)]
292pub struct WaveHeight(f32);
293
294impl WaveHeight {
295 #[must_use]
297 pub const fn new(meters: f32) -> Self {
298 Self(meters)
299 }
300 #[must_use]
302 pub const fn meters(self) -> f32 {
303 self.0
304 }
305}
306
307impl serde::Serialize for WaveHeight {
308 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
309 serializer.serialize_f32(self.0)
310 }
311}
312
313impl<'de> serde::Deserialize<'de> for WaveHeight {
314 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
315 Ok(Self(f32::deserialize(deserializer)?))
316 }
317}
318
319#[derive(Debug, Clone, Copy, PartialEq)]
321pub struct WaveLength(f32);
322
323impl WaveLength {
324 #[must_use]
326 pub const fn new(meters: f32) -> Self {
327 Self(meters)
328 }
329 #[must_use]
331 pub const fn meters(self) -> f32 {
332 self.0
333 }
334}
335
336impl serde::Serialize for WaveLength {
337 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
338 serializer.serialize_f32(self.0)
339 }
340}
341
342impl<'de> serde::Deserialize<'de> for WaveLength {
343 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
344 Ok(Self(f32::deserialize(deserializer)?))
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
350pub struct PercentVariance(u8);
351
352impl PercentVariance {
353 pub fn try_new(percent: u8) -> Result<Self, ParseError> {
359 if percent > 100 {
360 return Err(ParseError::OutOfRange {
361 field: "percent_variance",
362 value: percent.to_string(),
363 allowed: "0..=100",
364 });
365 }
366 Ok(Self(percent))
367 }
368 #[must_use]
370 pub const fn percent(self) -> u8 {
371 self.0
372 }
373}
374
375impl serde::Serialize for PercentVariance {
376 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
377 serializer.serialize_u8(self.0)
378 }
379}
380
381impl<'de> serde::Deserialize<'de> for PercentVariance {
382 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
383 let v = u8::deserialize(deserializer)?;
384 Self::try_new(v).map_err(serde::de::Error::custom)
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct WaveEffects {
397 pub speed: bool,
399 pub steer: bool,
401}
402
403impl serde::Serialize for WaveEffects {
404 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
405 use serde::ser::SerializeStruct as _;
406 let mut s = serializer.serialize_struct("WaveEffects", 2)?;
407 s.serialize_field("speed", &u8::from(self.speed))?;
408 s.serialize_field("steer", &u8::from(self.steer))?;
409 s.end()
410 }
411}
412
413impl<'de> serde::Deserialize<'de> for WaveEffects {
414 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
415 #[derive(serde::Deserialize)]
417 #[serde(untagged)]
418 enum Raw {
419 Bitmask(u8),
422 Object {
424 #[serde(default)]
426 speed: u8,
427 #[serde(default)]
429 steer: u8,
430 },
431 }
432 let raw = Raw::deserialize(deserializer)?;
433 Ok(match raw {
434 Raw::Bitmask(n) => Self {
435 speed: (n & 0b10) != 0,
436 steer: (n & 0b01) != 0,
437 },
438 Raw::Object { speed, steer } => Self {
439 speed: speed != 0,
440 steer: steer != 0,
441 },
442 })
443 }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq)]
453pub struct WaterDepth(f32);
454
455impl WaterDepth {
456 pub fn try_new(meters: f32) -> Result<Self, ParseError> {
462 if meters > 0.0 {
463 Ok(Self(meters))
464 } else {
465 Err(ParseError::OutOfRange {
466 field: "water_depth",
467 value: meters.to_string(),
468 allowed: "> 0",
469 })
470 }
471 }
472 #[must_use]
474 pub const fn meters(self) -> f32 {
475 self.0
476 }
477}
478
479impl serde::Serialize for WaterDepth {
480 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
481 serializer.serialize_f32(self.0)
482 }
483}
484
485#[derive(Debug, Clone, Copy, PartialEq)]
490pub enum WaterDepthSetting {
491 Disabled,
493 Set(WaterDepth),
495}
496
497impl serde::Serialize for WaterDepthSetting {
498 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
499 match *self {
500 Self::Disabled => serializer.serialize_f32(0.0),
501 Self::Set(d) => serializer.serialize_f32(d.meters()),
502 }
503 }
504}
505
506impl<'de> serde::Deserialize<'de> for WaterDepthSetting {
507 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
508 let v = f32::deserialize(deserializer)?;
509 let is_zero = v == 0.0;
512 if is_zero {
513 Ok(Self::Disabled)
514 } else {
515 WaterDepth::try_new(v)
516 .map(Self::Set)
517 .map_err(serde::de::Error::custom)
518 }
519 }
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
527pub struct MarginMeters(u8);
528
529impl MarginMeters {
530 #[must_use]
532 pub fn new_clamped(meters: u16) -> Self {
533 if meters > 100 {
534 tracing::warn!(
535 requested = meters,
536 clamped = 100u8,
537 "GLW margin exceeds documented maximum (100m); clamping",
538 );
539 Self(100)
540 } else {
541 Self(u8::try_from(meters).unwrap_or(100))
543 }
544 }
545 #[must_use]
547 pub const fn meters(self) -> u8 {
548 self.0
549 }
550}
551
552impl serde::Serialize for MarginMeters {
553 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
554 serializer.serialize_u8(self.0)
555 }
556}
557
558impl<'de> serde::Deserialize<'de> for MarginMeters {
559 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
560 Ok(Self::new_clamped(u16::deserialize(deserializer)?))
561 }
562}
563
564#[derive(Debug, Clone, Copy, PartialEq)]
566pub struct RadiusMeters(f32);
567
568impl RadiusMeters {
569 pub fn try_new(meters: f32) -> Result<Self, ParseError> {
575 if meters < 0.0 {
576 return Err(ParseError::OutOfRange {
577 field: "radius_meters",
578 value: meters.to_string(),
579 allowed: ">= 0",
580 });
581 }
582 Ok(Self(meters))
583 }
584 #[must_use]
586 pub const fn meters(self) -> f32 {
587 self.0
588 }
589}
590
591impl serde::Serialize for RadiusMeters {
592 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
593 serializer.serialize_f32(self.0)
594 }
595}
596
597impl<'de> serde::Deserialize<'de> for RadiusMeters {
598 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
599 let v = f32::deserialize(deserializer)?;
600 Self::try_new(v).map_err(serde::de::Error::custom)
601 }
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
608pub struct Overlap(bool);
609
610impl Overlap {
611 #[must_use]
613 pub const fn new(v: bool) -> Self {
614 Self(v)
615 }
616 #[must_use]
618 pub const fn get(self) -> bool {
619 self.0
620 }
621}
622
623impl serde::Serialize for Overlap {
624 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
625 serializer.serialize_u8(u8::from(self.0))
626 }
627}
628
629impl<'de> serde::Deserialize<'de> for Overlap {
630 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
631 Ok(Self(u8::deserialize(deserializer)? != 0))
632 }
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
642pub struct BaseWind {
643 #[serde(rename = "dir")]
645 pub direction: WindDirection,
646 pub speed: KnotSpeed,
648 pub gusts: GustsPercent,
650 pub shifts: ShiftsDegrees,
652 pub period: Period,
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct BaseWaves {
661 pub height: WaveHeight,
664 pub speed: KnotSpeed,
666 pub length: WaveLength,
668 pub effects: WaveEffects,
670 pub height_var: PercentVariance,
672 pub length_var: PercentVariance,
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct BaseCurrents {
681 pub speed: KnotSpeed,
684 #[serde(rename = "dir")]
687 pub direction: WindDirection,
688 pub water_depth: WaterDepthSetting,
691}
692
693#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
695pub struct Base {
696 pub wind: BaseWind,
698 pub waves: BaseWaves,
700 pub currents: BaseCurrents,
702}
703
704#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
711#[serde(default)]
712pub struct WindOverride {
713 #[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
715 pub direction: Option<WindDirection>,
716 #[serde(skip_serializing_if = "Option::is_none")]
718 pub speed: Option<KnotSpeed>,
719 #[serde(skip_serializing_if = "Option::is_none")]
721 pub gusts: Option<GustsPercent>,
722 #[serde(skip_serializing_if = "Option::is_none")]
724 pub shifts: Option<ShiftsDegrees>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub period: Option<Period>,
728}
729
730impl WindOverride {
731 #[must_use]
733 pub const fn is_empty(&self) -> bool {
734 self.direction.is_none()
735 && self.speed.is_none()
736 && self.gusts.is_none()
737 && self.shifts.is_none()
738 && self.period.is_none()
739 }
740}
741
742#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
744#[serde(default, rename_all = "camelCase")]
745pub struct WavesOverride {
746 #[serde(skip_serializing_if = "Option::is_none")]
748 pub height: Option<WaveHeight>,
749 #[serde(skip_serializing_if = "Option::is_none")]
751 pub speed: Option<KnotSpeed>,
752 #[serde(skip_serializing_if = "Option::is_none")]
754 pub length: Option<WaveLength>,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub effects: Option<WaveEffects>,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub height_var: Option<PercentVariance>,
761 #[serde(skip_serializing_if = "Option::is_none")]
763 pub length_var: Option<PercentVariance>,
764}
765
766impl WavesOverride {
767 #[must_use]
769 pub const fn is_empty(&self) -> bool {
770 self.height.is_none()
771 && self.speed.is_none()
772 && self.length.is_none()
773 && self.effects.is_none()
774 && self.height_var.is_none()
775 && self.length_var.is_none()
776 }
777}
778
779#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
786#[serde(default, rename_all = "camelCase")]
787pub struct CurrentsOverride {
788 #[serde(skip_serializing_if = "Option::is_none")]
790 pub speed: Option<KnotSpeed>,
791 #[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
793 pub direction: Option<WindDirection>,
794 #[serde(skip_serializing_if = "Option::is_none")]
797 pub water_depth: Option<WaterDepthSetting>,
798}
799
800impl CurrentsOverride {
801 #[must_use]
803 pub const fn is_empty(&self) -> bool {
804 self.speed.is_none() && self.direction.is_none() && self.water_depth.is_none()
805 }
806}
807
808#[derive(Debug, Clone, PartialEq)]
818pub struct Area {
819 pub name: String,
821 pub grid_rectangle: sl_types::map::GridRectangle,
823 pub margin: MarginMeters,
825 pub overlap: Overlap,
828 pub wind: Option<WindOverride>,
830 pub waves: Option<WavesOverride>,
832 pub currents: Option<CurrentsOverride>,
834}
835
836#[derive(Debug, Clone, PartialEq)]
840pub struct Circle {
841 pub name: String,
843 pub center_sim: sl_types::map::GridCoordinates,
845 pub center_point: sl_types::map::RegionCoordinates,
848 pub radius: RadiusMeters,
850 pub margin: MarginMeters,
852 pub overlap: Overlap,
855 pub wind: Option<WindOverride>,
857 pub waves: Option<WavesOverride>,
859 pub currents: Option<CurrentsOverride>,
861}
862
863#[derive(Debug, Clone, Default, PartialEq)]
869pub struct AreaList(pub Vec<Area>);
870
871impl AreaList {
872 #[must_use]
874 pub fn as_slice(&self) -> &[Area] {
875 &self.0
876 }
877 #[must_use]
879 pub const fn len(&self) -> usize {
880 self.0.len()
881 }
882 #[must_use]
884 pub const fn is_empty(&self) -> bool {
885 self.0.is_empty()
886 }
887}
888
889impl serde::Serialize for AreaList {
890 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
891 use serde::ser::SerializeMap as _;
892 let mut map = serializer.serialize_map(Some(self.0.len()))?;
893 for area in &self.0 {
894 map.serialize_entry(&area.name, &AreaSerializable::from(area))?;
895 }
896 map.end()
897 }
898}
899
900#[derive(serde::Serialize)]
904struct AreaSerializable<'a> {
905 #[serde(rename = "coordSW")]
907 coord_sw: SimCoord,
908 #[serde(rename = "coordNE")]
910 coord_ne: SimCoord,
911 margin: &'a MarginMeters,
913 overlap: &'a Overlap,
916 #[serde(skip_serializing_if = "Option::is_none")]
918 wind: Option<&'a WindOverride>,
919 #[serde(skip_serializing_if = "Option::is_none")]
921 waves: Option<&'a WavesOverride>,
922 #[serde(skip_serializing_if = "Option::is_none")]
924 currents: Option<&'a CurrentsOverride>,
925}
926
927impl<'a> From<&'a Area> for AreaSerializable<'a> {
928 fn from(a: &'a Area) -> Self {
929 Self {
930 coord_sw: SimCoord::from_grid(&a.grid_rectangle, GridCorner::Sw),
931 coord_ne: SimCoord::from_grid(&a.grid_rectangle, GridCorner::Ne),
932 margin: &a.margin,
933 overlap: &a.overlap,
934 wind: a.wind.as_ref(),
935 waves: a.waves.as_ref(),
936 currents: a.currents.as_ref(),
937 }
938 }
939}
940
941#[derive(Debug, Clone, Default, PartialEq)]
947pub struct CircleList(pub Vec<Circle>);
948
949impl CircleList {
950 #[must_use]
952 pub fn as_slice(&self) -> &[Circle] {
953 &self.0
954 }
955 #[must_use]
957 pub const fn len(&self) -> usize {
958 self.0.len()
959 }
960 #[must_use]
962 pub const fn is_empty(&self) -> bool {
963 self.0.is_empty()
964 }
965}
966
967impl serde::Serialize for CircleList {
968 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
969 use serde::ser::SerializeMap as _;
970 let mut map = serializer.serialize_map(Some(self.0.len()))?;
971 for circle in &self.0 {
972 map.serialize_entry(&circle.name, &CircleSerializable::from(circle))?;
973 }
974 map.end()
975 }
976}
977
978#[derive(serde::Serialize)]
981struct CircleSerializable<'a> {
982 #[serde(rename = "centerSim")]
984 center_sim: SimCoord,
985 #[serde(rename = "centerPoint")]
987 center_point: RegionXY,
988 radius: &'a RadiusMeters,
990 margin: &'a MarginMeters,
992 overlap: &'a Overlap,
994 #[serde(skip_serializing_if = "Option::is_none")]
996 wind: Option<&'a WindOverride>,
997 #[serde(skip_serializing_if = "Option::is_none")]
999 waves: Option<&'a WavesOverride>,
1000 #[serde(skip_serializing_if = "Option::is_none")]
1002 currents: Option<&'a CurrentsOverride>,
1003}
1004
1005impl<'a> From<&'a Circle> for CircleSerializable<'a> {
1006 fn from(c: &'a Circle) -> Self {
1007 Self {
1008 center_sim: SimCoord {
1009 x: c.center_sim.x(),
1010 y: c.center_sim.y(),
1011 },
1012 center_point: RegionXY {
1013 x: c.center_point.x(),
1014 y: c.center_point.y(),
1015 },
1016 radius: &c.radius,
1017 margin: &c.margin,
1018 overlap: &c.overlap,
1019 wind: c.wind.as_ref(),
1020 waves: c.waves.as_ref(),
1021 currents: c.currents.as_ref(),
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone, Copy)]
1028enum GridCorner {
1029 Sw,
1031 Ne,
1033}
1034
1035#[derive(Debug, Clone, Copy, serde::Serialize)]
1038struct SimCoord {
1039 x: u32,
1041 y: u32,
1043}
1044
1045impl SimCoord {
1046 fn from_grid(rect: &sl_types::map::GridRectangle, which: GridCorner) -> Self {
1049 use sl_types::map::GridRectangleLike as _;
1050 let gc = match which {
1051 GridCorner::Sw => rect.lower_left_corner(),
1052 GridCorner::Ne => rect.upper_right_corner(),
1053 };
1054 Self {
1055 x: gc.x(),
1056 y: gc.y(),
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Clone, Copy, serde::Serialize)]
1065struct RegionXY {
1066 x: f32,
1068 y: f32,
1070}
1071
1072#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct GlwEvent {
1079 pub event_id: EventId,
1081 pub event_name: String,
1083 pub event_key: GlwEventKey,
1085 #[serde(default)]
1087 pub event_num: Option<u32>,
1088 pub director_name: String,
1090 pub director_key: String,
1093 #[serde(default)]
1096 pub sail_mode: Option<u8>,
1097 #[serde(default)]
1100 pub extra1: String,
1101 #[serde(default)]
1104 pub extra2: String,
1105 pub base: Base,
1107 #[serde(default, deserialize_with = "crate::parse::deserialize_area_list")]
1110 pub areas: AreaList,
1111 #[serde(default, deserialize_with = "crate::parse::deserialize_circle_list")]
1113 pub circles: CircleList,
1114}
1115
1116#[cfg(test)]
1117mod test {
1118 use super::*;
1119 use pretty_assertions::assert_eq;
1120
1121 #[test]
1122 fn wind_direction_rejects_out_of_range() {
1123 assert!(WindDirection::try_new(0).is_ok(), "0 is in range");
1124 assert!(WindDirection::try_new(359).is_ok(), "359 is in range");
1125 assert!(WindDirection::try_new(360).is_err(), "360 is out of range");
1126 assert!(WindDirection::try_new(999).is_err(), "999 is out of range");
1127 }
1128
1129 #[test]
1130 fn gusts_rejects_out_of_range() {
1131 assert!(GustsPercent::try_new(0).is_ok(), "0 is in range");
1132 assert!(GustsPercent::try_new(100).is_ok(), "100 is in range");
1133 assert!(GustsPercent::try_new(101).is_err(), "101 is out of range");
1134 }
1135
1136 #[test]
1137 fn shifts_rejects_out_of_range() {
1138 assert!(ShiftsDegrees::try_new(180).is_ok(), "180 is in range");
1139 assert!(ShiftsDegrees::try_new(181).is_err(), "181 is out of range");
1140 }
1141
1142 #[test]
1143 fn margin_clamps_at_100() {
1144 assert_eq!(MarginMeters::new_clamped(0).meters(), 0);
1145 assert_eq!(MarginMeters::new_clamped(100).meters(), 100);
1146 assert_eq!(MarginMeters::new_clamped(150).meters(), 100);
1147 }
1148
1149 #[test]
1150 fn water_depth_rejects_zero_and_negative() {
1151 assert!(WaterDepth::try_new(1.0).is_ok(), "1.0 is positive");
1152 assert!(WaterDepth::try_new(0.0).is_err(), "0.0 is rejected");
1153 assert!(WaterDepth::try_new(-0.5).is_err(), "negative rejected");
1154 }
1155
1156 #[test]
1157 fn water_depth_setting_maps_zero_to_disabled() -> Result<(), Box<dyn std::error::Error>> {
1158 let disabled: WaterDepthSetting = serde_json::from_str("0")?;
1159 assert_eq!(disabled, WaterDepthSetting::Disabled);
1160 let positive: WaterDepthSetting = serde_json::from_str("6.5")?;
1161 match positive {
1162 WaterDepthSetting::Set(d) => {
1163 assert!((d.meters() - 6.5).abs() < 1e-6, "6.5 round-trips");
1164 }
1165 WaterDepthSetting::Disabled => {
1166 return Err("6.5 must not become Disabled".into());
1167 }
1168 }
1169 let negative: Result<WaterDepthSetting, _> = serde_json::from_str("-1");
1170 assert!(negative.is_err(), "negative rejected");
1171 Ok(())
1172 }
1173
1174 #[test]
1175 fn overlap_from_0_and_1() -> Result<(), Box<dyn std::error::Error>> {
1176 let off: Overlap = serde_json::from_str("0")?;
1177 assert_eq!(off, Overlap::new(false));
1178 let on: Overlap = serde_json::from_str("1")?;
1179 assert_eq!(on, Overlap::new(true));
1180 Ok(())
1181 }
1182
1183 #[test]
1184 fn wave_effects_from_object() -> Result<(), Box<dyn std::error::Error>> {
1185 let we: WaveEffects = serde_json::from_str(r#"{"speed":1,"steer":0}"#)?;
1186 assert!(we.speed, "speed=1 turns on");
1187 assert!(!we.steer, "steer=0 stays off");
1188 Ok(())
1189 }
1190
1191 #[test]
1192 fn wave_effects_from_bitmask() -> Result<(), Box<dyn std::error::Error>> {
1193 let we: WaveEffects = serde_json::from_str("3")?;
1194 assert!(we.speed && we.steer, "bitmask 3 = both on");
1195 let we: WaveEffects = serde_json::from_str("2")?;
1196 assert!(we.speed && !we.steer, "bitmask 2 = speed only");
1197 let we: WaveEffects = serde_json::from_str("1")?;
1198 assert!(!we.speed && we.steer, "bitmask 1 = steer only");
1199 let we: WaveEffects = serde_json::from_str("0")?;
1200 assert!(!we.speed && !we.steer, "bitmask 0 = neither");
1201 Ok(())
1202 }
1203}