1use anyhow::Context;
2use aviation_calc_util::{
3 geo::{Bearing, GeoPoint},
4 units::{Angle, Length},
5};
6use display::{AtcDisplay, AtcDisplayBackground, AtcDisplayType, DisplayDefaultConfig};
7use flate2::{read::GzDecoder, write::GzEncoder, Compression, GzBuilder};
8use geojson::{Feature, FeatureCollection, GeoJson, Geometry, Value};
9use map::{AtcMap, AtcMapData};
10use serde::{Deserialize, Serialize};
11use serde_json::Map;
12use tar::{Archive, Header};
13use uuid::Uuid;
14use std::{collections::HashMap, env, fmt::format, fs::{self, File}, hash::Hash, io::{BufReader, BufWriter, Read, Write}, path::{Path, PathBuf}};
15use symbol::{AtcMapSymbol, SymbolDrawItem, SymbolIcon};
16
17use crate::loaders::euroscope::{
18 colour::Colour,
19 line::{ColouredLine, LineGroup},
20 loader::EuroScopeResult,
21 sector::{LabelGroup, RegionGroup},
22 symbology::SymbologyItemType,
23 EsAsr,
24};
25
26mod facility;
27use crate::loaders::vnas_crc::{CrcPackage, CrcVideoMapRef};
28use crate::package::display::AtcDisplayItem;
29pub use facility::AtcFacility;
30
31pub mod display;
32pub mod map;
33pub mod position;
34pub mod symbol;
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct AtcScopePackage {
38 pub facilities: Vec<AtcFacility>,
39 pub maps: HashMap<String, AtcMap>,
40 pub symbols: HashMap<String, AtcMapSymbol>,
41 pub display_types: HashMap<String, AtcDisplayType>,
42}
43
44impl TryFrom<EuroScopeResult> for AtcScopePackage {
45 type Error = anyhow::Error;
46
47 fn try_from(value: EuroScopeResult) -> anyhow::Result<Self> {
48 let mut maps = HashMap::new();
49 let mut symbols = HashMap::new();
50 let mut display_types = HashMap::new();
51 let mut facilities = Vec::new();
52
53 for sector in value.sectors {
55 for geo in sector.1 .0.geo_entries {
57 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::Geo.to_key_string(), geo)?;
58
59 maps.insert(val.name.to_string(), val);
60 }
61
62 for entry in sector.1 .0.artcc_entries {
64 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::ArtccBoundary.to_key_string(), entry)?;
65
66 maps.insert(val.name.to_string(), val);
67 }
68
69 for entry in sector.1 .0.artcc_low_entries {
71 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::ArtccLowBoundary.to_key_string(), entry)?;
72
73 maps.insert(val.name.to_string(), val);
74 }
75
76 for entry in sector.1 .0.artcc_high_entries {
78 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::ArtccHighBoundary.to_key_string(), entry)?;
79
80 maps.insert(val.name.to_string(), val);
81 }
82
83 for entry in sector.1 .0.low_airways {
85 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::LowAirways.to_key_string(), entry)?;
86
87 maps.insert(val.name.to_string(), val);
88 }
89
90 for entry in sector.1 .0.high_airways {
92 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::HighAirways.to_key_string(), entry)?;
93
94 maps.insert(val.name.to_string(), val);
95 }
96
97 for entry in sector.1 .0.sid_entries {
99 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::Sids.to_key_string(), entry)?;
100
101 maps.insert(val.name.to_string(), val);
102 }
103
104 for entry in sector.1 .0.star_entries {
106 let val = AtcMap::try_from_es_line_group(sector.0.to_string(), SymbologyItemType::Stars.to_key_string(), entry)?;
107
108 maps.insert(val.name.to_string(), val);
109 }
110
111 for entry in sector.1 .0.regions {
113 let val = AtcMap::try_from_es_region_group(sector.0.to_string(), SymbologyItemType::Region.to_key_string(), entry)?;
114
115 maps.insert(val.name.to_string(), val);
116 }
117
118 for entry in sector.1 .0.labels {
120 let val = AtcMap::try_from_es_labels_group(sector.0.to_string(), SymbologyItemType::Label.to_key_string(), entry)?;
121
122 maps.insert(val.name.to_string(), val);
123 }
124
125 if let Some(ese_file) = sector.1 .1 {
127 for entry in ese_file.free_text {
128 let val = AtcMap::try_from_es_freetext_group(sector.0.to_string(), SymbologyItemType::Label.to_key_string(), entry)?;
129
130 maps.insert(val.name.to_string(), val);
131 }
132 }
133
134 for entry in sector.1 .0.airports {
136 let val = AtcMapSymbol::try_from_es_position(
137 sector.0.to_string(),
138 SymbologyItemType::Airports.to_key_string(),
139 entry.identifier,
140 entry.position,
141 )?;
142 symbols.insert(val.name.to_string(), val);
143 }
144
145 for entry in sector.1 .0.fixes {
147 let val = AtcMapSymbol::try_from_es_position(
148 sector.0.to_string(),
149 SymbologyItemType::Fixes.to_key_string(),
150 entry.identifier,
151 entry.position,
152 )?;
153 symbols.insert(val.name.to_string(), val);
154 }
155
156 for entry in sector.1 .0.vors {
158 let val = AtcMapSymbol::try_from_es_position(
159 sector.0.to_string(),
160 SymbologyItemType::Vors.to_key_string(),
161 entry.identifier,
162 entry.position,
163 )?;
164 symbols.insert(val.name.to_string(), val);
165 }
166
167 for entry in sector.1 .0.ndbs {
169 let val = AtcMapSymbol::try_from_es_position(
170 sector.0.to_string(),
171 SymbologyItemType::Ndbs.to_key_string(),
172 entry.identifier,
173 entry.position,
174 )?;
175 symbols.insert(val.name.to_string(), val);
176 }
177 }
178
179 for prf in value.profiles {
180 let mut facility = AtcFacility::default();
181 facility.name = prf.prf_name;
182
183 display_types.insert(
185 prf.prf_file.to_string(),
186 AtcDisplayType::try_from_es_symbology(prf.prf_file.to_string(), prf.symbology)?,
187 );
188
189 for asr in prf.asrs {
191 let mut disp = AtcDisplay::from_es_asr(prf.default_sector_id.to_string(), prf.prf_file.to_string(), asr.1);
192 facility.displays.push(disp);
193 }
194
195 facilities.push(facility);
196 }
197
198 Ok(AtcScopePackage {
199 facilities: facilities,
200 symbols: symbols,
201 maps: maps,
202 display_types,
203 })
204 }
205}
206
207impl TryFrom<&CrcPackage> for AtcScopePackage {
208 type Error = anyhow::Error;
209
210 fn try_from(value: &CrcPackage) -> Result<Self, Self::Error> {
211 let mut package = AtcScopePackage::default();
212 let mut maps_map: HashMap<String, CrcVideoMapRef> = HashMap::new();
213
214 for map_ref in &value.video_maps {
216 package.maps.insert(
217 map_ref.id.to_string(),
218 AtcMap::try_from_crc_video_map(map_ref, &value.file_path, value.id.to_string())?,
219 );
220 maps_map.insert(map_ref.id.to_string(), map_ref.clone());
221 }
222
223 package.facilities.push(AtcFacility::try_from_crc(&value.facility, &maps_map)?);
225
226 package.display_types.insert(
228 "eram".to_string(),
229 AtcDisplayType {
230 id: "eram".to_string(),
231 map_defaults: Default::default(),
232 symbol_defaults: Default::default(),
233 symbol_icons: Self::get_eram_symbols(),
234 line_types: Self::get_eram_lines(),
235 background: AtcDisplayBackground::Color("#000000".to_string()),
236 },
237 );
238
239 package.display_types.insert(
240 "stars".to_string(),
241 AtcDisplayType {
242 id: "stars".to_string(),
243 map_defaults: Default::default(),
244 symbol_defaults: Default::default(),
245 symbol_icons: Default::default(),
246 line_types: Default::default(),
247 background: AtcDisplayBackground::Color("#000000".to_string()),
248 },
249 );
250
251 package.display_types.insert(
252 "asdex-day".to_string(),
253 AtcDisplayType {
254 id: "asdex-day".to_string(),
255 map_defaults: HashMap::from([
256 (
257 "taxiway".to_string(),
258 DisplayDefaultConfig {
259 color: Colour { r: 45, g: 45, b: 45 },
260 ..Default::default()
261 },
262 ),
263 (
264 "apron".to_string(),
265 DisplayDefaultConfig {
266 color: Colour { r: 70, g: 70, b: 70 },
267 ..Default::default()
268 },
269 ),
270 (
271 "structure".to_string(),
272 DisplayDefaultConfig {
273 color: Colour { r: 96, g: 96, b: 96 },
274 ..Default::default()
275 },
276 ),
277 (
278 "runway".to_string(),
279 DisplayDefaultConfig {
280 color: Colour { r: 0, g: 0, b: 0 },
281 ..Default::default()
282 },
283 ),
284 ]),
285 symbol_defaults: Default::default(),
286 symbol_icons: Default::default(),
287 line_types: Default::default(),
288 background: AtcDisplayBackground::Color("#005C73".to_string()),
289 },
290 );
291
292 package.display_types.insert(
293 "asdex-night".to_string(),
294 AtcDisplayType {
295 id: "asdex-night".to_string(),
296 map_defaults: HashMap::from([
297 (
298 "taxiway".to_string(),
299 DisplayDefaultConfig {
300 color: Colour { r: 16, g: 37, b: 76 },
301 ..Default::default()
302 },
303 ),
304 (
305 "apron".to_string(),
306 DisplayDefaultConfig {
307 color: Colour { r: 17, g: 52, b: 93 },
308 ..Default::default()
309 },
310 ),
311 (
312 "structure".to_string(),
313 DisplayDefaultConfig {
314 color: Colour { r: 32, g: 60, b: 98 },
315 ..Default::default()
316 },
317 ),
318 (
319 "runway".to_string(),
320 DisplayDefaultConfig {
321 color: Colour { r: 0, g: 0, b: 0 },
322 ..Default::default()
323 },
324 ),
325 ]),
326 symbol_defaults: Default::default(),
327 symbol_icons: Default::default(),
328 line_types: Default::default(),
329 background: AtcDisplayBackground::Color("#393939".to_string()),
330 },
331 );
332
333 package.display_types.insert(
334 "twrcab".to_string(),
335 AtcDisplayType {
336 id: "twrcab".to_string(),
337 map_defaults: Default::default(),
338 symbol_defaults: Default::default(),
339 symbol_icons: Default::default(),
340 line_types: Default::default(),
341 background: display::AtcDisplayBackground::Satellite,
342 },
343 );
344
345 Ok(package)
346 }
347}
348
349impl AtcScopePackage {
350 fn get_eram_symbols() -> HashMap<String, SymbolIcon> {
351 HashMap::from([
352 (
353 "vor".to_string(),
354 SymbolIcon {
355 symbol_type: "vor".to_string(),
356 draw_items: vec![SymbolDrawItem::Ellipse {
357 center: (0, 0),
358 radius: (3, 5),
359 inner_radius: (0, 0),
360 rotation: 0,
361 start_angle: 0,
362 end_angle: 360,
363 fill: false,
364 }],
365 },
366 ),
367 (
368 "ndb".to_string(),
369 SymbolIcon {
370 symbol_type: "ndb".to_string(),
371 draw_items: vec![
372 SymbolDrawItem::Ellipse {
373 center: (0, 0),
374 radius: (3, 5),
375 inner_radius: (0, 0),
376 rotation: 0,
377 start_angle: 0,
378 end_angle: 360,
379 fill: false,
380 },
381 SymbolDrawItem::Line {
382 start: (-4, -7),
383 end: (4, 7),
384 },
385 ],
386 },
387 ),
388 (
389 "obstruction1".to_string(),
390 SymbolIcon {
391 symbol_type: "obstruction1".to_string(),
392 draw_items: vec![
393 SymbolDrawItem::Line {
394 start: (-3, -6),
395 end: (3, 6),
396 },
397 SymbolDrawItem::Line {
398 start: (-3, 6),
399 end: (3, -6),
400 },
401 SymbolDrawItem::Line { start: (-3, 7), end: (3, 7) },
402 ],
403 },
404 ),
405 (
406 "obstruction2".to_string(),
407 SymbolIcon {
408 symbol_type: "obstruction2".to_string(),
409 draw_items: vec![
410 SymbolDrawItem::Line {
411 start: (-5, -6),
412 end: (0, 6),
413 },
414 SymbolDrawItem::Line { start: (0, 6), end: (5, -6) },
415 SymbolDrawItem::Arc {
416 center: (0, -4),
417 radius: 2,
418 inner_radius: 0,
419 start_angle: 0,
420 end_angle: 360,
421 fill: true,
422 },
423 ],
424 },
425 ),
426 (
427 "heliport".to_string(),
428 SymbolIcon {
429 symbol_type: "heliport".to_string(),
430 draw_items: vec![
431 SymbolDrawItem::Arc {
432 center: (0, 0),
433 radius: 6,
434 inner_radius: 0,
435 start_angle: 0,
436 end_angle: 360,
437 fill: false,
438 },
439 SymbolDrawItem::Line { start: (-2, 0), end: (2, 0) },
440 SymbolDrawItem::Line {
441 start: (-2, 2),
442 end: (-2, -2),
443 },
444 SymbolDrawItem::Line { start: (2, 2), end: (2, -2) },
445 ],
446 },
447 ),
448 (
449 "nuclear".to_string(),
450 SymbolIcon {
451 symbol_type: "nuclear".to_string(),
452 draw_items: vec![
453 SymbolDrawItem::Line { start: (-1, 0), end: (1, 0) },
454 SymbolDrawItem::Line { start: (0, 1), end: (0, -1) },
455 SymbolDrawItem::Arc {
456 center: (0, 0),
457 radius: 6,
458 inner_radius: 3,
459 start_angle: 180,
460 end_angle: 240,
461 fill: true,
462 },
463 SymbolDrawItem::Arc {
464 center: (0, 0),
465 radius: 6,
466 inner_radius: 3,
467 start_angle: 300,
468 end_angle: 360,
469 fill: true,
470 },
471 SymbolDrawItem::Arc {
472 center: (0, 0),
473 radius: 6,
474 inner_radius: 3,
475 start_angle: 60,
476 end_angle: 120,
477 fill: true,
478 },
479 ],
480 },
481 ),
482 (
483 "emergencyairport".to_string(),
484 SymbolIcon {
485 symbol_type: "emergencyairport".to_string(),
486 draw_items: vec![
487 SymbolDrawItem::Line { start: (-2, 2), end: (2, 2) },
488 SymbolDrawItem::Line { start: (2, 2), end: (2, -2) },
489 SymbolDrawItem::Line {
490 start: (2, -2),
491 end: (-2, -2),
492 },
493 SymbolDrawItem::Line {
494 start: (-2, -2),
495 end: (-2, 2),
496 },
497 SymbolDrawItem::Line {
498 start: (-5, 5),
499 end: (-2, 2),
500 },
501 SymbolDrawItem::Line {
502 start: (2, -2),
503 end: (5, -5),
504 },
505 ],
506 },
507 ),
508 (
509 "radar".to_string(),
510 SymbolIcon {
511 symbol_type: "radar".to_string(),
512 draw_items: vec![
513 SymbolDrawItem::Line { start: (0, 3), end: (0, -3) },
514 SymbolDrawItem::Line {
515 start: (0, 3),
516 end: (-4, -2),
517 },
518 SymbolDrawItem::Line { start: (0, -3), end: (4, 2) },
519 SymbolDrawItem::Arc {
520 center: (1, 1),
521 radius: 7,
522 inner_radius: 0,
523 start_angle: 90,
524 end_angle: 180,
525 fill: false,
526 },
527 ],
528 },
529 ),
530 (
531 "iaf".to_string(),
532 SymbolIcon {
533 symbol_type: "iaf".to_string(),
534 draw_items: vec![
535 SymbolDrawItem::Line { start: (-4, 0), end: (4, 0) },
536 SymbolDrawItem::Line { start: (0, 4), end: (0, -4) },
537 SymbolDrawItem::Arc {
538 center: (0, 0),
539 radius: 4,
540 inner_radius: 0,
541 start_angle: 0,
542 end_angle: 360,
543 fill: false,
544 },
545 ],
546 },
547 ),
548 (
549 "rnavonlywaypoint".to_string(),
550 SymbolIcon {
551 symbol_type: "rnavonlywaypoint".to_string(),
552 draw_items: vec![
553 SymbolDrawItem::Arc {
554 center: (0, 0),
555 radius: 3,
556 inner_radius: 0,
557 start_angle: 0,
558 end_angle: 360,
559 fill: false,
560 },
561 SymbolDrawItem::Arc {
562 center: (0, 0),
563 radius: 5,
564 inner_radius: 0,
565 start_angle: 0,
566 end_angle: 360,
567 fill: false,
568 },
569 ],
570 },
571 ),
572 (
573 "rnav".to_string(),
574 SymbolIcon {
575 symbol_type: "rnav".to_string(),
576 draw_items: vec![
577 SymbolDrawItem::Line { start: (-2, 0), end: (2, 0) },
578 SymbolDrawItem::Line { start: (0, 2), end: (0, -2) },
579 SymbolDrawItem::Line { start: (-2, 2), end: (2, 2) },
580 SymbolDrawItem::Line { start: (2, 2), end: (2, -2) },
581 SymbolDrawItem::Line {
582 start: (2, -2),
583 end: (-2, -2),
584 },
585 SymbolDrawItem::Line {
586 start: (-2, -2),
587 end: (-2, 2),
588 },
589 ],
590 },
591 ),
592 (
593 "airwayintersections".to_string(),
594 SymbolIcon {
595 symbol_type: "airwayintersections".to_string(),
596 draw_items: vec![
597 SymbolDrawItem::Line {
598 start: (-3, -3),
599 end: (3, -3),
600 },
601 SymbolDrawItem::Line { start: (3, -3), end: (0, 3) },
602 SymbolDrawItem::Line {
603 start: (0, 3),
604 end: (-3, -3),
605 },
606 ],
607 },
608 ),
609 (
610 "otherwaypoints".to_string(),
611 SymbolIcon {
612 symbol_type: "otherwaypoints".to_string(),
613 draw_items: vec![
614 SymbolDrawItem::Line { start: (-5, 0), end: (5, 0) },
615 SymbolDrawItem::Line { start: (0, 5), end: (0, -5) },
616 ],
617 },
618 ),
619 (
620 "airport".to_string(),
621 SymbolIcon {
622 symbol_type: "airport".to_string(),
623 draw_items: vec![
624 SymbolDrawItem::Line { start: (-2, 2), end: (2, 2) },
625 SymbolDrawItem::Line { start: (2, 2), end: (2, -2) },
626 SymbolDrawItem::Line {
627 start: (2, -2),
628 end: (-2, -2),
629 },
630 SymbolDrawItem::Line {
631 start: (-2, -2),
632 end: (-2, 2),
633 },
634 ],
635 },
636 ),
637 (
638 "satelliteairport".to_string(),
639 SymbolIcon {
640 symbol_type: "satelliteairport".to_string(),
641 draw_items: vec![
642 SymbolDrawItem::Line {
643 start: (-6, -4),
644 end: (-6, 3),
645 },
646 SymbolDrawItem::Line { start: (-6, 3), end: (5, 3) },
647 ],
648 },
649 ),
650 (
651 "tacan".to_string(),
652 SymbolIcon {
653 symbol_type: "tacan".to_string(),
654 draw_items: vec![
655 SymbolDrawItem::Ellipse {
656 center: (0, 0),
657 radius: (3, 5),
658 inner_radius: (0, 0),
659 rotation: 0,
660 start_angle: 0,
661 end_angle: 360,
662 fill: false,
663 },
664 SymbolDrawItem::Line { start: (-2, 0), end: (2, 0) },
665 SymbolDrawItem::Line { start: (0, 2), end: (0, -2) },
666 ],
667 },
668 ),
669 ])
670 }
671
672 fn get_eram_lines() -> HashMap<String, Vec<u8>> {
673 HashMap::from([
674 ("solid".to_string(), vec![1]),
675 ("shortdashed".to_string(), vec![8, 8]),
676 ("longdashed".to_string(), vec![16, 16]),
677 ("longdashshortdash".to_string(), vec![14, 6, 6, 6]),
678 ])
679 }
680
681 fn write_json_to_targz<W, T>(tar_builder: &mut tar::Builder<W>, tar_path: impl AsRef<Path>, temp_dir: impl AsRef<Path>, file_name: &str, value: &T) -> anyhow::Result<()>
682 where W: Write, T: ?Sized + Serialize {
683 let map_file_path = &temp_dir.as_ref().join(&file_name);
685 let data = serde_json::to_vec(&value).context("Writing json file.")?;
686
687 let mut header = Header::new_gnu();
689 header.set_mode(0o755);
690 header.set_size(data.len() as u64);
691 &tar_builder.append_data(
692 &mut header,
693 &tar_path.as_ref().join(&file_name),
694 &*data)
695 .context("Creating map tar entry.")?;
696
697 Ok(())
698 }
699
700 pub fn export_to_gzip(&self, file_name: impl AsRef<Path>, maps_dir: impl AsRef<Path>) -> anyhow::Result<()> {
714 let temp_dir = env::temp_dir().join("sct_reader");
716 fs::create_dir_all(&temp_dir).context("Creating temp dir.")?;
717
718 let gz_file = File::create(file_name)?;
720 let gz_builder = GzBuilder::new().write(BufWriter::new(gz_file), Compression::default());
721 let mut tar_builder = tar::Builder::new(gz_builder);
722
723 let new_maps = self.maps.iter()
725 .map(|(id, map)| {
726 match &map.data {
727 AtcMapData::Embedded { features } => {
728 let map_uuid = Uuid::new_v4().simple().to_string();
730
731 let map_file_name = format!("{}.geojson", &map_uuid);
733 Self::write_json_to_targz(&mut tar_builder, "maps", &temp_dir, &map_file_name, &features)?;
734
735 Ok((
737 id.clone(),
738 AtcMap {
739 name: map.name.clone(),
740 data: AtcMapData::ExternalFile {
741 filename: format!("{}.geojson", &map_uuid)
742 }
743 }
744 ))
745 },
746 AtcMapData::ExternalFile { filename } => {
747 tar_builder.append_file(
749 Path::new("maps").join(filename),
750 &mut File::open(maps_dir.as_ref().join(filename))?
751 )?;
752 Ok((id.clone(), map.clone()))
753 },
754 }
755 })
756 .collect::<anyhow::Result<HashMap<String, AtcMap>>>()?;
757
758 let new_package = AtcScopePackage {
760 facilities: self.facilities.clone(),
761 maps: new_maps,
762 symbols: self.symbols.clone(),
763 display_types: self.display_types.clone()
764 };
765
766 Self::write_json_to_targz(&mut tar_builder, "", &temp_dir, "ScopePackage.json", &new_package)?;
768
769 tar_builder.finish().context("Writing TAR file.")?;
771
772 Ok(())
773 }
774
775 pub fn import_from_gzip(file_name: impl AsRef<Path>, out_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
776 let tar_gz = File::open(file_name)?;
778 let tar = GzDecoder::new(tar_gz);
779 let mut archive = Archive::new(tar);
780 archive.unpack(&out_dir)?;
781
782 let package = serde_json::from_reader(BufReader::new(
784 File::open(&out_dir.as_ref().join("ScopePackage.json"))?
785 ))?;
786
787 Ok(package)
788 }
789
790 pub fn try_load_map_data(&mut self, map_id: &str, maps_dir: impl AsRef<Path>) -> anyhow::Result<Option<&AtcMap>> {
796 if let Some(map) = self.maps.get_mut(map_id) {
797 if let AtcMapData::ExternalFile { filename } = &map.data {
798 map.data = AtcMapData::Embedded {
799 features: serde_json::from_reader(
800 BufReader::new(File::open(maps_dir.as_ref().join(&filename))?)
801 )?
802 };
803 }
804
805 Ok(Some(map))
806 } else {
807 Ok(None)
808 }
809 }
810}