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
package/dist/rinex.cjs
ADDED
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/rinex/index.ts
|
|
21
|
+
var rinex_exports = {};
|
|
22
|
+
__export(rinex_exports, {
|
|
23
|
+
EMPTY_WARNINGS: () => EMPTY_WARNINGS,
|
|
24
|
+
SYSTEM_ORDER: () => SYSTEM_ORDER,
|
|
25
|
+
WarningAccumulator: () => WarningAccumulator,
|
|
26
|
+
crxDecompress: () => crxDecompress,
|
|
27
|
+
crxRepair: () => crxRepair,
|
|
28
|
+
fmtF: () => fmtF,
|
|
29
|
+
hdrLine: () => hdrLine,
|
|
30
|
+
padL: () => padL,
|
|
31
|
+
padR: () => padR,
|
|
32
|
+
parseCrxDataLine: () => parseCrxDataLine,
|
|
33
|
+
parseNavFile: () => parseNavFile,
|
|
34
|
+
parseRinexStream: () => parseRinexStream,
|
|
35
|
+
systemCmp: () => systemCmp,
|
|
36
|
+
systemName: () => systemName,
|
|
37
|
+
writeRinexNav: () => writeRinexNav
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(rinex_exports);
|
|
40
|
+
|
|
41
|
+
// src/rinex/crx.ts
|
|
42
|
+
function crxRepair(old, diff) {
|
|
43
|
+
const chars = old.split("");
|
|
44
|
+
for (let i = 0; i < diff.length; i++) {
|
|
45
|
+
const dc = diff[i];
|
|
46
|
+
if (dc === " ") {
|
|
47
|
+
} else if (dc === "&") {
|
|
48
|
+
chars[i] = " ";
|
|
49
|
+
} else {
|
|
50
|
+
chars[i] = dc;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (diff.length > chars.length) {
|
|
54
|
+
for (let i = chars.length; i < diff.length; i++) {
|
|
55
|
+
chars[i] = diff[i] === "&" ? " " : diff[i];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return chars.join("");
|
|
59
|
+
}
|
|
60
|
+
function parseCrxDataLine(line, ntype) {
|
|
61
|
+
const fields = [];
|
|
62
|
+
let pos = 0;
|
|
63
|
+
for (let j = 0; j < ntype; j++) {
|
|
64
|
+
if (pos >= line.length || line[pos] === "\0") {
|
|
65
|
+
fields.push({ empty: true, init: false, arcOrder: -1, value: 0 });
|
|
66
|
+
pos++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line[pos] === " ") {
|
|
70
|
+
fields.push({ empty: true, init: false, arcOrder: -1, value: 0 });
|
|
71
|
+
pos++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const start = pos;
|
|
75
|
+
while (pos < line.length && line[pos] !== " ") pos++;
|
|
76
|
+
const token = line.substring(start, pos);
|
|
77
|
+
if (pos < line.length && line[pos] === " ") pos++;
|
|
78
|
+
if (token.length >= 3 && token[1] === "&") {
|
|
79
|
+
const arcOrder = parseInt(token[0]);
|
|
80
|
+
const value = parseInt(token.substring(2));
|
|
81
|
+
fields.push({
|
|
82
|
+
empty: false,
|
|
83
|
+
init: true,
|
|
84
|
+
arcOrder,
|
|
85
|
+
value: isNaN(value) ? 0 : value
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
const value = parseInt(token);
|
|
89
|
+
fields.push({
|
|
90
|
+
empty: false,
|
|
91
|
+
init: false,
|
|
92
|
+
arcOrder: -1,
|
|
93
|
+
value: isNaN(value) ? 0 : value
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const flagStr = pos < line.length ? line.substring(pos) : "";
|
|
98
|
+
return { fields, flagStr };
|
|
99
|
+
}
|
|
100
|
+
function crxDecompress(prev, field) {
|
|
101
|
+
if (field.init) {
|
|
102
|
+
const a = new Array(Math.max(field.arcOrder + 1, 1)).fill(0);
|
|
103
|
+
a[0] = field.value;
|
|
104
|
+
return {
|
|
105
|
+
state: { accum: a, order: 0, arcOrder: field.arcOrder },
|
|
106
|
+
result: field.value
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!prev) {
|
|
110
|
+
return { state: { accum: [0], order: 0, arcOrder: 0 }, result: 0 };
|
|
111
|
+
}
|
|
112
|
+
let order = prev.order;
|
|
113
|
+
if (order < prev.arcOrder) order++;
|
|
114
|
+
const accum = [...prev.accum];
|
|
115
|
+
while (accum.length <= order) accum.push(0);
|
|
116
|
+
accum[order] = field.value;
|
|
117
|
+
for (let k = order; k >= 1; k--) {
|
|
118
|
+
accum[k - 1] += accum[k];
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
state: { accum, order, arcOrder: prev.arcOrder },
|
|
122
|
+
result: accum[0]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/rinex/format.ts
|
|
127
|
+
function padL(s, w) {
|
|
128
|
+
return s.length >= w ? s.slice(0, w) : s + " ".repeat(w - s.length);
|
|
129
|
+
}
|
|
130
|
+
function padR(s, w) {
|
|
131
|
+
return s.length >= w ? s.slice(0, w) : " ".repeat(w - s.length) + s;
|
|
132
|
+
}
|
|
133
|
+
function fmtF(val, width, dec) {
|
|
134
|
+
return padR(val.toFixed(dec), width);
|
|
135
|
+
}
|
|
136
|
+
function hdrLine(content, label) {
|
|
137
|
+
return padL(content, 60) + padL(label, 20);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/constants/gnss.ts
|
|
141
|
+
var SYSTEM_NAMES = {
|
|
142
|
+
G: "GPS",
|
|
143
|
+
R: "GLONASS",
|
|
144
|
+
E: "Galileo",
|
|
145
|
+
C: "BeiDou",
|
|
146
|
+
J: "QZSS",
|
|
147
|
+
I: "NavIC",
|
|
148
|
+
S: "SBAS"
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// src/rinex/parser.ts
|
|
152
|
+
var CHUNK_SIZE = 2 * 1024 * 1024;
|
|
153
|
+
var yieldToMain = () => new Promise((r) => setTimeout(r, 0));
|
|
154
|
+
var noYield = () => Promise.resolve();
|
|
155
|
+
var SYSTEM_ORDER = ["G", "R", "E", "C", "J", "I", "S"];
|
|
156
|
+
var SYSTEM_RANK = Object.fromEntries(
|
|
157
|
+
SYSTEM_ORDER.map((s, i) => [s, i])
|
|
158
|
+
);
|
|
159
|
+
function systemCmp(a, b) {
|
|
160
|
+
const ra = SYSTEM_RANK[a.charAt(0)] ?? 99;
|
|
161
|
+
const rb = SYSTEM_RANK[b.charAt(0)] ?? 99;
|
|
162
|
+
if (ra !== rb) return ra - rb;
|
|
163
|
+
return a.localeCompare(b);
|
|
164
|
+
}
|
|
165
|
+
function systemName(code) {
|
|
166
|
+
return SYSTEM_NAMES[code] ?? code;
|
|
167
|
+
}
|
|
168
|
+
function parseHeader(lines) {
|
|
169
|
+
const h = {
|
|
170
|
+
version: 0,
|
|
171
|
+
type: "",
|
|
172
|
+
satSystem: "",
|
|
173
|
+
markerName: "",
|
|
174
|
+
markerType: "",
|
|
175
|
+
observer: "",
|
|
176
|
+
agency: "",
|
|
177
|
+
receiverNumber: "",
|
|
178
|
+
receiverType: "",
|
|
179
|
+
receiverVersion: "",
|
|
180
|
+
antNumber: "",
|
|
181
|
+
antType: "",
|
|
182
|
+
approxPosition: null,
|
|
183
|
+
antDelta: null,
|
|
184
|
+
interval: null,
|
|
185
|
+
timeOfFirstObs: null,
|
|
186
|
+
timeOfLastObs: null,
|
|
187
|
+
obsTypes: {},
|
|
188
|
+
glonassSlots: {},
|
|
189
|
+
isCrx: false,
|
|
190
|
+
crxVersion: 0
|
|
191
|
+
};
|
|
192
|
+
let currentObsSys = "";
|
|
193
|
+
for (const raw of lines) {
|
|
194
|
+
const label = raw.substring(60).trim();
|
|
195
|
+
const data = raw.substring(0, 60);
|
|
196
|
+
switch (label) {
|
|
197
|
+
case "CRINEX VERS / TYPE": {
|
|
198
|
+
h.isCrx = true;
|
|
199
|
+
h.crxVersion = parseFloat(data.substring(0, 9)) || 0;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case "RINEX VERSION / TYPE": {
|
|
203
|
+
h.version = parseFloat(data.substring(0, 9));
|
|
204
|
+
h.type = data.substring(20, 21).trim();
|
|
205
|
+
h.satSystem = data.substring(40, 41).trim();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case "MARKER NAME":
|
|
209
|
+
h.markerName = data.trim();
|
|
210
|
+
break;
|
|
211
|
+
case "MARKER TYPE":
|
|
212
|
+
h.markerType = data.trim();
|
|
213
|
+
break;
|
|
214
|
+
case "OBSERVER / AGENCY":
|
|
215
|
+
h.observer = data.substring(0, 20).trim();
|
|
216
|
+
h.agency = data.substring(20).trim();
|
|
217
|
+
break;
|
|
218
|
+
case "REC # / TYPE / VERS":
|
|
219
|
+
h.receiverNumber = data.substring(0, 20).trim();
|
|
220
|
+
h.receiverType = data.substring(20, 40).trim();
|
|
221
|
+
h.receiverVersion = data.substring(40, 60).trim();
|
|
222
|
+
break;
|
|
223
|
+
case "ANT # / TYPE":
|
|
224
|
+
h.antNumber = data.substring(0, 20).trim();
|
|
225
|
+
h.antType = data.substring(20).trim();
|
|
226
|
+
break;
|
|
227
|
+
case "ANTENNA: DELTA H/E/N": {
|
|
228
|
+
const d = data.trim().split(/\s+/).map(Number);
|
|
229
|
+
if (d.length >= 3 && !d.slice(0, 3).some(isNaN)) {
|
|
230
|
+
h.antDelta = [d[0], d[1], d[2]];
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "APPROX POSITION XYZ": {
|
|
235
|
+
const p = data.trim().split(/\s+/).map(Number);
|
|
236
|
+
if (p.length >= 3 && p[0] !== void 0 && p[1] !== void 0 && p[2] !== void 0 && !isNaN(p[0]) && !isNaN(p[1]) && !isNaN(p[2])) {
|
|
237
|
+
h.approxPosition = [p[0], p[1], p[2]];
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "INTERVAL":
|
|
242
|
+
h.interval = parseFloat(data);
|
|
243
|
+
break;
|
|
244
|
+
case "TIME OF FIRST OBS":
|
|
245
|
+
h.timeOfFirstObs = parseHeaderTime(data);
|
|
246
|
+
break;
|
|
247
|
+
case "TIME OF LAST OBS":
|
|
248
|
+
h.timeOfLastObs = parseHeaderTime(data);
|
|
249
|
+
break;
|
|
250
|
+
case "SYS / # / OBS TYPES": {
|
|
251
|
+
const sys = data[0]?.trim();
|
|
252
|
+
if (sys) {
|
|
253
|
+
currentObsSys = sys;
|
|
254
|
+
const count = parseInt(data.substring(3, 6));
|
|
255
|
+
const codes = data.substring(7, 60).trim().split(/\s+/).filter(Boolean);
|
|
256
|
+
h.obsTypes[currentObsSys] = codes;
|
|
257
|
+
void count;
|
|
258
|
+
} else if (currentObsSys && h.obsTypes[currentObsSys]) {
|
|
259
|
+
const codes = data.substring(7, 60).trim().split(/\s+/).filter(Boolean);
|
|
260
|
+
h.obsTypes[currentObsSys].push(...codes);
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "GLONASS SLOT / FRQ #": {
|
|
265
|
+
const nSats = parseInt(data.substring(0, 3));
|
|
266
|
+
void nSats;
|
|
267
|
+
for (let i = 0; i < 8; i++) {
|
|
268
|
+
const off = 4 + i * 7;
|
|
269
|
+
const sat = data.substring(off, off + 3).trim();
|
|
270
|
+
const ch = data.substring(off + 3, off + 7).trim();
|
|
271
|
+
if (sat && sat[0] === "R" && ch) {
|
|
272
|
+
const slot = parseInt(sat.substring(1));
|
|
273
|
+
const k = parseInt(ch);
|
|
274
|
+
if (!isNaN(slot) && !isNaN(k)) h.glonassSlots[slot] = k;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "# / TYPES OF OBSERV": {
|
|
280
|
+
const existing = h.obsTypes["_v2"];
|
|
281
|
+
const codes = data.substring(6, 60).trim().split(/\s+/).filter(Boolean);
|
|
282
|
+
if (!existing || existing.length === 0) {
|
|
283
|
+
h.obsTypes["_v2"] = codes;
|
|
284
|
+
} else {
|
|
285
|
+
existing.push(...codes);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return h;
|
|
292
|
+
}
|
|
293
|
+
function parseHeaderTime(data) {
|
|
294
|
+
const p = data.trim().split(/\s+/).map(Number);
|
|
295
|
+
if (p.length < 6) return null;
|
|
296
|
+
const yr = p[0], mo = p[1], dy = p[2], hr = p[3], mn = p[4], sc = p[5];
|
|
297
|
+
if ([yr, mo, dy, hr, mn, sc].some(isNaN)) return null;
|
|
298
|
+
const wholeSec = Math.floor(sc);
|
|
299
|
+
const ms = Math.round((sc - wholeSec) * 1e3);
|
|
300
|
+
return new Date(Date.UTC(yr, mo - 1, dy, hr, mn, wholeSec, ms));
|
|
301
|
+
}
|
|
302
|
+
function snrIndicesWithBand(obsTypes, system) {
|
|
303
|
+
const codes = obsTypes[system];
|
|
304
|
+
if (!codes) return [];
|
|
305
|
+
const result = [];
|
|
306
|
+
for (let i = 0; i < codes.length; i++) {
|
|
307
|
+
const c = codes[i];
|
|
308
|
+
if (c.startsWith("S")) result.push({ idx: i, band: c[1] });
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function snrIndicesWithBandV2(obsTypes) {
|
|
313
|
+
const codes = obsTypes["_v2"];
|
|
314
|
+
if (!codes) return [];
|
|
315
|
+
const result = [];
|
|
316
|
+
for (let i = 0; i < codes.length; i++) {
|
|
317
|
+
const c = codes[i];
|
|
318
|
+
if (c.startsWith("S")) result.push({ idx: i, band: c[1] });
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
function readObsValue(line, index) {
|
|
323
|
+
const start = index * 16;
|
|
324
|
+
if (start + 14 > line.length) return null;
|
|
325
|
+
const raw = line.substring(start, start + 14).trim();
|
|
326
|
+
if (!raw) return null;
|
|
327
|
+
const v = parseFloat(raw);
|
|
328
|
+
return isNaN(v) ? null : v;
|
|
329
|
+
}
|
|
330
|
+
function parseEpochLine3(line) {
|
|
331
|
+
if (line[0] !== ">") return null;
|
|
332
|
+
const yr = parseInt(line.substring(2, 6));
|
|
333
|
+
const mo = parseInt(line.substring(7, 9));
|
|
334
|
+
const dy = parseInt(line.substring(10, 12));
|
|
335
|
+
const hr = parseInt(line.substring(13, 15));
|
|
336
|
+
const mn = parseInt(line.substring(16, 18));
|
|
337
|
+
const sc = parseFloat(line.substring(19, 29));
|
|
338
|
+
const flag = parseInt(line.substring(31, 32));
|
|
339
|
+
const numSats = parseInt(line.substring(32, 35));
|
|
340
|
+
if ([yr, mo, dy, hr, mn].some(isNaN) || isNaN(sc)) return null;
|
|
341
|
+
const wholeSec = Math.floor(sc);
|
|
342
|
+
const ms = Math.round((sc - wholeSec) * 1e3);
|
|
343
|
+
const time = Date.UTC(yr, mo - 1, dy, hr, mn, wholeSec, ms);
|
|
344
|
+
return {
|
|
345
|
+
time,
|
|
346
|
+
flag: isNaN(flag) ? 0 : flag,
|
|
347
|
+
numSats: isNaN(numSats) ? 0 : numSats
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function parseEpochLine2(line) {
|
|
351
|
+
const yr2 = parseInt(line.substring(1, 3));
|
|
352
|
+
const mo = parseInt(line.substring(4, 6));
|
|
353
|
+
const dy = parseInt(line.substring(7, 9));
|
|
354
|
+
const hr = parseInt(line.substring(10, 12));
|
|
355
|
+
const mn = parseInt(line.substring(13, 15));
|
|
356
|
+
const sc = parseFloat(line.substring(15, 26));
|
|
357
|
+
const flag = parseInt(line.substring(28, 29));
|
|
358
|
+
const numSats = parseInt(line.substring(29, 32));
|
|
359
|
+
if ([yr2, mo, dy, hr, mn].some(isNaN) || isNaN(sc)) return null;
|
|
360
|
+
const yr = yr2 >= 80 ? 1900 + yr2 : 2e3 + yr2;
|
|
361
|
+
const wholeSec = Math.floor(sc);
|
|
362
|
+
const ms = Math.round((sc - wholeSec) * 1e3);
|
|
363
|
+
const time = Date.UTC(yr, mo - 1, dy, hr, mn, wholeSec, ms);
|
|
364
|
+
const satIds = [];
|
|
365
|
+
const satPart = line.substring(32);
|
|
366
|
+
for (let i = 0; i + 3 <= satPart.length; i += 3) {
|
|
367
|
+
const id = satPart.substring(i, i + 3).trim();
|
|
368
|
+
if (id) satIds.push(id);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
time,
|
|
372
|
+
flag: isNaN(flag) ? 0 : flag,
|
|
373
|
+
numSats: isNaN(numSats) ? 0 : numSats,
|
|
374
|
+
satIds
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function parseRinexStream(file, onProgress, signal, onSatObs, workerMode) {
|
|
378
|
+
const skipEpochs = !!workerMode;
|
|
379
|
+
const yield_ = workerMode ? noYield : yieldToMain;
|
|
380
|
+
const decoder = new TextDecoder("ascii");
|
|
381
|
+
let buffer = "";
|
|
382
|
+
let header = null;
|
|
383
|
+
const headerLines = [];
|
|
384
|
+
const epochs = [];
|
|
385
|
+
const satellitesSeen = {};
|
|
386
|
+
let epochInfo = null;
|
|
387
|
+
let satsPerSystem = {};
|
|
388
|
+
let snrValues = [];
|
|
389
|
+
let snrPerSystemAccum = {};
|
|
390
|
+
let snrPerSatAccum = {};
|
|
391
|
+
let snrPerSatBandAccum = {};
|
|
392
|
+
let satLinesRemaining = 0;
|
|
393
|
+
let eventSkipLines = 0;
|
|
394
|
+
let v2SatIds = [];
|
|
395
|
+
let v2SatIndex = 0;
|
|
396
|
+
let v2LinesPerSat = 0;
|
|
397
|
+
let v2CurrentSatLine = 0;
|
|
398
|
+
let v2SnrBandInfo = [];
|
|
399
|
+
let v2ContinuationSatsRemaining = 0;
|
|
400
|
+
let crxPhase = "epoch";
|
|
401
|
+
let crxSatList = [];
|
|
402
|
+
let crxSatIndex = 0;
|
|
403
|
+
let crxPrevEpochLine = "";
|
|
404
|
+
const crxDiffStates = /* @__PURE__ */ new Map();
|
|
405
|
+
function trackSat(sys, prn) {
|
|
406
|
+
if (!satellitesSeen[sys]) satellitesSeen[sys] = /* @__PURE__ */ new Set();
|
|
407
|
+
satellitesSeen[sys].add(prn);
|
|
408
|
+
satsPerSystem[sys] = (satsPerSystem[sys] ?? 0) + 1;
|
|
409
|
+
}
|
|
410
|
+
function pushSnr(sys, prn, val, band) {
|
|
411
|
+
if (skipEpochs) return;
|
|
412
|
+
snrValues.push(val);
|
|
413
|
+
if (!snrPerSystemAccum[sys]) snrPerSystemAccum[sys] = [];
|
|
414
|
+
snrPerSystemAccum[sys].push(val);
|
|
415
|
+
if (!snrPerSatAccum[prn]) snrPerSatAccum[prn] = [];
|
|
416
|
+
snrPerSatAccum[prn].push(val);
|
|
417
|
+
if (band) {
|
|
418
|
+
const key = `${prn}:${band}`;
|
|
419
|
+
if (!snrPerSatBandAccum[key]) snrPerSatBandAccum[key] = [];
|
|
420
|
+
snrPerSatBandAccum[key].push(val);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function finishEpoch() {
|
|
424
|
+
if (!epochInfo) return;
|
|
425
|
+
if (!skipEpochs) {
|
|
426
|
+
const snrBySys = {};
|
|
427
|
+
for (const [sys, vals] of Object.entries(snrPerSystemAccum)) {
|
|
428
|
+
if (vals.length > 0)
|
|
429
|
+
snrBySys[sys] = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
430
|
+
}
|
|
431
|
+
const meanSnr = snrValues.length > 0 ? snrValues.reduce((a, b) => a + b, 0) / snrValues.length : null;
|
|
432
|
+
const snrPerSat = {};
|
|
433
|
+
for (const [prn, vals] of Object.entries(snrPerSatAccum)) {
|
|
434
|
+
if (vals.length > 0)
|
|
435
|
+
snrPerSat[prn] = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
436
|
+
}
|
|
437
|
+
const snrPerSatBand = {};
|
|
438
|
+
for (const [key, vals] of Object.entries(snrPerSatBandAccum)) {
|
|
439
|
+
if (vals.length > 0)
|
|
440
|
+
snrPerSatBand[key] = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
441
|
+
}
|
|
442
|
+
epochs.push({
|
|
443
|
+
time: epochInfo.time,
|
|
444
|
+
totalSats: Object.values(satsPerSystem).reduce((a, b) => a + b, 0),
|
|
445
|
+
satsPerSystem: { ...satsPerSystem },
|
|
446
|
+
meanSnr,
|
|
447
|
+
snrPerSystem: snrBySys,
|
|
448
|
+
snrPerSat,
|
|
449
|
+
snrPerSatBand
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
epochInfo = null;
|
|
453
|
+
satsPerSystem = {};
|
|
454
|
+
snrValues = [];
|
|
455
|
+
snrPerSystemAccum = {};
|
|
456
|
+
snrPerSatAccum = {};
|
|
457
|
+
snrPerSatBandAccum = {};
|
|
458
|
+
}
|
|
459
|
+
function resetEpochAccum(info) {
|
|
460
|
+
epochInfo = info;
|
|
461
|
+
satsPerSystem = {};
|
|
462
|
+
snrValues = [];
|
|
463
|
+
snrPerSystemAccum = {};
|
|
464
|
+
snrPerSatAccum = {};
|
|
465
|
+
snrPerSatBandAccum = {};
|
|
466
|
+
}
|
|
467
|
+
const snrCache = /* @__PURE__ */ new Map();
|
|
468
|
+
function getSnrIndices(sys) {
|
|
469
|
+
let cached = snrCache.get(sys);
|
|
470
|
+
if (!cached) {
|
|
471
|
+
cached = snrIndicesWithBand(header.obsTypes, sys);
|
|
472
|
+
snrCache.set(sys, cached);
|
|
473
|
+
}
|
|
474
|
+
return cached;
|
|
475
|
+
}
|
|
476
|
+
function processSatLineV3(line) {
|
|
477
|
+
if (!header || line.length < 3) return;
|
|
478
|
+
const sys = line[0];
|
|
479
|
+
const prn = line.substring(0, 3);
|
|
480
|
+
trackSat(sys, prn);
|
|
481
|
+
const obsLine = line.substring(3);
|
|
482
|
+
const codes = header.obsTypes[sys] ?? [];
|
|
483
|
+
if (onSatObs && epochInfo) {
|
|
484
|
+
const values = new Array(codes.length);
|
|
485
|
+
for (let i = 0; i < codes.length; i++)
|
|
486
|
+
values[i] = readObsValue(obsLine, i);
|
|
487
|
+
if (!skipEpochs) {
|
|
488
|
+
for (const { idx, band } of getSnrIndices(sys)) {
|
|
489
|
+
const val = values[idx] ?? null;
|
|
490
|
+
if (val !== null && val > 0) pushSnr(sys, prn, val, band);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
onSatObs(epochInfo.time, prn, codes, values);
|
|
494
|
+
} else if (!skipEpochs) {
|
|
495
|
+
for (const { idx, band } of getSnrIndices(sys)) {
|
|
496
|
+
const val = readObsValue(obsLine, idx);
|
|
497
|
+
if (val !== null && val > 0) pushSnr(sys, prn, val, band);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
let v2ObsAccum = [];
|
|
502
|
+
function processSatLineV2(line) {
|
|
503
|
+
if (!header || v2SatIndex >= v2SatIds.length) return;
|
|
504
|
+
const satId = v2SatIds[v2SatIndex];
|
|
505
|
+
let sys = satId[0];
|
|
506
|
+
if (sys === " " || /\d/.test(sys)) sys = "G";
|
|
507
|
+
const prn = sys + satId.substring(1).padStart(2, "0");
|
|
508
|
+
if (v2CurrentSatLine === 0) {
|
|
509
|
+
trackSat(sys, prn);
|
|
510
|
+
if (onSatObs) v2ObsAccum = [];
|
|
511
|
+
}
|
|
512
|
+
const lineOffset = v2CurrentSatLine * 5;
|
|
513
|
+
for (const { idx, band } of v2SnrBandInfo) {
|
|
514
|
+
if (idx >= lineOffset && idx < lineOffset + 5) {
|
|
515
|
+
const val = readObsValue(line, idx - lineOffset);
|
|
516
|
+
if (val !== null && val > 0) pushSnr(sys, prn, val, band);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (onSatObs) {
|
|
520
|
+
for (let i = 0; i < 5; i++) {
|
|
521
|
+
if (lineOffset + i < (header.obsTypes["_v2"] ?? []).length) {
|
|
522
|
+
v2ObsAccum.push(readObsValue(line, i));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
v2CurrentSatLine++;
|
|
527
|
+
if (v2CurrentSatLine >= v2LinesPerSat) {
|
|
528
|
+
if (onSatObs && epochInfo) {
|
|
529
|
+
const codes = header.obsTypes["_v2"] ?? [];
|
|
530
|
+
onSatObs(epochInfo.time, prn, codes, v2ObsAccum);
|
|
531
|
+
}
|
|
532
|
+
v2CurrentSatLine = 0;
|
|
533
|
+
v2SatIndex++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function processCrxSatDataLine(prn, line) {
|
|
537
|
+
if (!header) return;
|
|
538
|
+
const sys = prn[0];
|
|
539
|
+
trackSat(sys, prn);
|
|
540
|
+
const ntype = header.version >= 3 ? (header.obsTypes[sys] ?? []).length : (header.obsTypes["_v2"] ?? []).length;
|
|
541
|
+
if (ntype === 0) return;
|
|
542
|
+
const { fields } = parseCrxDataLine(line, ntype);
|
|
543
|
+
let states = crxDiffStates.get(prn);
|
|
544
|
+
if (!states) {
|
|
545
|
+
states = new Array(ntype).fill(null);
|
|
546
|
+
crxDiffStates.set(prn, states);
|
|
547
|
+
}
|
|
548
|
+
while (states.length < ntype) states.push(null);
|
|
549
|
+
const sBandMap = skipEpochs ? null : new Map(
|
|
550
|
+
(header.version >= 3 ? getSnrIndices(sys) : snrIndicesWithBandV2(header.obsTypes)).map((s) => [s.idx, s.band])
|
|
551
|
+
);
|
|
552
|
+
const obsValues = onSatObs ? new Array(ntype).fill(null) : [];
|
|
553
|
+
for (let j = 0; j < ntype; j++) {
|
|
554
|
+
const field = fields[j];
|
|
555
|
+
if (field.empty) continue;
|
|
556
|
+
const { state, result } = crxDecompress(states[j] ?? null, field);
|
|
557
|
+
states[j] = state;
|
|
558
|
+
if (onSatObs) obsValues[j] = result / 1e3;
|
|
559
|
+
if (sBandMap) {
|
|
560
|
+
const band = sBandMap.get(j);
|
|
561
|
+
if (band !== void 0) {
|
|
562
|
+
const snrFloat = result / 1e3;
|
|
563
|
+
if (snrFloat > 0) pushSnr(sys, prn, snrFloat, band);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (onSatObs && epochInfo) {
|
|
568
|
+
const codes = header.version >= 3 ? header.obsTypes[sys] ?? [] : header.obsTypes["_v2"] ?? [];
|
|
569
|
+
onSatObs(epochInfo.time, prn, codes, obsValues);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
let crxEpochTopFrom = ">";
|
|
573
|
+
let crxEpochTopTo = ">";
|
|
574
|
+
let crxEventSkipLines = 0;
|
|
575
|
+
function processCrxLine(line) {
|
|
576
|
+
if (!header) return;
|
|
577
|
+
const isV3 = header.version >= 3;
|
|
578
|
+
if (header.crxVersion < 3) {
|
|
579
|
+
crxEpochTopFrom = "&";
|
|
580
|
+
crxEpochTopTo = " ";
|
|
581
|
+
}
|
|
582
|
+
if (crxEventSkipLines > 0) {
|
|
583
|
+
crxEventSkipLines--;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (crxPhase === "epoch") {
|
|
587
|
+
if (header.crxVersion >= 3 && line[0] === "&") return;
|
|
588
|
+
const isInitEpoch = line[0] === crxEpochTopFrom;
|
|
589
|
+
const isTextDiff = !isInitEpoch && crxPrevEpochLine.length > 0;
|
|
590
|
+
if (isInitEpoch || isTextDiff) {
|
|
591
|
+
let epochLine;
|
|
592
|
+
if (isInitEpoch) {
|
|
593
|
+
epochLine = crxEpochTopTo + line.substring(1);
|
|
594
|
+
crxPrevEpochLine = "";
|
|
595
|
+
} else {
|
|
596
|
+
epochLine = crxRepair(crxPrevEpochLine, line);
|
|
597
|
+
}
|
|
598
|
+
if (isV3) {
|
|
599
|
+
if (epochLine[0] !== ">") return;
|
|
600
|
+
const info = parseEpochLine3(epochLine);
|
|
601
|
+
if (!info) {
|
|
602
|
+
crxPrevEpochLine = epochLine;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (info.flag >= 2) {
|
|
606
|
+
crxPrevEpochLine = epochLine;
|
|
607
|
+
crxEventSkipLines = info.numSats;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (info.flag !== 0) {
|
|
611
|
+
crxPrevEpochLine = epochLine;
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
resetEpochAccum(info);
|
|
615
|
+
const satPart = epochLine.substring(41).replace(/\s+$/, "");
|
|
616
|
+
crxSatList = [];
|
|
617
|
+
for (let i = 0; i + 3 <= satPart.length; i += 3) {
|
|
618
|
+
crxSatList.push(satPart.substring(i, i + 3));
|
|
619
|
+
}
|
|
620
|
+
crxSatIndex = 0;
|
|
621
|
+
crxPrevEpochLine = epochLine;
|
|
622
|
+
crxPhase = "clock";
|
|
623
|
+
} else {
|
|
624
|
+
const info = parseEpochLine2(epochLine);
|
|
625
|
+
if (!info) {
|
|
626
|
+
crxPrevEpochLine = epochLine;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (info.flag >= 2) {
|
|
630
|
+
crxPrevEpochLine = epochLine;
|
|
631
|
+
crxEventSkipLines = info.numSats;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (info.flag !== 0) {
|
|
635
|
+
crxPrevEpochLine = epochLine;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
resetEpochAccum(info);
|
|
639
|
+
crxSatList = (info.satIds ?? []).map((id) => {
|
|
640
|
+
let sys = id[0];
|
|
641
|
+
if (sys === " " || /\d/.test(sys)) sys = "G";
|
|
642
|
+
return sys + id.substring(1).padStart(2, "0");
|
|
643
|
+
});
|
|
644
|
+
crxSatIndex = 0;
|
|
645
|
+
crxPrevEpochLine = epochLine;
|
|
646
|
+
crxPhase = "clock";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else if (crxPhase === "clock") {
|
|
650
|
+
if (crxSatList.length > 0) {
|
|
651
|
+
crxPhase = "satdata";
|
|
652
|
+
} else {
|
|
653
|
+
crxPhase = "epoch";
|
|
654
|
+
finishEpoch();
|
|
655
|
+
}
|
|
656
|
+
} else if (crxPhase === "satdata") {
|
|
657
|
+
const prn = crxSatList[crxSatIndex];
|
|
658
|
+
processCrxSatDataLine(prn, line);
|
|
659
|
+
crxSatIndex++;
|
|
660
|
+
if (crxSatIndex >= crxSatList.length) {
|
|
661
|
+
crxPhase = "epoch";
|
|
662
|
+
finishEpoch();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function processRinexLine(line) {
|
|
667
|
+
if (!header) return;
|
|
668
|
+
if (eventSkipLines > 0) {
|
|
669
|
+
eventSkipLines--;
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (header.version >= 3) {
|
|
673
|
+
if (satLinesRemaining > 0) {
|
|
674
|
+
processSatLineV3(line);
|
|
675
|
+
satLinesRemaining--;
|
|
676
|
+
if (satLinesRemaining === 0) finishEpoch();
|
|
677
|
+
} else if (line[0] === ">") {
|
|
678
|
+
const info = parseEpochLine3(line);
|
|
679
|
+
if (!info) return;
|
|
680
|
+
if (info.flag >= 2 && info.flag <= 5) {
|
|
681
|
+
eventSkipLines = info.numSats;
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (info.flag === 0) {
|
|
685
|
+
resetEpochAccum(info);
|
|
686
|
+
satLinesRemaining = info.numSats;
|
|
687
|
+
if (info.numSats === 0) finishEpoch();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
if (v2ContinuationSatsRemaining > 0) {
|
|
692
|
+
const satPart = line.substring(32);
|
|
693
|
+
for (let i = 0; i + 3 <= satPart.length; i += 3) {
|
|
694
|
+
const id = satPart.substring(i, i + 3).trim();
|
|
695
|
+
if (id) v2SatIds.push(id);
|
|
696
|
+
}
|
|
697
|
+
v2ContinuationSatsRemaining--;
|
|
698
|
+
if (v2ContinuationSatsRemaining === 0) {
|
|
699
|
+
v2SatIndex = 0;
|
|
700
|
+
v2CurrentSatLine = 0;
|
|
701
|
+
satLinesRemaining = v2SatIds.length * v2LinesPerSat;
|
|
702
|
+
if (satLinesRemaining === 0) finishEpoch();
|
|
703
|
+
}
|
|
704
|
+
} else if (satLinesRemaining > 0) {
|
|
705
|
+
processSatLineV2(line);
|
|
706
|
+
satLinesRemaining--;
|
|
707
|
+
if (satLinesRemaining === 0) finishEpoch();
|
|
708
|
+
} else {
|
|
709
|
+
const info = parseEpochLine2(line);
|
|
710
|
+
if (!info) return;
|
|
711
|
+
if (info.flag >= 2 && info.flag <= 5) {
|
|
712
|
+
eventSkipLines = info.numSats;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (info.flag === 0) {
|
|
716
|
+
resetEpochAccum(info);
|
|
717
|
+
v2SatIds = info.satIds ?? [];
|
|
718
|
+
const v2ObsPerSat = (header.obsTypes["_v2"] ?? []).length;
|
|
719
|
+
v2LinesPerSat = Math.ceil(v2ObsPerSat / 5) || 1;
|
|
720
|
+
v2SnrBandInfo = snrIndicesWithBandV2(header.obsTypes);
|
|
721
|
+
v2SatIndex = 0;
|
|
722
|
+
v2CurrentSatLine = 0;
|
|
723
|
+
const continuationLines = Math.max(
|
|
724
|
+
0,
|
|
725
|
+
Math.ceil((info.numSats - 12) / 12)
|
|
726
|
+
);
|
|
727
|
+
if (continuationLines > 0) {
|
|
728
|
+
v2ContinuationSatsRemaining = continuationLines;
|
|
729
|
+
} else {
|
|
730
|
+
satLinesRemaining = v2SatIds.length * v2LinesPerSat;
|
|
731
|
+
if (satLinesRemaining === 0) finishEpoch();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function processLine(line) {
|
|
738
|
+
if (!header) return;
|
|
739
|
+
if (header.isCrx) {
|
|
740
|
+
processCrxLine(line);
|
|
741
|
+
} else {
|
|
742
|
+
processRinexLine(line);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function processChunkText(text, isLast) {
|
|
746
|
+
buffer += text;
|
|
747
|
+
const lastNl = buffer.lastIndexOf("\n");
|
|
748
|
+
if (lastNl === -1 && !isLast) return;
|
|
749
|
+
const chunk = isLast ? buffer : buffer.substring(0, lastNl);
|
|
750
|
+
buffer = isLast ? "" : buffer.substring(lastNl + 1);
|
|
751
|
+
const lines = chunk.split("\n");
|
|
752
|
+
for (const rawLine of lines) {
|
|
753
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
754
|
+
if (!line && !header) continue;
|
|
755
|
+
if (!header) {
|
|
756
|
+
headerLines.push(line);
|
|
757
|
+
if (line.includes("END OF HEADER")) header = parseHeader(headerLines);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
processLine(line);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const magic = new Uint8Array(await file.slice(0, 2).arrayBuffer());
|
|
764
|
+
const isGz = magic[0] === 31 && magic[1] === 139;
|
|
765
|
+
if (magic[0] === 31 && magic[1] === 157) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
"Unix compress (.Z) files are not supported. Please decompress first (e.g. uncompress or gzip -d)."
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (isGz) {
|
|
771
|
+
let bytesRead = 0;
|
|
772
|
+
const raw = file.stream();
|
|
773
|
+
const decompressed = raw.pipeThrough(new DecompressionStream("gzip"));
|
|
774
|
+
const reader = decompressed.getReader();
|
|
775
|
+
for (; ; ) {
|
|
776
|
+
if (signal?.aborted) {
|
|
777
|
+
reader.cancel();
|
|
778
|
+
throw new DOMException("Aborted", "AbortError");
|
|
779
|
+
}
|
|
780
|
+
const { done, value } = await reader.read();
|
|
781
|
+
if (done) break;
|
|
782
|
+
bytesRead += value.byteLength;
|
|
783
|
+
processChunkText(decoder.decode(value, { stream: true }), false);
|
|
784
|
+
onProgress?.(
|
|
785
|
+
Math.min(99, Math.round(bytesRead / (file.size * 4) * 100))
|
|
786
|
+
);
|
|
787
|
+
await yield_();
|
|
788
|
+
}
|
|
789
|
+
processChunkText(decoder.decode(), true);
|
|
790
|
+
} else {
|
|
791
|
+
for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
|
|
792
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
793
|
+
const end = Math.min(offset + CHUNK_SIZE, file.size);
|
|
794
|
+
const slice = file.slice(offset, end);
|
|
795
|
+
const arrayBuf = await slice.arrayBuffer();
|
|
796
|
+
processChunkText(
|
|
797
|
+
decoder.decode(arrayBuf, { stream: end < file.size }),
|
|
798
|
+
false
|
|
799
|
+
);
|
|
800
|
+
onProgress?.(Math.min(99, Math.round(end / file.size * 100)));
|
|
801
|
+
await yield_();
|
|
802
|
+
}
|
|
803
|
+
if (buffer.length > 0) {
|
|
804
|
+
processChunkText("", true);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (epochInfo && satLinesRemaining === 0 && crxPhase === "epoch")
|
|
808
|
+
finishEpoch();
|
|
809
|
+
if (!header)
|
|
810
|
+
throw new Error("No valid RINEX header found (missing END OF HEADER).");
|
|
811
|
+
onProgress?.(100);
|
|
812
|
+
return {
|
|
813
|
+
header,
|
|
814
|
+
epochs,
|
|
815
|
+
stats: computeStats(header, epochs, satellitesSeen)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function computeStats(header, epochs, satellitesSeen) {
|
|
819
|
+
const n = epochs.length;
|
|
820
|
+
const startTime = n > 0 ? new Date(epochs[0].time) : header.timeOfFirstObs;
|
|
821
|
+
const endTime = n > 0 ? new Date(epochs[n - 1].time) : header.timeOfLastObs;
|
|
822
|
+
let duration = null;
|
|
823
|
+
if (startTime && endTime)
|
|
824
|
+
duration = (endTime.getTime() - startTime.getTime()) / 1e3;
|
|
825
|
+
let interval = header.interval;
|
|
826
|
+
if (interval === null && n >= 2)
|
|
827
|
+
interval = (epochs[1].time - epochs[0].time) / 1e3;
|
|
828
|
+
const uniqueSatsPerSystem = {};
|
|
829
|
+
let totalUnique = 0;
|
|
830
|
+
const systems = [];
|
|
831
|
+
for (const [sys, prns] of Object.entries(satellitesSeen)) {
|
|
832
|
+
uniqueSatsPerSystem[sys] = prns.size;
|
|
833
|
+
totalUnique += prns.size;
|
|
834
|
+
systems.push(sys);
|
|
835
|
+
}
|
|
836
|
+
systems.sort(systemCmp);
|
|
837
|
+
const meanSatellites = n > 0 ? epochs.reduce((s, e) => s + e.totalSats, 0) / n : 0;
|
|
838
|
+
const snrEpochs = epochs.filter((e) => e.meanSnr !== null);
|
|
839
|
+
const meanSnr = snrEpochs.length > 0 ? snrEpochs.reduce((s, e) => s + e.meanSnr, 0) / snrEpochs.length : null;
|
|
840
|
+
return {
|
|
841
|
+
totalEpochs: n,
|
|
842
|
+
validEpochs: n,
|
|
843
|
+
duration,
|
|
844
|
+
startTime,
|
|
845
|
+
endTime,
|
|
846
|
+
interval,
|
|
847
|
+
uniqueSatellites: totalUnique,
|
|
848
|
+
uniqueSatsPerSystem,
|
|
849
|
+
systems,
|
|
850
|
+
meanSatellites,
|
|
851
|
+
meanSnr
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/rinex/warnings.ts
|
|
856
|
+
var EMPTY_WARNINGS = {
|
|
857
|
+
items: [],
|
|
858
|
+
errorCount: 0,
|
|
859
|
+
warningCount: 0,
|
|
860
|
+
infoCount: 0
|
|
861
|
+
};
|
|
862
|
+
var MAX_EXAMPLES = 3;
|
|
863
|
+
var WarningAccumulator = class {
|
|
864
|
+
items = /* @__PURE__ */ new Map();
|
|
865
|
+
// Epoch-level state
|
|
866
|
+
prevEpochTime = null;
|
|
867
|
+
epochTimes = /* @__PURE__ */ new Set();
|
|
868
|
+
intervalMs = null;
|
|
869
|
+
flagCounts = /* @__PURE__ */ new Map();
|
|
870
|
+
seenPrns = /* @__PURE__ */ new Set();
|
|
871
|
+
/* ── Header checks ──────────────────────────────────────────── */
|
|
872
|
+
checkHeader(header) {
|
|
873
|
+
const pos = header.approxPosition;
|
|
874
|
+
if (!pos || pos[0] === 0 && pos[1] === 0 && pos[2] === 0) {
|
|
875
|
+
this.add(
|
|
876
|
+
"MISSING_POSITION",
|
|
877
|
+
"warning",
|
|
878
|
+
"Missing or zero approximate position (APPROX POSITION XYZ)"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (!header.antType) {
|
|
882
|
+
this.add(
|
|
883
|
+
"MISSING_ANT_TYPE",
|
|
884
|
+
"info",
|
|
885
|
+
"Missing antenna type (ANT # / TYPE)"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
if (!header.receiverType) {
|
|
889
|
+
this.add(
|
|
890
|
+
"MISSING_REC_TYPE",
|
|
891
|
+
"info",
|
|
892
|
+
"Missing receiver type (REC # / TYPE / VERS)"
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (!header.markerName) {
|
|
896
|
+
this.add("MISSING_MARKER", "info", "Missing marker name (MARKER NAME)");
|
|
897
|
+
}
|
|
898
|
+
if (header.interval == null) {
|
|
899
|
+
this.add("MISSING_INTERVAL", "info", "No observation interval in header");
|
|
900
|
+
}
|
|
901
|
+
if (Object.keys(header.obsTypes).length === 0) {
|
|
902
|
+
this.add(
|
|
903
|
+
"NO_OBS_TYPES",
|
|
904
|
+
"error",
|
|
905
|
+
"No observation types defined in header"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
setInterval(intervalMs) {
|
|
910
|
+
this.intervalMs = intervalMs;
|
|
911
|
+
}
|
|
912
|
+
/* ── Epoch checks ───────────────────────────────────────────── */
|
|
913
|
+
onEpoch(time, flag) {
|
|
914
|
+
if (flag !== 0) {
|
|
915
|
+
this.flagCounts.set(flag, (this.flagCounts.get(flag) ?? 0) + 1);
|
|
916
|
+
}
|
|
917
|
+
if (flag !== 0) return;
|
|
918
|
+
if (this.epochTimes.has(time)) {
|
|
919
|
+
const d = new Date(time);
|
|
920
|
+
this.add(
|
|
921
|
+
"DUPLICATE_EPOCH",
|
|
922
|
+
"warning",
|
|
923
|
+
"Duplicate epoch timestamps",
|
|
924
|
+
d.toISOString().replace("T", " ").replace(".000Z", "")
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
this.epochTimes.add(time);
|
|
928
|
+
if (this.prevEpochTime !== null && time < this.prevEpochTime) {
|
|
929
|
+
const d = new Date(time);
|
|
930
|
+
this.add(
|
|
931
|
+
"OUT_OF_ORDER",
|
|
932
|
+
"warning",
|
|
933
|
+
"Epochs not in chronological order",
|
|
934
|
+
d.toISOString().replace("T", " ").replace(".000Z", "")
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (this.prevEpochTime !== null) {
|
|
938
|
+
const gap = time - this.prevEpochTime;
|
|
939
|
+
const threshold = this.intervalMs ? this.intervalMs * 10 : 36e5;
|
|
940
|
+
if (gap > threshold && gap > 0) {
|
|
941
|
+
const gapMin = (gap / 6e4).toFixed(1);
|
|
942
|
+
this.add(
|
|
943
|
+
"LARGE_GAP",
|
|
944
|
+
"info",
|
|
945
|
+
"Large time gaps between epochs",
|
|
946
|
+
`${gapMin} min gap at ${new Date(this.prevEpochTime).toISOString().slice(11, 19)}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
this.prevEpochTime = time;
|
|
951
|
+
}
|
|
952
|
+
/* ── PRN checks ─────────────────────────────────────────────── */
|
|
953
|
+
onPrn(prn) {
|
|
954
|
+
if (this.seenPrns.has(prn)) return;
|
|
955
|
+
this.seenPrns.add(prn);
|
|
956
|
+
const sys = prn[0];
|
|
957
|
+
const num = parseInt(prn.slice(1), 10);
|
|
958
|
+
if (!sys || isNaN(num)) {
|
|
959
|
+
this.add("INVALID_PRN", "warning", "Invalid PRN format", prn);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const ranges = {
|
|
963
|
+
G: [1, 32],
|
|
964
|
+
R: [1, 27],
|
|
965
|
+
E: [1, 50],
|
|
966
|
+
C: [1, 63],
|
|
967
|
+
J: [1, 10],
|
|
968
|
+
I: [1, 14],
|
|
969
|
+
S: [20, 59]
|
|
970
|
+
};
|
|
971
|
+
const range = ranges[sys];
|
|
972
|
+
if (!range) {
|
|
973
|
+
this.add(
|
|
974
|
+
"UNKNOWN_SYSTEM",
|
|
975
|
+
"info",
|
|
976
|
+
"Unknown satellite system letter",
|
|
977
|
+
prn
|
|
978
|
+
);
|
|
979
|
+
} else if (num < range[0] || num > range[1]) {
|
|
980
|
+
this.add(
|
|
981
|
+
"PRN_OUT_OF_RANGE",
|
|
982
|
+
"info",
|
|
983
|
+
`PRN number outside expected range`,
|
|
984
|
+
`${prn} (expected ${sys}${String(range[0]).padStart(2, "0")}-${sys}${String(range[1]).padStart(2, "0")})`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/* ── Finalize ───────────────────────────────────────────────── */
|
|
989
|
+
finalize() {
|
|
990
|
+
for (const [flag, count] of this.flagCounts) {
|
|
991
|
+
const labels = {
|
|
992
|
+
1: "Receiver clock reset",
|
|
993
|
+
2: "Moving antenna event",
|
|
994
|
+
3: "Header information follows",
|
|
995
|
+
4: "External event",
|
|
996
|
+
5: "Cycle slip records",
|
|
997
|
+
6: "Power failure recovery"
|
|
998
|
+
};
|
|
999
|
+
const label = labels[flag] ?? `Flag ${flag}`;
|
|
1000
|
+
this.add(
|
|
1001
|
+
`EPOCH_FLAG_${flag}`,
|
|
1002
|
+
"info",
|
|
1003
|
+
`${label} events (flag ${flag})`,
|
|
1004
|
+
`${count} epoch(s)`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
const items = [...this.items.values()];
|
|
1008
|
+
const order = { error: 0, warning: 1, info: 2 };
|
|
1009
|
+
items.sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9));
|
|
1010
|
+
return {
|
|
1011
|
+
items,
|
|
1012
|
+
errorCount: items.filter((w) => w.severity === "error").length,
|
|
1013
|
+
warningCount: items.filter((w) => w.severity === "warning").length,
|
|
1014
|
+
infoCount: items.filter((w) => w.severity === "info").length
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/* ── Internal ───────────────────────────────────────────────── */
|
|
1018
|
+
add(code, severity, message, example) {
|
|
1019
|
+
let w = this.items.get(code);
|
|
1020
|
+
if (!w) {
|
|
1021
|
+
w = { code, severity, message, count: 0 };
|
|
1022
|
+
this.items.set(code, w);
|
|
1023
|
+
}
|
|
1024
|
+
w.count++;
|
|
1025
|
+
if (example) {
|
|
1026
|
+
if (!w.examples) w.examples = [];
|
|
1027
|
+
if (w.examples.length < MAX_EXAMPLES) w.examples.push(example);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/rinex/nav.ts
|
|
1033
|
+
var DATA_LINES = {
|
|
1034
|
+
G: 7,
|
|
1035
|
+
E: 7,
|
|
1036
|
+
C: 7,
|
|
1037
|
+
J: 7,
|
|
1038
|
+
// Keplerian
|
|
1039
|
+
R: 3,
|
|
1040
|
+
// GLONASS state vector
|
|
1041
|
+
S: 3,
|
|
1042
|
+
I: 7
|
|
1043
|
+
// SBAS / NavIC
|
|
1044
|
+
};
|
|
1045
|
+
var R4_MSG_LINES = {
|
|
1046
|
+
// EPH types we parse (same Keplerian/state-vector layout as RINEX 3)
|
|
1047
|
+
LNAV: 8,
|
|
1048
|
+
INAV: 8,
|
|
1049
|
+
FNAV: 8,
|
|
1050
|
+
D1: 8,
|
|
1051
|
+
D2: 8,
|
|
1052
|
+
SBAS: 4,
|
|
1053
|
+
FDMA: 5,
|
|
1054
|
+
// RINEX 4 adds orbit-4 (status flags) vs 3 in RINEX 3
|
|
1055
|
+
// EPH types we skip (different field layout)
|
|
1056
|
+
CNAV: 9,
|
|
1057
|
+
CNV1: 10,
|
|
1058
|
+
CNV2: 10,
|
|
1059
|
+
CNV3: 9,
|
|
1060
|
+
L1NV: 8,
|
|
1061
|
+
L1OC: 9,
|
|
1062
|
+
L3OC: 9,
|
|
1063
|
+
// Non-EPH records
|
|
1064
|
+
STO: 2,
|
|
1065
|
+
EOP: 3,
|
|
1066
|
+
ION: 3
|
|
1067
|
+
};
|
|
1068
|
+
var SUPPORTED_EPH_MSGS = /* @__PURE__ */ new Set([
|
|
1069
|
+
"LNAV",
|
|
1070
|
+
"INAV",
|
|
1071
|
+
"FNAV",
|
|
1072
|
+
"D1",
|
|
1073
|
+
"D2",
|
|
1074
|
+
"SBAS",
|
|
1075
|
+
"FDMA"
|
|
1076
|
+
]);
|
|
1077
|
+
function parseFloat19(s) {
|
|
1078
|
+
return parseFloat(s.trim().replace(/[dD]/g, "E"));
|
|
1079
|
+
}
|
|
1080
|
+
function parseNavEpochV3(line) {
|
|
1081
|
+
const prn = line.substring(0, 3).trim();
|
|
1082
|
+
const yr = parseInt(line.substring(3, 8));
|
|
1083
|
+
const mo = parseInt(line.substring(8, 11));
|
|
1084
|
+
const dy = parseInt(line.substring(11, 14));
|
|
1085
|
+
const hr = parseInt(line.substring(14, 17));
|
|
1086
|
+
const mn = parseInt(line.substring(17, 20));
|
|
1087
|
+
const sc = parseInt(line.substring(20, 23));
|
|
1088
|
+
const date = new Date(Date.UTC(yr, mo - 1, dy, hr, mn, sc));
|
|
1089
|
+
const values = [];
|
|
1090
|
+
for (let i = 0; i < 3; i++) {
|
|
1091
|
+
const start = 23 + i * 19;
|
|
1092
|
+
if (start < line.length) {
|
|
1093
|
+
values.push(parseFloat19(line.substring(start, start + 19)));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return { prn, date, values };
|
|
1097
|
+
}
|
|
1098
|
+
function parseNavEpochV2(line, defaultSys) {
|
|
1099
|
+
const prnNum = parseInt(line.substring(0, 2));
|
|
1100
|
+
const yr = parseInt(line.substring(2, 5));
|
|
1101
|
+
const mo = parseInt(line.substring(5, 8));
|
|
1102
|
+
const dy = parseInt(line.substring(8, 11));
|
|
1103
|
+
const hr = parseInt(line.substring(11, 14));
|
|
1104
|
+
const mn = parseInt(line.substring(14, 17));
|
|
1105
|
+
const sc = parseFloat(line.substring(17, 22));
|
|
1106
|
+
const fullYr = yr < 80 ? 2e3 + yr : 1900 + yr;
|
|
1107
|
+
const date = new Date(Date.UTC(fullYr, mo - 1, dy, hr, mn, Math.floor(sc)));
|
|
1108
|
+
const prn = `${defaultSys}${String(prnNum).padStart(2, "0")}`;
|
|
1109
|
+
const values = [];
|
|
1110
|
+
for (let i = 0; i < 3; i++) {
|
|
1111
|
+
const start = 22 + i * 19;
|
|
1112
|
+
if (start < line.length) {
|
|
1113
|
+
values.push(parseFloat19(line.substring(start, start + 19)));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return { prn, date, values };
|
|
1117
|
+
}
|
|
1118
|
+
function parseDataLine(line, colOffset = 4) {
|
|
1119
|
+
const values = [];
|
|
1120
|
+
for (let i = 0; i < 4; i++) {
|
|
1121
|
+
const start = colOffset + i * 19;
|
|
1122
|
+
if (start >= line.length) break;
|
|
1123
|
+
const s = line.substring(start, start + 19).trim();
|
|
1124
|
+
if (s.length > 0) {
|
|
1125
|
+
const v = parseFloat19(s);
|
|
1126
|
+
values.push(Number.isFinite(v) ? v : 0);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return values;
|
|
1130
|
+
}
|
|
1131
|
+
function buildKeplerEphemeris(sys, prn, date, epochVals, data) {
|
|
1132
|
+
const d = [];
|
|
1133
|
+
for (const line of data) {
|
|
1134
|
+
for (let i = 0; i < 4; i++) d.push(line[i] ?? 0);
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
system: sys,
|
|
1138
|
+
prn,
|
|
1139
|
+
tocDate: date,
|
|
1140
|
+
toc: date.getTime() / 1e3 % (7 * 86400),
|
|
1141
|
+
// seconds of week approx
|
|
1142
|
+
af0: epochVals[0] ?? 0,
|
|
1143
|
+
af1: epochVals[1] ?? 0,
|
|
1144
|
+
af2: epochVals[2] ?? 0,
|
|
1145
|
+
iode: d[0] ?? 0,
|
|
1146
|
+
crs: d[1] ?? 0,
|
|
1147
|
+
deltaN: d[2] ?? 0,
|
|
1148
|
+
m0: d[3] ?? 0,
|
|
1149
|
+
cuc: d[4] ?? 0,
|
|
1150
|
+
e: d[5] ?? 0,
|
|
1151
|
+
cus: d[6] ?? 0,
|
|
1152
|
+
sqrtA: d[7] ?? 0,
|
|
1153
|
+
toe: d[8] ?? 0,
|
|
1154
|
+
cic: d[9] ?? 0,
|
|
1155
|
+
omega0: d[10] ?? 0,
|
|
1156
|
+
cis: d[11] ?? 0,
|
|
1157
|
+
i0: d[12] ?? 0,
|
|
1158
|
+
crc: d[13] ?? 0,
|
|
1159
|
+
omega: d[14] ?? 0,
|
|
1160
|
+
omegaDot: d[15] ?? 0,
|
|
1161
|
+
idot: d[16] ?? 0,
|
|
1162
|
+
// d[17] = codes on L2 (GPS) or data sources (GAL)
|
|
1163
|
+
week: d[18] ?? 0,
|
|
1164
|
+
// d[19] = L2P flag (GPS) or spare
|
|
1165
|
+
// d[20] = SV accuracy (URA index for GPS, SISA for GAL)
|
|
1166
|
+
svHealth: d[21] ?? 0,
|
|
1167
|
+
tgd: d[22] ?? 0
|
|
1168
|
+
// d[23] = BGD (GAL) or IODC (GPS)
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function buildStateVectorEphemeris(sys, prn, date, epochVals, data) {
|
|
1172
|
+
const d = data.flat();
|
|
1173
|
+
return {
|
|
1174
|
+
system: sys,
|
|
1175
|
+
prn,
|
|
1176
|
+
tocDate: date,
|
|
1177
|
+
tauN: epochVals[0] ?? 0,
|
|
1178
|
+
gammaN: epochVals[1] ?? 0,
|
|
1179
|
+
messageFrameTime: epochVals[2] ?? 0,
|
|
1180
|
+
x: d[0] ?? 0,
|
|
1181
|
+
xDot: d[1] ?? 0,
|
|
1182
|
+
xAcc: d[2] ?? 0,
|
|
1183
|
+
health: d[3] ?? 0,
|
|
1184
|
+
y: d[4] ?? 0,
|
|
1185
|
+
yDot: d[5] ?? 0,
|
|
1186
|
+
yAcc: d[6] ?? 0,
|
|
1187
|
+
freqNum: d[7] ?? 0,
|
|
1188
|
+
z: d[8] ?? 0,
|
|
1189
|
+
zDot: d[9] ?? 0,
|
|
1190
|
+
zAcc: d[10] ?? 0
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function parseNavFile(text) {
|
|
1194
|
+
const lines = text.split("\n");
|
|
1195
|
+
const header = {
|
|
1196
|
+
version: 0,
|
|
1197
|
+
type: "",
|
|
1198
|
+
leapSeconds: null,
|
|
1199
|
+
ionoCorrections: {}
|
|
1200
|
+
};
|
|
1201
|
+
const ephemerides = [];
|
|
1202
|
+
let inHeader = true;
|
|
1203
|
+
let i = 0;
|
|
1204
|
+
while (i < lines.length) {
|
|
1205
|
+
const line = lines[i];
|
|
1206
|
+
const label = line.substring(60).trim();
|
|
1207
|
+
if (label === "END OF HEADER") {
|
|
1208
|
+
inHeader = false;
|
|
1209
|
+
i++;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
if (label === "RINEX VERSION / TYPE") {
|
|
1213
|
+
header.version = parseFloat(line.substring(0, 9));
|
|
1214
|
+
header.type = line.substring(20, 40).trim();
|
|
1215
|
+
} else if (label === "LEAP SECONDS") {
|
|
1216
|
+
header.leapSeconds = parseInt(line.substring(0, 6));
|
|
1217
|
+
} else if (label === "IONOSPHERIC CORR") {
|
|
1218
|
+
const corrType = line.substring(0, 4).trim();
|
|
1219
|
+
const vals = [];
|
|
1220
|
+
for (let j = 0; j < 4; j++) {
|
|
1221
|
+
const s = line.substring(5 + j * 12, 5 + (j + 1) * 12).trim();
|
|
1222
|
+
if (s) vals.push(parseFloat19(s));
|
|
1223
|
+
}
|
|
1224
|
+
header.ionoCorrections[corrType] = vals;
|
|
1225
|
+
} else if (label === "ION ALPHA") {
|
|
1226
|
+
const vals = [];
|
|
1227
|
+
for (let j = 0; j < 4; j++) {
|
|
1228
|
+
const s = line.substring(2 + j * 12, 2 + (j + 1) * 12).trim();
|
|
1229
|
+
if (s) vals.push(parseFloat19(s));
|
|
1230
|
+
}
|
|
1231
|
+
header.ionoCorrections["GPSA"] = vals;
|
|
1232
|
+
} else if (label === "ION BETA") {
|
|
1233
|
+
const vals = [];
|
|
1234
|
+
for (let j = 0; j < 4; j++) {
|
|
1235
|
+
const s = line.substring(2 + j * 12, 2 + (j + 1) * 12).trim();
|
|
1236
|
+
if (s) vals.push(parseFloat19(s));
|
|
1237
|
+
}
|
|
1238
|
+
header.ionoCorrections["GPSB"] = vals;
|
|
1239
|
+
}
|
|
1240
|
+
i++;
|
|
1241
|
+
}
|
|
1242
|
+
if (inHeader) return { header, ephemerides };
|
|
1243
|
+
const isV2 = header.version > 0 && header.version < 3;
|
|
1244
|
+
let defaultSys = "G";
|
|
1245
|
+
if (isV2) {
|
|
1246
|
+
const ht = header.type.toUpperCase();
|
|
1247
|
+
if (ht.includes("GLONASS")) defaultSys = "R";
|
|
1248
|
+
else if (ht.includes("GEO")) defaultSys = "S";
|
|
1249
|
+
else if (ht.includes("GALILEO")) defaultSys = "E";
|
|
1250
|
+
else if (ht.includes("BEIDOU") || ht.includes("BDS")) defaultSys = "C";
|
|
1251
|
+
}
|
|
1252
|
+
const isV4 = header.version >= 4;
|
|
1253
|
+
while (i < lines.length) {
|
|
1254
|
+
const line = lines[i];
|
|
1255
|
+
if (line.trim().length === 0) {
|
|
1256
|
+
i++;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
if (isV4 && line.charAt(0) === ">") {
|
|
1260
|
+
const parts = line.substring(2).trim().split(/\s+/);
|
|
1261
|
+
const recType = parts[0] ?? "";
|
|
1262
|
+
const msgType = parts[2] ?? recType;
|
|
1263
|
+
if (recType !== "EPH" || !SUPPORTED_EPH_MSGS.has(msgType)) {
|
|
1264
|
+
const totalLines = R4_MSG_LINES[msgType] ?? 2;
|
|
1265
|
+
i += totalLines + 1;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
const numDataLines2 = (R4_MSG_LINES[msgType] ?? 8) - 1;
|
|
1269
|
+
i++;
|
|
1270
|
+
if (i >= lines.length) break;
|
|
1271
|
+
const epochLine = lines[i];
|
|
1272
|
+
const sys2 = epochLine.charAt(0);
|
|
1273
|
+
const parsed = parseNavEpochV3(epochLine);
|
|
1274
|
+
const prn2 = `${sys2}${parsed.prn.substring(1)}`;
|
|
1275
|
+
const dataLines2 = [];
|
|
1276
|
+
for (let j = 0; j < numDataLines2; j++) {
|
|
1277
|
+
i++;
|
|
1278
|
+
if (i >= lines.length) break;
|
|
1279
|
+
dataLines2.push(parseDataLine(lines[i], 4));
|
|
1280
|
+
}
|
|
1281
|
+
if (dataLines2.length === numDataLines2) {
|
|
1282
|
+
if (sys2 === "R" || sys2 === "S") {
|
|
1283
|
+
ephemerides.push(
|
|
1284
|
+
buildStateVectorEphemeris(
|
|
1285
|
+
sys2,
|
|
1286
|
+
prn2,
|
|
1287
|
+
parsed.date,
|
|
1288
|
+
parsed.values,
|
|
1289
|
+
dataLines2
|
|
1290
|
+
)
|
|
1291
|
+
);
|
|
1292
|
+
} else if (sys2 === "G" || sys2 === "E" || sys2 === "C" || sys2 === "J" || sys2 === "I") {
|
|
1293
|
+
ephemerides.push(
|
|
1294
|
+
buildKeplerEphemeris(
|
|
1295
|
+
sys2,
|
|
1296
|
+
prn2,
|
|
1297
|
+
parsed.date,
|
|
1298
|
+
parsed.values,
|
|
1299
|
+
dataLines2
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
i++;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
let sys;
|
|
1308
|
+
let prn;
|
|
1309
|
+
let date;
|
|
1310
|
+
let epochVals;
|
|
1311
|
+
if (isV2) {
|
|
1312
|
+
const parsed = parseNavEpochV2(line, defaultSys);
|
|
1313
|
+
sys = defaultSys;
|
|
1314
|
+
prn = parsed.prn;
|
|
1315
|
+
date = parsed.date;
|
|
1316
|
+
epochVals = parsed.values;
|
|
1317
|
+
} else {
|
|
1318
|
+
sys = line.charAt(0);
|
|
1319
|
+
const parsed = parseNavEpochV3(line);
|
|
1320
|
+
prn = `${sys}${parsed.prn.substring(1)}`;
|
|
1321
|
+
date = parsed.date;
|
|
1322
|
+
epochVals = parsed.values;
|
|
1323
|
+
}
|
|
1324
|
+
const numDataLines = DATA_LINES[sys];
|
|
1325
|
+
if (numDataLines == null) {
|
|
1326
|
+
i++;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const dataLines = [];
|
|
1330
|
+
for (let j = 0; j < numDataLines; j++) {
|
|
1331
|
+
i++;
|
|
1332
|
+
if (i >= lines.length) break;
|
|
1333
|
+
dataLines.push(parseDataLine(lines[i], isV2 ? 3 : 4));
|
|
1334
|
+
}
|
|
1335
|
+
if (dataLines.length === numDataLines) {
|
|
1336
|
+
if (sys === "R" || sys === "S") {
|
|
1337
|
+
ephemerides.push(
|
|
1338
|
+
buildStateVectorEphemeris(sys, prn, date, epochVals, dataLines)
|
|
1339
|
+
);
|
|
1340
|
+
} else if (sys === "G" || sys === "E" || sys === "C" || sys === "J" || sys === "I") {
|
|
1341
|
+
ephemerides.push(
|
|
1342
|
+
buildKeplerEphemeris(sys, prn, date, epochVals, dataLines)
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
i++;
|
|
1347
|
+
}
|
|
1348
|
+
return { header, ephemerides };
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/rinex/nav-writer.ts
|
|
1352
|
+
function fmtD(val) {
|
|
1353
|
+
if (val === 0) return " 0.000000000000E+00";
|
|
1354
|
+
const sign = val < 0 ? "-" : " ";
|
|
1355
|
+
const abs = Math.abs(val);
|
|
1356
|
+
const exp = Math.floor(Math.log10(abs));
|
|
1357
|
+
const mantissa = abs / 10 ** exp;
|
|
1358
|
+
const mStr = mantissa.toFixed(12);
|
|
1359
|
+
const expSign = exp >= 0 ? "+" : "-";
|
|
1360
|
+
const expStr = String(Math.abs(exp)).padStart(2, "0");
|
|
1361
|
+
return `${sign}${mStr}E${expSign}${expStr}`;
|
|
1362
|
+
}
|
|
1363
|
+
function fmtEpoch(d) {
|
|
1364
|
+
const Y = padR(String(d.getUTCFullYear()), 4);
|
|
1365
|
+
const M = padR(String(d.getUTCMonth() + 1), 3);
|
|
1366
|
+
const D = padR(String(d.getUTCDate()), 3);
|
|
1367
|
+
const h = padR(String(d.getUTCHours()), 3);
|
|
1368
|
+
const m = padR(String(d.getUTCMinutes()), 3);
|
|
1369
|
+
const s = padR(String(d.getUTCSeconds()), 3);
|
|
1370
|
+
return `${Y}${M}${D}${h}${m}${s}`;
|
|
1371
|
+
}
|
|
1372
|
+
function isKepler(eph) {
|
|
1373
|
+
return "af0" in eph;
|
|
1374
|
+
}
|
|
1375
|
+
function writeHeader(nav) {
|
|
1376
|
+
const lines = [];
|
|
1377
|
+
lines.push(
|
|
1378
|
+
hdrLine(
|
|
1379
|
+
padL(" 3.04", 9) + " " + padL("NAVIGATION DATA", 20) + padL("M", 20),
|
|
1380
|
+
"RINEX VERSION / TYPE"
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
const now = /* @__PURE__ */ new Date();
|
|
1384
|
+
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";
|
|
1385
|
+
lines.push(
|
|
1386
|
+
hdrLine(
|
|
1387
|
+
padL("GNSSCalc", 20) + padL("", 20) + padL(dateStr, 20),
|
|
1388
|
+
"PGM / RUN BY / DATE"
|
|
1389
|
+
)
|
|
1390
|
+
);
|
|
1391
|
+
const ionoOrder = ["GPSA", "GPSB", "GAL", "BDSA", "BDSB"];
|
|
1392
|
+
for (const key of ionoOrder) {
|
|
1393
|
+
const vals = nav.header.ionoCorrections[key];
|
|
1394
|
+
if (!vals) continue;
|
|
1395
|
+
const valStr = vals.map((v) => fmtD(v)).join("");
|
|
1396
|
+
lines.push(hdrLine(padL(key, 5) + valStr, "IONOSPHERIC CORR"));
|
|
1397
|
+
}
|
|
1398
|
+
for (const key of Object.keys(nav.header.ionoCorrections)) {
|
|
1399
|
+
if (ionoOrder.includes(key)) continue;
|
|
1400
|
+
const vals = nav.header.ionoCorrections[key];
|
|
1401
|
+
if (!vals) continue;
|
|
1402
|
+
const valStr = vals.map((v) => fmtD(v)).join("");
|
|
1403
|
+
lines.push(hdrLine(padL(key, 5) + valStr, "IONOSPHERIC CORR"));
|
|
1404
|
+
}
|
|
1405
|
+
if (nav.header.leapSeconds != null) {
|
|
1406
|
+
lines.push(
|
|
1407
|
+
hdrLine(padR(String(nav.header.leapSeconds), 6), "LEAP SECONDS")
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
lines.push(hdrLine("", "END OF HEADER"));
|
|
1411
|
+
return lines.join("\n") + "\n";
|
|
1412
|
+
}
|
|
1413
|
+
function orbitLine(values) {
|
|
1414
|
+
return " " + values.map(fmtD).join("");
|
|
1415
|
+
}
|
|
1416
|
+
function writeKeplerRecord(eph) {
|
|
1417
|
+
const lines = [];
|
|
1418
|
+
lines.push(
|
|
1419
|
+
`${padL(eph.prn, 3)} ${fmtEpoch(eph.tocDate)}${fmtD(eph.af0)}${fmtD(eph.af1)}${fmtD(eph.af2)}`
|
|
1420
|
+
);
|
|
1421
|
+
lines.push(orbitLine([eph.iode, eph.crs, eph.deltaN, eph.m0]));
|
|
1422
|
+
lines.push(orbitLine([eph.cuc, eph.e, eph.cus, eph.sqrtA]));
|
|
1423
|
+
lines.push(orbitLine([eph.toe, eph.cic, eph.omega0, eph.cis]));
|
|
1424
|
+
lines.push(orbitLine([eph.i0, eph.crc, eph.omega, eph.omegaDot]));
|
|
1425
|
+
lines.push(orbitLine([eph.idot, 0, eph.week, 0]));
|
|
1426
|
+
lines.push(orbitLine([0, eph.svHealth, eph.tgd, 0]));
|
|
1427
|
+
lines.push(orbitLine([eph.toe, 0, 0, 0]));
|
|
1428
|
+
return lines.join("\n");
|
|
1429
|
+
}
|
|
1430
|
+
function writeGlonassRecord(eph) {
|
|
1431
|
+
const lines = [];
|
|
1432
|
+
lines.push(
|
|
1433
|
+
`${padL(eph.prn, 3)} ${fmtEpoch(eph.tocDate)}${fmtD(-eph.tauN)}${fmtD(eph.gammaN)}${fmtD(eph.messageFrameTime)}`
|
|
1434
|
+
);
|
|
1435
|
+
lines.push(orbitLine([eph.x, eph.xDot, eph.xAcc, eph.health]));
|
|
1436
|
+
lines.push(orbitLine([eph.y, eph.yDot, eph.yAcc, eph.freqNum]));
|
|
1437
|
+
lines.push(orbitLine([eph.z, eph.zDot, eph.zAcc, 0]));
|
|
1438
|
+
return lines.join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
function writeRinexNav(nav) {
|
|
1441
|
+
const header = writeHeader(nav);
|
|
1442
|
+
const sorted = [...nav.ephemerides].sort((a, b) => {
|
|
1443
|
+
const prnCmp = a.prn.localeCompare(b.prn);
|
|
1444
|
+
if (prnCmp !== 0) return prnCmp;
|
|
1445
|
+
return a.tocDate.getTime() - b.tocDate.getTime();
|
|
1446
|
+
});
|
|
1447
|
+
const records = sorted.map(
|
|
1448
|
+
(eph) => isKepler(eph) ? writeKeplerRecord(eph) : writeGlonassRecord(eph)
|
|
1449
|
+
);
|
|
1450
|
+
return header + records.join("\n") + "\n";
|
|
1451
|
+
}
|
|
1452
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1453
|
+
0 && (module.exports = {
|
|
1454
|
+
EMPTY_WARNINGS,
|
|
1455
|
+
SYSTEM_ORDER,
|
|
1456
|
+
WarningAccumulator,
|
|
1457
|
+
crxDecompress,
|
|
1458
|
+
crxRepair,
|
|
1459
|
+
fmtF,
|
|
1460
|
+
hdrLine,
|
|
1461
|
+
padL,
|
|
1462
|
+
padR,
|
|
1463
|
+
parseCrxDataLine,
|
|
1464
|
+
parseNavFile,
|
|
1465
|
+
parseRinexStream,
|
|
1466
|
+
systemCmp,
|
|
1467
|
+
systemName,
|
|
1468
|
+
writeRinexNav
|
|
1469
|
+
});
|