use schemars::JsonSchema;
use serde::{Deserialize, Deserializer};
use super::form::{Presets, preset_or, presets};
use crate::Color;
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(remote = "Self", rename_all = "camelCase", deny_unknown_fields)]
#[schemars(transform = presets::<Space>)]
pub enum Space {
Earth(Earth),
Plane(Plane),
Plot(Plot),
Diagram(Diagram),
}
impl Presets for Space {
const NAMES: &'static [&'static str] = &["plane", "diagram", "mercator", "globe", "plot"];
fn preset(name: &str) -> Option<Space> {
Some(match name {
"plane" => Space::Plane(Plane::default()),
"diagram" => Space::Diagram(Diagram::default()),
"mercator" => Space::Earth(Earth::mercator()),
"globe" => Space::Earth(Earth::globe()),
"plot" => Space::Plot(Plot::default()),
_ => return None,
})
}
fn options<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Space, D::Error> {
Space::deserialize(deserializer)
}
}
impl<'de> Deserialize<'de> for Space {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Space, D::Error> {
preset_or(deserializer)
}
}
impl Default for Space {
fn default() -> Space {
Space::Plane(Plane::default())
}
}
impl From<Diagram> for Space {
fn from(diagram: Diagram) -> Space {
Space::Diagram(diagram)
}
}
impl From<Earth> for Space {
fn from(earth: Earth) -> Space {
Space::Earth(earth)
}
}
impl From<Plane> for Space {
fn from(plane: Plane) -> Space {
Space::Plane(plane)
}
}
impl From<Plot> for Space {
fn from(plot: Plot) -> Space {
Space::Plot(plot)
}
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct Earth {
pub projection: Projection,
pub surface: Option<Color>,
pub graticule: bool,
pub daylight: Option<f64>,
pub atmosphere: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Projection {
#[default]
Mercator,
Globe,
}
impl Earth {
pub fn mercator() -> Earth {
Earth { projection: Projection::Mercator, surface: None, graticule: true, daylight: None, atmosphere: false }
}
pub fn globe() -> Earth {
Earth { projection: Projection::Globe, ..Earth::mercator() }
}
#[must_use]
pub fn daylight(mut self, seconds: f64) -> Earth {
self.daylight = Some(seconds);
self
}
#[must_use]
pub fn atmosphere(mut self, on: bool) -> Earth {
self.atmosphere = on;
self
}
}
pub(crate) fn sun(seconds: f64) -> [f64; 3] {
let n = seconds / 86_400.0 - 10_957.5;
let mean = (280.460 + 0.985_647_4 * n).rem_euclid(360.0);
let anomaly = (357.528 + 0.985_600_3 * n).rem_euclid(360.0).to_radians();
let ecliptic = (mean + 1.915 * anomaly.sin() + 0.020 * (2.0 * anomaly).sin()).to_radians();
let tilt = (23.439 - 0.000_000_4 * n).to_radians();
let ascension = (tilt.cos() * ecliptic.sin()).atan2(ecliptic.cos());
let declination = (tilt.sin() * ecliptic.sin()).asin();
let sidereal = (280.460_618_37 + 360.985_647_366_29 * n).rem_euclid(360.0).to_radians();
let longitude = ascension - sidereal;
[declination.cos() * longitude.cos(), declination.cos() * longitude.sin(), declination.sin()]
}
impl Default for Earth {
fn default() -> Earth {
Earth::mercator()
}
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct Plane {
pub grid: Grid,
}
impl Default for Plane {
fn default() -> Plane {
Plane { grid: Grid::Lines { pitch: 10.0 } }
}
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct Diagram {
pub grid: Grid,
}
impl Default for Diagram {
fn default() -> Diagram {
Diagram { grid: Grid::Dots { pitch: 20.0, snap: false } }
}
}
#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum Grid {
Off,
Lines {
pitch: f64,
},
Dots {
pitch: f64,
#[serde(default)]
snap: bool,
},
}
#[derive(Clone, PartialEq, Debug, Default, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct Plot {
pub x: Axis,
pub y: Axis,
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct Axis {
pub label: Option<String>,
pub grid: bool,
pub hidden: bool,
#[serde(deserialize_with = "ticks")]
#[schemars(with = "Option<Vec<Tick>>")]
pub ticks: Ticks,
pub format: Format,
#[schemars(range(max = 15))]
pub precision: Option<u8>,
}
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
enum Tick {
At(f64),
Labeled(f64, Option<String>),
}
type Ticks = Option<Vec<(f64, Option<String>)>>;
fn ticks<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Ticks, D::Error> {
let ticks = Option::<Vec<Tick>>::deserialize(deserializer)?;
Ok(ticks.map(|ticks| {
ticks
.into_iter()
.map(|tick| match tick {
Tick::At(v) => (v, None),
Tick::Labeled(v, label) => (v, label),
})
.collect()
}))
}
impl Default for Axis {
fn default() -> Axis {
Axis { label: None, grid: true, hidden: false, ticks: None, format: Format::Auto, precision: None }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Format {
#[default]
Auto,
Fixed,
Scientific,
Engineering,
}
#[cfg(test)]
mod tests {
use super::sun;
#[test]
fn the_sun_stands_where_the_almanac_puts_it() {
let [x, y, z] = sun(1_782_043_200.0);
let (declination, longitude) = (z.asin().to_degrees(), y.atan2(x).to_degrees());
assert!((declination - 23.44).abs() < 0.05, "{declination}");
assert!(longitude.abs() < 1.0, "{longitude}");
let [x, y, _] = sun(1_782_043_200.0 + 6.0 * 3600.0);
assert!((y.atan2(x).to_degrees() + 90.0).abs() < 1.5);
}
}