1use core::fmt::Write as _;
17
18use crate::astro::time::civil::civil_from_julian_day_number as civil_from_jdn;
19use crate::constants::{KM_TO_M, SECONDS_PER_DAY, US_TO_S};
20
21use super::{
22 Sp3, Sp3DataType, Sp3Flags, Sp3TimeSystem, Sp3Version, BAD_CLOCK_US, CLOCK_RATE_TO_S_PER_S,
23 DM_S_TO_M_S, MISSING_POSITION_KM, MISSING_VELOCITY_DM_S,
24};
25
26const SATS_PER_LINE: usize = 17;
28const MIN_PLUS_LINES: usize = 5;
30const SP3_TIME_TICKS_PER_SECOND: i64 = 100_000_000;
31const SP3_TIME_TICKS_PER_MINUTE: i64 = 60 * SP3_TIME_TICKS_PER_SECOND;
32const SP3_TIME_TICKS_PER_HOUR: i64 = 60 * SP3_TIME_TICKS_PER_MINUTE;
33const SP3_TIME_TICKS_PER_DAY: i64 = 24 * SP3_TIME_TICKS_PER_HOUR;
34
35impl Sp3 {
36 pub fn to_sp3_string(&self) -> String {
42 let mut out =
43 String::with_capacity(self.epochs.len() * (self.header.satellites.len() + 4) * 61);
44 self.write_header(&mut out);
45 self.write_records(&mut out);
46 out.push_str("EOF\n");
47 out
48 }
49
50 fn write_header(&self, out: &mut String) {
51 let h = &self.header;
52 let version = match h.version {
53 Sp3Version::A => 'a',
54 Sp3Version::B => 'b',
55 Sp3Version::C => 'c',
56 Sp3Version::D => 'd',
57 };
58 let dtype = match h.data_type {
59 Sp3DataType::Position => 'P',
60 Sp3DataType::Velocity => 'V',
61 };
62
63 let (y, mo, d, hh, mi, ss) = self
67 .epochs
68 .first()
69 .map(|epoch| julian_to_civil(epoch, h.time_system))
70 .unwrap_or((2000, 1, 1, 0, 0, 0.0));
71 let dt = format_calendar(y, mo, d, hh, mi, ss);
72 let _ = writeln!(
73 out,
74 "#{version}{dtype}{dt} {n:>7} {data:<5}{coord:>6}{orbit:>4} {agency}",
75 n = self.epochs.len(),
76 data = "ORBIT",
77 coord = h.coordinate_system,
78 orbit = h.orbit_type,
79 agency = h.agency,
80 );
81
82 let _ = writeln!(
84 out,
85 "## {wk:>4} {sow:15.8} {interval:14.8} {mjd:>5} {frac:.13}",
86 wk = h.gnss_week,
87 sow = h.seconds_of_week,
88 interval = h.epoch_interval_s,
89 mjd = h.mjd,
90 frac = h.mjd_fraction,
91 );
92
93 let sats = &h.satellites;
95 let n_lines = MIN_PLUS_LINES.max(sats.len().div_ceil(SATS_PER_LINE));
96 for line in 0..n_lines {
97 if line == 0 {
99 let _ = write!(out, "+ {:>3} ", sats.len());
100 } else {
101 out.push_str("+ ");
102 }
103 for slot in 0..SATS_PER_LINE {
104 match sats.get(line * SATS_PER_LINE + slot) {
105 Some(sat) => {
106 let _ = write!(out, "{sat}");
107 }
108 None => out.push_str(" 0"),
109 }
110 }
111 out.push('\n');
112 }
113 for line in 0..n_lines {
114 out.push_str("++ ");
115 for slot in 0..SATS_PER_LINE {
116 let idx = line * SATS_PER_LINE + slot;
117 let code = if idx < sats.len() {
118 h.satellite_accuracy_codes.get(idx).copied().unwrap_or(0)
119 } else {
120 0
121 };
122 let _ = write!(out, "{code:>3}");
123 }
124 out.push('\n');
125 }
126
127 let tsys = h.time_system.label();
131 let _ = writeln!(
132 out,
133 "%c M cc {tsys} ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc"
134 );
135 out.push_str("%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n");
136 out.push_str("%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n");
137 out.push_str("%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n");
138 out.push_str("%i 0 0 0 0 0 0 0 0 0\n");
139 out.push_str("%i 0 0 0 0 0 0 0 0 0\n");
140
141 for comment in &self.comments {
143 let _ = writeln!(out, "/* {comment}");
144 }
145 }
146
147 fn write_records(&self, out: &mut String) {
148 let with_velocity = matches!(self.header.data_type, Sp3DataType::Velocity);
149 for (idx, epoch) in self.epochs.iter().enumerate() {
150 let (y, mo, d, hh, mi, ss) = julian_to_civil(epoch, self.header.time_system);
151 let _ = writeln!(out, "* {}", format_calendar(y, mo, d, hh, mi, ss));
152
153 let states = &self.states[idx];
154 for sat in &self.header.satellites {
159 if let Some(state) = states.get(sat) {
160 let p = state.position;
161 let clk = clock_field_us(state.clock_s);
162 let _ = write!(
163 out,
164 "P{sat}{:14.6}{:14.6}{:14.6}{clk:14.6}",
165 p.x_m / KM_TO_M,
166 p.y_m / KM_TO_M,
167 p.z_m / KM_TO_M,
168 );
169 write_record_flags(out, state.flags);
170 out.push('\n');
171 if with_velocity {
172 write_velocity_record(out, sat, state.velocity, state.clock_rate_s_s);
173 }
174 } else {
175 let _ = writeln!(
176 out,
177 "P{sat}{:14.6}{:14.6}{:14.6}{:14.6}",
178 MISSING_POSITION_KM, MISSING_POSITION_KM, MISSING_POSITION_KM, BAD_CLOCK_US
179 );
180 if with_velocity {
181 write_velocity_record(out, sat, None, None);
182 }
183 }
184 }
185 }
186 }
187}
188
189fn clock_field_us(clock_s: Option<f64>) -> f64 {
191 match clock_s {
192 Some(s) => s / US_TO_S,
193 None => BAD_CLOCK_US,
194 }
195}
196
197fn clock_rate_field(rate_s_s: Option<f64>) -> f64 {
199 match rate_s_s {
200 Some(r) => r / CLOCK_RATE_TO_S_PER_S,
201 None => BAD_CLOCK_US,
202 }
203}
204
205fn write_velocity_record(
206 out: &mut String,
207 sat: &crate::id::GnssSatelliteId,
208 velocity: Option<crate::frame::ItrfVelocityMS>,
209 clock_rate_s_s: Option<f64>,
210) {
211 let ((vx, vy, vz), rate) = match velocity {
212 Some(v) => (
213 (
214 v.vx_m_s / DM_S_TO_M_S,
215 v.vy_m_s / DM_S_TO_M_S,
216 v.vz_m_s / DM_S_TO_M_S,
217 ),
218 clock_rate_field(clock_rate_s_s),
219 ),
220 None => (
221 (
222 MISSING_VELOCITY_DM_S,
223 MISSING_VELOCITY_DM_S,
224 MISSING_VELOCITY_DM_S,
225 ),
226 BAD_CLOCK_US,
227 ),
228 };
229 let _ = writeln!(out, "V{sat}{vx:14.6}{vy:14.6}{vz:14.6}{rate:14.6}");
230}
231
232fn write_record_flags(out: &mut String, flags: Sp3Flags) {
233 let last_col = if flags.orbit_predicted {
234 Some(79)
235 } else if flags.maneuver {
236 Some(78)
237 } else if flags.clock_predicted {
238 Some(75)
239 } else if flags.clock_event {
240 Some(74)
241 } else {
242 None
243 };
244 let Some(last_col) = last_col else {
245 return;
246 };
247
248 for col in 60..=last_col {
249 out.push(match col {
250 74 if flags.clock_event => 'E',
251 75 if flags.clock_predicted => 'P',
252 78 if flags.maneuver => 'M',
253 79 if flags.orbit_predicted => 'P',
254 _ => ' ',
255 });
256 }
257}
258
259fn format_calendar(
261 year: i64,
262 month: i64,
263 day: i64,
264 hour: i64,
265 minute: i64,
266 seconds: f64,
267) -> String {
268 format!("{year:4} {month:>2} {day:>2} {hour:>2} {minute:>2} {seconds:11.8}")
269}
270
271fn julian_to_civil(
274 epoch: &crate::astro::time::model::Instant,
275 time_system: Sp3TimeSystem,
276) -> (i64, i64, i64, i64, i64, f64) {
277 let Some(split) = epoch.julian_date() else {
278 return (2000, 1, 1, 0, 0, 0.0);
279 };
280 let ticks =
281 (split.fraction * SECONDS_PER_DAY * SP3_TIME_TICKS_PER_SECOND as f64).round() as i64;
282
283 let mut jdn = (split.jd_whole + 0.5).round() as i64;
286 if is_utc_like(time_system)
287 && (SP3_TIME_TICKS_PER_DAY..SP3_TIME_TICKS_PER_DAY + SP3_TIME_TICKS_PER_SECOND)
288 .contains(&ticks)
289 {
290 let (year, month, day) = civil_from_jdn(jdn);
291 let seconds =
292 60.0 + (ticks - SP3_TIME_TICKS_PER_DAY) as f64 / SP3_TIME_TICKS_PER_SECOND as f64;
293 return (year, month, day, 23, 59, seconds);
294 }
295
296 jdn += ticks.div_euclid(SP3_TIME_TICKS_PER_DAY);
297 let ticks = ticks.rem_euclid(SP3_TIME_TICKS_PER_DAY);
298 let (year, month, day) = civil_from_jdn(jdn);
299
300 let hour = ticks / SP3_TIME_TICKS_PER_HOUR;
301 let rem = ticks % SP3_TIME_TICKS_PER_HOUR;
302 let minute = rem / SP3_TIME_TICKS_PER_MINUTE;
303 let seconds = (rem % SP3_TIME_TICKS_PER_MINUTE) as f64 / SP3_TIME_TICKS_PER_SECOND as f64;
304 (year, month, day, hour, minute, seconds)
305}
306
307fn is_utc_like(time_system: Sp3TimeSystem) -> bool {
308 matches!(time_system, Sp3TimeSystem::Glonass | Sp3TimeSystem::Utc)
309}