1use core::fmt::Write as _;
17
18use crate::constants::{KM_TO_M, SECONDS_PER_DAY, US_TO_S};
19
20use super::{
21 Sp3, Sp3DataType, Sp3Flags, Sp3TimeSystem, Sp3Version, BAD_CLOCK_US, CLOCK_RATE_TO_S_PER_S,
22 DM_S_TO_M_S, MISSING_POSITION_KM, MISSING_VELOCITY_DM_S,
23};
24
25const SATS_PER_LINE: usize = 17;
27const MIN_PLUS_LINES: usize = 5;
29const SP3_TIME_TICKS_PER_SECOND: i64 = 100_000_000;
30const SP3_TIME_TICKS_PER_MINUTE: i64 = 60 * SP3_TIME_TICKS_PER_SECOND;
31const SP3_TIME_TICKS_PER_HOUR: i64 = 60 * SP3_TIME_TICKS_PER_MINUTE;
32const SP3_TIME_TICKS_PER_DAY: i64 = 24 * SP3_TIME_TICKS_PER_HOUR;
33
34impl Sp3 {
35 pub fn to_sp3_string(&self) -> String {
41 let mut out =
42 String::with_capacity(self.epochs.len() * (self.header.satellites.len() + 4) * 61);
43 self.write_header(&mut out);
44 self.write_records(&mut out);
45 out.push_str("EOF\n");
46 out
47 }
48
49 fn write_header(&self, out: &mut String) {
50 let h = &self.header;
51 let version = match h.version {
52 Sp3Version::A => 'a',
53 Sp3Version::B => 'b',
54 Sp3Version::C => 'c',
55 Sp3Version::D => 'd',
56 };
57 let dtype = match h.data_type {
58 Sp3DataType::Position => 'P',
59 Sp3DataType::Velocity => 'V',
60 };
61
62 let (y, mo, d, hh, mi, ss) = self
66 .epochs
67 .first()
68 .map(|epoch| julian_to_civil(epoch, h.time_system))
69 .unwrap_or((2000, 1, 1, 0, 0, 0.0));
70 let dt = format_calendar(y, mo, d, hh, mi, ss);
71 let _ = writeln!(
72 out,
73 "#{version}{dtype}{dt} {n:>7} {data:<5}{coord:>6}{orbit:>4} {agency}",
74 n = self.epochs.len(),
75 data = "ORBIT",
76 coord = h.coordinate_system,
77 orbit = h.orbit_type,
78 agency = h.agency,
79 );
80
81 let _ = writeln!(
83 out,
84 "## {wk:>4} {sow:15.8} {interval:14.8} {mjd:>5} {frac:.13}",
85 wk = h.gnss_week,
86 sow = h.seconds_of_week,
87 interval = h.epoch_interval_s,
88 mjd = h.mjd,
89 frac = h.mjd_fraction,
90 );
91
92 let sats = &h.satellites;
94 let n_lines = MIN_PLUS_LINES.max(sats.len().div_ceil(SATS_PER_LINE));
95 for line in 0..n_lines {
96 if line == 0 {
98 let _ = write!(out, "+ {:>3} ", sats.len());
99 } else {
100 out.push_str("+ ");
101 }
102 for slot in 0..SATS_PER_LINE {
103 match sats.get(line * SATS_PER_LINE + slot) {
104 Some(sat) => {
105 let _ = write!(out, "{sat}");
106 }
107 None => out.push_str(" 0"),
108 }
109 }
110 out.push('\n');
111 }
112 for line in 0..n_lines {
113 out.push_str("++ ");
114 for slot in 0..SATS_PER_LINE {
115 let idx = line * SATS_PER_LINE + slot;
116 let code = if idx < sats.len() {
117 h.satellite_accuracy_codes.get(idx).copied().unwrap_or(0)
118 } else {
119 0
120 };
121 let _ = write!(out, "{code:>3}");
122 }
123 out.push('\n');
124 }
125
126 let tsys = h.time_system.label();
130 let _ = writeln!(
131 out,
132 "%c M cc {tsys} ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc"
133 );
134 out.push_str("%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc\n");
135 out.push_str("%f 1.2500000 1.025000000 0.00000000000 0.000000000000000\n");
136 out.push_str("%f 0.0000000 0.000000000 0.00000000000 0.000000000000000\n");
137 out.push_str("%i 0 0 0 0 0 0 0 0 0\n");
138 out.push_str("%i 0 0 0 0 0 0 0 0 0\n");
139
140 for comment in &self.comments {
142 let _ = writeln!(out, "/* {comment}");
143 }
144 }
145
146 fn write_records(&self, out: &mut String) {
147 let with_velocity = matches!(self.header.data_type, Sp3DataType::Velocity);
148 for (idx, epoch) in self.epochs.iter().enumerate() {
149 let (y, mo, d, hh, mi, ss) = julian_to_civil(epoch, self.header.time_system);
150 let _ = writeln!(out, "* {}", format_calendar(y, mo, d, hh, mi, ss));
151
152 let states = &self.states[idx];
153 for sat in &self.header.satellites {
158 match states.get(sat) {
159 Some(state) => {
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 }
175 None => {
176 let _ = writeln!(
177 out,
178 "P{sat}{:14.6}{:14.6}{:14.6}{:14.6}",
179 MISSING_POSITION_KM,
180 MISSING_POSITION_KM,
181 MISSING_POSITION_KM,
182 BAD_CLOCK_US
183 );
184 if with_velocity {
185 write_velocity_record(out, sat, None, None);
186 }
187 }
188 }
189 }
190 }
191 }
192}
193
194fn clock_field_us(clock_s: Option<f64>) -> f64 {
196 match clock_s {
197 Some(s) => s / US_TO_S,
198 None => BAD_CLOCK_US,
199 }
200}
201
202fn clock_rate_field(rate_s_s: Option<f64>) -> f64 {
204 match rate_s_s {
205 Some(r) => r / CLOCK_RATE_TO_S_PER_S,
206 None => BAD_CLOCK_US,
207 }
208}
209
210fn write_velocity_record(
211 out: &mut String,
212 sat: &crate::id::GnssSatelliteId,
213 velocity: Option<crate::frame::ItrfVelocityMS>,
214 clock_rate_s_s: Option<f64>,
215) {
216 let ((vx, vy, vz), rate) = match velocity {
217 Some(v) => (
218 (
219 v.vx_m_s / DM_S_TO_M_S,
220 v.vy_m_s / DM_S_TO_M_S,
221 v.vz_m_s / DM_S_TO_M_S,
222 ),
223 clock_rate_field(clock_rate_s_s),
224 ),
225 None => (
226 (
227 MISSING_VELOCITY_DM_S,
228 MISSING_VELOCITY_DM_S,
229 MISSING_VELOCITY_DM_S,
230 ),
231 BAD_CLOCK_US,
232 ),
233 };
234 let _ = writeln!(out, "V{sat}{vx:14.6}{vy:14.6}{vz:14.6}{rate:14.6}");
235}
236
237fn write_record_flags(out: &mut String, flags: Sp3Flags) {
238 let last_col = if flags.orbit_predicted {
239 Some(79)
240 } else if flags.maneuver {
241 Some(78)
242 } else if flags.clock_predicted {
243 Some(75)
244 } else if flags.clock_event {
245 Some(74)
246 } else {
247 None
248 };
249 let Some(last_col) = last_col else {
250 return;
251 };
252
253 for col in 60..=last_col {
254 out.push(match col {
255 74 if flags.clock_event => 'E',
256 75 if flags.clock_predicted => 'P',
257 78 if flags.maneuver => 'M',
258 79 if flags.orbit_predicted => 'P',
259 _ => ' ',
260 });
261 }
262}
263
264fn format_calendar(
266 year: i64,
267 month: i64,
268 day: i64,
269 hour: i64,
270 minute: i64,
271 seconds: f64,
272) -> String {
273 format!("{year:4} {month:>2} {day:>2} {hour:>2} {minute:>2} {seconds:11.8}")
274}
275
276fn julian_to_civil(
279 epoch: &crate::astro::time::model::Instant,
280 time_system: Sp3TimeSystem,
281) -> (i64, i64, i64, i64, i64, f64) {
282 let split = match epoch.julian_date() {
283 Some(s) => s,
284 None => 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}
316
317fn civil_from_jdn(jdn: i64) -> (i64, i64, i64) {
318 let l = jdn + 68_569;
319 let n = 4 * l / 146_097;
320 let l = l - (146_097 * n + 3) / 4;
321 let i = 4_000 * (l + 1) / 1_461_001;
322 let l = l - 1_461 * i / 4 + 31;
323 let j = 80 * l / 2_447;
324 let day = l - 2_447 * j / 80;
325 let l = j / 11;
326 let month = j + 2 - 12 * l;
327 let year = 100 * (n - 49) + i + l;
328 (year, month, day)
329}