Skip to main content

sl_glw/
types.rs

1//! GlobalWind (GLW) data types.
2//!
3//! All types correspond directly to fields in the JSON shape documented
4//! at the top of the repository `TODO.md`. Numeric values are wrapped in
5//! validating newtypes so out-of-range data fails at parse time rather
6//! than silently producing wrong wind arrows on the map.
7//!
8//! Direction conventions (verified against the GLW LSL source):
9//! - 0° = North, increasing clockwise, in degrees.
10//! - Wind direction is the direction the wind blows **from**
11//!   (meteorological), so a north wind blows toward the south.
12//! - Current direction follows the same convention.
13
14use crate::error::ParseError;
15
16// ---------------------------------------------------------------------------
17// Identifiers
18// ---------------------------------------------------------------------------
19
20/// Numeric event identifier assigned by the GLW server.
21///
22/// Used as the value of the `?id=…` query parameter on the GLW
23/// `glwDataReq.php` endpoint.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct EventId(u32);
26
27impl EventId {
28    /// Wrap a raw numeric id.
29    #[must_use]
30    pub const fn new(id: u32) -> Self {
31        Self(id)
32    }
33    /// The underlying numeric id.
34    #[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/// String event key (the `eventKey` field of the JSON document).
59///
60/// The GLW server treats this as opaque text and looks it up via the
61/// `?key=…` query parameter on the `glwDataReq.php` endpoint. Named with
62/// the `Glw` prefix to avoid colliding with `sl_types::key::EventKey`
63/// which represents a Second Life UUID for an event listing.
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct GlwEventKey(String);
66
67impl GlwEventKey {
68    /// Wrap a raw event-key string.
69    #[must_use]
70    pub fn new<S: Into<String>>(key: S) -> Self {
71        Self(key.into())
72    }
73    /// Borrow the underlying string.
74    #[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// ---------------------------------------------------------------------------
99// Validated scalar newtypes
100// ---------------------------------------------------------------------------
101
102/// Wind or current direction in degrees from North, increasing clockwise.
103///
104/// Valid range is `0..=359`. By GLW/meteorological convention, a value
105/// of e.g. `180` means the wind is coming **from** the south.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub struct WindDirection(u16);
108
109impl WindDirection {
110    /// Construct after range validation.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`ParseError::OutOfRange`] if `degrees` is not in `0..=359`.
115    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    /// Raw integer degrees in `0..=359`.
126    #[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/// Wind / wave / current speed in knots.
146///
147/// Stored as `f32`; GLW JSON may use either integer or floating-point
148/// representations and serde_json promotes integers transparently. We do
149/// not range-validate here because reasonable upper bounds on wind speed
150/// are domain-dependent.
151#[derive(Debug, Clone, Copy, PartialEq)]
152pub struct KnotSpeed(f32);
153
154impl KnotSpeed {
155    /// Wrap a raw float.
156    #[must_use]
157    pub const fn new(knots: f32) -> Self {
158        Self(knots)
159    }
160    /// The raw value in knots.
161    #[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/// Gust intensity as a percentage of base wind speed, valid in `0..=100`.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct GustsPercent(u8);
182
183impl GustsPercent {
184    /// Construct after range validation.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`ParseError::OutOfRange`] if `percent` is greater than 100.
189    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    /// Raw percentage in `0..=100`.
200    #[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/// Maximum wind shift in degrees per period (`0..=180`).
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub struct ShiftsDegrees(u16);
222
223impl ShiftsDegrees {
224    /// Construct after range validation.
225    ///
226    /// # Errors
227    ///
228    /// Returns [`ParseError::OutOfRange`] if `degrees` is greater than 180.
229    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    /// Raw degrees in `0..=180`.
240    #[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/// Wind shift and gust period in seconds.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261pub struct Period(u32);
262
263impl Period {
264    /// Wrap a raw count of seconds.
265    #[must_use]
266    pub const fn new(seconds: u32) -> Self {
267        Self(seconds)
268    }
269    /// The raw period in seconds.
270    #[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/// Wave height in meters. A value of 0 in JSON is preserved literally
289/// (per the GLW spec: "if height=0 there are no waves, but it is set to
290/// fill the values of the variations").
291#[derive(Debug, Clone, Copy, PartialEq)]
292pub struct WaveHeight(f32);
293
294impl WaveHeight {
295    /// Wrap a raw float.
296    #[must_use]
297    pub const fn new(meters: f32) -> Self {
298        Self(meters)
299    }
300    /// The raw height in meters.
301    #[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/// Wave length in meters.
320#[derive(Debug, Clone, Copy, PartialEq)]
321pub struct WaveLength(f32);
322
323impl WaveLength {
324    /// Wrap a raw float.
325    #[must_use]
326    pub const fn new(meters: f32) -> Self {
327        Self(meters)
328    }
329    /// The raw length in meters.
330    #[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/// Percentage variance applied to a wave height/length (`0..=100`).
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
350pub struct PercentVariance(u8);
351
352impl PercentVariance {
353    /// Construct after range validation.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`ParseError::OutOfRange`] if `percent` is greater than 100.
358    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    /// Raw percent in `0..=100`.
369    #[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/// Two boolean toggles describing how waves affect a boat: whether they
389/// modulate its speed and whether they perturb its heading.
390///
391/// In GLW JSON the field appears either as an object
392/// `{"speed": 0|1, "steer": 0|1}` or as an integer bitmask
393/// (`0 = none`, `1 = steer`, `2 = speed`, `3 = both`). Both forms are
394/// accepted.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct WaveEffects {
397    /// Whether waves modulate the boat's forward speed.
398    pub speed: bool,
399    /// Whether waves perturb the boat's heading.
400    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        /// Internal helper to deserialize either of the two JSON shapes.
416        #[derive(serde::Deserialize)]
417        #[serde(untagged)]
418        enum Raw {
419            /// JSON integer 0..=3, treated as a bitmask
420            /// (bit 0 = steer, bit 1 = speed) per the GLW LSL convention.
421            Bitmask(u8),
422            /// JSON object with `speed` and `steer` ints (0 or 1).
423            Object {
424                /// `1` if the speed effect is on, `0` otherwise.
425                #[serde(default)]
426                speed: u8,
427                /// `1` if the steer effect is on, `0` otherwise.
428                #[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/// A strictly positive water depth in meters.
447///
448/// The GLW JSON uses `0` to mean "depth-attenuation disabled"; this
449/// crate models that as the absence of a value (see
450/// [`WaterDepthSetting`]) instead of allowing a zero value here. Negative
451/// values are rejected at parse time.
452#[derive(Debug, Clone, Copy, PartialEq)]
453pub struct WaterDepth(f32);
454
455impl WaterDepth {
456    /// Construct after validation.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`ParseError::OutOfRange`] if `meters <= 0.0`.
461    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    /// The raw depth in meters (always strictly positive).
473    #[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/// Tri-state setting for water depth on a `BaseCurrents` block.
486///
487/// JSON value 0 maps to [`Self::Disabled`]. Positive values map to
488/// [`Self::Set`]. Negative values are rejected.
489#[derive(Debug, Clone, Copy, PartialEq)]
490pub enum WaterDepthSetting {
491    /// Depth attenuation is disabled (JSON `0`).
492    Disabled,
493    /// A positive depth in meters.
494    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        // exact 0.0 is the GLW disabled sentinel; serde_json maps the
510        // integer 0 to exactly 0.0_f32 so the comparison is well-defined.
511        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/// Per-area or per-circle margin in meters, clamped to `0..=100`.
523///
524/// The GLW spec documents a maximum of 100 m; values above 100 are
525/// clamped to 100 and emit a `tracing::warn!` rather than failing.
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
527pub struct MarginMeters(u8);
528
529impl MarginMeters {
530    /// Construct, clamping values above 100 (with a `tracing::warn!`).
531    #[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            // safe: meters <= 100 fits in u8
542            Self(u8::try_from(meters).unwrap_or(100))
543        }
544    }
545    /// Raw margin in meters (`0..=100`).
546    #[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/// Radius of a GLW circle in meters.
565#[derive(Debug, Clone, Copy, PartialEq)]
566pub struct RadiusMeters(f32);
567
568impl RadiusMeters {
569    /// Construct after non-negative validation.
570    ///
571    /// # Errors
572    ///
573    /// Returns [`ParseError::OutOfRange`] if `meters < 0.0`.
574    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    /// Raw radius in meters.
585    #[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/// Whether an area or circle is a nested override on top of another
605/// area or circle (`overlap = 1`) rather than directly modifying the
606/// base wind.
607#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
608pub struct Overlap(bool);
609
610impl Overlap {
611    /// Wrap a raw bool.
612    #[must_use]
613    pub const fn new(v: bool) -> Self {
614        Self(v)
615    }
616    /// The raw bool value.
617    #[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// ---------------------------------------------------------------------------
636// Base wind / waves / currents (always fully populated)
637// ---------------------------------------------------------------------------
638
639/// Base wind parameters that apply to the whole event unless an area or
640/// circle override is in effect.
641#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
642pub struct BaseWind {
643    /// Direction the wind blows **from**, in degrees clockwise from North.
644    #[serde(rename = "dir")]
645    pub direction: WindDirection,
646    /// Base wind speed in knots.
647    pub speed: KnotSpeed,
648    /// Gust intensity as a percentage of base wind speed.
649    pub gusts: GustsPercent,
650    /// Maximum wind shift in degrees per period.
651    pub shifts: ShiftsDegrees,
652    /// Period of shift/gust oscillation in seconds.
653    pub period: Period,
654}
655
656/// Base wave parameters that apply to the whole event unless an area or
657/// circle override is in effect.
658#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct BaseWaves {
661    /// Wave height in meters. A height of zero means "no waves", though
662    /// other fields may still carry meaningful variance values.
663    pub height: WaveHeight,
664    /// Wave speed in knots.
665    pub speed: KnotSpeed,
666    /// Wave length in meters.
667    pub length: WaveLength,
668    /// Whether waves affect boat speed and/or steering.
669    pub effects: WaveEffects,
670    /// Variance applied to wave height, as a percentage.
671    pub height_var: PercentVariance,
672    /// Variance applied to wave length, as a percentage.
673    pub length_var: PercentVariance,
674}
675
676/// Base current parameters that apply to the whole event unless an area
677/// or circle override is in effect.
678#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct BaseCurrents {
681    /// Current speed in knots. Zero means no current; the other fields
682    /// still carry meaningful variance values.
683    pub speed: KnotSpeed,
684    /// Direction the current flows **from**, in degrees clockwise from
685    /// North.
686    #[serde(rename = "dir")]
687    pub direction: WindDirection,
688    /// Water-depth setting controlling how currents attenuate with
689    /// real-world sea depth.
690    pub water_depth: WaterDepthSetting,
691}
692
693/// The full base block: wind, waves, currents combined.
694#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
695pub struct Base {
696    /// Base wind parameters.
697    pub wind: BaseWind,
698    /// Base wave parameters.
699    pub waves: BaseWaves,
700    /// Base current parameters.
701    pub currents: BaseCurrents,
702}
703
704// ---------------------------------------------------------------------------
705// Per-area / per-circle overrides (every field optional)
706// ---------------------------------------------------------------------------
707
708/// Wind override block. Each field is independently optional: only the
709/// fields actually overridden by the area/circle are present.
710#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
711#[serde(default)]
712pub struct WindOverride {
713    /// Direction the wind blows from, in degrees clockwise from North.
714    #[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
715    pub direction: Option<WindDirection>,
716    /// Wind speed in knots.
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub speed: Option<KnotSpeed>,
719    /// Gust intensity (`0..=100`).
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub gusts: Option<GustsPercent>,
722    /// Maximum wind shift in degrees per period.
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub shifts: Option<ShiftsDegrees>,
725    /// Period of shift/gust oscillation in seconds.
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub period: Option<Period>,
728}
729
730impl WindOverride {
731    /// `true` if every field is `None`.
732    #[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/// Wave override block. Each field is independently optional.
743#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
744#[serde(default, rename_all = "camelCase")]
745pub struct WavesOverride {
746    /// Wave height in meters.
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub height: Option<WaveHeight>,
749    /// Wave speed in knots.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub speed: Option<KnotSpeed>,
752    /// Wave length in meters.
753    #[serde(skip_serializing_if = "Option::is_none")]
754    pub length: Option<WaveLength>,
755    /// Whether waves affect boat speed and/or steering.
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub effects: Option<WaveEffects>,
758    /// Variance applied to wave height, as a percentage.
759    #[serde(skip_serializing_if = "Option::is_none")]
760    pub height_var: Option<PercentVariance>,
761    /// Variance applied to wave length, as a percentage.
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub length_var: Option<PercentVariance>,
764}
765
766impl WavesOverride {
767    /// `true` if every field is `None`.
768    #[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/// Currents override block. Each field is independently optional.
780///
781/// Note `water_depth: Option<WaterDepthSetting>`: outer `None` means the
782/// override does not touch water depth at all; inner
783/// [`WaterDepthSetting::Disabled`] means the override explicitly
784/// disables depth attenuation.
785#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
786#[serde(default, rename_all = "camelCase")]
787pub struct CurrentsOverride {
788    /// Current speed in knots.
789    #[serde(skip_serializing_if = "Option::is_none")]
790    pub speed: Option<KnotSpeed>,
791    /// Direction the current flows from, in degrees clockwise from North.
792    #[serde(rename = "dir", skip_serializing_if = "Option::is_none")]
793    pub direction: Option<WindDirection>,
794    /// Water-depth setting controlling how currents attenuate with
795    /// real-world sea depth.
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub water_depth: Option<WaterDepthSetting>,
798}
799
800impl CurrentsOverride {
801    /// `true` if every field is `None`.
802    #[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// ---------------------------------------------------------------------------
809// Area / Circle / Event
810// ---------------------------------------------------------------------------
811
812/// A rectangular GLW area covering one or more whole regions.
813///
814/// `name` is the original JSON key (e.g. `"area1"`). Document order is
815/// preserved by [`AreaList`] so that `overlap = true` layering matches
816/// the GLW LSL/PHP reference implementation.
817#[derive(Debug, Clone, PartialEq)]
818pub struct Area {
819    /// JSON key for this area (e.g. `"area1"`).
820    pub name: String,
821    /// Region rectangle covered by this area.
822    pub grid_rectangle: sl_types::map::GridRectangle,
823    /// Margin width in meters where conditions blend with the base.
824    pub margin: MarginMeters,
825    /// `true` when this area overlays another area/circle rather than the
826    /// base wind directly.
827    pub overlap: Overlap,
828    /// Optional wind override.
829    pub wind: Option<WindOverride>,
830    /// Optional wave override.
831    pub waves: Option<WavesOverride>,
832    /// Optional currents override.
833    pub currents: Option<CurrentsOverride>,
834}
835
836/// A circular GLW area.
837///
838/// `name` is the original JSON key (e.g. `"circle1"`).
839#[derive(Debug, Clone, PartialEq)]
840pub struct Circle {
841    /// JSON key for this circle (e.g. `"circle1"`).
842    pub name: String,
843    /// Region containing the centre point.
844    pub center_sim: sl_types::map::GridCoordinates,
845    /// Coordinates of the centre within `center_sim`, in meters from the
846    /// SW corner of the region.
847    pub center_point: sl_types::map::RegionCoordinates,
848    /// Radius of the circle in meters.
849    pub radius: RadiusMeters,
850    /// Margin width in meters where conditions blend with the base.
851    pub margin: MarginMeters,
852    /// `true` when this circle overlays another area/circle rather than
853    /// the base wind directly.
854    pub overlap: Overlap,
855    /// Optional wind override.
856    pub wind: Option<WindOverride>,
857    /// Optional wave override.
858    pub waves: Option<WavesOverride>,
859    /// Optional currents override.
860    pub currents: Option<CurrentsOverride>,
861}
862
863/// Document-ordered list of [`Area`]s.
864///
865/// Order matters: overlap layering applies in document order. Serializes
866/// back as a JSON object keyed by `Area::name` so it round-trips against
867/// the original GLW JSON shape.
868#[derive(Debug, Clone, Default, PartialEq)]
869pub struct AreaList(pub Vec<Area>);
870
871impl AreaList {
872    /// Borrow the underlying `Vec`.
873    #[must_use]
874    pub fn as_slice(&self) -> &[Area] {
875        &self.0
876    }
877    /// Number of areas in the list.
878    #[must_use]
879    pub const fn len(&self) -> usize {
880        self.0.len()
881    }
882    /// `true` when the list contains no areas.
883    #[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/// Private view of an [`Area`] without its `name` field, used only for
901/// serialization so the area's body matches the GLW JSON shape (the
902/// name is the JSON key, not a body field).
903#[derive(serde::Serialize)]
904struct AreaSerializable<'a> {
905    /// SW corner (sim coordinates).
906    #[serde(rename = "coordSW")]
907    coord_sw: SimCoord,
908    /// NE corner (sim coordinates).
909    #[serde(rename = "coordNE")]
910    coord_ne: SimCoord,
911    /// Margin width in meters.
912    margin: &'a MarginMeters,
913    /// Whether this area overlays another area/circle rather than the
914    /// base wind directly.
915    overlap: &'a Overlap,
916    /// Wind override block.
917    #[serde(skip_serializing_if = "Option::is_none")]
918    wind: Option<&'a WindOverride>,
919    /// Wave override block.
920    #[serde(skip_serializing_if = "Option::is_none")]
921    waves: Option<&'a WavesOverride>,
922    /// Currents override block.
923    #[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/// Document-ordered list of [`Circle`]s.
942///
943/// Order matters: overlap layering applies in document order. Serializes
944/// back as a JSON object keyed by `Circle::name` so it round-trips
945/// against the original GLW JSON shape.
946#[derive(Debug, Clone, Default, PartialEq)]
947pub struct CircleList(pub Vec<Circle>);
948
949impl CircleList {
950    /// Borrow the underlying `Vec`.
951    #[must_use]
952    pub fn as_slice(&self) -> &[Circle] {
953        &self.0
954    }
955    /// Number of circles in the list.
956    #[must_use]
957    pub const fn len(&self) -> usize {
958        self.0.len()
959    }
960    /// `true` when the list contains no circles.
961    #[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/// Private view of a [`Circle`] without its `name` field, used only for
979/// serialization.
980#[derive(serde::Serialize)]
981struct CircleSerializable<'a> {
982    /// Sim containing the circle centre.
983    #[serde(rename = "centerSim")]
984    center_sim: SimCoord,
985    /// Coordinates of the centre within `centerSim`.
986    #[serde(rename = "centerPoint")]
987    center_point: RegionXY,
988    /// Circle radius in meters.
989    radius: &'a RadiusMeters,
990    /// Margin width in meters.
991    margin: &'a MarginMeters,
992    /// Whether this circle overlays another area/circle.
993    overlap: &'a Overlap,
994    /// Wind override block.
995    #[serde(skip_serializing_if = "Option::is_none")]
996    wind: Option<&'a WindOverride>,
997    /// Wave override block.
998    #[serde(skip_serializing_if = "Option::is_none")]
999    waves: Option<&'a WavesOverride>,
1000    /// Currents override block.
1001    #[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/// Helper enum used by [`SimCoord::from_grid`] to pick a corner.
1027#[derive(Debug, Clone, Copy)]
1028enum GridCorner {
1029    /// South-west (lower-left) corner.
1030    Sw,
1031    /// North-east (upper-right) corner.
1032    Ne,
1033}
1034
1035/// Serialization-side `{ "x": u16, "y": u16 }` shape (mirrors the
1036/// `GridCoordRaw` in `parse.rs`).
1037#[derive(Debug, Clone, Copy, serde::Serialize)]
1038struct SimCoord {
1039    /// Sim x grid coordinate.
1040    x: u32,
1041    /// Sim y grid coordinate.
1042    y: u32,
1043}
1044
1045impl SimCoord {
1046    /// Project one corner of a [`sl_types::map::GridRectangle`] into the
1047    /// flat `{x,y}` shape used in JSON.
1048    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/// Serialization-side `{ "x": f32, "y": f32 }` shape for circle
1062/// centerPoints (z is intentionally dropped to match GLW JSON, which
1063/// only carries x and y).
1064#[derive(Debug, Clone, Copy, serde::Serialize)]
1065struct RegionXY {
1066    /// X meters from the western edge of the sim.
1067    x: f32,
1068    /// Y meters from the southern edge of the sim.
1069    y: f32,
1070}
1071
1072/// A full GLW event as returned by `glwDataReq.php`.
1073///
1074/// Unknown top-level fields are silently ignored — the GLW author may
1075/// extend the schema, and unknown fields do not break parsing.
1076#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct GlwEvent {
1079    /// Numeric event id assigned by the server.
1080    pub event_id: EventId,
1081    /// Free-text event name (e.g. `"TYC Cruise"`).
1082    pub event_name: String,
1083    /// Opaque event key string.
1084    pub event_key: GlwEventKey,
1085    /// Free-form event number (informational; not used by GLW).
1086    #[serde(default)]
1087    pub event_num: Option<u32>,
1088    /// Display name of the event director.
1089    pub director_name: String,
1090    /// Second Life UUID of the event director (stored as a string; this
1091    /// crate does not require the `uuid` dependency).
1092    pub director_key: String,
1093    /// Sail-mode flag. Documented as "not use for now"; kept optional
1094    /// for forward compatibility.
1095    #[serde(default)]
1096    pub sail_mode: Option<u8>,
1097    /// Opaque string field forwarded to boats (max 15 chars in the
1098    /// reference schema; not validated here).
1099    #[serde(default)]
1100    pub extra1: String,
1101    /// Opaque string field forwarded to boats (max 15 chars in the
1102    /// reference schema; not validated here).
1103    #[serde(default)]
1104    pub extra2: String,
1105    /// Base wind/waves/currents that apply unless overridden.
1106    pub base: Base,
1107    /// Per-area overrides, in document order. JSON key names are
1108    /// preserved on each [`Area`].
1109    #[serde(default, deserialize_with = "crate::parse::deserialize_area_list")]
1110    pub areas: AreaList,
1111    /// Per-circle overrides, in document order.
1112    #[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}