pub mod czml;
pub mod geojson;
pub mod kml;
pub mod scene;
pub mod stk;
mod time;
pub use time::UtcEpoch;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Format {
Czml,
Kml,
GeoJson,
Stk,
Sigmf,
}
impl Format {
pub const ALL: [Format; 5] = [
Format::Czml,
Format::Kml,
Format::GeoJson,
Format::Stk,
Format::Sigmf,
];
pub fn as_str(self) -> &'static str {
match self {
Format::Czml => "czml",
Format::Kml => "kml",
Format::GeoJson => "geojson",
Format::Stk => "stk",
Format::Sigmf => "sigmf",
}
}
pub fn parse(s: &str) -> Result<Format, String> {
match s.to_ascii_lowercase().as_str() {
"czml" => Ok(Format::Czml),
"kml" => Ok(Format::Kml),
"geojson" | "json-geo" => Ok(Format::GeoJson),
"stk" | "stk-e" | "e" => Ok(Format::Stk),
"sigmf" => Ok(Format::Sigmf),
other => Err(format!(
"unknown export format '{other}'; expected one of czml, kml, geojson, stk, sigmf, all or list"
)),
}
}
pub fn spec_url(self) -> &'static str {
match self {
Format::Czml => {
"https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CZML-Structure"
}
Format::Kml => "https://www.ogc.org/standard/kml/",
Format::GeoJson => "https://www.rfc-editor.org/rfc/rfc7946",
Format::Stk => "https://help.agi.com/stk/#stk/importfiles-02.htm",
Format::Sigmf => "https://github.com/sigmf/SigMF/blob/main/sigmf-spec.md",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExportFile {
pub suffix: String,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExportError {
NotApplicable(String),
Failed(String),
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExportError::NotApplicable(why) => write!(f, "not applicable: {why}"),
ExportError::Failed(e) => write!(f, "{e}"),
}
}
}
pub fn export(src: &str, fmt: Format) -> Result<Vec<ExportFile>, ExportError> {
if crate::api::ScenarioKind::classify(src).ok() == Some(crate::api::ScenarioKind::Campaign) {
return export_campaign(src, fmt);
}
if fmt == Format::Sigmf {
return export_sigmf(src);
}
let scene = scene::scene_of(src)?;
export_scene(&scene, fmt)
}
pub fn export_scene(scene: &scene::Scene, fmt: Format) -> Result<Vec<ExportFile>, ExportError> {
match fmt {
Format::Czml => Ok(vec![ExportFile {
suffix: ".czml".into(),
bytes: czml::write(scene).into_bytes(),
}]),
Format::Kml => Ok(vec![ExportFile {
suffix: ".kml".into(),
bytes: kml::write(scene).into_bytes(),
}]),
Format::GeoJson => Ok(vec![ExportFile {
suffix: ".geojson".into(),
bytes: geojson::write(scene).into_bytes(),
}]),
Format::Stk => stk::write_all(scene),
Format::Sigmf => Err(ExportError::NotApplicable(
"SigMF records a signal, not geometry; it is written from the scenario, not a scene"
.into(),
)),
}
}
pub fn campaign_members(src: &str) -> Result<Vec<(String, String)>, ExportError> {
let members = crate::campaign::member_scenarios(src).map_err(ExportError::Failed)?;
let mut out: Vec<(String, String)> = Vec::new();
for (label, scn) in members {
let text = toml::to_string(&scn)
.map_err(|e| ExportError::Failed(format!("member {label}: {e}")))?;
if !out.iter().any(|(_, t)| *t == text) {
out.push((label, text));
}
}
Ok(out)
}
pub fn export_campaign(src: &str, fmt: Format) -> Result<Vec<ExportFile>, ExportError> {
let mut files = Vec::new();
for (label, text) in campaign_members(src)? {
if crate::api::ScenarioKind::classify(&text).ok()
== Some(crate::api::ScenarioKind::Campaign)
{
continue;
}
match export(&text, fmt) {
Ok(fs) => {
let part = stk::file_part(&label);
files.extend(fs.into_iter().map(|f| ExportFile {
suffix: format!(".{part}{}", f.suffix),
bytes: f.bytes,
}))
}
Err(ExportError::NotApplicable(_)) => {}
Err(ExportError::Failed(e)) => {
return Err(ExportError::Failed(format!("member {label}: {e}")))
}
}
}
if files.is_empty() {
return Err(ExportError::NotApplicable(CAMPAIGN_NONE.into()));
}
Ok(files)
}
const CAMPAIGN_NONE: &str = "no member scenario of the campaign has anything this format \
describes; export a member on its own to see its reason";
pub fn plan(src: &str) -> Vec<(Format, Result<(), String>)> {
if crate::api::ScenarioKind::classify(src).ok() == Some(crate::api::ScenarioKind::Campaign) {
let members = match campaign_members(src) {
Ok(m) => m,
Err(e) => {
let why = reason_of(&e);
return Format::ALL.iter().map(|&f| (f, Err(why.clone()))).collect();
}
};
let plans: Vec<Vec<(Format, Result<(), String>)>> = members
.iter()
.filter(|(_, t)| {
crate::api::ScenarioKind::classify(t).ok()
!= Some(crate::api::ScenarioKind::Campaign)
})
.map(|(_, t)| plan(t))
.collect();
return Format::ALL
.iter()
.enumerate()
.map(|(i, &f)| {
let ok = plans.iter().any(|p| p[i].1.is_ok());
(
f,
if ok {
Ok(())
} else {
Err(CAMPAIGN_NONE.to_string())
},
)
})
.collect();
}
let scene = scene::scene_of(src);
Format::ALL
.iter()
.map(|&f| {
let r = match f {
Format::Sigmf => sigmf_applicability(src),
Format::Stk => match &scene {
Ok(s) => stk::applicability(s),
Err(e) => Err(reason_of(e)),
},
_ => match &scene {
Ok(_) => Ok(()),
Err(e) => Err(reason_of(e)),
},
};
(f, r)
})
.collect()
}
fn reason_of(e: &ExportError) -> String {
match e {
ExportError::NotApplicable(w) => w.clone(),
ExportError::Failed(m) => format!("the scenario could not be exported: {m}"),
}
}
fn sigmf_applicability(src: &str) -> Result<(), String> {
let kind = crate::api::ScenarioKind::classify(src).map_err(|e| e.to_string())?;
if kind != crate::api::ScenarioKind::Spectrum {
return Err(
"SigMF holds complex baseband samples, which only the `spectrum` kind synthesises"
.into(),
);
}
let scn: crate::spectrum::SpectrumScenario =
toml::from_str(src).map_err(|e| format!("invalid spectrum scenario: {e}"))?;
if scn.iq.is_none() {
return Err(
"this spectrum scenario has no [iq] block, so no samples are synthesised to record"
.into(),
);
}
Ok(())
}
fn export_sigmf(src: &str) -> Result<Vec<ExportFile>, ExportError> {
sigmf_applicability(src).map_err(ExportError::NotApplicable)?;
let scn: crate::spectrum::SpectrumScenario = toml::from_str(src)
.map_err(|e| ExportError::Failed(format!("invalid spectrum scenario: {e}")))?;
let (meta, data) = scn
.export_sigmf()
.map_err(ExportError::Failed)?
.ok_or_else(|| ExportError::NotApplicable("no [iq] block".into()))?;
Ok(vec![
ExportFile {
suffix: ".sigmf-meta".into(),
bytes: meta.into_bytes(),
},
ExportFile {
suffix: ".sigmf-data".into(),
bytes: data,
},
])
}
pub fn scenario_table_md(scenarios: &[(String, String)]) -> String {
let mut out = String::new();
out.push_str("| Scenario | Kind | CZML | KML | GeoJSON | STK `.e` | SigMF |\n");
out.push_str("|---|---|---|---|---|---|---|\n");
let mut reasons: Vec<String> = Vec::new();
for (name, src) in scenarios {
let kind = crate::api::ScenarioKind::classify(src)
.map(|k| k.as_str().to_string())
.unwrap_or_else(|_| "?".into());
out.push_str(&format!("| `{name}` | `{kind}` |"));
for (_, r) in plan(src) {
match r {
Ok(()) => out.push_str(" yes |"),
Err(why) => {
let idx = match reasons.iter().position(|w| *w == why) {
Some(i) => i,
None => {
reasons.push(why);
reasons.len() - 1
}
};
out.push_str(&format!(" no [{}] |", idx + 1));
}
}
}
out.push('\n');
}
out.push_str("\nWhy a format does not apply:\n\n");
for (i, w) in reasons.iter().enumerate() {
out.push_str(&format!("{}. {}\n", i + 1, w));
}
out
}
pub(crate) fn round_dp(v: f64, dp: i32) -> f64 {
let s = 10f64.powi(dp);
let r = (v * s).round() / s;
if r == 0.0 {
0.0
} else {
r
}
}
pub(crate) fn fmt_dp(v: f64, dp: usize) -> String {
let v = round_dp(v, dp as i32);
let s = format!("{v:.dp$}");
if s.contains('.') {
let t = s.trim_end_matches('0').trim_end_matches('.');
if t == "-0" {
"0".into()
} else {
t.to_string()
}
} else {
s
}
}