1use core::fmt;
4
5use pleiades_types::CelestialBody;
6
7use crate::channels::ChannelKind;
8use crate::codec::{
9 validate_artifact_profile, validate_canonical_body_order, validate_unique_values,
10};
11use crate::error::{CompressionError, CompressionErrorKind};
12
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
16#[non_exhaustive]
17pub enum EndianPolicy {
18 LittleEndian,
20}
21
22impl EndianPolicy {
23 pub const fn label(self) -> &'static str {
25 match self {
26 Self::LittleEndian => "little-endian",
27 }
28 }
29}
30
31impl fmt::Display for EndianPolicy {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.write_str(self.label())
34 }
35}
36
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
40#[non_exhaustive]
41pub enum ArtifactOutput {
42 EclipticCoordinates,
44 EquatorialCoordinates,
46 ApparentCorrections,
48 TopocentricCoordinates,
50 SiderealCoordinates,
52 Motion,
54}
55
56impl ArtifactOutput {
57 pub const fn all() -> [Self; 6] {
59 [
60 Self::EclipticCoordinates,
61 Self::EquatorialCoordinates,
62 Self::ApparentCorrections,
63 Self::TopocentricCoordinates,
64 Self::SiderealCoordinates,
65 Self::Motion,
66 ]
67 }
68
69 pub(crate) const fn ordinal(self) -> u8 {
70 match self {
71 Self::EclipticCoordinates => 0,
72 Self::EquatorialCoordinates => 1,
73 Self::ApparentCorrections => 2,
74 Self::TopocentricCoordinates => 3,
75 Self::SiderealCoordinates => 4,
76 Self::Motion => 5,
77 }
78 }
79
80 pub const fn label(self) -> &'static str {
82 match self {
83 Self::EclipticCoordinates => "EclipticCoordinates",
84 Self::EquatorialCoordinates => "EquatorialCoordinates",
85 Self::ApparentCorrections => "ApparentCorrections",
86 Self::TopocentricCoordinates => "TopocentricCoordinates",
87 Self::SiderealCoordinates => "SiderealCoordinates",
88 Self::Motion => "Motion",
89 }
90 }
91}
92
93impl fmt::Display for ArtifactOutput {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 f.write_str(self.label())
96 }
97}
98
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
102#[non_exhaustive]
103pub enum ArtifactOutputSupport {
104 Stored,
106 Derived,
108 Approximated,
110 Unsupported,
112 Unlisted,
114}
115
116impl ArtifactOutputSupport {
117 pub const fn label(self) -> &'static str {
119 match self {
120 Self::Stored => "stored",
121 Self::Derived => "derived",
122 Self::Approximated => "approximated",
123 Self::Unsupported => "unsupported",
124 Self::Unlisted => "unlisted",
125 }
126 }
127}
128
129impl fmt::Display for ArtifactOutputSupport {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 f.write_str(self.label())
132 }
133}
134
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
138#[non_exhaustive]
139pub enum SpeedPolicy {
140 Unsupported,
142 Stored,
144 FittedDerivative,
146 NumericalDifference,
148}
149
150impl SpeedPolicy {
151 pub const fn label(self) -> &'static str {
153 match self {
154 Self::Unsupported => "Unsupported",
155 Self::Stored => "Stored",
156 Self::FittedDerivative => "FittedDerivative",
157 Self::NumericalDifference => "NumericalDifference",
158 }
159 }
160
161 pub const fn motion_output_support(self) -> ArtifactOutputSupport {
163 match self {
164 Self::Unsupported => ArtifactOutputSupport::Unsupported,
165 Self::Stored => ArtifactOutputSupport::Stored,
166 Self::FittedDerivative => ArtifactOutputSupport::Derived,
167 Self::NumericalDifference => ArtifactOutputSupport::Approximated,
168 }
169 }
170}
171
172impl fmt::Display for SpeedPolicy {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.write_str(self.label())
175 }
176}
177
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
180#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct ArtifactProfile {
182 pub stored_channels: Vec<ChannelKind>,
184 pub derived_outputs: Vec<ArtifactOutput>,
186 pub unsupported_outputs: Vec<ArtifactOutput>,
188 pub speed_policy: SpeedPolicy,
190}
191
192impl ArtifactProfile {
193 pub fn new(
195 stored_channels: Vec<ChannelKind>,
196 derived_outputs: Vec<ArtifactOutput>,
197 unsupported_outputs: Vec<ArtifactOutput>,
198 speed_policy: SpeedPolicy,
199 ) -> Self {
200 Self {
201 stored_channels,
202 derived_outputs,
203 unsupported_outputs,
204 speed_policy,
205 }
206 }
207
208 pub fn validate(&self) -> Result<(), CompressionError> {
215 validate_artifact_profile(self)
216 }
217
218 pub fn summary(&self) -> String {
221 self.summary_line()
222 }
223
224 pub fn motion_output_support(&self) -> ArtifactOutputSupport {
226 self.speed_policy.motion_output_support()
227 }
228
229 pub fn summary_line(&self) -> String {
232 format!(
233 "stored channels: {}; derived outputs: {}; unsupported outputs: {}; speed policy: {}",
234 format_bracketed_labels(&self.stored_channels),
235 format_bracketed_labels(&self.derived_outputs),
236 format_bracketed_labels(&self.unsupported_outputs),
237 self.speed_policy,
238 )
239 }
240
241 pub fn validated_summary_line(&self) -> Result<String, CompressionError> {
243 self.validate()?;
244 Ok(self.summary_line())
245 }
246
247 pub fn validated_output_support_entries_summary_line(
249 &self,
250 ) -> Result<String, CompressionError> {
251 self.validate()?;
252 Ok(self.output_support_entries_summary_line())
253 }
254
255 pub fn validated_output_support_summary_line(&self) -> Result<String, CompressionError> {
257 self.validate()?;
258 Ok(self.output_support_summary_line())
259 }
260
261 pub fn summary_for_body_count(&self, body_count: usize) -> String {
263 format!(
264 "{}; applies to {} bundled bodies",
265 self.summary_line(),
266 body_count
267 )
268 }
269
270 pub fn summary_line_with_body_count(&self, body_count: usize) -> String {
272 self.summary_for_body_count(body_count)
273 }
274
275 pub fn output_support_entries_summary_line(&self) -> String {
277 ArtifactOutput::all()
278 .into_iter()
279 .map(|output| format!("{output}={}", self.output_support(output)))
280 .collect::<Vec<_>>()
281 .join(", ")
282 }
283
284 pub fn output_support_summary_line(&self) -> String {
292 let mut stored_count = 0usize;
293 let mut derived_count = 0usize;
294 let mut approximated_count = 0usize;
295 let mut unsupported_count = 0usize;
296 let mut unlisted_count = 0usize;
297 let mut unlisted_outputs = Vec::new();
298
299 for output in ArtifactOutput::all() {
300 match self.output_support(output) {
301 ArtifactOutputSupport::Stored => stored_count += 1,
302 ArtifactOutputSupport::Derived => derived_count += 1,
303 ArtifactOutputSupport::Approximated => approximated_count += 1,
304 ArtifactOutputSupport::Unsupported => unsupported_count += 1,
305 ArtifactOutputSupport::Unlisted => {
306 unlisted_count += 1;
307 unlisted_outputs.push(output);
308 }
309 }
310 }
311
312 format!(
313 "{}; unlisted outputs: {}; support counts: stored={}, derived={}, approximated={}, unsupported={}, unlisted={}",
314 self.output_support_entries_summary_line(),
315 format_bracketed_labels(&unlisted_outputs),
316 stored_count,
317 derived_count,
318 approximated_count,
319 unsupported_count,
320 unlisted_count,
321 )
322 }
323
324 pub fn output_support(&self, output: ArtifactOutput) -> ArtifactOutputSupport {
326 if output == ArtifactOutput::Motion {
327 self.motion_output_support()
328 } else if self.derived_outputs.contains(&output) {
329 ArtifactOutputSupport::Derived
330 } else if self.unsupported_outputs.contains(&output) {
331 ArtifactOutputSupport::Unsupported
332 } else {
333 ArtifactOutputSupport::Unlisted
334 }
335 }
336
337 pub fn supports_output(&self, output: ArtifactOutput) -> bool {
339 matches!(
340 self.output_support(output),
341 ArtifactOutputSupport::Stored
342 | ArtifactOutputSupport::Derived
343 | ArtifactOutputSupport::Approximated
344 )
345 }
346
347 pub fn is_unsupported_output(&self, output: ArtifactOutput) -> bool {
349 matches!(
350 self.output_support(output),
351 ArtifactOutputSupport::Unsupported
352 )
353 }
354
355 pub fn ecliptic_longitude_latitude_distance() -> Self {
361 Self::ecliptic_longitude_latitude_distance_with_derived_equatorial()
362 }
363
364 pub fn packaged_ecliptic_longitude_latitude_distance_with_derived_equatorial() -> Self {
368 Self::new(
369 vec![
370 ChannelKind::Longitude,
371 ChannelKind::Latitude,
372 ChannelKind::DistanceAu,
373 ],
374 vec![
375 ArtifactOutput::EclipticCoordinates,
376 ArtifactOutput::EquatorialCoordinates,
377 ArtifactOutput::Motion,
378 ],
379 vec![
380 ArtifactOutput::ApparentCorrections,
381 ArtifactOutput::TopocentricCoordinates,
382 ArtifactOutput::SiderealCoordinates,
383 ],
384 SpeedPolicy::FittedDerivative,
385 )
386 }
387
388 pub fn ecliptic_longitude_latitude_distance_with_derived_equatorial() -> Self {
392 Self::packaged_ecliptic_longitude_latitude_distance_with_derived_equatorial()
393 }
394}
395
396impl fmt::Display for ArtifactProfile {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 f.write_str(&self.summary_line())
399 }
400}
401
402#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404#[derive(Clone, Debug, PartialEq, Eq)]
405pub struct ArtifactProfileCoverageSummary {
406 pub body_count: usize,
408 pub bodies: Vec<CelestialBody>,
410 pub profile: ArtifactProfile,
412}
413
414impl ArtifactProfileCoverageSummary {
415 pub fn new(profile: ArtifactProfile, bodies: Vec<CelestialBody>) -> Self {
417 let body_count = bodies.len();
418 Self {
419 body_count,
420 bodies,
421 profile,
422 }
423 }
424
425 pub fn validate(&self) -> Result<(), CompressionError> {
428 self.profile.validate()?;
429 if self.bodies.is_empty() {
430 return Err(CompressionError::new(
431 CompressionErrorKind::InvalidFormat,
432 "artifact profile coverage bundled body list must not be empty",
433 ));
434 }
435 validate_unique_values("artifact profile coverage bundled bodies", &self.bodies)?;
436 validate_canonical_body_order("artifact profile coverage bundled bodies", &self.bodies)?;
437 if self.body_count != self.bodies.len() {
438 return Err(CompressionError::new(
439 CompressionErrorKind::InvalidFormat,
440 "artifact profile coverage body count does not match bundled body list",
441 ));
442 }
443
444 Ok(())
445 }
446
447 pub fn summary_line(&self) -> String {
450 self.profile.summary_for_body_count(self.bodies.len())
451 }
452
453 pub fn validated_summary_line(&self) -> Result<String, CompressionError> {
456 self.validate()?;
457 Ok(self.summary_line())
458 }
459
460 pub fn summary_line_with_bodies(&self) -> String {
463 format!(
464 "{}; bundled bodies: {}",
465 self.summary_line(),
466 crate::join_display(&self.bodies)
467 )
468 }
469
470 pub fn validated_summary_line_with_bodies(&self) -> Result<String, CompressionError> {
472 self.validate()?;
473 Ok(self.summary_line_with_bodies())
474 }
475}
476
477impl fmt::Display for ArtifactProfileCoverageSummary {
478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479 f.write_str(&self.summary_line())
480 }
481}
482
483#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
485#[derive(Clone, Debug, PartialEq, Eq)]
486pub struct ArtifactResidualBodyCoverageSummary {
487 pub body_count: usize,
489 pub bodies: Vec<CelestialBody>,
491}
492
493impl ArtifactResidualBodyCoverageSummary {
494 pub fn new(bodies: Vec<CelestialBody>) -> Self {
496 let body_count = bodies.len();
497 Self { body_count, bodies }
498 }
499
500 pub fn validate(
502 &self,
503 artifact: &crate::artifact::CompressedArtifact,
504 ) -> Result<(), CompressionError> {
505 let expected_bodies = artifact.residual_bodies();
506
507 if self.body_count != self.bodies.len() {
508 return Err(CompressionError::new(
509 CompressionErrorKind::InvalidFormat,
510 "artifact residual-body coverage body count does not match the body list",
511 ));
512 }
513 validate_unique_values("artifact residual-body coverage bodies", &self.bodies)?;
514
515 if self.body_count != expected_bodies.len() {
516 return Err(CompressionError::new(
517 CompressionErrorKind::InvalidFormat,
518 "artifact residual-body coverage body count does not match residual body list",
519 ));
520 }
521
522 if self.bodies != expected_bodies {
523 return Err(CompressionError::new(
524 CompressionErrorKind::InvalidFormat,
525 "artifact residual-body coverage body list does not match the current artifact",
526 ));
527 }
528
529 Ok(())
530 }
531
532 pub fn summary_line(&self) -> String {
534 match self.bodies.as_slice() {
535 [] => "residual bodies: none".to_string(),
536 bodies => format!("residual bodies: {}", crate::join_display(bodies)),
537 }
538 }
539
540 pub fn validated_summary_line(
542 &self,
543 artifact: &crate::artifact::CompressedArtifact,
544 ) -> Result<String, CompressionError> {
545 self.validate(artifact)?;
546 Ok(self.summary_line())
547 }
548
549 pub fn summary_line_with_body_count(&self) -> String {
551 format!(
552 "{}; applies to {}",
553 self.summary_line(),
554 self.body_count_suffix()
555 )
556 }
557
558 pub fn validated_summary_line_with_body_count(
560 &self,
561 artifact: &crate::artifact::CompressedArtifact,
562 ) -> Result<String, CompressionError> {
563 let summary = self.validated_summary_line(artifact)?;
564 Ok(format!(
565 "{}; applies to {}",
566 summary,
567 self.body_count_suffix()
568 ))
569 }
570
571 fn body_count_suffix(&self) -> String {
572 match self.body_count {
573 1 => "1 bundled body".to_string(),
574 count => format!("{count} bundled bodies"),
575 }
576 }
577}
578
579impl fmt::Display for ArtifactResidualBodyCoverageSummary {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 f.write_str(&self.summary_line())
582 }
583}
584
585#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
589#[derive(Clone, Debug, PartialEq, Eq)]
590pub struct ArtifactHeader {
591 pub version: u16,
593 pub generation_label: String,
595 pub source: String,
597 pub endian_policy: EndianPolicy,
599 pub profile: ArtifactProfile,
601}
602
603impl ArtifactHeader {
604 pub fn new(generation_label: impl Into<String>, source: impl Into<String>) -> Self {
610 Self::with_profile(
611 generation_label,
612 source,
613 ArtifactProfile::packaged_ecliptic_longitude_latitude_distance_with_derived_equatorial(
614 ),
615 )
616 }
617
618 pub fn with_profile(
621 generation_label: impl Into<String>,
622 source: impl Into<String>,
623 profile: ArtifactProfile,
624 ) -> Self {
625 Self::with_profile_and_endian(
626 generation_label,
627 source,
628 EndianPolicy::LittleEndian,
629 profile,
630 )
631 }
632
633 pub fn with_profile_and_endian(
635 generation_label: impl Into<String>,
636 source: impl Into<String>,
637 endian_policy: EndianPolicy,
638 profile: ArtifactProfile,
639 ) -> Self {
640 Self {
641 version: crate::ARTIFACT_VERSION,
642 generation_label: generation_label.into(),
643 source: source.into(),
644 endian_policy,
645 profile,
646 }
647 }
648
649 pub fn summary(&self) -> String {
652 self.summary_line()
653 }
654
655 pub fn summary_line(&self) -> String {
658 format!("byte order: {}; {}", self.endian_policy, self.profile)
659 }
660
661 pub fn validate(&self) -> Result<(), CompressionError> {
668 if self.version != crate::ARTIFACT_VERSION {
669 return Err(CompressionError::new(
670 CompressionErrorKind::InvalidFormat,
671 format!(
672 "artifact header version {} does not match the current format version {}",
673 self.version,
674 crate::ARTIFACT_VERSION
675 ),
676 ));
677 }
678
679 crate::codec::validate_canonical_header_text(
680 "artifact header generation label",
681 &self.generation_label,
682 )?;
683 crate::codec::validate_canonical_header_text("artifact header source", &self.source)?;
684
685 self.profile.validate()
686 }
687
688 pub fn summary_for_body_count(&self, body_count: usize) -> String {
690 format!(
691 "{}; applies to {} bundled bodies",
692 self.summary_line(),
693 body_count
694 )
695 }
696}
697
698impl fmt::Display for ArtifactHeader {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 f.write_str(&self.summary_line())
701 }
702}
703
704pub(crate) fn format_bracketed_labels<T: fmt::Display>(values: &[T]) -> String {
707 format!("[{}]", crate::join_display(values))
708}