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