#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::non_ascii_literal)]
use std::fmt;
use std::io::BufRead;
use std::mem;
use lazy_static::lazy_static;
use log::{debug, trace};
use regex::Regex;
#[cfg(feature = "serde")]
use serde::Serialize;
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum Class {
A,
B,
C,
D,
E,
F,
G,
CTR,
Restricted,
Danger,
Prohibited,
GliderProhibited,
WaveWindow,
RadioMandatoryZone,
TransponderMandatoryZone,
}
impl fmt::Display for Class {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl Class {
fn parse(data: &str) -> Result<Self, String> {
match data {
"A" => Ok(Class::A),
"B" => Ok(Class::B),
"C" => Ok(Class::C),
"D" => Ok(Class::D),
"E" => Ok(Class::E),
"F" => Ok(Class::F),
"G" => Ok(Class::G),
"CTR" => Ok(Class::CTR),
"R" => Ok(Class::Restricted),
"Q" => Ok(Class::Danger),
"P" => Ok(Class::Prohibited),
"GP" => Ok(Class::GliderProhibited),
"W" => Ok(Class::WaveWindow),
"RMZ" => Ok(Class::RadioMandatoryZone),
"TMZ" => Ok(Class::TransponderMandatoryZone),
other => Err(format!("Invalid class: {}", other))
}
}
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", content = "val"))]
pub enum Altitude {
Gnd,
FeetAmsl(i32),
FeetAgl(i32),
FlightLevel(u16),
Unlimited,
Other(String),
}
impl fmt::Display for Altitude {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Altitude::Gnd => write!(f, "GND"),
Altitude::FeetAmsl(ft) => write!(f, "{} ft AMSL", ft),
Altitude::FeetAgl(ft) => write!(f, "{} ft AGL", ft),
Altitude::FlightLevel(ft) => write!(f, "FL{}", ft),
Altitude::Unlimited => write!(f, "Unlimited"),
Altitude::Other(val) => write!(f, "?({})", val),
}
}
}
impl Altitude {
#[allow(clippy::cast_possible_truncation)]
fn m2ft(val: i32) -> Result<i32, &'static str> {
if val > 654_553_015 {
return Err("m2ft out of bounds (too large)");
} else if val < -654_553_016 {
return Err("m2ft out of bounds (too small)");
}
let m = f64::from(val);
let feet = m / 0.3048;
Ok(feet.round() as i32)
}
fn parse(data: &str) -> Result<Self, String> {
match data {
"gnd" | "Gnd" | "GND" |
"sfc" | "Sfc" | "SFC" |
"0" => {
Ok(Altitude::Gnd)
}
"unl" | "Unl" | "UNL" |
"unlim" | "Unlim" | "UNLIM" |
"unltd" | "Unltd" | "UNLTD" |
"unlimited" | "Unlimited" | "UNLIMITED" => {
Ok(Altitude::Unlimited)
}
fl if fl.starts_with("fl") || fl.starts_with("Fl") || fl.starts_with("FL") => {
match fl[2..].trim().parse::<u16>() {
Ok(val) => Ok(Altitude::FlightLevel(val)),
Err(_) => Err(format!("Invalid altitude: {}", fl)),
}
}
other => {
let is_digit = |c: &char| c.is_digit(10);
let number: String = other.chars().take_while(is_digit).collect();
let rest: String = other.chars().skip_while(is_digit).collect();
lazy_static! {
static ref RE_FT_AMSL: Regex = Regex::new(r"(?i)^ft(:? amsl)?$").unwrap();
static ref RE_M_AMSL: Regex = Regex::new(r"(?i)^m(:?sl)?$").unwrap();
static ref RE_FT_AGL: Regex = Regex::new(r"(?i)^(:?ft )?(:?agl|gnd|sfc)$").unwrap();
static ref RE_M_AGL: Regex = Regex::new(r"(?i)^(:?m )?(:?agl|gnd|sfc)$").unwrap();
}
if let Ok(val) = number.parse::<i32>() {
let trimmed = rest.trim();
if RE_FT_AMSL.is_match(trimmed) {
return Ok(Altitude::FeetAmsl(val))
} else if RE_FT_AGL.is_match(trimmed) {
return Ok(Altitude::FeetAgl(val))
} else if RE_M_AMSL.is_match(trimmed) {
return Ok(Altitude::FeetAmsl(Self::m2ft(val)?))
} else if RE_M_AGL.is_match(trimmed) {
return Ok(Altitude::FeetAgl(Self::m2ft(val)?))
}
}
Ok(Altitude::Other(other.to_string()))
}
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Direction {
Cw,
Ccw,
}
impl Default for Direction {
fn default() -> Self {
Direction::Cw
}
}
impl Direction {
fn parse(data: &str) -> Result<Self, String> {
match data {
"+" => Ok(Direction::Cw),
"-" => Ok(Direction::Ccw),
_ => Err(format!("Invalid direction: {}", data)),
}
}
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Coord {
lat: f64,
lng: f64,
}
impl Coord {
fn parse_number_opt(val: Option<&str>) -> Result<u16, ()> {
val.and_then(|v| v.parse::<u16>().ok()).ok_or(())
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn parse_component(val: &str) -> Result<f64, ()> {
let mut parts = val.split(|c| c == ':' || c == '.');
let deg = Self::parse_number_opt(parts.next())?;
let min = Self::parse_number_opt(parts.next())?;
let sec = Self::parse_number_opt(parts.next())?;
let mut total = f64::from(deg) + f64::from(min) / 60.0 + f64::from(sec) / 3600.0;
if let Some(fractional) = parts.next() {
let frac = fractional.parse::<u16>().map_err(|_| ())?;
total += f64::from(frac)
/ 10_f64.powi(fractional.len() as i32)
/ 3600.0
}
Ok(total)
}
fn multiplier_lat(val: &str) -> Result<f64, ()> {
match val {
"N" | "n" => Ok(1.0),
"S" | "s" => Ok(-1.0),
_ => Err(())
}
}
fn multiplier_lng(val: &str) -> Result<f64, ()> {
match val {
"E" | "e" => Ok(1.0),
"W" | "w" => Ok(-1.0),
_ => Err(())
}
}
fn parse(data: &str) -> Result<Self, String> {
lazy_static! {
static ref RE: Regex = Regex::new(r"(?xi)
([0-9]{1,3}[\.:][0-9]{1,3}[\.:][0-9]{1,3}(:?\.?[0-9]{1,3})?) # Lat
\s*
([NS]) # North / South
\s*,?\s*
([0-9]{1,3}[\.:][0-9]{1,3}[\.:][0-9]{1,3}(:?\.?[0-9]{1,3})?) # Lon
\s*
([EW]) # East / West
").unwrap();
}
let invalid = |_| format!("Invalid coord: \"{}\"", data);
let cap = RE.captures(data).ok_or_else(|| format!("Invalid coord: \"{}\"", data))?;
let lat = Self::multiplier_lat(&cap[3]).map_err(invalid)?
* Self::parse_component(&cap[1]).map_err(invalid)?;
let lng = Self::multiplier_lng(&cap[6]).map_err(invalid)?
* Self::parse_component(&cap[4]).map_err(invalid)?;
Ok(Self { lat, lng })
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ArcSegment {
centerpoint: Coord,
radius: f32,
angle_start: f32,
angle_end: f32,
direction: Direction,
}
impl ArcSegment {
fn validate_angle(val: f32) -> Result<f32, String> {
if val > 360.0 {
return Err(format!("Angle {} too large", val));
}
if val < 0.0 {
return Err(format!("Angle {} is negative", val));
}
Ok(val)
}
fn parse(data: &str, centerpoint: Coord, direction: Direction) -> Result<Self, String> {
let errmsg = || format!("Invalid arc segment data: {}", data);
let parts: Vec<f32> = data
.split(',')
.map(str::trim)
.map(str::parse)
.collect::<Result<Vec<f32>, _>>()
.map_err(|_| errmsg())?;
if parts.len() != 3 {
return Err(errmsg());
}
Ok(Self {
centerpoint,
radius: parts[0],
angle_start: Self::validate_angle(parts[1])?,
angle_end: Self::validate_angle(parts[2])?,
direction,
})
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Arc {
centerpoint: Coord,
start: Coord,
end: Coord,
direction: Direction,
}
impl Arc {
fn parse(data: &str, centerpoint: Coord, direction: Direction) -> Result<Self, String> {
let errmsg = || format!("Invalid arc data: {}", data);
let parts: Vec<Coord> = data
.split(',')
.map(str::trim)
.map(Coord::parse)
.collect::<Result<Vec<Coord>, _>>()
.map_err(|_| errmsg())?;
if parts.len() != 2 {
return Err(errmsg());
}
let mut coords = parts.into_iter();
Ok(Self {
centerpoint,
start: coords.next().unwrap(),
end: coords.next().unwrap(),
direction,
})
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
pub enum PolygonSegment {
Point(Coord),
Arc(Arc),
ArcSegment(ArcSegment),
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
pub enum Geometry {
Polygon {
segments: Vec<PolygonSegment>
},
Circle {
centerpoint: Coord,
radius: f32,
},
}
impl fmt::Display for Geometry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Geometry::Polygon { segments } => write!(f, "Polygon[{}]", segments.len()),
Geometry::Circle { radius, .. } => write!(f, "Circle[r={}NM]", radius),
}
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Airspace {
pub name: String,
pub class: Class,
#[cfg_attr(feature = "serde", serde(rename = "type"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub type_: Option<String>,
pub lower_bound: Altitude,
pub upper_bound: Altitude,
pub geom: Geometry,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub frequency: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub call_sign: Option<String>,
}
impl fmt::Display for Airspace {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{} [{}] ({} → {}) {{{}}}",
self.name,
self.class,
self.lower_bound,
self.upper_bound,
self.geom,
)
}
}
#[derive(Debug)]
struct AirspaceBuilder {
new: bool,
name: Option<String>,
class: Option<Class>,
lower_bound: Option<Altitude>,
upper_bound: Option<Altitude>,
geom: Option<Geometry>,
type_: Option<String>,
frequency: Option<String>,
call_sign: Option<String>,
var_x: Option<Coord>,
var_d: Option<Direction>,
}
macro_rules! setter {
(ONCE, $method:ident, $field:ident, $type:ty) => {
fn $method(&mut self, $field: $type) -> Result<(), String> {
self.new = false;
if self.$field.is_some() {
Err(format!("Could not set {} (already defined)", stringify!($field)))
} else {
self.$field = Some($field);
Ok(())
}
}
};
(MANY, $method:ident, $field:ident, $type:ty) => {
fn $method(&mut self, $field: $type) {
self.new = false;
self.$field = Some($field);
}
};
}
impl AirspaceBuilder {
fn new() -> Self {
Self {
new: true,
name: None,
class: None,
lower_bound: None,
upper_bound: None,
geom: None,
type_: None,
frequency: None,
call_sign: None,
var_x: None,
var_d: None,
}
}
setter!(ONCE, set_name, name, String);
setter!(ONCE, set_class, class, Class);
setter!(ONCE, set_lower_bound, lower_bound, Altitude);
setter!(ONCE, set_upper_bound, upper_bound, Altitude);
setter!(ONCE, set_type, type_, String);
setter!(ONCE, set_frequency, frequency, String);
setter!(ONCE, set_call_sign, call_sign, String);
setter!(MANY, set_var_x, var_x, Coord);
setter!(MANY, set_var_d, var_d, Direction);
fn add_segment(&mut self, segment: PolygonSegment) -> Result<(), String> {
self.new = false;
match &mut self.geom {
None => {
self.geom = Some(Geometry::Polygon {
segments: vec![segment],
})
}
Some(Geometry::Polygon { ref mut segments }) => {
segments.push(segment);
}
Some(Geometry::Circle { .. }) => {
return Err("Cannot add a point to a circle".into());
}
}
Ok(())
}
fn set_circle_radius(&mut self, radius: f32) -> Result<(), String> {
self.new = false;
match (&self.geom, &self.var_x) {
(None, Some(centerpoint)) => {
self.geom = Some(Geometry::Circle { centerpoint: centerpoint.clone(), radius });
Ok(())
}
(Some(_), _) => {
Err("Geometry already set".into())
}
(_, None) => {
Err("Centerpoint missing".into())
}
}
}
fn finish(self) -> Result<Airspace, String> {
debug!("Finish {:?}", self.name);
let name = self.name.ok_or("Missing name")?;
let class = self.class.ok_or_else(|| format!("Missing class for '{}'", name))?;
let lower_bound = self.lower_bound.ok_or_else(|| format!("Missing lower bound for '{}'", name))?;
let upper_bound = self.upper_bound.ok_or_else(|| format!("Missing upper bound for '{}'", name))?;
let geom = self.geom.ok_or_else(|| format!("Missing geom for '{}'", name))?;
Ok(Airspace {
name,
class,
type_: self.type_,
lower_bound,
upper_bound,
geom,
frequency: self.frequency,
call_sign: self.call_sign,
})
}
}
#[inline]
fn starts_airspace(line: &str) -> bool {
line.starts_with("AC ")
}
fn process(builder: &mut AirspaceBuilder, line: &str) -> Result<(), String> {
if line.trim().is_empty() {
trace!("Empty line, ignoring");
return Ok(())
}
let mut chars = line.chars().filter(|c: &char| !c.is_ascii_whitespace());
let t1 = chars.next().ok_or_else(|| "Line too short".to_string())?;
let t2 = chars.next().unwrap_or(' ');
let data = line.splitn(2, ' ').nth(1).unwrap_or("").trim();
trace!("Input: \"{:1}{:1}\"", t1, t2);
match (t1, t2) {
('*', _) => trace!("-> Comment, ignore"),
('A', 'C') => {
let class = Class::parse(data)?;
trace!("-> Found class: {}", class);
builder.set_class(class)?;
}
('A', 'N') => {
trace!("-> Found name: {}", data);
builder.set_name(data.to_string())?;
}
('A', 'L') => {
let altitude = Altitude::parse(data)?;
trace!("-> Found lower bound: {}", altitude);
builder.set_lower_bound(altitude)?;
}
('A', 'H') => {
let altitude = Altitude::parse(data)?;
trace!("-> Found upper bound: {}", altitude);
builder.set_upper_bound(altitude)?;
}
('A', 'T') => {
trace!("-> Label placement hint, ignore");
}
('A', 'Y') => {
trace!("-> Found type: {}", data);
builder.set_type(data.to_string())?;
}
('A', 'F') => {
trace!("-> Found frequency: {}", data);
builder.set_frequency(data.to_string())?;
}
('A', 'G') => {
trace!("-> Found call sign: {}", data);
builder.set_call_sign(data.to_string())?;
}
('S', 'P') => trace!("-> Pen, ignore"),
('S', 'B') => trace!("-> Brush, ignore"),
('V', 'X') => {
trace!("-> Found X variable");
let coord = Coord::parse(data.get(2..).unwrap_or(""))?;
builder.set_var_x(coord);
}
('V', 'D') => {
trace!("-> Found D variable");
let direction = Direction::parse(data.get(2..).unwrap_or(""))?;
builder.set_var_d(direction);
}
('D', 'P') => {
trace!("-> Found point");
let coord = Coord::parse(data)?;
builder.add_segment(PolygonSegment::Point(coord))?;
}
('D', 'C') => {
trace!("-> Found circle radius");
let radius = data.parse::<f32>().map_err(|_| format!("Invalid radius: {}", data))?;
builder.set_circle_radius(radius)?;
}
('D', 'A') => {
trace!("-> Found arc segment");
let centerpoint = builder.var_x.clone().ok_or("Centerpoint missing")?;
let direction = builder.var_d.unwrap_or_default();
let arc_segment = ArcSegment::parse(data, centerpoint, direction)?;
builder.add_segment(PolygonSegment::ArcSegment(arc_segment))?;
}
('D', 'B') => {
trace!("-> Found arc");
let centerpoint = builder.var_x.clone().ok_or("Centerpoint missing")?;
let direction = builder.var_d.unwrap_or_default();
let arc = Arc::parse(data, centerpoint, direction)?;
builder.add_segment(PolygonSegment::Arc(arc))?;
}
(t1, t2) => {
return Err(format!("Parse error (unexpected \"{:1}{:1}\")", t1, t2))
}
}
Ok(())
}
pub fn parse<R: BufRead>(reader: &mut R) -> Result<Vec<Airspace>, String> {
let mut airspaces = vec![];
let mut builder = AirspaceBuilder::new();
let mut buf: Vec<u8> = vec![];
loop {
buf.clear();
let bytes_read = reader.read_until(0x0a, &mut buf)
.map_err(|e| format!("Could not read line: {}", e))?;
if bytes_read == 0 {
trace!("Reached EOF");
airspaces.push(builder.finish()?);
return Ok(airspaces);
}
let line = String::from_utf8_lossy(&buf);
let trimmed_line = line.trim_start_matches('\u{feff}').trim();
let start_of_airspace = starts_airspace(trimmed_line);
if start_of_airspace && !builder.new {
let old_builder = mem::replace(&mut builder, AirspaceBuilder::new());
airspaces.push(old_builder.finish()?);
}
process(&mut builder, trimmed_line)?;
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
mod coord {
use super::*;
#[test]
#[allow(clippy::unreadable_literal)]
fn parse_valid() {
assert_eq!(
Coord::parse("46:51:44 N 009:19:42 E"),
Ok(Coord { lat: 46.86222222222222, lng: 9.328333333333333 })
);
assert_eq!(
Coord::parse("46:51:44N 009:19:42E"),
Ok(Coord { lat: 46.86222222222222, lng: 9.328333333333333 })
);
assert_eq!(
Coord::parse("46:51.44 N 009:19.42 E"),
Ok(Coord { lat: 46.86222222222222, lng: 9.328333333333333 })
);
assert_eq!(
Coord::parse("46:51:44 S 009:19:42 W"),
Ok(Coord { lat: -46.86222222222222, lng: -9.328333333333333 })
);
assert_eq!(
Coord::parse("1:0:0.123 N 2:0:1.2 E"),
Ok(Coord { lat: 1.0 + 0.123 / 3600.0, lng: 2.0 + 1.2 / 3600.0 })
);
assert!(Coord::parse("45:42:21 N, 000:38:41 W").is_ok());
assert!(Coord::parse("49:33:8 n 5:47:37 e").is_ok());
}
#[test]
fn parse_invalid() {
assert_eq!(
Coord::parse("46:51:44 Q 009:19:42 R"),
Err("Invalid coord: \"46:51:44 Q 009:19:42 R\"".to_string())
);
assert_eq!(
Coord::parse("46x51x44 S 009x19x42 W"),
Err("Invalid coord: \"46x51x44 S 009x19x42 W\"".to_string())
);
}
}
mod altitude {
use super::*;
#[test]
fn m2ft() {
assert_eq!(Altitude::m2ft(0).unwrap(), 0);
assert_eq!(Altitude::m2ft(1).unwrap(), 3);
assert_eq!(Altitude::m2ft(2).unwrap(), 7);
assert_eq!(Altitude::m2ft(100).unwrap(), 328);
assert_eq!(Altitude::m2ft(654_553_015).unwrap(), 2_147_483_645);
assert_eq!(Altitude::m2ft(-654_553_016).unwrap(), -2_147_483_648);
assert!(Altitude::m2ft(654_553_016).is_err());
assert!(Altitude::m2ft(-654_553_017).is_err());
}
#[test]
fn parse_gnd() {
assert_eq!(Altitude::parse("gnd").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("Gnd").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("GND").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("sfc").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("Sfc").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("SFC").unwrap(), Altitude::Gnd);
}
#[test]
fn parse_amsl() {
assert_eq!(Altitude::parse("42 ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42 FT").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42 ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42 ft AMSL").unwrap(), Altitude::FeetAmsl(42));
}
#[test]
fn parse_agl() {
assert_eq!(Altitude::parse("42 ft agl").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42FT Agl").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42 ft GND").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42 GND").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42SFC").unwrap(), Altitude::FeetAgl(42));
}
#[test]
fn parse_fl() {
assert_eq!(Altitude::parse("fl50").unwrap(), Altitude::FlightLevel(50));
assert_eq!(Altitude::parse("FL 180").unwrap(), Altitude::FlightLevel(180));
assert_eq!(Altitude::parse("FL130").unwrap(), Altitude::FlightLevel(130));
}
}
mod arc_segment {
use super::*;
static COORD: Coord = Coord { lat: 1.0, lng: 2.0 };
#[test]
fn parse_ok() {
assert_eq!(
ArcSegment::parse("10,270,290", COORD.clone(), Direction::Cw).unwrap(),
ArcSegment {
centerpoint: COORD.clone(),
radius: 10.0,
angle_start: 270.0,
angle_end: 290.0,
direction: Direction::Cw,
}
);
assert_eq!(
ArcSegment::parse("23,0,30", COORD.clone(), Direction::Ccw).unwrap(),
ArcSegment {
centerpoint: COORD.clone(),
radius: 23.0,
angle_start: 0.0,
angle_end: 30.0,
direction: Direction::Ccw,
}
);
}
#[test]
fn parse_with_spaces() {
assert_eq!(
ArcSegment::parse(" 10 , 270 ,290", COORD.clone(), Direction::Cw).unwrap(),
ArcSegment {
centerpoint: COORD.clone(),
radius: 10.0,
angle_start: 270.0,
angle_end: 290.0,
direction: Direction::Cw,
}
);
}
#[test]
fn parse_invalid_too_many() {
assert!(ArcSegment::parse(" 10 , 270 ,290,", COORD.clone(), Direction::Cw).is_err());
}
#[test]
fn parse_invalid_angle_too_large() {
assert!(ArcSegment::parse("10,270,361", COORD.clone(), Direction::Cw).is_err());
}
#[test]
fn parse_invalid_angle_negative() {
assert!(ArcSegment::parse("10,270,-10", COORD.clone(), Direction::Cw).is_err());
}
}
mod parse_airspace {
use super::*;
#[test]
fn flyland_buochs() {
let mut airspace = indoc!("
AC D
AN BUOCHS Be CTR 119.625
AL GND
AH 12959 ft
DP 46:57:13 N 008:27:52 E
DP 46:57:46 N 008:30:41 E
DP 46:57:55 N 008:28:40 E
DP 46:58:28 N 008:27:56 E
DP 46:57:13 N 008:27:52 E
* n-Points: 5
").as_bytes();
let mut spaces = parse(&mut airspace).unwrap();
assert_eq!(spaces.len(), 1);
let space: Airspace = spaces.pop().unwrap();
assert_eq!(space.name, "BUOCHS Be CTR 119.625");
assert_eq!(space.lower_bound, Altitude::Gnd);
assert_eq!(space.upper_bound, Altitude::FeetAmsl(12959));
if let Geometry::Polygon { segments } = space.geom {
assert_eq!(segments.len(), 5);
} else {
panic!("Unexpected enum variant");
}
}
#[test]
fn inverted_bounds() {
let mut a1 = indoc!("
AC D
AN SOMESPACE
AL GND
AH 12959 ft
DP 46:57:13 N 008:27:52 E
DP 46:57:46 N 008:30:41 E
*
").as_bytes();
let mut a2 = indoc!("
AC D
AN SOMESPACE
AH 12959 ft
AL GND
DP 46:57:13 N 008:27:52 E
DP 46:57:46 N 008:30:41 E
*
").as_bytes();
let space1 = parse(&mut a1).unwrap().pop().unwrap();
let space2 = parse(&mut a2).unwrap().pop().unwrap();
assert_eq!(space1, space2);
}
#[test]
fn multi_variable() {
let mut a = indoc!("
AC D
AN SOMESPACE
AL GND
AH FL100
V X=52:00:00N 013:00:00E
V D=+
DA 2,0,30
V X=52:00:00N 013:00:00E
V D=-
DA 4,60,30
*
").as_bytes();
let airspace = parse(&mut a).unwrap().pop().unwrap();
assert_eq!(airspace.geom, Geometry::Polygon {
segments: vec![
PolygonSegment::ArcSegment(ArcSegment {
centerpoint: Coord { lat: 52.0, lng: 13.0 },
radius: 2.0,
angle_start: 0.0,
angle_end: 30.0,
direction: Direction::Cw,
}),
PolygonSegment::ArcSegment(ArcSegment {
centerpoint: Coord { lat: 52.0, lng: 13.0 },
radius: 4.0,
angle_start: 60.0,
angle_end: 30.0,
direction: Direction::Ccw,
}),
],
});
}
#[test]
fn extension_records() {
let mut a = indoc!("
AC D
AN SOMESPACE
AL GND
AH 100 ft AGL
AY AWY
AF 132.350
AG Dutch Mil
V X=52:00:00 N 013:00:00 E
DC 5
").as_bytes();
let airspace = parse(&mut a).unwrap().pop().unwrap();
assert_eq!(airspace.type_, Some("AWY".to_string()));
assert_eq!(airspace.frequency, Some("132.350".to_string()));
assert_eq!(airspace.call_sign, Some("Dutch Mil".to_string()));
}
}
#[cfg(feature = "serde")]
mod serde {
use super::*;
use serde_json::to_string;
#[test]
fn serialize_json() {
let airspace = Airspace {
name: "SUPERSPACE".into(),
class: Class::Prohibited,
lower_bound: Altitude::Gnd,
upper_bound: Altitude::FeetAgl(3000),
geom: Geometry::Polygon {
segments: vec![
PolygonSegment::Point(Coord { lat: 1.0, lng: 2.0 }),
PolygonSegment::Point(Coord { lat: 1.1, lng: 2.0 }),
PolygonSegment::Arc(Arc {
centerpoint: Coord { lat: 1.05, lng: 2.05 },
start: Coord { lat: 1.1, lng: 2.0 },
end: Coord { lat: 1.0, lng: 2.1 },
direction: Direction::Cw,
}),
PolygonSegment::ArcSegment(ArcSegment {
centerpoint: Coord { lat: 3.0, lng: 3.0 },
radius: 1.5,
angle_start: 30.0,
angle_end: 45.0,
direction: Direction::Ccw,
}),
PolygonSegment::Point(Coord { lat: 1.0, lng: 2.0 }),
],
},
type_: None,
frequency: None,
call_sign: None,
};
assert_eq!(
to_string(&airspace).unwrap(),
"{\"name\":\"SUPERSPACE\",\
\"class\":\"Prohibited\",\
\"lowerBound\":{\"type\":\"Gnd\"},\
\"upperBound\":{\"type\":\"FeetAgl\",\"val\":3000},\
\"geom\":{\
\"type\":\"Polygon\",\
\"segments\":[\
{\"type\":\"Point\",\"lat\":1.0,\"lng\":2.0},\
{\"type\":\"Point\",\"lat\":1.1,\"lng\":2.0},\
{\"type\":\"Arc\",\
\"centerpoint\":{\"lat\":1.05,\"lng\":2.05},\
\"start\":{\"lat\":1.1,\"lng\":2.0},\
\"end\":{\"lat\":1.0,\"lng\":2.1},\
\"direction\":\"cw\"},\
{\"type\":\"ArcSegment\",\
\"centerpoint\":{\"lat\":3.0,\"lng\":3.0},\
\"radius\":1.5,\
\"angleStart\":30.0,\
\"angleEnd\":45.0,\
\"direction\":\"ccw\"},\
{\"type\":\"Point\",\"lat\":1.0,\"lng\":2.0}\
]\
}\
}"
);
}
}
}