Skip to main content

rfham_bands/
lib.rs

1//!
2//! Provides ..., a one-line description
3//!
4//! More detailed description
5//!
6//! # Examples
7//!
8//! ```rust
9//! ```
10//!
11
12use colored::Colorize;
13use rfham_core::{
14    agency::Agency,
15    country::CountryCode,
16    error::CoreError,
17    frequency::{Frequency, FrequencyRange},
18    power::Power,
19};
20use rfham_itu::{allocations::FrequencyAllocation, regions::Region};
21use rfham_markdown::{
22    Table, ToMarkdown, ToMarkdownWith, blank_line, bulleted_list, bulleted_list_item, header,
23    link_to_string, numbered_list_item, plain_text,
24};
25use serde::{Deserialize, Serialize};
26use serde_with::{DeserializeFromStr, SerializeDisplay};
27use std::{cmp::Ordering, collections::HashMap, fmt::Display, str::FromStr};
28
29// ------------------------------------------------------------------------------------------------
30// Public Macros
31// ------------------------------------------------------------------------------------------------
32
33// ------------------------------------------------------------------------------------------------
34// Public Types
35// ------------------------------------------------------------------------------------------------
36
37pub type DisplayName = String;
38pub type LicenseKey = String;
39
40#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
41pub struct BandPlan {
42    name: DisplayName,
43    maintainer: Agency,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    regulator: Option<Agency>,
46    region: Region,
47    #[serde(skip_serializing_if = "Vec::is_empty")]
48    countries: Vec<CountryCode>,
49    #[serde(skip_serializing_if = "HashMap::is_empty")]
50    licenses: HashMap<LicenseKey, LicenseClass>,
51    #[serde(skip_serializing_if = "HashMap::is_empty")]
52    bands: HashMap<FrequencyAllocation, PlanBand>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    default_max_power: Option<Power>,
55    #[serde(skip_serializing_if = "Vec::is_empty")]
56    notes: Vec<String>,
57}
58
59#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
60pub struct Band {
61    allocation: FrequencyAllocation,
62    #[serde(flatten)]
63    range: FrequencyRange,
64}
65
66#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
67pub struct PlanBand {
68    band: Band,
69    is_primary_user: bool,
70    #[serde(skip_serializing_if = "BandRestrictions::is_unrestricted")]
71    restrictions: BandRestrictions,
72    #[serde(skip_serializing_if = "Vec::is_empty")]
73    segments: Vec<Segment>,
74    #[serde(skip_serializing_if = "Vec::is_empty")]
75    calling_frequencies: Vec<CallingFrequency>,
76    #[serde(skip_serializing_if = "Vec::is_empty")]
77    notes: Vec<String>,
78}
79
80#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
81pub struct Segment {
82    #[serde(flatten)]
83    range: FrequencyRange,
84    #[serde(skip_serializing_if = "BandRestrictions::is_unrestricted")]
85    restrictions: BandRestrictions,
86    #[serde(skip_serializing_if = "Vec::is_empty")]
87    notes: Vec<String>,
88}
89
90#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
91pub struct BandRestrictions {
92    #[serde(skip_serializing_if = "Vec::is_empty")]
93    license_restrictions: Vec<LicenseKey>,
94    #[serde(skip_serializing_if = "Vec::is_empty")]
95    usage_restrictions: Vec<UsageRestriction>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    power_restriction: Option<PowerRestriction>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    bandwidth_restriction: Option<BandwidthRestriction>,
100}
101
102#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
103pub struct CallingFrequency {
104    frequency: Frequency,
105    name: Option<DisplayName>,
106    #[serde(skip_serializing_if = "Vec::is_empty")]
107    usage_restrictions: Vec<UsageRestriction>,
108}
109
110#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
111pub struct LicenseClass {
112    ordinal: u32,
113    name: DisplayName,
114    is_active: bool,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
118pub enum UsageRestriction {
119    Beacon,
120    CW,
121    Data,
122    Digital,
123    Dx,
124    EarthMoonEarth,
125    Phone,
126    Qrp,
127    RemoteControl,
128    Repeater(RepeaterUsage),
129    Rtty,
130    Satellite(SatelliteUsage),
131    Simplex,
132    SlowScanTv,
133    Test(RepeaterUsage),
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
137pub enum PhoneMode {
138    AmplitudeModulated,
139    FrequencyModulated,
140    SingleSideband,
141    SingleSidebandLower,
142    SingleSidebandUpper,
143}
144
145#[derive(Clone, Copy, Debug, Default, PartialEq, DeserializeFromStr, SerializeDisplay)]
146pub enum PowerMeasure {
147    #[default]
148    PeakEnvelopePower,
149    EffectiveRadiatedPower,
150    EffectiveIsotropicRadiatedPower,
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
154pub struct PowerRestriction {
155    max_power: Power,
156    measure: PowerMeasure,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
160pub enum RepeaterUsage {
161    Input,
162    Output,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)]
166pub enum SatelliteUsage {
167    Downlink,
168    Uplink,
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
172pub struct BandwidthRestriction {
173    max_bandwidth: Frequency,
174}
175
176// ------------------------------------------------------------------------------------------------
177// Public Functions
178// ------------------------------------------------------------------------------------------------
179
180// ------------------------------------------------------------------------------------------------
181// Private Macros
182// ------------------------------------------------------------------------------------------------
183
184// ------------------------------------------------------------------------------------------------
185// Private Types
186// ------------------------------------------------------------------------------------------------
187
188const START_COL_WIDTH: usize = 10;
189const END_COL_WIDTH: usize = 10;
190const CLASS_COL_WIDTH: usize = 14;
191const USAGE_COL_WIDTH: usize = 20;
192const POWER_COL_WIDTH: usize = 14;
193const BANDWIDTH_COL_WIDTH: usize = 14;
194const CLASS_OR_USAGE_DEFAULT: &str = "any";
195const POWER_DEFAULT: &str = "full power";
196const BANDWIDTH_DEFAULT: &str = "-";
197
198// ------------------------------------------------------------------------------------------------
199// Implementations
200// ------------------------------------------------------------------------------------------------
201
202impl ToMarkdownWith for BandPlan {
203    type Context = Vec<FrequencyAllocation>;
204
205    fn write_markdown_with<W: std::io::Write>(
206        &self,
207        writer: &mut W,
208        context: Self::Context,
209    ) -> Result<(), CoreError> {
210        header(writer, 1, &self.name)?;
211        blank_line(writer)?;
212
213        if let Some(regulator) = self.regulator.as_ref() {
214            bulleted_list_item(
215                writer,
216                1,
217                &if let Some(url) = &regulator.url() {
218                    format!(
219                        "Regulatory Agency: {}",
220                        link_to_string(regulator.to_string(), url)
221                    )
222                } else {
223                    format!("Regulatory Agency: {}", regulator)
224                },
225            )?;
226        }
227        bulleted_list_item(
228            writer,
229            1,
230            &if let Some(url) = &self.maintainer.url() {
231                format!(
232                    "Maintaining Agency: {}",
233                    link_to_string(self.maintainer.to_string(), url)
234                )
235            } else {
236                format!("Maintaining Agency: {}", self.maintainer)
237            },
238        )?;
239        bulleted_list_item(writer, 1, format!("Region: {:#}", self.region))?;
240        if !self.countries.is_empty() {
241            bulleted_list_item(
242                writer,
243                1,
244                format!(
245                    "Countries: {}",
246                    self.countries
247                        .iter()
248                        .map(|c| c.as_str())
249                        .collect::<Vec<_>>()
250                        .join(", ")
251                ),
252            )?;
253        }
254        if let Some(default_max_power) = &self.default_max_power {
255            bulleted_list_item(
256                writer,
257                1,
258                format!("Default maximum power: {default_max_power}"),
259            )?;
260        }
261        blank_line(writer)?;
262
263        if !self.notes.is_empty() {
264            plain_text(writer, "Notes:")?;
265            blank_line(writer)?;
266            bulleted_list(writer, 1, &self.notes)?;
267            blank_line(writer)?;
268        }
269
270        header(writer, 2, "License Classes")?;
271        blank_line(writer)?;
272
273        let mut ordered_licenses = self.licenses.iter().collect::<Vec<_>>();
274        ordered_licenses.sort_by(|a, b| a.1.ordinal.cmp(&b.1.ordinal));
275
276        for (number, (key, license)) in ordered_licenses.iter().enumerate() {
277            numbered_list_item(
278                writer,
279                1,
280                number,
281                format!(
282                    "{} ({}){}",
283                    license.name,
284                    key,
285                    if !license.is_active {
286                        format!(" {}", "inactive class".italic())
287                    } else {
288                        String::default()
289                    }
290                ),
291            )?;
292        }
293        blank_line(writer)?;
294
295        let mut ordered_bands = self.bands.values().collect::<Vec<_>>();
296        ordered_bands.sort_by(|a, b| {
297            a.band
298                .range
299                .start()
300                .value()
301                .partial_cmp(&b.band.range.start().value())
302                .unwrap_or(Ordering::Equal)
303        });
304
305        for band in ordered_bands {
306            if context.is_empty() || context.contains(&band.band.allocation) {
307                band.write_markdown(writer)?;
308            }
309        }
310
311        Ok(())
312    }
313}
314
315impl BandPlan {
316    // TODO: factor this out in the future.
317    #[allow(clippy::too_many_arguments)]
318    const fn inner_new(
319        maintaining_agency: Agency,
320        name: String,
321        region: Region,
322        countries: Vec<CountryCode>,
323        licenses: HashMap<String, LicenseClass>,
324        bands: HashMap<FrequencyAllocation, PlanBand>,
325        default_max_power: Option<Power>,
326        notes: Vec<String>,
327    ) -> Self {
328        Self {
329            maintainer: maintaining_agency,
330            regulator: None,
331            name,
332            region,
333            countries,
334            licenses,
335            bands,
336            default_max_power,
337            notes,
338        }
339    }
340
341    pub fn new<S: Into<String>>(maintainer: Agency, region: Region, name: S) -> Self {
342        Self::inner_new(
343            maintainer,
344            name.into(),
345            region,
346            Vec::default(),
347            HashMap::default(),
348            HashMap::default(),
349            Option::default(),
350            Vec::default(),
351        )
352    }
353
354    pub fn with_regulator(mut self, regulator: Agency) -> Self {
355        self.regulator = Some(regulator);
356        self
357    }
358
359    pub fn in_country(mut self, country: CountryCode) -> Self {
360        self.countries.push(country);
361        self
362    }
363
364    pub fn in_countries(mut self, countries: &[CountryCode]) -> Self {
365        self.countries.extend(countries.to_vec());
366        self
367    }
368
369    pub fn with_licenses(mut self, licenses: HashMap<LicenseKey, LicenseClass>) -> Self {
370        self.licenses = licenses;
371        self
372    }
373
374    pub fn with_license_list(mut self, licenses: Vec<(DisplayName, bool)>) -> Self {
375        let licenses: HashMap<LicenseKey, LicenseClass> = licenses
376            .into_iter()
377            .enumerate()
378            .map(|(ordinal, (name, is_active))| {
379                (
380                    name.clone(),
381                    LicenseClass::new(ordinal as u32, &name, is_active),
382                )
383            })
384            .collect();
385        self.licenses = licenses;
386        self
387    }
388
389    pub fn with_bands(mut self, bands: HashMap<FrequencyAllocation, PlanBand>) -> Self {
390        self.bands = bands;
391        self
392    }
393
394    pub fn with_bands_list(self, bands: Vec<PlanBand>) -> Self {
395        self.with_bands(
396            bands
397                .into_iter()
398                .map(|band| (band.band.allocation, band))
399                .collect(),
400        )
401    }
402
403    pub fn with_default_max_power(mut self, default_max_power: Power) -> Self {
404        self.default_max_power = Some(default_max_power);
405        self
406    }
407
408    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
409        self.notes = notes;
410        self
411    }
412
413    pub fn name(&self) -> &str {
414        self.name.as_str()
415    }
416
417    pub fn band(&self, itu: &FrequencyAllocation) -> Option<&PlanBand> {
418        self.bands.get(itu)
419    }
420
421    pub fn maintaining_agency(&self) -> &Agency {
422        &self.maintainer
423    }
424}
425
426// ------------------------------------------------------------------------------------------------
427
428impl Display for Band {
429    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430        write!(f, "{} ({})", self.allocation, self.range)
431    }
432}
433
434impl Band {
435    pub fn new_default(allocation: FrequencyAllocation, region: Region) -> Self {
436        let range = allocation
437            .range(region)
438            .expect("Could not create band from allocation missing in {region:#}.");
439        Self::new(allocation, range.start(), range.end())
440    }
441
442    pub fn new(allocation: FrequencyAllocation, start: Frequency, end: Frequency) -> Self {
443        assert!(start < end);
444        Self {
445            allocation,
446            range: FrequencyRange::new(start, end),
447        }
448    }
449
450    pub fn allocation(&self) -> &FrequencyAllocation {
451        &self.allocation
452    }
453
454    pub fn range(&self) -> &FrequencyRange {
455        &self.range
456    }
457}
458
459// ------------------------------------------------------------------------------------------------
460
461impl ToMarkdown for PlanBand {
462    fn write_markdown<W: std::io::Write>(&self, writer: &mut W) -> Result<(), CoreError> {
463        let actual = &self.band;
464        header(
465            writer,
466            2,
467            format!("{} Band ({})", actual.allocation, actual.range),
468        )?;
469        blank_line(writer)?;
470
471        plain_text(
472            writer,
473            format!(
474                "Note: amateur operators are the {} users on this band.",
475                if self.is_primary_user {
476                    "primary"
477                } else {
478                    "secondary"
479                }
480                .bold()
481            ),
482        )?;
483        blank_line(writer)?;
484
485        if !self.calling_frequencies.is_empty() {
486            plain_text(writer, "Calling Frequencies:")?;
487            blank_line(writer)?;
488
489            let mut ordered_calling = self.calling_frequencies.iter().collect::<Vec<_>>();
490            ordered_calling.sort_by(|a, b| {
491                a.frequency
492                    .value()
493                    .partial_cmp(&b.frequency.value())
494                    .unwrap_or(Ordering::Equal)
495            });
496
497            for (i, calling) in ordered_calling.iter().enumerate() {
498                numbered_list_item(
499                    writer,
500                    1,
501                    i,
502                    format!(
503                        "{}{}{}",
504                        calling.frequency,
505                        if let Some(label) = &calling.name {
506                            format!(" {label}")
507                        } else {
508                            String::default()
509                        },
510                        if !calling.usage_restrictions.is_empty() {
511                            format!(
512                                " ({})",
513                                calling
514                                    .usage_restrictions
515                                    .iter()
516                                    .map(|r| r.to_string())
517                                    .collect::<Vec<_>>()
518                                    .join(", ")
519                            )
520                        } else {
521                            String::default()
522                        }
523                    ),
524                )?;
525            }
526        }
527
528        if !self.restrictions.is_unrestricted() || !self.segments.is_empty() {
529            plain_text(writer, "Band restrictions:")?;
530            blank_line(writer)?;
531
532            let table = Table::new(vec![
533                ("Start", START_COL_WIDTH).into(),
534                ("End", END_COL_WIDTH).into(),
535                ("License Class", CLASS_COL_WIDTH).into(),
536                ("Usage/Mode", USAGE_COL_WIDTH).into(),
537                ("Power", POWER_COL_WIDTH).into(),
538                ("Max Bandwidth", BANDWIDTH_COL_WIDTH).into(),
539            ]);
540            table.headers(writer)?;
541
542            if !self.restrictions.is_unrestricted() {
543                self.restrictions
544                    .write_markdown_with(writer, (actual.range.clone(), table.clone()))?;
545            }
546
547            if !self.segments.is_empty() {
548                let mut ordered_segments = self.segments.iter().collect::<Vec<_>>();
549                ordered_segments.sort_by(|a, b| {
550                    a.range
551                        .start()
552                        .value()
553                        .partial_cmp(&b.range.start().value())
554                        .unwrap_or(Ordering::Equal)
555                });
556
557                for segment in ordered_segments {
558                    segment.write_markdown_with(writer, (segment.range.clone(), table.clone()))?;
559                }
560                blank_line(writer)?;
561            }
562        }
563
564        if !self.notes.is_empty() {
565            plain_text(writer, "Notes:")?;
566            blank_line(writer)?;
567
568            bulleted_list(writer, 1, &self.notes)?;
569        }
570        blank_line(writer)?;
571        Ok(())
572    }
573}
574
575impl PlanBand {
576    fn inner_new(
577        band: Band,
578        primary_user: bool,
579        restrictions: BandRestrictions,
580        segments: Vec<Segment>,
581        calling_frequencies: Vec<CallingFrequency>,
582        notes: Vec<String>,
583    ) -> Self {
584        Self {
585            band,
586            is_primary_user: primary_user,
587            restrictions,
588            segments,
589            calling_frequencies,
590            notes,
591        }
592    }
593    pub fn new(band: Band) -> Self {
594        Self::inner_new(
595            band,
596            true,
597            BandRestrictions::none(),
598            Vec::default(),
599            Vec::default(),
600            Vec::default(),
601        )
602    }
603    pub fn with_is_primary_user(mut self, is_primary_user: bool) -> Self {
604        self.is_primary_user = is_primary_user;
605        self
606    }
607    pub fn with_restrictions(mut self, restrictions: BandRestrictions) -> Self {
608        self.restrictions = restrictions;
609        self
610    }
611    pub fn with_segments(mut self, segments: Vec<Segment>) -> Self {
612        self.segments = segments;
613        self
614    }
615    pub fn with_calling_frequencies(mut self, calling_frequencies: Vec<CallingFrequency>) -> Self {
616        self.calling_frequencies = calling_frequencies;
617        self
618    }
619    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
620        self.notes = notes;
621        self
622    }
623
624    pub fn band(&self) -> &Band {
625        &self.band
626    }
627}
628
629// ------------------------------------------------------------------------------------------------
630
631impl ToMarkdownWith for Segment {
632    type Context = (FrequencyRange, Table);
633
634    fn write_markdown_with<W: std::io::Write>(
635        &self,
636        writer: &mut W,
637        context: Self::Context,
638    ) -> Result<(), CoreError> {
639        let (range, table) = context;
640        if self.restrictions.is_unrestricted() {
641            table.data_row(
642                writer,
643                &[
644                    range.start().to_string(),
645                    range.end().to_string(),
646                    CLASS_OR_USAGE_DEFAULT.italic().to_string(),
647                    CLASS_OR_USAGE_DEFAULT.italic().to_string(),
648                    POWER_DEFAULT.italic().to_string(),
649                    BANDWIDTH_DEFAULT.to_string(),
650                ],
651            )?;
652        } else {
653            table.data_row(
654                writer,
655                &[
656                    range.start().to_string(),
657                    range.end().to_string(),
658                    if self.restrictions.license_restrictions.is_empty() {
659                        CLASS_OR_USAGE_DEFAULT.to_string()
660                    } else {
661                        #[allow(clippy::iter_cloned_collect)] // false positive
662                        self.restrictions
663                            .license_restrictions
664                            .iter()
665                            .cloned()
666                            .collect::<Vec<_>>()
667                            .join(", ")
668                    },
669                    if self.restrictions.usage_restrictions.is_empty() {
670                        CLASS_OR_USAGE_DEFAULT.to_string()
671                    } else if self.restrictions.usage_restrictions.len() == 1 {
672                        format!(
673                            "{} only",
674                            self.restrictions.usage_restrictions.first().unwrap()
675                        )
676                    } else {
677                        #[allow(clippy::iter_cloned_collect)] // false positive
678                        self.restrictions
679                            .usage_restrictions
680                            .iter()
681                            .map(|r| r.to_string())
682                            .collect::<Vec<_>>()
683                            .join(", ")
684                    },
685                    if let Some(power_restriction) = self.restrictions.power_restriction() {
686                        power_restriction.to_string()
687                    } else {
688                        POWER_DEFAULT.to_string()
689                    },
690                    if let Some(bandwidth_restriction) = self.restrictions.bandwidth_restriction() {
691                        bandwidth_restriction.to_string()
692                    } else {
693                        BANDWIDTH_DEFAULT.to_string()
694                    },
695                ],
696            )?;
697        }
698        Ok(())
699    }
700}
701
702impl Segment {
703    pub fn new(frequency_start: Frequency, frequency_end: Frequency) -> Self {
704        Self {
705            range: FrequencyRange::new(frequency_start, frequency_end),
706            restrictions: Default::default(),
707            notes: Vec::default(),
708        }
709    }
710    pub fn with_restrictions(mut self, restrictions: BandRestrictions) -> Self {
711        self.restrictions = restrictions;
712        self
713    }
714    pub fn with_notes(mut self, notes: Vec<String>) -> Self {
715        self.notes = notes;
716        self
717    }
718}
719
720// ------------------------------------------------------------------------------------------------
721
722impl ToMarkdownWith for BandRestrictions {
723    type Context = (FrequencyRange, Table);
724
725    fn write_markdown_with<W: std::io::Write>(
726        &self,
727        writer: &mut W,
728        context: Self::Context,
729    ) -> Result<(), CoreError> {
730        let (range, table) = context;
731        table.data_row(
732            writer,
733            &[
734                range.start().to_string(),
735                range.end().to_string(),
736                if self.license_restrictions.is_empty() {
737                    CLASS_OR_USAGE_DEFAULT.to_string()
738                } else {
739                    #[allow(clippy::iter_cloned_collect)] // false positive
740                    self.license_restrictions
741                        .iter()
742                        .cloned()
743                        .collect::<Vec<_>>()
744                        .join(", ")
745                },
746                if self.usage_restrictions.is_empty() {
747                    CLASS_OR_USAGE_DEFAULT.to_string()
748                } else if self.usage_restrictions.len() == 1 {
749                    format!("{} only", self.usage_restrictions.first().unwrap())
750                } else {
751                    #[allow(clippy::iter_cloned_collect)] // false positive
752                    self.usage_restrictions
753                        .iter()
754                        .map(|r| r.to_string())
755                        .collect::<Vec<_>>()
756                        .join(", ")
757                },
758                if let Some(power_restriction) = self.power_restriction {
759                    power_restriction.to_string()
760                } else {
761                    POWER_DEFAULT.to_string()
762                },
763                if let Some(bandwidth_restriction) = self.bandwidth_restriction() {
764                    bandwidth_restriction.to_string()
765                } else {
766                    BANDWIDTH_DEFAULT.to_string()
767                },
768            ],
769        )?;
770        blank_line(writer)?;
771        Ok(())
772    }
773}
774
775impl BandRestrictions {
776    pub fn none() -> Self {
777        Self::default()
778    }
779    pub fn with_license_restrictions(mut self, license_restrictions: Vec<LicenseKey>) -> Self {
780        self.license_restrictions = license_restrictions;
781        self
782    }
783    pub fn with_usage_restrictions(mut self, usage_restrictions: Vec<UsageRestriction>) -> Self {
784        self.usage_restrictions = usage_restrictions;
785        self
786    }
787    pub fn with_power_restriction(mut self, power_restriction: PowerRestriction) -> Self {
788        self.power_restriction = Some(power_restriction);
789        self
790    }
791    pub fn with_bandwidth_restriction(mut self, max_bandwidth: BandwidthRestriction) -> Self {
792        self.bandwidth_restriction = Some(max_bandwidth);
793        self
794    }
795    pub fn is_unrestricted(&self) -> bool {
796        self.license_restrictions.is_empty()
797            && self.usage_restrictions.is_empty()
798            && self.power_restriction.is_none()
799    }
800    pub fn license_restrictions(&self) -> impl Iterator<Item = &LicenseKey> {
801        self.license_restrictions.iter()
802    }
803    pub fn usage_restrictions(&self) -> impl Iterator<Item = &UsageRestriction> {
804        self.usage_restrictions.iter()
805    }
806    pub fn power_restriction(&self) -> Option<&PowerRestriction> {
807        self.power_restriction.as_ref()
808    }
809    pub fn bandwidth_restriction(&self) -> Option<&BandwidthRestriction> {
810        self.bandwidth_restriction.as_ref()
811    }
812}
813
814// ------------------------------------------------------------------------------------------------
815
816impl Display for CallingFrequency {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        write!(
819            f,
820            "{:.3}: Calling{}",
821            self.frequency,
822            if self.usage_restrictions.is_empty() {
823                String::default()
824            } else {
825                self.usage_restrictions
826                    .iter()
827                    .map(|v| v.to_string())
828                    .collect::<Vec<_>>()
829                    .join(" ")
830            }
831        )
832    }
833}
834
835impl CallingFrequency {
836    pub const fn new(frequency: Frequency, usage_restrictions: Vec<UsageRestriction>) -> Self {
837        Self {
838            frequency,
839            name: None,
840            usage_restrictions,
841        }
842    }
843    pub const fn unrestricted(frequency: Frequency) -> Self {
844        Self {
845            frequency,
846            name: None,
847            usage_restrictions: Vec::new(),
848        }
849    }
850    pub fn with_usage_restrictions(mut self, usage_restrictions: Vec<UsageRestriction>) -> Self {
851        self.usage_restrictions = usage_restrictions;
852        self
853    }
854    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
855        self.name = Some(label.into());
856        self
857    }
858    pub const fn frequency(&self) -> Frequency {
859        self.frequency
860    }
861    pub fn usage_restrictions(&self) -> impl Iterator<Item = &UsageRestriction> {
862        self.usage_restrictions.iter()
863    }
864}
865
866// ------------------------------------------------------------------------------------------------
867
868impl Display for LicenseClass {
869    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
870        write!(
871            f,
872            "{}",
873            if f.alternate() {
874                format!(
875                    "{:02}: {} ({})",
876                    self.ordinal,
877                    self.name,
878                    if self.is_active { "active" } else { "inactive" }
879                )
880            } else {
881                format!(
882                    "{}{}",
883                    self.name,
884                    if self.is_active { "" } else { " (inactive)" }
885                )
886            }
887        )
888    }
889}
890
891impl LicenseClass {
892    pub fn new(ordinal: u32, name: &str, is_active: bool) -> Self {
893        Self {
894            name: name.to_string(),
895            ordinal,
896            is_active,
897        }
898    }
899    pub fn active(ordinal: u32, name: &str) -> Self {
900        Self {
901            name: name.to_string(),
902            ordinal,
903            is_active: true,
904        }
905    }
906    pub fn inactive(ordinal: u32, name: &str) -> Self {
907        Self {
908            name: name.to_string(),
909            ordinal,
910            is_active: false,
911        }
912    }
913
914    pub fn name(&self) -> &DisplayName {
915        &self.name
916    }
917
918    pub fn ordinal(&self) -> u32 {
919        self.ordinal
920    }
921
922    pub fn is_active(&self) -> bool {
923        self.is_active
924    }
925}
926
927// ------------------------------------------------------------------------------------------------
928
929impl Display for UsageRestriction {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        write!(
932            f,
933            "{}",
934            match self {
935                UsageRestriction::Beacon => "beacon".to_string(),
936                UsageRestriction::CW => "CW".to_string(),
937                UsageRestriction::Data => "data".to_string(),
938                UsageRestriction::Digital => "digital".to_string(),
939                UsageRestriction::Dx => "DX".to_string(),
940                UsageRestriction::EarthMoonEarth => "EME".to_string(),
941                UsageRestriction::Phone => "phone".to_string(),
942                UsageRestriction::Qrp => "QRP".to_string(),
943                UsageRestriction::RemoteControl => "remote-control".to_string(),
944                UsageRestriction::Repeater(repeater_usage) => format!("repeater {repeater_usage}"),
945                UsageRestriction::Rtty => "RTTY".to_string(),
946                UsageRestriction::Satellite(satellite_usage) =>
947                    format!("satellite {satellite_usage}"),
948                UsageRestriction::Simplex => "simplex".to_string(),
949                UsageRestriction::SlowScanTv => "SSTV".to_string(),
950                UsageRestriction::Test(repeater_usage) => format!("test-pair {repeater_usage}"),
951            }
952        )
953    }
954}
955
956impl FromStr for UsageRestriction {
957    type Err = CoreError;
958
959    fn from_str(s: &str) -> Result<Self, Self::Err> {
960        match s {
961            "beacon" => Ok(Self::Beacon),
962            "CW" => Ok(Self::CW),
963            "data" => Ok(Self::Data),
964            "digital" => Ok(Self::Digital),
965            "DX" => Ok(Self::Dx),
966            "EQE" => Ok(Self::EarthMoonEarth),
967            "QRP" => Ok(Self::Qrp),
968            "remote-control" => Ok(Self::RemoteControl),
969            "RTTY" => Ok(Self::Rtty),
970            "simplex" => Ok(Self::Simplex),
971            "SSTV" => Ok(Self::SlowScanTv),
972            _ => {
973                if let Some(repeater) = s.strip_prefix("repeater") {
974                    Ok(Self::Repeater(RepeaterUsage::from_str(repeater.trim())?))
975                } else if let Some(satellite) = s.strip_prefix("satellite") {
976                    Ok(Self::Satellite(SatelliteUsage::from_str(satellite.trim())?))
977                } else if let Some(test_pair) = s.strip_prefix("test-pair") {
978                    Ok(Self::Test(RepeaterUsage::from_str(test_pair.trim())?))
979                } else {
980                    Err(CoreError::InvalidValueFromStr(
981                        s.to_string(),
982                        "UsageRestriction",
983                    ))
984                }
985            }
986        }
987    }
988}
989
990// ------------------------------------------------------------------------------------------------
991
992impl Display for PhoneMode {
993    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
994        write!(
995            f,
996            "{}",
997            match (self, f.alternate()) {
998                (PhoneMode::AmplitudeModulated, true) => "Amplitude Modulation (AM)",
999                (PhoneMode::AmplitudeModulated, false) => "AM",
1000                (PhoneMode::FrequencyModulated, true) => "FrequencyModulation (FM)",
1001                (PhoneMode::FrequencyModulated, false) => "FM",
1002                (PhoneMode::SingleSideband, true) => "Single Sideband (SSB)",
1003                (PhoneMode::SingleSideband, false) => "SSB",
1004                (PhoneMode::SingleSidebandLower, true) => "Lower Sideband (LSB)",
1005                (PhoneMode::SingleSidebandLower, false) => "LSB",
1006                (PhoneMode::SingleSidebandUpper, true) => "Upper Sideband (USB)",
1007                (PhoneMode::SingleSidebandUpper, false) => "USB",
1008            }
1009        )
1010    }
1011}
1012
1013impl FromStr for PhoneMode {
1014    type Err = CoreError;
1015
1016    fn from_str(s: &str) -> Result<Self, Self::Err> {
1017        match s {
1018            "AM" => Ok(Self::AmplitudeModulated),
1019            "FM" => Ok(Self::FrequencyModulated),
1020            "SSB" => Ok(Self::SingleSideband),
1021            "LSB" => Ok(Self::SingleSidebandLower),
1022            "USB" => Ok(Self::SingleSidebandUpper),
1023            _ => Err(CoreError::InvalidValueFromStr(s.to_string(), "PhoneMode")),
1024        }
1025    }
1026}
1027
1028// ------------------------------------------------------------------------------------------------
1029
1030impl Display for PowerMeasure {
1031    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1032        write!(
1033            f,
1034            "{}",
1035            match (self, f.alternate()) {
1036                (PowerMeasure::PeakEnvelopePower, true) => "Peak Envelope Power (PEP)",
1037                (PowerMeasure::PeakEnvelopePower, false) => "PEP",
1038                (PowerMeasure::EffectiveRadiatedPower, true) => "Effective Radiated Power (ERP)",
1039                (PowerMeasure::EffectiveRadiatedPower, false) => "ERP",
1040                (PowerMeasure::EffectiveIsotropicRadiatedPower, true) =>
1041                    "Effective Isotropic Radiated Power (EIRP)",
1042                (PowerMeasure::EffectiveIsotropicRadiatedPower, false) => "EIRP",
1043            }
1044        )
1045    }
1046}
1047
1048impl FromStr for PowerMeasure {
1049    type Err = CoreError;
1050
1051    fn from_str(s: &str) -> Result<Self, Self::Err> {
1052        match s {
1053            "PEP" => Ok(Self::PeakEnvelopePower),
1054            "ERP" => Ok(Self::EffectiveRadiatedPower),
1055            "EIRP" => Ok(Self::EffectiveIsotropicRadiatedPower),
1056            _ => Err(CoreError::InvalidValueFromStr(
1057                s.to_string(),
1058                "PowerMeasure",
1059            )),
1060        }
1061    }
1062}
1063
1064// ------------------------------------------------------------------------------------------------
1065
1066impl Display for PowerRestriction {
1067    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068        write!(f, "{} {}", self.max_power, self.measure)
1069    }
1070}
1071
1072impl<T> From<T> for PowerRestriction
1073where
1074    T: Into<Power>,
1075{
1076    fn from(value: T) -> Self {
1077        Self::pep(value.into())
1078    }
1079}
1080
1081impl PowerRestriction {
1082    pub fn new(max_power: Power, measure: PowerMeasure) -> Self {
1083        Self { max_power, measure }
1084    }
1085    pub fn pep(max_power: Power) -> Self {
1086        Self {
1087            max_power,
1088            measure: PowerMeasure::PeakEnvelopePower,
1089        }
1090    }
1091    pub fn erp(max_power: Power) -> Self {
1092        Self {
1093            max_power,
1094            measure: PowerMeasure::EffectiveRadiatedPower,
1095        }
1096    }
1097    pub fn eirp(max_power: Power) -> Self {
1098        Self {
1099            max_power,
1100            measure: PowerMeasure::EffectiveIsotropicRadiatedPower,
1101        }
1102    }
1103    pub fn max_power(&self) -> Power {
1104        self.max_power
1105    }
1106    pub fn measure(&self) -> PowerMeasure {
1107        self.measure
1108    }
1109}
1110
1111// ------------------------------------------------------------------------------------------------
1112
1113impl Display for RepeaterUsage {
1114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1115        write!(
1116            f,
1117            "{}",
1118            match self {
1119                RepeaterUsage::Input => "input",
1120                RepeaterUsage::Output => "output",
1121            }
1122        )
1123    }
1124}
1125
1126impl FromStr for RepeaterUsage {
1127    type Err = CoreError;
1128
1129    fn from_str(s: &str) -> Result<Self, Self::Err> {
1130        match s {
1131            "input" => Ok(Self::Input),
1132            "output" => Ok(Self::Output),
1133            _ => Err(CoreError::InvalidValueFromStr(
1134                s.to_string(),
1135                "RepeaterUsage",
1136            )),
1137        }
1138    }
1139}
1140
1141// ------------------------------------------------------------------------------------------------
1142
1143impl Display for SatelliteUsage {
1144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1145        write!(
1146            f,
1147            "{}",
1148            match self {
1149                SatelliteUsage::Downlink => "downlink",
1150                SatelliteUsage::Uplink => "uplink",
1151            }
1152        )
1153    }
1154}
1155
1156impl FromStr for SatelliteUsage {
1157    type Err = CoreError;
1158
1159    fn from_str(s: &str) -> Result<Self, Self::Err> {
1160        match s {
1161            "downlink" => Ok(Self::Downlink),
1162            "uplink" => Ok(Self::Uplink),
1163            _ => Err(CoreError::InvalidValueFromStr(
1164                s.to_string(),
1165                "SatelliteUsage",
1166            )),
1167        }
1168    }
1169}
1170
1171// ------------------------------------------------------------------------------------------------
1172
1173impl Display for BandwidthRestriction {
1174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1175        write!(f, "{}", self.max_bandwidth)
1176    }
1177}
1178
1179impl From<Frequency> for BandwidthRestriction {
1180    fn from(value: Frequency) -> Self {
1181        Self::new(value)
1182    }
1183}
1184
1185impl From<f64> for BandwidthRestriction {
1186    fn from(value: f64) -> Self {
1187        Self::new(value)
1188    }
1189}
1190
1191impl BandwidthRestriction {
1192    pub fn new<F: Into<Frequency>>(max_bandwidth: F) -> Self {
1193        Self {
1194            max_bandwidth: max_bandwidth.into(),
1195        }
1196    }
1197
1198    pub fn max_bandwidth(&self) -> Frequency {
1199        self.max_bandwidth
1200    }
1201}
1202
1203// ------------------------------------------------------------------------------------------------
1204// Sub-Modules
1205// ------------------------------------------------------------------------------------------------
1206pub mod uk_rsgb;
1207pub mod us_fcc;