gnss-js 0.1.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +171 -5
- package/dist/analysis.cjs +1488 -0
- package/dist/analysis.d.cts +211 -0
- package/dist/analysis.d.ts +211 -0
- package/dist/analysis.js +14 -0
- package/dist/antex.cjs +239 -0
- package/dist/antex.d.cts +77 -0
- package/dist/antex.d.ts +77 -0
- package/dist/antex.js +8 -0
- package/dist/chunk-37QNKGTC.js +65 -0
- package/dist/chunk-4YN353Q7.js +440 -0
- package/dist/chunk-5CMSYIYM.js +214 -0
- package/dist/chunk-5S5MONFK.js +0 -0
- package/dist/chunk-6FAL6P4G.js +12 -0
- package/dist/chunk-7NOFXKET.js +433 -0
- package/dist/chunk-BJHTBYNG.js +154 -0
- package/dist/chunk-HBLU2EJ4.js +802 -0
- package/dist/chunk-HKN3PUGN.js +800 -0
- package/dist/chunk-LEEU5OIO.js +75 -0
- package/dist/chunk-LWNTWBHB.js +268 -0
- package/dist/chunk-MIIM4LDY.js +239 -0
- package/dist/chunk-SO3POWWR.js +502 -0
- package/dist/chunk-W4YMQKWH.js +622 -0
- package/dist/chunk-WP2JFDLA.js +1475 -0
- package/dist/chunk-YDMYO3YK.js +220 -0
- package/dist/constants.cjs +423 -0
- package/dist/constants.d.cts +14 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +103 -0
- package/dist/coordinates.cjs +337 -0
- package/dist/coordinates.d.cts +81 -0
- package/dist/coordinates.d.ts +81 -0
- package/dist/coordinates.js +39 -0
- package/dist/ecef-CF0uAysr.d.cts +40 -0
- package/dist/ecef-CF0uAysr.d.ts +40 -0
- package/dist/ephemeris-C10stHhM.d.cts +114 -0
- package/dist/ephemeris-C10stHhM.d.ts +114 -0
- package/dist/gnss-BT6ulR17.d.cts +56 -0
- package/dist/gnss-C-tgoYNa.d.ts +56 -0
- package/dist/index.cjs +5940 -78
- package/dist/index.d.cts +17 -96
- package/dist/index.d.ts +17 -96
- package/dist/index.js +352 -415
- package/dist/nav-BAI1a9vK.d.cts +108 -0
- package/dist/nav-BAI1a9vK.d.ts +108 -0
- package/dist/nmea.cjs +247 -0
- package/dist/nmea.d.cts +37 -0
- package/dist/nmea.d.ts +37 -0
- package/dist/nmea.js +12 -0
- package/dist/ntrip.cjs +180 -0
- package/dist/ntrip.d.cts +98 -0
- package/dist/ntrip.d.ts +98 -0
- package/dist/ntrip.js +10 -0
- package/dist/orbit.cjs +511 -0
- package/dist/orbit.d.cts +117 -0
- package/dist/orbit.d.ts +117 -0
- package/dist/orbit.js +29 -0
- package/dist/parser-JPjjFgeP.d.cts +73 -0
- package/dist/parser-JPjjFgeP.d.ts +73 -0
- package/dist/rinex.cjs +1469 -0
- package/dist/rinex.d.cts +116 -0
- package/dist/rinex.d.ts +116 -0
- package/dist/rinex.js +37 -0
- package/dist/rtcm3.cjs +1512 -0
- package/dist/rtcm3.d.cts +143 -0
- package/dist/rtcm3.d.ts +143 -0
- package/dist/rtcm3.js +31 -0
- package/dist/signals.cjs +858 -0
- package/dist/signals.d.cts +93 -0
- package/dist/signals.d.ts +93 -0
- package/dist/signals.js +70 -0
- package/dist/time-DnI1VpE8.d.cts +33 -0
- package/dist/time-DnI1VpE8.d.ts +33 -0
- package/dist/time.cjs +585 -0
- package/dist/time.d.cts +307 -0
- package/dist/time.d.ts +307 -0
- package/dist/time.js +154 -0
- package/package.json +112 -14
- package/.prettierrc +0 -5
- package/jest.config.ts +0 -9
- package/test/functions.test.ts +0 -63
- package/test/gnss-scales.test.ts +0 -95
- package/test/julian.test.ts +0 -145
- package/test/rinex.test.ts +0 -11
- package/test/test.ts +0 -10
- package/test/utc.test.ts +0 -25
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// src/rinex/format.ts
|
|
2
|
+
function padL(s, w) {
|
|
3
|
+
return s.length >= w ? s.slice(0, w) : s + " ".repeat(w - s.length);
|
|
4
|
+
}
|
|
5
|
+
function padR(s, w) {
|
|
6
|
+
return s.length >= w ? s.slice(0, w) : " ".repeat(w - s.length) + s;
|
|
7
|
+
}
|
|
8
|
+
function fmtF(val, width, dec) {
|
|
9
|
+
return padR(val.toFixed(dec), width);
|
|
10
|
+
}
|
|
11
|
+
function hdrLine(content, label) {
|
|
12
|
+
return padL(content, 60) + padL(label, 20);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/rinex/warnings.ts
|
|
16
|
+
var EMPTY_WARNINGS = {
|
|
17
|
+
items: [],
|
|
18
|
+
errorCount: 0,
|
|
19
|
+
warningCount: 0,
|
|
20
|
+
infoCount: 0
|
|
21
|
+
};
|
|
22
|
+
var MAX_EXAMPLES = 3;
|
|
23
|
+
var WarningAccumulator = class {
|
|
24
|
+
items = /* @__PURE__ */ new Map();
|
|
25
|
+
// Epoch-level state
|
|
26
|
+
prevEpochTime = null;
|
|
27
|
+
epochTimes = /* @__PURE__ */ new Set();
|
|
28
|
+
intervalMs = null;
|
|
29
|
+
flagCounts = /* @__PURE__ */ new Map();
|
|
30
|
+
seenPrns = /* @__PURE__ */ new Set();
|
|
31
|
+
/* ── Header checks ──────────────────────────────────────────── */
|
|
32
|
+
checkHeader(header) {
|
|
33
|
+
const pos = header.approxPosition;
|
|
34
|
+
if (!pos || pos[0] === 0 && pos[1] === 0 && pos[2] === 0) {
|
|
35
|
+
this.add(
|
|
36
|
+
"MISSING_POSITION",
|
|
37
|
+
"warning",
|
|
38
|
+
"Missing or zero approximate position (APPROX POSITION XYZ)"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!header.antType) {
|
|
42
|
+
this.add(
|
|
43
|
+
"MISSING_ANT_TYPE",
|
|
44
|
+
"info",
|
|
45
|
+
"Missing antenna type (ANT # / TYPE)"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!header.receiverType) {
|
|
49
|
+
this.add(
|
|
50
|
+
"MISSING_REC_TYPE",
|
|
51
|
+
"info",
|
|
52
|
+
"Missing receiver type (REC # / TYPE / VERS)"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (!header.markerName) {
|
|
56
|
+
this.add("MISSING_MARKER", "info", "Missing marker name (MARKER NAME)");
|
|
57
|
+
}
|
|
58
|
+
if (header.interval == null) {
|
|
59
|
+
this.add("MISSING_INTERVAL", "info", "No observation interval in header");
|
|
60
|
+
}
|
|
61
|
+
if (Object.keys(header.obsTypes).length === 0) {
|
|
62
|
+
this.add(
|
|
63
|
+
"NO_OBS_TYPES",
|
|
64
|
+
"error",
|
|
65
|
+
"No observation types defined in header"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
setInterval(intervalMs) {
|
|
70
|
+
this.intervalMs = intervalMs;
|
|
71
|
+
}
|
|
72
|
+
/* ── Epoch checks ───────────────────────────────────────────── */
|
|
73
|
+
onEpoch(time, flag) {
|
|
74
|
+
if (flag !== 0) {
|
|
75
|
+
this.flagCounts.set(flag, (this.flagCounts.get(flag) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
if (flag !== 0) return;
|
|
78
|
+
if (this.epochTimes.has(time)) {
|
|
79
|
+
const d = new Date(time);
|
|
80
|
+
this.add(
|
|
81
|
+
"DUPLICATE_EPOCH",
|
|
82
|
+
"warning",
|
|
83
|
+
"Duplicate epoch timestamps",
|
|
84
|
+
d.toISOString().replace("T", " ").replace(".000Z", "")
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
this.epochTimes.add(time);
|
|
88
|
+
if (this.prevEpochTime !== null && time < this.prevEpochTime) {
|
|
89
|
+
const d = new Date(time);
|
|
90
|
+
this.add(
|
|
91
|
+
"OUT_OF_ORDER",
|
|
92
|
+
"warning",
|
|
93
|
+
"Epochs not in chronological order",
|
|
94
|
+
d.toISOString().replace("T", " ").replace(".000Z", "")
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (this.prevEpochTime !== null) {
|
|
98
|
+
const gap = time - this.prevEpochTime;
|
|
99
|
+
const threshold = this.intervalMs ? this.intervalMs * 10 : 36e5;
|
|
100
|
+
if (gap > threshold && gap > 0) {
|
|
101
|
+
const gapMin = (gap / 6e4).toFixed(1);
|
|
102
|
+
this.add(
|
|
103
|
+
"LARGE_GAP",
|
|
104
|
+
"info",
|
|
105
|
+
"Large time gaps between epochs",
|
|
106
|
+
`${gapMin} min gap at ${new Date(this.prevEpochTime).toISOString().slice(11, 19)}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.prevEpochTime = time;
|
|
111
|
+
}
|
|
112
|
+
/* ── PRN checks ─────────────────────────────────────────────── */
|
|
113
|
+
onPrn(prn) {
|
|
114
|
+
if (this.seenPrns.has(prn)) return;
|
|
115
|
+
this.seenPrns.add(prn);
|
|
116
|
+
const sys = prn[0];
|
|
117
|
+
const num = parseInt(prn.slice(1), 10);
|
|
118
|
+
if (!sys || isNaN(num)) {
|
|
119
|
+
this.add("INVALID_PRN", "warning", "Invalid PRN format", prn);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const ranges = {
|
|
123
|
+
G: [1, 32],
|
|
124
|
+
R: [1, 27],
|
|
125
|
+
E: [1, 50],
|
|
126
|
+
C: [1, 63],
|
|
127
|
+
J: [1, 10],
|
|
128
|
+
I: [1, 14],
|
|
129
|
+
S: [20, 59]
|
|
130
|
+
};
|
|
131
|
+
const range = ranges[sys];
|
|
132
|
+
if (!range) {
|
|
133
|
+
this.add(
|
|
134
|
+
"UNKNOWN_SYSTEM",
|
|
135
|
+
"info",
|
|
136
|
+
"Unknown satellite system letter",
|
|
137
|
+
prn
|
|
138
|
+
);
|
|
139
|
+
} else if (num < range[0] || num > range[1]) {
|
|
140
|
+
this.add(
|
|
141
|
+
"PRN_OUT_OF_RANGE",
|
|
142
|
+
"info",
|
|
143
|
+
`PRN number outside expected range`,
|
|
144
|
+
`${prn} (expected ${sys}${String(range[0]).padStart(2, "0")}-${sys}${String(range[1]).padStart(2, "0")})`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/* ── Finalize ───────────────────────────────────────────────── */
|
|
149
|
+
finalize() {
|
|
150
|
+
for (const [flag, count] of this.flagCounts) {
|
|
151
|
+
const labels = {
|
|
152
|
+
1: "Receiver clock reset",
|
|
153
|
+
2: "Moving antenna event",
|
|
154
|
+
3: "Header information follows",
|
|
155
|
+
4: "External event",
|
|
156
|
+
5: "Cycle slip records",
|
|
157
|
+
6: "Power failure recovery"
|
|
158
|
+
};
|
|
159
|
+
const label = labels[flag] ?? `Flag ${flag}`;
|
|
160
|
+
this.add(
|
|
161
|
+
`EPOCH_FLAG_${flag}`,
|
|
162
|
+
"info",
|
|
163
|
+
`${label} events (flag ${flag})`,
|
|
164
|
+
`${count} epoch(s)`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const items = [...this.items.values()];
|
|
168
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
169
|
+
items.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9));
|
|
170
|
+
return {
|
|
171
|
+
items,
|
|
172
|
+
errorCount: items.filter((w) => w.severity === "error").length,
|
|
173
|
+
warningCount: items.filter((w) => w.severity === "warning").length,
|
|
174
|
+
infoCount: items.filter((w) => w.severity === "info").length
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/* ── Internal ───────────────────────────────────────────────── */
|
|
178
|
+
add(code, severity, message, example) {
|
|
179
|
+
let w = this.items.get(code);
|
|
180
|
+
if (!w) {
|
|
181
|
+
w = { code, severity, message, count: 0 };
|
|
182
|
+
this.items.set(code, w);
|
|
183
|
+
}
|
|
184
|
+
w.count++;
|
|
185
|
+
if (example) {
|
|
186
|
+
if (!w.examples) w.examples = [];
|
|
187
|
+
if (w.examples.length < MAX_EXAMPLES) w.examples.push(example);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/rinex/nav.ts
|
|
193
|
+
var DATA_LINES = {
|
|
194
|
+
G: 7,
|
|
195
|
+
E: 7,
|
|
196
|
+
C: 7,
|
|
197
|
+
J: 7,
|
|
198
|
+
// Keplerian
|
|
199
|
+
R: 3,
|
|
200
|
+
// GLONASS state vector
|
|
201
|
+
S: 3,
|
|
202
|
+
I: 7
|
|
203
|
+
// SBAS / NavIC
|
|
204
|
+
};
|
|
205
|
+
var R4_MSG_LINES = {
|
|
206
|
+
// EPH types we parse (same Keplerian/state-vector layout as RINEX 3)
|
|
207
|
+
LNAV: 8,
|
|
208
|
+
INAV: 8,
|
|
209
|
+
FNAV: 8,
|
|
210
|
+
D1: 8,
|
|
211
|
+
D2: 8,
|
|
212
|
+
SBAS: 4,
|
|
213
|
+
FDMA: 5,
|
|
214
|
+
// RINEX 4 adds orbit-4 (status flags) vs 3 in RINEX 3
|
|
215
|
+
// EPH types we skip (different field layout)
|
|
216
|
+
CNAV: 9,
|
|
217
|
+
CNV1: 10,
|
|
218
|
+
CNV2: 10,
|
|
219
|
+
CNV3: 9,
|
|
220
|
+
L1NV: 8,
|
|
221
|
+
L1OC: 9,
|
|
222
|
+
L3OC: 9,
|
|
223
|
+
// Non-EPH records
|
|
224
|
+
STO: 2,
|
|
225
|
+
EOP: 3,
|
|
226
|
+
ION: 3
|
|
227
|
+
};
|
|
228
|
+
var SUPPORTED_EPH_MSGS = /* @__PURE__ */ new Set([
|
|
229
|
+
"LNAV",
|
|
230
|
+
"INAV",
|
|
231
|
+
"FNAV",
|
|
232
|
+
"D1",
|
|
233
|
+
"D2",
|
|
234
|
+
"SBAS",
|
|
235
|
+
"FDMA"
|
|
236
|
+
]);
|
|
237
|
+
function parseFloat19(s) {
|
|
238
|
+
return parseFloat(s.trim().replace(/[dD]/g, "E"));
|
|
239
|
+
}
|
|
240
|
+
function parseNavEpochV3(line) {
|
|
241
|
+
const prn = line.substring(0, 3).trim();
|
|
242
|
+
const yr = parseInt(line.substring(3, 8));
|
|
243
|
+
const mo = parseInt(line.substring(8, 11));
|
|
244
|
+
const dy = parseInt(line.substring(11, 14));
|
|
245
|
+
const hr = parseInt(line.substring(14, 17));
|
|
246
|
+
const mn = parseInt(line.substring(17, 20));
|
|
247
|
+
const sc = parseInt(line.substring(20, 23));
|
|
248
|
+
const date = new Date(Date.UTC(yr, mo - 1, dy, hr, mn, sc));
|
|
249
|
+
const values = [];
|
|
250
|
+
for (let i = 0; i < 3; i++) {
|
|
251
|
+
const start = 23 + i * 19;
|
|
252
|
+
if (start < line.length) {
|
|
253
|
+
values.push(parseFloat19(line.substring(start, start + 19)));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { prn, date, values };
|
|
257
|
+
}
|
|
258
|
+
function parseNavEpochV2(line, defaultSys) {
|
|
259
|
+
const prnNum = parseInt(line.substring(0, 2));
|
|
260
|
+
const yr = parseInt(line.substring(2, 5));
|
|
261
|
+
const mo = parseInt(line.substring(5, 8));
|
|
262
|
+
const dy = parseInt(line.substring(8, 11));
|
|
263
|
+
const hr = parseInt(line.substring(11, 14));
|
|
264
|
+
const mn = parseInt(line.substring(14, 17));
|
|
265
|
+
const sc = parseFloat(line.substring(17, 22));
|
|
266
|
+
const fullYr = yr < 80 ? 2e3 + yr : 1900 + yr;
|
|
267
|
+
const date = new Date(Date.UTC(fullYr, mo - 1, dy, hr, mn, Math.floor(sc)));
|
|
268
|
+
const prn = `${defaultSys}${String(prnNum).padStart(2, "0")}`;
|
|
269
|
+
const values = [];
|
|
270
|
+
for (let i = 0; i < 3; i++) {
|
|
271
|
+
const start = 22 + i * 19;
|
|
272
|
+
if (start < line.length) {
|
|
273
|
+
values.push(parseFloat19(line.substring(start, start + 19)));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { prn, date, values };
|
|
277
|
+
}
|
|
278
|
+
function parseDataLine(line, colOffset = 4) {
|
|
279
|
+
const values = [];
|
|
280
|
+
for (let i = 0; i < 4; i++) {
|
|
281
|
+
const start = colOffset + i * 19;
|
|
282
|
+
if (start >= line.length) break;
|
|
283
|
+
const s = line.substring(start, start + 19).trim();
|
|
284
|
+
if (s.length > 0) {
|
|
285
|
+
const v = parseFloat19(s);
|
|
286
|
+
values.push(Number.isFinite(v) ? v : 0);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return values;
|
|
290
|
+
}
|
|
291
|
+
function buildKeplerEphemeris(sys, prn, date, epochVals, data) {
|
|
292
|
+
const d = [];
|
|
293
|
+
for (const line of data) {
|
|
294
|
+
for (let i = 0; i < 4; i++) d.push(line[i] ?? 0);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
system: sys,
|
|
298
|
+
prn,
|
|
299
|
+
tocDate: date,
|
|
300
|
+
toc: date.getTime() / 1e3 % (7 * 86400),
|
|
301
|
+
// seconds of week approx
|
|
302
|
+
af0: epochVals[0] ?? 0,
|
|
303
|
+
af1: epochVals[1] ?? 0,
|
|
304
|
+
af2: epochVals[2] ?? 0,
|
|
305
|
+
iode: d[0] ?? 0,
|
|
306
|
+
crs: d[1] ?? 0,
|
|
307
|
+
deltaN: d[2] ?? 0,
|
|
308
|
+
m0: d[3] ?? 0,
|
|
309
|
+
cuc: d[4] ?? 0,
|
|
310
|
+
e: d[5] ?? 0,
|
|
311
|
+
cus: d[6] ?? 0,
|
|
312
|
+
sqrtA: d[7] ?? 0,
|
|
313
|
+
toe: d[8] ?? 0,
|
|
314
|
+
cic: d[9] ?? 0,
|
|
315
|
+
omega0: d[10] ?? 0,
|
|
316
|
+
cis: d[11] ?? 0,
|
|
317
|
+
i0: d[12] ?? 0,
|
|
318
|
+
crc: d[13] ?? 0,
|
|
319
|
+
omega: d[14] ?? 0,
|
|
320
|
+
omegaDot: d[15] ?? 0,
|
|
321
|
+
idot: d[16] ?? 0,
|
|
322
|
+
// d[17] = codes on L2 (GPS) or data sources (GAL)
|
|
323
|
+
week: d[18] ?? 0,
|
|
324
|
+
// d[19] = L2P flag (GPS) or spare
|
|
325
|
+
// d[20] = SV accuracy (URA index for GPS, SISA for GAL)
|
|
326
|
+
svHealth: d[21] ?? 0,
|
|
327
|
+
tgd: d[22] ?? 0
|
|
328
|
+
// d[23] = BGD (GAL) or IODC (GPS)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function buildStateVectorEphemeris(sys, prn, date, epochVals, data) {
|
|
332
|
+
const d = data.flat();
|
|
333
|
+
return {
|
|
334
|
+
system: sys,
|
|
335
|
+
prn,
|
|
336
|
+
tocDate: date,
|
|
337
|
+
tauN: epochVals[0] ?? 0,
|
|
338
|
+
gammaN: epochVals[1] ?? 0,
|
|
339
|
+
messageFrameTime: epochVals[2] ?? 0,
|
|
340
|
+
x: d[0] ?? 0,
|
|
341
|
+
xDot: d[1] ?? 0,
|
|
342
|
+
xAcc: d[2] ?? 0,
|
|
343
|
+
health: d[3] ?? 0,
|
|
344
|
+
y: d[4] ?? 0,
|
|
345
|
+
yDot: d[5] ?? 0,
|
|
346
|
+
yAcc: d[6] ?? 0,
|
|
347
|
+
freqNum: d[7] ?? 0,
|
|
348
|
+
z: d[8] ?? 0,
|
|
349
|
+
zDot: d[9] ?? 0,
|
|
350
|
+
zAcc: d[10] ?? 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function parseNavFile(text) {
|
|
354
|
+
const lines = text.split("\n");
|
|
355
|
+
const header = {
|
|
356
|
+
version: 0,
|
|
357
|
+
type: "",
|
|
358
|
+
leapSeconds: null,
|
|
359
|
+
ionoCorrections: {}
|
|
360
|
+
};
|
|
361
|
+
const ephemerides = [];
|
|
362
|
+
let inHeader = true;
|
|
363
|
+
let i = 0;
|
|
364
|
+
while (i < lines.length) {
|
|
365
|
+
const line = lines[i];
|
|
366
|
+
const label = line.substring(60).trim();
|
|
367
|
+
if (label === "END OF HEADER") {
|
|
368
|
+
inHeader = false;
|
|
369
|
+
i++;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (label === "RINEX VERSION / TYPE") {
|
|
373
|
+
header.version = parseFloat(line.substring(0, 9));
|
|
374
|
+
header.type = line.substring(20, 40).trim();
|
|
375
|
+
} else if (label === "LEAP SECONDS") {
|
|
376
|
+
header.leapSeconds = parseInt(line.substring(0, 6));
|
|
377
|
+
} else if (label === "IONOSPHERIC CORR") {
|
|
378
|
+
const corrType = line.substring(0, 4).trim();
|
|
379
|
+
const vals = [];
|
|
380
|
+
for (let j = 0; j < 4; j++) {
|
|
381
|
+
const s = line.substring(5 + j * 12, 5 + (j + 1) * 12).trim();
|
|
382
|
+
if (s) vals.push(parseFloat19(s));
|
|
383
|
+
}
|
|
384
|
+
header.ionoCorrections[corrType] = vals;
|
|
385
|
+
} else if (label === "ION ALPHA") {
|
|
386
|
+
const vals = [];
|
|
387
|
+
for (let j = 0; j < 4; j++) {
|
|
388
|
+
const s = line.substring(2 + j * 12, 2 + (j + 1) * 12).trim();
|
|
389
|
+
if (s) vals.push(parseFloat19(s));
|
|
390
|
+
}
|
|
391
|
+
header.ionoCorrections["GPSA"] = vals;
|
|
392
|
+
} else if (label === "ION BETA") {
|
|
393
|
+
const vals = [];
|
|
394
|
+
for (let j = 0; j < 4; j++) {
|
|
395
|
+
const s = line.substring(2 + j * 12, 2 + (j + 1) * 12).trim();
|
|
396
|
+
if (s) vals.push(parseFloat19(s));
|
|
397
|
+
}
|
|
398
|
+
header.ionoCorrections["GPSB"] = vals;
|
|
399
|
+
}
|
|
400
|
+
i++;
|
|
401
|
+
}
|
|
402
|
+
if (inHeader) return { header, ephemerides };
|
|
403
|
+
const isV2 = header.version > 0 && header.version < 3;
|
|
404
|
+
let defaultSys = "G";
|
|
405
|
+
if (isV2) {
|
|
406
|
+
const ht = header.type.toUpperCase();
|
|
407
|
+
if (ht.includes("GLONASS")) defaultSys = "R";
|
|
408
|
+
else if (ht.includes("GEO")) defaultSys = "S";
|
|
409
|
+
else if (ht.includes("GALILEO")) defaultSys = "E";
|
|
410
|
+
else if (ht.includes("BEIDOU") || ht.includes("BDS")) defaultSys = "C";
|
|
411
|
+
}
|
|
412
|
+
const isV4 = header.version >= 4;
|
|
413
|
+
while (i < lines.length) {
|
|
414
|
+
const line = lines[i];
|
|
415
|
+
if (line.trim().length === 0) {
|
|
416
|
+
i++;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (isV4 && line.charAt(0) === ">") {
|
|
420
|
+
const parts = line.substring(2).trim().split(/\s+/);
|
|
421
|
+
const recType = parts[0] ?? "";
|
|
422
|
+
const msgType = parts[2] ?? recType;
|
|
423
|
+
if (recType !== "EPH" || !SUPPORTED_EPH_MSGS.has(msgType)) {
|
|
424
|
+
const totalLines = R4_MSG_LINES[msgType] ?? 2;
|
|
425
|
+
i += totalLines + 1;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const numDataLines2 = (R4_MSG_LINES[msgType] ?? 8) - 1;
|
|
429
|
+
i++;
|
|
430
|
+
if (i >= lines.length) break;
|
|
431
|
+
const epochLine = lines[i];
|
|
432
|
+
const sys2 = epochLine.charAt(0);
|
|
433
|
+
const parsed = parseNavEpochV3(epochLine);
|
|
434
|
+
const prn2 = `${sys2}${parsed.prn.substring(1)}`;
|
|
435
|
+
const dataLines2 = [];
|
|
436
|
+
for (let j = 0; j < numDataLines2; j++) {
|
|
437
|
+
i++;
|
|
438
|
+
if (i >= lines.length) break;
|
|
439
|
+
dataLines2.push(parseDataLine(lines[i], 4));
|
|
440
|
+
}
|
|
441
|
+
if (dataLines2.length === numDataLines2) {
|
|
442
|
+
if (sys2 === "R" || sys2 === "S") {
|
|
443
|
+
ephemerides.push(
|
|
444
|
+
buildStateVectorEphemeris(
|
|
445
|
+
sys2,
|
|
446
|
+
prn2,
|
|
447
|
+
parsed.date,
|
|
448
|
+
parsed.values,
|
|
449
|
+
dataLines2
|
|
450
|
+
)
|
|
451
|
+
);
|
|
452
|
+
} else if (sys2 === "G" || sys2 === "E" || sys2 === "C" || sys2 === "J" || sys2 === "I") {
|
|
453
|
+
ephemerides.push(
|
|
454
|
+
buildKeplerEphemeris(
|
|
455
|
+
sys2,
|
|
456
|
+
prn2,
|
|
457
|
+
parsed.date,
|
|
458
|
+
parsed.values,
|
|
459
|
+
dataLines2
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
i++;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
let sys;
|
|
468
|
+
let prn;
|
|
469
|
+
let date;
|
|
470
|
+
let epochVals;
|
|
471
|
+
if (isV2) {
|
|
472
|
+
const parsed = parseNavEpochV2(line, defaultSys);
|
|
473
|
+
sys = defaultSys;
|
|
474
|
+
prn = parsed.prn;
|
|
475
|
+
date = parsed.date;
|
|
476
|
+
epochVals = parsed.values;
|
|
477
|
+
} else {
|
|
478
|
+
sys = line.charAt(0);
|
|
479
|
+
const parsed = parseNavEpochV3(line);
|
|
480
|
+
prn = `${sys}${parsed.prn.substring(1)}`;
|
|
481
|
+
date = parsed.date;
|
|
482
|
+
epochVals = parsed.values;
|
|
483
|
+
}
|
|
484
|
+
const numDataLines = DATA_LINES[sys];
|
|
485
|
+
if (numDataLines == null) {
|
|
486
|
+
i++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const dataLines = [];
|
|
490
|
+
for (let j = 0; j < numDataLines; j++) {
|
|
491
|
+
i++;
|
|
492
|
+
if (i >= lines.length) break;
|
|
493
|
+
dataLines.push(parseDataLine(lines[i], isV2 ? 3 : 4));
|
|
494
|
+
}
|
|
495
|
+
if (dataLines.length === numDataLines) {
|
|
496
|
+
if (sys === "R" || sys === "S") {
|
|
497
|
+
ephemerides.push(
|
|
498
|
+
buildStateVectorEphemeris(sys, prn, date, epochVals, dataLines)
|
|
499
|
+
);
|
|
500
|
+
} else if (sys === "G" || sys === "E" || sys === "C" || sys === "J" || sys === "I") {
|
|
501
|
+
ephemerides.push(
|
|
502
|
+
buildKeplerEphemeris(sys, prn, date, epochVals, dataLines)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
i++;
|
|
507
|
+
}
|
|
508
|
+
return { header, ephemerides };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/rinex/nav-writer.ts
|
|
512
|
+
function fmtD(val) {
|
|
513
|
+
if (val === 0) return " 0.000000000000E+00";
|
|
514
|
+
const sign = val < 0 ? "-" : " ";
|
|
515
|
+
const abs = Math.abs(val);
|
|
516
|
+
const exp = Math.floor(Math.log10(abs));
|
|
517
|
+
const mantissa = abs / 10 ** exp;
|
|
518
|
+
const mStr = mantissa.toFixed(12);
|
|
519
|
+
const expSign = exp >= 0 ? "+" : "-";
|
|
520
|
+
const expStr = String(Math.abs(exp)).padStart(2, "0");
|
|
521
|
+
return `${sign}${mStr}E${expSign}${expStr}`;
|
|
522
|
+
}
|
|
523
|
+
function fmtEpoch(d) {
|
|
524
|
+
const Y = padR(String(d.getUTCFullYear()), 4);
|
|
525
|
+
const M = padR(String(d.getUTCMonth() + 1), 3);
|
|
526
|
+
const D = padR(String(d.getUTCDate()), 3);
|
|
527
|
+
const h = padR(String(d.getUTCHours()), 3);
|
|
528
|
+
const m = padR(String(d.getUTCMinutes()), 3);
|
|
529
|
+
const s = padR(String(d.getUTCSeconds()), 3);
|
|
530
|
+
return `${Y}${M}${D}${h}${m}${s}`;
|
|
531
|
+
}
|
|
532
|
+
function isKepler(eph) {
|
|
533
|
+
return "af0" in eph;
|
|
534
|
+
}
|
|
535
|
+
function writeHeader(nav) {
|
|
536
|
+
const lines = [];
|
|
537
|
+
lines.push(
|
|
538
|
+
hdrLine(
|
|
539
|
+
padL(" 3.04", 9) + " " + padL("NAVIGATION DATA", 20) + padL("M", 20),
|
|
540
|
+
"RINEX VERSION / TYPE"
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
const now = /* @__PURE__ */ new Date();
|
|
544
|
+
const dateStr = now.getUTCFullYear().toString() + String(now.getUTCMonth() + 1).padStart(2, "0") + String(now.getUTCDate()).padStart(2, "0") + " " + String(now.getUTCHours()).padStart(2, "0") + String(now.getUTCMinutes()).padStart(2, "0") + String(now.getUTCSeconds()).padStart(2, "0") + " UTC";
|
|
545
|
+
lines.push(
|
|
546
|
+
hdrLine(
|
|
547
|
+
padL("GNSSCalc", 20) + padL("", 20) + padL(dateStr, 20),
|
|
548
|
+
"PGM / RUN BY / DATE"
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
const ionoOrder = ["GPSA", "GPSB", "GAL", "BDSA", "BDSB"];
|
|
552
|
+
for (const key of ionoOrder) {
|
|
553
|
+
const vals = nav.header.ionoCorrections[key];
|
|
554
|
+
if (!vals) continue;
|
|
555
|
+
const valStr = vals.map((v) => fmtD(v)).join("");
|
|
556
|
+
lines.push(hdrLine(padL(key, 5) + valStr, "IONOSPHERIC CORR"));
|
|
557
|
+
}
|
|
558
|
+
for (const key of Object.keys(nav.header.ionoCorrections)) {
|
|
559
|
+
if (ionoOrder.includes(key)) continue;
|
|
560
|
+
const vals = nav.header.ionoCorrections[key];
|
|
561
|
+
if (!vals) continue;
|
|
562
|
+
const valStr = vals.map((v) => fmtD(v)).join("");
|
|
563
|
+
lines.push(hdrLine(padL(key, 5) + valStr, "IONOSPHERIC CORR"));
|
|
564
|
+
}
|
|
565
|
+
if (nav.header.leapSeconds != null) {
|
|
566
|
+
lines.push(
|
|
567
|
+
hdrLine(padR(String(nav.header.leapSeconds), 6), "LEAP SECONDS")
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
lines.push(hdrLine("", "END OF HEADER"));
|
|
571
|
+
return lines.join("\n") + "\n";
|
|
572
|
+
}
|
|
573
|
+
function orbitLine(values) {
|
|
574
|
+
return " " + values.map(fmtD).join("");
|
|
575
|
+
}
|
|
576
|
+
function writeKeplerRecord(eph) {
|
|
577
|
+
const lines = [];
|
|
578
|
+
lines.push(
|
|
579
|
+
`${padL(eph.prn, 3)} ${fmtEpoch(eph.tocDate)}${fmtD(eph.af0)}${fmtD(eph.af1)}${fmtD(eph.af2)}`
|
|
580
|
+
);
|
|
581
|
+
lines.push(orbitLine([eph.iode, eph.crs, eph.deltaN, eph.m0]));
|
|
582
|
+
lines.push(orbitLine([eph.cuc, eph.e, eph.cus, eph.sqrtA]));
|
|
583
|
+
lines.push(orbitLine([eph.toe, eph.cic, eph.omega0, eph.cis]));
|
|
584
|
+
lines.push(orbitLine([eph.i0, eph.crc, eph.omega, eph.omegaDot]));
|
|
585
|
+
lines.push(orbitLine([eph.idot, 0, eph.week, 0]));
|
|
586
|
+
lines.push(orbitLine([0, eph.svHealth, eph.tgd, 0]));
|
|
587
|
+
lines.push(orbitLine([eph.toe, 0, 0, 0]));
|
|
588
|
+
return lines.join("\n");
|
|
589
|
+
}
|
|
590
|
+
function writeGlonassRecord(eph) {
|
|
591
|
+
const lines = [];
|
|
592
|
+
lines.push(
|
|
593
|
+
`${padL(eph.prn, 3)} ${fmtEpoch(eph.tocDate)}${fmtD(-eph.tauN)}${fmtD(eph.gammaN)}${fmtD(eph.messageFrameTime)}`
|
|
594
|
+
);
|
|
595
|
+
lines.push(orbitLine([eph.x, eph.xDot, eph.xAcc, eph.health]));
|
|
596
|
+
lines.push(orbitLine([eph.y, eph.yDot, eph.yAcc, eph.freqNum]));
|
|
597
|
+
lines.push(orbitLine([eph.z, eph.zDot, eph.zAcc, 0]));
|
|
598
|
+
return lines.join("\n");
|
|
599
|
+
}
|
|
600
|
+
function writeRinexNav(nav) {
|
|
601
|
+
const header = writeHeader(nav);
|
|
602
|
+
const sorted = [...nav.ephemerides].sort((a, b) => {
|
|
603
|
+
const prnCmp = a.prn.localeCompare(b.prn);
|
|
604
|
+
if (prnCmp !== 0) return prnCmp;
|
|
605
|
+
return a.tocDate.getTime() - b.tocDate.getTime();
|
|
606
|
+
});
|
|
607
|
+
const records = sorted.map(
|
|
608
|
+
(eph) => isKepler(eph) ? writeKeplerRecord(eph) : writeGlonassRecord(eph)
|
|
609
|
+
);
|
|
610
|
+
return header + records.join("\n") + "\n";
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export {
|
|
614
|
+
padL,
|
|
615
|
+
padR,
|
|
616
|
+
fmtF,
|
|
617
|
+
hdrLine,
|
|
618
|
+
EMPTY_WARNINGS,
|
|
619
|
+
WarningAccumulator,
|
|
620
|
+
parseNavFile,
|
|
621
|
+
writeRinexNav
|
|
622
|
+
};
|