1use std::collections::HashMap;
2use std::ops::Deref;
3use aviation_calc_util::{geo::{Bearing, GeoPoint}, units::{Angle, Length}};
4use geojson::{Feature, FeatureCollection, Geometry, Value};
5use serde::{Deserialize, Serialize};
6use serde_json::Map;
7
8use crate::loaders::euroscope::{colour::Colour, line::{ColouredLine, LineGroup}, sector::RegionGroup, symbology::{self, SymbologyInfo, SymbologyItemType}, EsAsr};
9use crate::loaders::euroscope::partial::SidStarType::Star;
10use crate::loaders::vnas_crc::CrcVideoMapRef;
11use crate::loaders::vnas_crc::eram::EramConfig;
12use crate::loaders::vnas_crc::stars::{StarsArea, StarsConfiguration};
13use crate::loaders::vnas_crc::tower::TowerCabConfig;
14use super::symbol::SymbolIcon;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum AtcDisplayItem {
18 Map{id: String, visible: bool},
19 Symbol{id: String, show_symbol: bool, show_label: bool},
20 NavdataItem{symbol_type: String, ident: String, show_symbol: bool, show_label: bool},
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24#[repr(u8)]
25pub enum TextAlign {
26 #[default]
27 TopLeft = 0,
28 CenterLeft = 1,
29 BottomLeft = 2,
30 TopCenter = 3,
31 CenterCenter = 4,
32 BottomCenter = 5,
33 TopRight = 6,
34 CenterRight = 7,
35 BottomRight = 8
36}
37
38impl From<u8> for TextAlign {
39 fn from(value: u8) -> Self {
40 match value {
41 1 => Self::CenterRight,
42 2 => Self::BottomLeft,
43 3 => Self::TopCenter,
44 4 => Self::CenterCenter,
45 5 => Self::BottomCenter,
46 6 => Self::TopRight,
47 7 => Self::CenterRight,
48 8 => Self::BottomRight,
49 _ => Self::TopLeft
50 }
51 }
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct DisplayDefaultConfig {
56 pub color: Colour,
57 pub size: f32,
58 pub line_weight: u8,
59 pub line_style: String,
60 pub text_align: TextAlign,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub enum AtcDisplayBackground {
65 #[default]
66 Blank,
67 Satellite,
68 Color(String),
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72pub struct AtcDisplayType {
73 pub id: String,
74 pub map_defaults: HashMap<String, DisplayDefaultConfig>,
75 pub symbol_defaults: HashMap<String, (DisplayDefaultConfig, DisplayDefaultConfig)>,
76 pub symbol_icons: HashMap<String, SymbolIcon>,
77 pub line_types: HashMap<String, Vec<u8>>,
78 pub background: AtcDisplayBackground
79}
80
81impl AtcDisplayType {
82 pub fn try_from_es_symbology(id: String, symbology: SymbologyInfo) -> anyhow::Result<Self> {
83 let mut map_defaults = HashMap::new();
84 let mut symbol_defaults = HashMap::new();
85 let mut symbol_icons = HashMap::new();
86 let mut background = AtcDisplayBackground::Blank;
87
88 for symbol in symbology.symbols {
89 if matches!(symbol.item_type, SymbologyItemType::Airports | SymbologyItemType::Fixes | SymbologyItemType::Vors | SymbologyItemType::Ndbs) {
90 let mut symb_cfg = DisplayDefaultConfig::default();
91 let mut name_cfg = DisplayDefaultConfig::default();
92 for attr in symbol.defs {
93 if attr.attribute == "name" {
94 name_cfg.color = attr.color;
95 name_cfg.line_style = Self::es_line_type_to_string(attr.line_style);
96 name_cfg.line_weight = attr.line_weight;
97 name_cfg.size = attr.size;
98 name_cfg.text_align = attr.text_align.into();
99 } else {
100 symb_cfg.color = attr.color;
101 symb_cfg.line_style = Self::es_line_type_to_string(attr.line_style);
102 symb_cfg.line_weight = attr.line_weight;
103 symb_cfg.size = attr.size;
104 symb_cfg.text_align = attr.text_align.into();
105 }
106 }
107 symbol_defaults.insert(symbol.item_type.to_key_string(), (symb_cfg, name_cfg));
108 } else if matches!(symbol.item_type, SymbologyItemType::ArtccBoundary | SymbologyItemType::ArtccHighBoundary | SymbologyItemType::ArtccLowBoundary | SymbologyItemType::Geo | SymbologyItemType::LowAirways | SymbologyItemType::HighAirways | SymbologyItemType::Region | SymbologyItemType::Sids | SymbologyItemType::Stars) {
109 let mut cfg = DisplayDefaultConfig::default();
110
111 for attr in symbol.defs {
112 if attr.attribute == "line" {
113 cfg.color = attr.color;
114 cfg.line_style = Self::es_line_type_to_string(attr.line_style);
115 cfg.line_weight = attr.line_weight;
116 cfg.size = attr.size;
117 cfg.text_align = attr.text_align.into();
118 }
119 }
120
121 map_defaults.insert(symbol.item_type.to_key_string(), cfg);
122 } else if symbol.item_type == SymbologyItemType::Sector {
123 for attr in symbol.defs {
124 if attr.attribute == "active sector background" {
125 background = AtcDisplayBackground::Color(format!("#{:02X}{:02X}{:02X}", attr.color.r, attr.color.g, attr.color.b));
126 }
127 }
128 }
129 }
130
131 for icon in symbology.symbol_icons {
132 let ret_icon = SymbolIcon::try_from_es_symbol_icon(icon.0, icon.1)?;
133
134 symbol_icons.insert(ret_icon.symbol_type.to_string(), ret_icon);
135 }
136
137 Ok(AtcDisplayType {
138 id: id.to_string(),
139 map_defaults,
140 symbol_defaults,
141 symbol_icons,
142 line_types: Self::get_es_line_types(),
143 background: background
144 })
145 }
146
147 fn get_es_line_types() -> HashMap<String, Vec<u8>> {
148 HashMap::from([
149 ("solid".to_string(), vec![1]),
150 ("dash".to_string(), vec![18, 6]),
151 ("dot".to_string(), vec![3, 3]),
152 ("dash-dot".to_string(), vec![9, 6, 3, 6]),
153 ("dash-dot-dot".to_string(), vec![9, 3, 3, 3, 3, 3])
154 ])
155 }
156
157 fn es_line_type_to_string(input: u8) -> String {
158 match input {
159 1 => "dash".to_string(),
160 2 => "dot".to_string(),
161 3 => "dash-dot".to_string(),
162 4 => "dash-dot-dot".to_string(),
163 _ => "solid".to_string()
164 }
165 }
166}
167
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct AtcDisplay {
170 pub name: String,
171 pub center: GeoPoint,
172 pub screen_height: Length,
173 pub rotation: Angle,
174 pub display_items: Vec<AtcDisplayItem>,
175 pub display_type: String
176}
177
178impl AtcDisplay {
179 pub fn from_es_asr(default_sector_id: String, display_type: String, value: EsAsr) -> Self {
180 let mut ret_val = AtcDisplay::default();
181 ret_val.name = value.name;
182 ret_val.display_type = display_type;
183
184 let dist = (value.window_area.1 - value.window_area.0) / 2;
186 let bearing = GeoPoint::initial_bearing(&value.window_area.0, &value.window_area.1);
187 let mut center = value.window_area.0.clone();
188 center.move_by(bearing, dist);
189 ret_val.center = center;
190
191 let theta = (Bearing::from_radians(0_f64) - bearing) + value.display_rotation;
193 ret_val.screen_height = dist * 2 * theta.as_radians().cos().abs();
194 ret_val.rotation = value.display_rotation;
195
196 let mut items = Vec::new();
197 let mut symbols_map = HashMap::<String, usize>::new();
198
199 let sector_id = value.sector_file_id.clone().unwrap_or(default_sector_id.to_string());
200
201 let mut loaded_freetexts= HashMap::new();
202
203 for item in value.display_items {
204 if matches!(item.item_type, SymbologyItemType::Airports | SymbologyItemType::Fixes | SymbologyItemType::Ndbs | SymbologyItemType::Vors) {
205 let ident = format!("{}_{}_{}", sector_id.to_string(), item.item_type.to_key_string(), item.name);
206
207 let symbol_opt = match symbols_map.get_mut(&ident) {
208 Some(symb_index) => items.get_mut(*symb_index),
209 None => {
210 let symb_index = items.len();
211 symbols_map.insert(ident.to_string(), symb_index);
212 items.push(AtcDisplayItem::Symbol { id: ident.to_string(), show_label: false, show_symbol: false});
213 items.get_mut(symb_index)
214 }
215 };
216 if let Some(symbol) = symbol_opt {
217 if let AtcDisplayItem::Symbol { id, show_symbol, show_label } = symbol {
218 if item.attribute == "symbol" {
219 *show_symbol = true;
220 } else if item.attribute == "name" {
221 *show_label = true;
222 }
223 }
224 }
225 } else if matches!(item.item_type, SymbologyItemType::ArtccBoundary | SymbologyItemType::ArtccHighBoundary | SymbologyItemType::ArtccLowBoundary | SymbologyItemType::Geo | SymbologyItemType::HighAirways | SymbologyItemType::LowAirways | SymbologyItemType::Region | SymbologyItemType::Sids | SymbologyItemType::Stars) {
226 items.push(AtcDisplayItem::Map { id: format!("{}_{}_{}", sector_id.to_string(), item.item_type.to_key_string(), item.name), visible: true })
227 } else if matches!(item.item_type, SymbologyItemType::Label) {
228 if item.attribute == "freetext" {
229 let name_split = item.name.split("\\").collect::<Vec<&str>>();
230 if name_split.len() >= 1 {
231 if !loaded_freetexts.contains_key(&name_split[0].to_string()) {
232 items.push(AtcDisplayItem::Map {id: format!("{}_{}_{}", sector_id.to_string(), item.item_type.to_key_string(), name_split[0].to_string()), visible: true});
233 loaded_freetexts.insert(name_split[0].to_string(), ());
234 }
235 }
236 }
237 }
238 }
239
240 ret_val.display_items = items;
241
242 ret_val
243 }
244
245 pub fn from_crc_twr_asdex(display_type: String, display_name: String, twr_cfg: &TowerCabConfig) -> AtcDisplay {
246 AtcDisplay {
247 name: display_name.to_string(),
248 display_type: display_type.to_string(),
249 center: twr_cfg.tower_location.unwrap_or_default(),
250 screen_height: Length::from_feet(f64::from(twr_cfg.default_zoom_range) * 200_f64),
251 rotation: Angle::from_degrees(twr_cfg.default_rotation.into()),
252 display_items: vec![AtcDisplayItem::Map {id: twr_cfg.video_map_id.to_string(), visible: true}]
253 }
254 }
255
256 fn get_tdm_maps_from_crc(video_map_ids: &Vec<String>, map_refs: &HashMap<String, CrcVideoMapRef>) -> (Vec<AtcDisplayItem>, Vec<AtcDisplayItem>) {
257 let mut display_items = Vec::new();
258 let mut display_items_tdm = Vec::new();
259
260 for video_map_id in video_map_ids {
261 if let Some(map_ref) = map_refs.get(video_map_id) {
263 display_items_tdm.push(AtcDisplayItem::Map {id: video_map_id.to_string(), visible: map_ref.stars_always_visible});
264 if !map_ref.tdm_only {
265 display_items.push(AtcDisplayItem::Map {id: video_map_id.to_string(), visible: map_ref.stars_always_visible});
266 }
267 }
268 }
269
270 (display_items, display_items_tdm)
271 }
272
273 pub fn from_crc_stars(stars_cfg: &StarsConfiguration, map_refs: &HashMap<String, CrcVideoMapRef>) -> Vec<AtcDisplay> {
274 let default_area = StarsArea::default();
275 let area = stars_cfg.areas.get(0).unwrap_or(&default_area);
276 let display_items = Self::get_tdm_maps_from_crc(&stars_cfg.video_map_ids, map_refs);
277
278 vec![
279 AtcDisplay {
280 name: "STARS".to_string(),
281 display_type: "stars".to_string(),
282 center: area.visibility_center,
283 screen_height: Length::from_nautical_miles(f64::from(area.surveillance_range) * 2_f64),
284 rotation: Angle::from_radians(0_f64),
285 display_items: display_items.0
286 },
287 AtcDisplay {
288 name: "STARS (Top Down Mode)".to_string(),
289 display_type: "stars".to_string(),
290 center: area.visibility_center,
291 screen_height: Length::from_nautical_miles(f64::from(area.surveillance_range) * 2_f64),
292 rotation: Angle::from_radians(0_f64),
293 display_items: display_items.1
294 }
295 ]
296 }
297
298 pub fn from_crc_eram(eram_cfg: &EramConfig, map_refs: &HashMap<String, CrcVideoMapRef>) -> Vec<AtcDisplay> {
299 let mut displays = Vec::new();
300 for geo_map in &eram_cfg.geo_maps {
301 let display_items = Self::get_tdm_maps_from_crc(&geo_map.video_map_ids, map_refs);
302
303 displays.push(AtcDisplay {
304 name: format!("ERAM {}", geo_map.name),
305 display_type: "eram".to_string(),
306 center: GeoPoint::default(),
307 screen_height: Length::from_nautical_miles(600_f64),
308 rotation: Angle::from_radians(0_f64),
309 display_items: display_items.0
310 });
311 displays.push(AtcDisplay {
312 name: format!("ERAM {} (Top Down Mode)", geo_map.name),
313 display_type: "eram".to_string(),
314 center: GeoPoint::default(),
315 screen_height: Length::from_nautical_miles(600_f64),
316 rotation: Angle::from_radians(0_f64),
317 display_items: display_items.1
318 });
319 }
320
321 displays
322 }
323}