gnss-js 1.10.0 → 1.12.0

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/dist/index.cjs CHANGED
@@ -118,6 +118,7 @@ __export(src_exports, {
118
118
  computeStats: () => computeStats2,
119
119
  computeVisibility: () => computeVisibility,
120
120
  connectToMountpoint: () => connectToMountpoint,
121
+ createGzipLineSink: () => createGzipLineSink,
121
122
  createStationMeta: () => createStationMeta,
122
123
  createStreamStats: () => createStreamStats,
123
124
  crxDecompress: () => crxDecompress,
@@ -128,6 +129,7 @@ __export(src_exports, {
128
129
  deg2dms: () => deg2dms,
129
130
  deg2rad: () => deg2rad,
130
131
  detrendIonoArcs: () => detrendIonoArcs,
132
+ dopplerHz: () => dopplerHz,
131
133
  ecefToAzEl: () => ecefToAzEl,
132
134
  ecefToGeodetic: () => ecefToGeodetic,
133
135
  ephInfoToEphemeris: () => ephInfoToEphemeris,
@@ -200,6 +202,7 @@ __export(src_exports, {
200
202
  horizonDistance: () => horizonDistance,
201
203
  ionoFree: () => ionoFree,
202
204
  keplerPosition: () => keplerPosition,
205
+ klobucharDelay: () => klobucharDelay,
203
206
  maskRadForAzimuth: () => maskRadForAzimuth,
204
207
  msmEpochToDate: () => msmEpochToDate,
205
208
  navTimesFromEph: () => navTimesFromEph,
@@ -214,11 +217,13 @@ __export(src_exports, {
214
217
  parseRinexStream: () => parseRinexStream,
215
218
  parseSinexBiasDcb: () => parseSinexBiasDcb,
216
219
  parseSourcetable: () => parseSourcetable,
220
+ parseSp3: () => parseSp3,
217
221
  phiAltBOC: () => phiAltBOC,
218
222
  phiBOCc: () => phiBOCc,
219
223
  phiBOCs: () => phiBOCs,
220
224
  phiBPSK: () => phiBPSK,
221
225
  rad2deg: () => rad2deg,
226
+ rangeRate: () => rangeRate,
222
227
  readString: () => readString,
223
228
  reportDecodeError: () => reportDecodeError,
224
229
  resetGloFreqCache: () => resetGloFreqCache,
@@ -229,6 +234,8 @@ __export(src_exports, {
229
234
  setGloFreqNumber: () => setGloFreqNumber,
230
235
  setRtcm3DebugHandler: () => setRtcm3DebugHandler,
231
236
  solveSpp: () => solveSpp,
237
+ sp3Position: () => sp3Position,
238
+ stationHeaderLines: () => stationHeaderLines,
232
239
  systemCmp: () => systemCmp,
233
240
  systemName: () => systemName,
234
241
  transformFrame: () => transformFrame,
@@ -236,7 +243,11 @@ __export(src_exports, {
236
243
  updateStreamStats: () => updateStreamStats,
237
244
  verifyChecksum: () => verifyChecksum,
238
245
  vincenty: () => vincenty,
239
- writeRinexNav: () => writeRinexNav
246
+ writeModernObsBlob: () => writeModernObsBlob,
247
+ writeRinex2ObsBlob: () => writeRinex2ObsBlob,
248
+ writeRinex4ObsBlob: () => writeRinex4ObsBlob,
249
+ writeRinexNav: () => writeRinexNav,
250
+ writeRinexObsBlob: () => writeRinexObsBlob
240
251
  });
241
252
  module.exports = __toCommonJS(src_exports);
242
253
 
@@ -2662,6 +2673,496 @@ function parseIonex(text) {
2662
2673
  return { epochs, lats, lons, maps };
2663
2674
  }
2664
2675
 
2676
+ // src/rinex/sp3.ts
2677
+ var BAD_CLOCK = 999999.999999;
2678
+ function parseSp3(text) {
2679
+ const lines = text.split("\n");
2680
+ const epochs = [];
2681
+ const satellites = {};
2682
+ let version = "";
2683
+ let intervalSec = 0;
2684
+ let timeSystem = "GPS";
2685
+ let timeSystemSet = false;
2686
+ let epochIdx = -1;
2687
+ for (const line of lines) {
2688
+ if (!version && line.startsWith("#") && line.charAt(1) !== "#") {
2689
+ version = line.charAt(1);
2690
+ }
2691
+ if (line.startsWith("##")) {
2692
+ const f = line.slice(2).trim().split(/\s+/);
2693
+ intervalSec = Number(f[2]) || 0;
2694
+ } else if (line.startsWith("%c") && !timeSystemSet) {
2695
+ const sys = line.slice(9, 12).trim();
2696
+ if (sys && !/^c+$/.test(sys)) {
2697
+ timeSystem = sys;
2698
+ timeSystemSet = true;
2699
+ }
2700
+ } else if (line.startsWith("*")) {
2701
+ const f = line.slice(1).trim().split(/\s+/).map(Number);
2702
+ if (f.length >= 6 && f.every(Number.isFinite)) {
2703
+ const sec = f[5];
2704
+ epochs.push(
2705
+ Date.UTC(
2706
+ f[0],
2707
+ f[1] - 1,
2708
+ f[2],
2709
+ f[3],
2710
+ f[4],
2711
+ Math.floor(sec),
2712
+ Math.round(sec % 1 * 1e3)
2713
+ )
2714
+ );
2715
+ epochIdx++;
2716
+ }
2717
+ } else if (line.startsWith("P") && epochIdx >= 0) {
2718
+ const prn = line.slice(1, 4).replace(/\s/g, "0");
2719
+ const f = line.slice(4).trim().split(/\s+/).map(Number);
2720
+ if (f.length < 3) continue;
2721
+ const [xKm, yKm, zKm] = f;
2722
+ let arr = satellites[prn];
2723
+ if (!arr) satellites[prn] = arr = [];
2724
+ while (arr.length < epochIdx) arr.push(null);
2725
+ if (xKm === 0 && yKm === 0 && zKm === 0) {
2726
+ arr.push(null);
2727
+ continue;
2728
+ }
2729
+ const clkUs = f[3];
2730
+ arr.push({
2731
+ x: xKm * 1e3,
2732
+ y: yKm * 1e3,
2733
+ z: zKm * 1e3,
2734
+ clk: clkUs !== void 0 && Math.abs(clkUs) < BAD_CLOCK ? clkUs * 1e-6 : null
2735
+ });
2736
+ }
2737
+ }
2738
+ for (const arr of Object.values(satellites)) {
2739
+ while (arr.length < epochs.length) arr.push(null);
2740
+ }
2741
+ return { version, epochs, intervalSec, timeSystem, satellites };
2742
+ }
2743
+ function sp3Position(sp3, prn, tMs, order = 9) {
2744
+ const samples = sp3.satellites[prn];
2745
+ const { epochs } = sp3;
2746
+ const n = epochs.length;
2747
+ if (!samples || n < order) return null;
2748
+ if (tMs < epochs[0] || tMs > epochs[n - 1]) return null;
2749
+ let lo = 0;
2750
+ let hi = n - 1;
2751
+ while (lo < hi) {
2752
+ const mid = lo + hi + 1 >> 1;
2753
+ if (epochs[mid] <= tMs) lo = mid;
2754
+ else hi = mid - 1;
2755
+ }
2756
+ let start = lo - (order >> 1) + (order % 2 === 0 ? 1 : 0);
2757
+ start = Math.max(0, Math.min(n - order, start));
2758
+ for (let i = start; i < start + order; i++) {
2759
+ if (!samples[i]) return null;
2760
+ }
2761
+ const t0 = epochs[start];
2762
+ const t = (tMs - t0) / 1e3;
2763
+ let x = 0;
2764
+ let y = 0;
2765
+ let z = 0;
2766
+ for (let i = 0; i < order; i++) {
2767
+ const ti = (epochs[start + i] - t0) / 1e3;
2768
+ let w = 1;
2769
+ for (let j = 0; j < order; j++) {
2770
+ if (j === i) continue;
2771
+ const tj = (epochs[start + j] - t0) / 1e3;
2772
+ w *= (t - tj) / (ti - tj);
2773
+ }
2774
+ const s = samples[start + i];
2775
+ x += w * s.x;
2776
+ y += w * s.y;
2777
+ z += w * s.z;
2778
+ }
2779
+ let clk = null;
2780
+ const a = samples[lo];
2781
+ const b = samples[Math.min(lo + 1, n - 1)];
2782
+ if (a?.clk != null && b?.clk != null) {
2783
+ const span = epochs[Math.min(lo + 1, n - 1)] - epochs[lo];
2784
+ const f = span > 0 ? (tMs - epochs[lo]) / span : 0;
2785
+ clk = a.clk * (1 - f) + b.clk * f;
2786
+ } else if (a?.clk != null) {
2787
+ clk = a.clk;
2788
+ }
2789
+ return { x, y, z, clk };
2790
+ }
2791
+
2792
+ // src/rinex/obs-writer-common.ts
2793
+ function createGzipLineSink() {
2794
+ const encoder = new TextEncoder();
2795
+ const compressor = new CompressionStream("gzip");
2796
+ const writer = compressor.writable.getWriter();
2797
+ const compressedChunks = [];
2798
+ const readerDone = (async () => {
2799
+ const reader = compressor.readable.getReader();
2800
+ for (; ; ) {
2801
+ const { done, value } = await reader.read();
2802
+ if (done) break;
2803
+ compressedChunks.push(value);
2804
+ }
2805
+ })();
2806
+ let batch = [];
2807
+ async function flush() {
2808
+ if (batch.length === 0) return;
2809
+ await writer.write(encoder.encode(batch.join("\n") + "\n"));
2810
+ batch = [];
2811
+ }
2812
+ return {
2813
+ push(line) {
2814
+ batch.push(line);
2815
+ },
2816
+ flush,
2817
+ async finish() {
2818
+ await flush();
2819
+ await writer.close();
2820
+ await readerDone;
2821
+ return new Blob(compressedChunks, {
2822
+ type: "application/gzip"
2823
+ });
2824
+ }
2825
+ };
2826
+ }
2827
+ function stationHeaderLines(header) {
2828
+ const pos = header.approxPosition ?? [0, 0, 0];
2829
+ const delta = header.antDelta ?? [0, 0, 0];
2830
+ return [
2831
+ hdrLine(header.markerName || "UNKNOWN", "MARKER NAME"),
2832
+ hdrLine("", "MARKER NUMBER"),
2833
+ hdrLine(
2834
+ `${padL(header.observer || "", 20)}${padL(header.agency || "", 40)}`,
2835
+ "OBSERVER / AGENCY"
2836
+ ),
2837
+ hdrLine(
2838
+ `${padL(header.receiverNumber || "", 20)}${padL(header.receiverType || "", 20)}${padL(header.receiverVersion || "", 20)}`,
2839
+ "REC # / TYPE / VERS"
2840
+ ),
2841
+ hdrLine(
2842
+ `${padL(header.antNumber || "", 20)}${padL(header.antType || "", 20)}`,
2843
+ "ANT # / TYPE"
2844
+ ),
2845
+ hdrLine(
2846
+ `${fmtF(pos[0], 14, 4)}${fmtF(pos[1], 14, 4)}${fmtF(pos[2], 14, 4)}`,
2847
+ "APPROX POSITION XYZ"
2848
+ ),
2849
+ hdrLine(
2850
+ `${fmtF(delta[0], 14, 4)}${fmtF(delta[1], 14, 4)}${fmtF(delta[2], 14, 4)}`,
2851
+ "ANTENNA: DELTA H/E/N"
2852
+ )
2853
+ ];
2854
+ }
2855
+
2856
+ // src/rinex/obs-writer.ts
2857
+ function writeRinexObsBlob(header, epochs, obsTypes) {
2858
+ return writeModernObsBlob(header, epochs, obsTypes, {
2859
+ version: "3.04",
2860
+ comment: "Merged from multiple RINEX files"
2861
+ });
2862
+ }
2863
+ async function writeModernObsBlob(header, epochs, obsTypes, opts) {
2864
+ if (epochs.length === 0) return new Blob([], { type: "application/gzip" });
2865
+ const sink = createGzipLineSink();
2866
+ const BATCH = 200;
2867
+ const systems = [...obsTypes.keys()];
2868
+ const sysChar = systems.length === 1 ? systems[0] : "M";
2869
+ sink.push(
2870
+ hdrLine(
2871
+ ` ${opts.version} OBSERVATION DATA ${sysChar}`,
2872
+ "RINEX VERSION / TYPE"
2873
+ )
2874
+ );
2875
+ const now = /* @__PURE__ */ new Date();
2876
+ const dateStr = `${now.getUTCFullYear()}${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`;
2877
+ sink.push(
2878
+ hdrLine(
2879
+ `${padL("GNSSCalc", 20)}${padL("", 20)}${dateStr}`,
2880
+ "PGM / RUN BY / DATE"
2881
+ )
2882
+ );
2883
+ sink.push(hdrLine(opts.comment, "COMMENT"));
2884
+ for (const line of stationHeaderLines(header)) sink.push(line);
2885
+ for (const [sys, types] of obsTypes) {
2886
+ for (let i = 0; i < types.length; i += 13) {
2887
+ const chunk = types.slice(i, i + 13);
2888
+ let content;
2889
+ if (i === 0) {
2890
+ content = `${sys} ${padR(String(types.length), 3)}`;
2891
+ } else {
2892
+ content = " ";
2893
+ }
2894
+ content += chunk.map((t) => ` ${padL(t, 3)}`).join("");
2895
+ sink.push(hdrLine(content, "SYS / # / OBS TYPES"));
2896
+ }
2897
+ }
2898
+ const timeSys = systems.includes("R") && systems.length === 1 ? "GLO" : "GPS";
2899
+ const obsTimeLine = (d) => ` ${d.getUTCFullYear()} ${String(d.getUTCMonth() + 1).padStart(2)} ${String(d.getUTCDate()).padStart(2)} ${String(d.getUTCHours()).padStart(2)} ${String(d.getUTCMinutes()).padStart(2)} ${(d.getUTCSeconds() + d.getUTCMilliseconds() / 1e3).toFixed(7).padStart(10)} ${timeSys}`;
2900
+ sink.push(
2901
+ hdrLine(obsTimeLine(new Date(epochs[0].time)), "TIME OF FIRST OBS")
2902
+ );
2903
+ sink.push(
2904
+ hdrLine(
2905
+ obsTimeLine(new Date(epochs[epochs.length - 1].time)),
2906
+ "TIME OF LAST OBS"
2907
+ )
2908
+ );
2909
+ if (epochs.length >= 2) {
2910
+ const interval = (epochs[1].time - epochs[0].time) / 1e3;
2911
+ if (interval > 0 && interval < 3600) {
2912
+ sink.push(hdrLine(fmtF(interval, 10, 3), "INTERVAL"));
2913
+ }
2914
+ }
2915
+ if (header.glonassSlots && Object.keys(header.glonassSlots).length > 0) {
2916
+ const entries = Object.entries(header.glonassSlots).sort(
2917
+ ([a], [b]) => Number(a) - Number(b)
2918
+ );
2919
+ for (let i = 0; i < entries.length; i += 8) {
2920
+ const chunk = entries.slice(i, i + 8);
2921
+ let content = i === 0 ? padR(String(entries.length), 3) + " " : " ";
2922
+ for (const [slot, freq] of chunk) {
2923
+ content += `R${padR(slot, 2)} ${padR(String(freq), 2)} `;
2924
+ }
2925
+ sink.push(hdrLine(content, "GLONASS SLOT / FRQ #"));
2926
+ }
2927
+ }
2928
+ sink.push(hdrLine("", "END OF HEADER"));
2929
+ await sink.flush();
2930
+ let epochCount = 0;
2931
+ for (const epoch of epochs) {
2932
+ const t = new Date(epoch.time);
2933
+ const sec = t.getUTCSeconds() + t.getUTCMilliseconds() / 1e3;
2934
+ const prns = [...epoch.sats.keys()].sort();
2935
+ sink.push(
2936
+ `> ${t.getUTCFullYear()} ${String(t.getUTCMonth() + 1).padStart(2, "0")} ${String(t.getUTCDate()).padStart(2, "0")} ${String(t.getUTCHours()).padStart(2, "0")} ${String(t.getUTCMinutes()).padStart(2, "0")}${fmtF(sec, 11, 7)} 0${padR(String(prns.length), 3)}`
2937
+ );
2938
+ for (const prn of prns) {
2939
+ const sys = prn[0];
2940
+ const sysTypes = obsTypes.get(sys);
2941
+ if (!sysTypes) continue;
2942
+ const valArr = epoch.sats.get(prn);
2943
+ let line = prn;
2944
+ for (let i = 0; i < sysTypes.length; i++) {
2945
+ const val = i < valArr.length ? valArr[i] : NaN;
2946
+ if (!isNaN(val)) {
2947
+ line += fmtF(val, 14, 3) + " ";
2948
+ } else {
2949
+ line += " ".repeat(16);
2950
+ }
2951
+ }
2952
+ sink.push(line);
2953
+ }
2954
+ epochCount++;
2955
+ if (epochCount % BATCH === 0) await sink.flush();
2956
+ }
2957
+ return sink.finish();
2958
+ }
2959
+
2960
+ // src/rinex/obs-writer-v2.ts
2961
+ var V3_TO_V2 = {
2962
+ // GPS
2963
+ C1C: "C1",
2964
+ C1S: "C1",
2965
+ C1L: "C1",
2966
+ C1X: "C1",
2967
+ C1W: "P1",
2968
+ C1P: "P1",
2969
+ C2C: "C2",
2970
+ C2S: "P2",
2971
+ C2L: "P2",
2972
+ C2X: "P2",
2973
+ C2W: "P2",
2974
+ C2P: "P2",
2975
+ C5I: "C5",
2976
+ C5Q: "C5",
2977
+ C5X: "C5",
2978
+ L1C: "L1",
2979
+ L1S: "L1",
2980
+ L1L: "L1",
2981
+ L1X: "L1",
2982
+ L1W: "L1",
2983
+ L1P: "L1",
2984
+ L2C: "L2",
2985
+ L2S: "L2",
2986
+ L2L: "L2",
2987
+ L2X: "L2",
2988
+ L2W: "L2",
2989
+ L2P: "L2",
2990
+ L5I: "L5",
2991
+ L5Q: "L5",
2992
+ L5X: "L5",
2993
+ D1C: "D1",
2994
+ D1S: "D1",
2995
+ D1L: "D1",
2996
+ D1X: "D1",
2997
+ D1W: "D1",
2998
+ D1P: "D1",
2999
+ D2C: "D2",
3000
+ D2S: "D2",
3001
+ D2L: "D2",
3002
+ D2X: "D2",
3003
+ D2W: "D2",
3004
+ D2P: "D2",
3005
+ D5I: "D5",
3006
+ D5Q: "D5",
3007
+ D5X: "D5",
3008
+ S1C: "S1",
3009
+ S1S: "S1",
3010
+ S1L: "S1",
3011
+ S1X: "S1",
3012
+ S1W: "S1",
3013
+ S1P: "S1",
3014
+ S2C: "S2",
3015
+ S2S: "S2",
3016
+ S2L: "S2",
3017
+ S2X: "S2",
3018
+ S2W: "S2",
3019
+ S2P: "S2",
3020
+ S5I: "S5",
3021
+ S5Q: "S5",
3022
+ S5X: "S5"
3023
+ };
3024
+ function v3ToV2Code(code3) {
3025
+ return V3_TO_V2[code3] ?? null;
3026
+ }
3027
+ async function writeRinex2ObsBlob(header, epochs, obsTypes) {
3028
+ if (epochs.length === 0) return new Blob([], { type: "application/gzip" });
3029
+ const gpsCodes = obsTypes.get("G") ?? obsTypes.get("R") ?? [];
3030
+ const v2Codes = [];
3031
+ const v2CodeSet = /* @__PURE__ */ new Set();
3032
+ const v3ToV2Map = /* @__PURE__ */ new Map();
3033
+ for (const code3 of gpsCodes) {
3034
+ const code2 = v3ToV2Code(code3);
3035
+ if (!code2) continue;
3036
+ if (!v2CodeSet.has(code2)) {
3037
+ v2CodeSet.add(code2);
3038
+ v3ToV2Map.set(code3, v2Codes.length);
3039
+ v2Codes.push(code2);
3040
+ } else {
3041
+ v3ToV2Map.set(code3, v2Codes.indexOf(code2));
3042
+ }
3043
+ }
3044
+ const gloCodes = obsTypes.get("R") ?? [];
3045
+ for (const code3 of gloCodes) {
3046
+ const code2 = v3ToV2Code(code3);
3047
+ if (!code2) continue;
3048
+ if (!v2CodeSet.has(code2)) {
3049
+ v2CodeSet.add(code2);
3050
+ v3ToV2Map.set(code3, v2Codes.length);
3051
+ v2Codes.push(code2);
3052
+ } else if (!v3ToV2Map.has(code3)) {
3053
+ v3ToV2Map.set(code3, v2Codes.indexOf(code2));
3054
+ }
3055
+ }
3056
+ const sink = createGzipLineSink();
3057
+ const BATCH = 200;
3058
+ const hasSystems = /* @__PURE__ */ new Set();
3059
+ for (const epoch of epochs) {
3060
+ for (const prn of epoch.sats.keys()) {
3061
+ const sys = prn[0];
3062
+ if (sys === "G" || sys === "R") hasSystems.add(sys);
3063
+ }
3064
+ }
3065
+ const sysChar = hasSystems.has("G") && hasSystems.has("R") ? "M" : hasSystems.has("R") ? "R" : "G";
3066
+ sink.push(
3067
+ hdrLine(
3068
+ ` 2.11 OBSERVATION DATA ${sysChar}`,
3069
+ "RINEX VERSION / TYPE"
3070
+ )
3071
+ );
3072
+ const now = /* @__PURE__ */ new Date();
3073
+ const dateStr = `${now.getUTCFullYear()}-${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")}`;
3074
+ sink.push(
3075
+ hdrLine(
3076
+ `${padL("GNSSCalc", 20)}${padL("", 20)}${dateStr}`,
3077
+ "PGM / RUN BY / DATE"
3078
+ )
3079
+ );
3080
+ sink.push(hdrLine("Converted from RINEX 3 by GNSSCalc", "COMMENT"));
3081
+ for (const line of stationHeaderLines(header)) sink.push(line);
3082
+ sink.push(hdrLine(" 1 1", "WAVELENGTH FACT L1/2"));
3083
+ for (let i = 0; i < v2Codes.length; i += 9) {
3084
+ const chunk = v2Codes.slice(i, i + 9);
3085
+ let content;
3086
+ if (i === 0) {
3087
+ content = padR(String(v2Codes.length), 6);
3088
+ } else {
3089
+ content = " ";
3090
+ }
3091
+ content += chunk.map((c) => padR(c, 6)).join("");
3092
+ sink.push(hdrLine(content, "# / TYPES OF OBSERV"));
3093
+ }
3094
+ const first = new Date(epochs[0].time);
3095
+ const timeSys = sysChar === "R" ? "GLO" : "GPS";
3096
+ sink.push(
3097
+ hdrLine(
3098
+ ` ${first.getUTCFullYear()} ${String(first.getUTCMonth() + 1).padStart(2)} ${String(first.getUTCDate()).padStart(2)} ${String(first.getUTCHours()).padStart(2)} ${String(first.getUTCMinutes()).padStart(2)} ${(first.getUTCSeconds() + first.getUTCMilliseconds() / 1e3).toFixed(7).padStart(10)} ${timeSys}`,
3099
+ "TIME OF FIRST OBS"
3100
+ )
3101
+ );
3102
+ sink.push(hdrLine("", "END OF HEADER"));
3103
+ await sink.flush();
3104
+ let epochCount = 0;
3105
+ for (const epoch of epochs) {
3106
+ const t = new Date(epoch.time);
3107
+ const sec = t.getUTCSeconds() + t.getUTCMilliseconds() / 1e3;
3108
+ const prns = [...epoch.sats.keys()].filter((p) => p[0] === "G" || p[0] === "R").sort();
3109
+ if (prns.length === 0) continue;
3110
+ const yy = t.getUTCFullYear() % 100;
3111
+ const yyS = padR(String(yy), 2);
3112
+ const moS = padR(String(t.getUTCMonth() + 1), 2);
3113
+ const ddS = padR(String(t.getUTCDate()), 2);
3114
+ const hhS = padR(String(t.getUTCHours()), 2);
3115
+ const mmS = padR(String(t.getUTCMinutes()), 2);
3116
+ const secS = fmtF(sec, 11, 7);
3117
+ const nSats = padR(String(prns.length), 3);
3118
+ let epochLine = ` ${yyS} ${moS} ${ddS} ${hhS} ${mmS}${secS} 0${nSats}`;
3119
+ for (let i = 0; i < prns.length; i++) {
3120
+ if (i > 0 && i % 12 === 0) {
3121
+ sink.push(epochLine);
3122
+ epochLine = " ".repeat(32);
3123
+ }
3124
+ epochLine += prns[i];
3125
+ }
3126
+ sink.push(epochLine);
3127
+ for (const prn of prns) {
3128
+ const sys = prn[0];
3129
+ const sysCodes3 = obsTypes.get(sys) ?? [];
3130
+ const valArr = epoch.sats.get(prn);
3131
+ const v2Vals = new Array(v2Codes.length).fill(NaN);
3132
+ for (let j = 0; j < sysCodes3.length; j++) {
3133
+ const v2Idx = v3ToV2Map.get(sysCodes3[j]);
3134
+ if (v2Idx != null && j < valArr.length && !isNaN(valArr[j])) {
3135
+ v2Vals[v2Idx] = valArr[j];
3136
+ }
3137
+ }
3138
+ let line = "";
3139
+ for (let j = 0; j < v2Codes.length; j++) {
3140
+ const val = v2Vals[j];
3141
+ if (!isNaN(val)) {
3142
+ line += fmtF(val, 14, 3) + " ";
3143
+ } else {
3144
+ line += " ".repeat(16);
3145
+ }
3146
+ if ((j + 1) % 5 === 0 || j === v2Codes.length - 1) {
3147
+ sink.push(line);
3148
+ line = "";
3149
+ }
3150
+ }
3151
+ }
3152
+ epochCount++;
3153
+ if (epochCount % BATCH === 0) await sink.flush();
3154
+ }
3155
+ return sink.finish();
3156
+ }
3157
+
3158
+ // src/rinex/obs-writer-v4.ts
3159
+ function writeRinex4ObsBlob(header, epochs, obsTypes) {
3160
+ return writeModernObsBlob(header, epochs, obsTypes, {
3161
+ version: "4.01",
3162
+ comment: "Converted to RINEX 4 by GNSSCalc"
3163
+ });
3164
+ }
3165
+
2665
3166
  // src/rtcm3/decoder.ts
2666
3167
  var debugHandler = null;
2667
3168
  function setRtcm3DebugHandler(fn) {
@@ -4104,6 +4605,7 @@ function gmForSystem(sys) {
4104
4605
  if (sys === "C") return GM_BDS;
4105
4606
  return GM_GPS;
4106
4607
  }
4608
+ var geoVelocityPass = false;
4107
4609
  function keplerPosition(eph, t) {
4108
4610
  const GM = gmForSystem(eph.system);
4109
4611
  const a = eph.sqrtA * eph.sqrtA;
@@ -4131,8 +4633,16 @@ function keplerPosition(eph, t) {
4131
4633
  const uk = phik + duk;
4132
4634
  const rk = a * (1 - eph.e * cosE) + drk;
4133
4635
  const ik = eph.i0 + dik + eph.idot * tk;
4636
+ const EkDot = n / (1 - eph.e * cosE);
4637
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
4638
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
4639
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
4640
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
4641
+ const ukDot = vkDot + dukDot;
4134
4642
  const xp = rk * Math.cos(uk);
4135
4643
  const yp = rk * Math.sin(uk);
4644
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
4645
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
4136
4646
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
4137
4647
  if (isBdsGeo) {
4138
4648
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -4148,23 +4658,43 @@ function keplerPosition(eph, t) {
4148
4658
  const sinPhi = Math.sin(phi);
4149
4659
  const COS5 = Math.cos(-5 * Math.PI / 180);
4150
4660
  const SIN5 = Math.sin(-5 * Math.PI / 180);
4661
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
4662
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
4663
+ const pz = -yg * SIN5 + zg * COS5;
4664
+ if (geoVelocityPass) {
4665
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
4666
+ }
4667
+ geoVelocityPass = true;
4668
+ const h = 0.5;
4669
+ const pm = keplerPosition(eph, t - h);
4670
+ const pp = keplerPosition(eph, t + h);
4671
+ geoVelocityPass = false;
4151
4672
  return {
4152
4673
  prn: eph.prn,
4153
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
4154
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
4155
- z: -yg * SIN5 + zg * COS5
4674
+ x: px,
4675
+ y: py,
4676
+ z: pz,
4677
+ vx: (pp.x - pm.x) / (2 * h),
4678
+ vy: (pp.y - pm.y) / (2 * h),
4679
+ vz: (pp.z - pm.z) / (2 * h)
4156
4680
  };
4157
4681
  }
4158
4682
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
4683
+ const omegakDot = eph.omegaDot - OMEGA_E;
4159
4684
  const cosO = Math.cos(omegak);
4160
4685
  const sinO = Math.sin(omegak);
4161
4686
  const cosI = Math.cos(ik);
4162
4687
  const sinI = Math.sin(ik);
4688
+ const x = xp * cosO - yp * cosI * sinO;
4689
+ const y = xp * sinO + yp * cosI * cosO;
4163
4690
  return {
4164
4691
  prn: eph.prn,
4165
- x: xp * cosO - yp * cosI * sinO,
4166
- y: xp * sinO + yp * cosI * cosO,
4167
- z: yp * sinI
4692
+ x,
4693
+ y,
4694
+ z: yp * sinI,
4695
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
4696
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
4697
+ vz: ypDot * sinI + yp * cosI * dikDot
4168
4698
  };
4169
4699
  }
4170
4700
  function gloDerivatives(state, acc) {
@@ -4218,7 +4748,25 @@ function glonassPosition(eph, tUtc) {
4218
4748
  if (Math.abs(remainder) > 1e-3) {
4219
4749
  state = rk4Step(state, acc, remainder);
4220
4750
  }
4221
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
4751
+ return {
4752
+ prn: eph.prn,
4753
+ x: state[0],
4754
+ y: state[1],
4755
+ z: state[2],
4756
+ vx: state[3],
4757
+ vy: state[4],
4758
+ vz: state[5]
4759
+ };
4760
+ }
4761
+ function rangeRate(sat, rx, rxVel = [0, 0, 0]) {
4762
+ const dx = sat.x - rx[0];
4763
+ const dy = sat.y - rx[1];
4764
+ const dz = sat.z - rx[2];
4765
+ const rho = Math.hypot(dx, dy, dz);
4766
+ return ((sat.vx - rxVel[0]) * dx + (sat.vy - rxVel[1]) * dy + (sat.vz - rxVel[2]) * dz) / rho;
4767
+ }
4768
+ function dopplerHz(rangeRateMs, freqHz) {
4769
+ return -rangeRateMs * freqHz / C_LIGHT;
4222
4770
  }
4223
4771
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
4224
4772
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -4837,6 +5385,37 @@ function dateToEpoch(date) {
4837
5385
  return year + (date.getTime() - start) / (end - start);
4838
5386
  }
4839
5387
 
5388
+ // src/positioning/klobuchar.ts
5389
+ function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
5390
+ const { alpha, beta } = coeffs;
5391
+ if (alpha.length < 4 || beta.length < 4) return 0;
5392
+ const E = elRad / Math.PI;
5393
+ const phiU = latRad / Math.PI;
5394
+ const lambdaU = lonRad / Math.PI;
5395
+ const psi = 0.0137 / (E + 0.11) - 0.022;
5396
+ let phiI = phiU + psi * Math.cos(azRad);
5397
+ if (phiI > 0.416) phiI = 0.416;
5398
+ else if (phiI < -0.416) phiI = -0.416;
5399
+ const lambdaI = lambdaU + psi * Math.sin(azRad) / Math.cos(phiI * Math.PI);
5400
+ const phiM = phiI + 0.064 * Math.cos((lambdaI - 1.617) * Math.PI);
5401
+ let t = 43200 * lambdaI + gpsTowSec;
5402
+ t %= 86400;
5403
+ if (t < 0) t += 86400;
5404
+ let amp = 0;
5405
+ let per = 0;
5406
+ for (let n = 3; n >= 0; n--) {
5407
+ amp = amp * phiM + alpha[n];
5408
+ per = per * phiM + beta[n];
5409
+ }
5410
+ if (amp < 0) amp = 0;
5411
+ if (per < 72e3) per = 72e3;
5412
+ const F = 1 + 16 * Math.pow(0.53 - E, 3);
5413
+ const x = 2 * Math.PI * (t - 50400) / per;
5414
+ if (Math.abs(x) >= 1.57) return F * 5e-9;
5415
+ const x2 = x * x;
5416
+ return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
5417
+ }
5418
+
4840
5419
  // src/positioning/index.ts
4841
5420
  var GM_GPS2 = 3986005e8;
4842
5421
  var F_REL = -4442807633e-19;
@@ -4863,6 +5442,16 @@ function ionoFree(p1, p2, f1, f2) {
4863
5442
  const g = f1 * f1 / (f1 * f1 - f2 * f2);
4864
5443
  return g * p1 - (g - 1) * p2;
4865
5444
  }
5445
+ var GPS_EPOCH_MS_SPP = Date.UTC(1980, 0, 6);
5446
+ var PRIMARY_FREQ_HZ = {
5447
+ G: 157542e4,
5448
+ E: 157542e4,
5449
+ J: 157542e4,
5450
+ S: 157542e4,
5451
+ C: 1561098e3,
5452
+ R: 1602e6
5453
+ };
5454
+ var F_L1 = 157542e4;
4866
5455
  function tropoDelay(elevationRad) {
4867
5456
  const sinEl = Math.sin(elevationRad);
4868
5457
  return 2.47 / (sinEl + 0.0121);
@@ -4897,8 +5486,10 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4897
5486
  troposphere = true,
4898
5487
  maxIterations = 15,
4899
5488
  convergenceM = 1e-4,
4900
- tgd = true
5489
+ tgd = true,
5490
+ iono
4901
5491
  } = opts;
5492
+ const gpsTow = (timeMs - GPS_EPOCH_MS_SPP) / 1e3 % 604800;
4902
5493
  const all = [...pseudoranges.keys()].filter((p) => ephemerides.has(p));
4903
5494
  const minSats = (list) => 3 + new Set(list.map((p) => p[0])).size;
4904
5495
  if (all.length < minSats(all)) return null;
@@ -4922,6 +5513,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4922
5513
  const Htv = new Array(dim).fill(0);
4923
5514
  let rows = 0;
4924
5515
  for (const k of Object.keys(residuals2)) delete residuals2[k];
5516
+ const positionSaneIter = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
5517
+ const [rxLat, rxLon] = positionSaneIter ? ecefToGeodetic(x2, y2, z2) : [0, 0];
4925
5518
  for (const prn of prns) {
4926
5519
  const psr = pseudoranges.get(prn);
4927
5520
  const eph = ephemerides.get(prn);
@@ -4940,17 +5533,24 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
4940
5533
  const uy = (y2 - sy) / rho;
4941
5534
  const uz = (z2 - sz) / rho;
4942
5535
  let elev = Math.PI / 2;
5536
+ let azim = 0;
4943
5537
  let weight = 1;
4944
- const positionSane = x2 * x2 + y2 * y2 + z2 * z2 > 1e12;
5538
+ const positionSane = positionSaneIter;
4945
5539
  if (positionSane) {
4946
5540
  const azel = ecefToAzEl(x2, y2, z2, sx, sy, sz);
4947
5541
  elev = azel.el;
5542
+ azim = azel.az;
4948
5543
  if (iter >= 2 && elev < elevationMaskDeg * Math.PI / 180) continue;
4949
5544
  const sinEl = Math.max(Math.sin(elev), 0.1);
4950
5545
  weight = sinEl * sinEl;
4951
5546
  }
4952
5547
  const tropo = troposphere && positionSane ? tropoDelay(elev) : 0;
4953
- const predicted = rho + clk - C_LIGHT * dts + tropo;
5548
+ let ionoM = 0;
5549
+ if (iono && positionSane) {
5550
+ const f = PRIMARY_FREQ_HZ[sys] ?? F_L1;
5551
+ ionoM = C_LIGHT * klobucharDelay(iono, rxLat, rxLon, azim, elev, gpsTow) * (F_L1 / f * (F_L1 / f));
5552
+ }
5553
+ const predicted = rho + clk - C_LIGHT * dts + tropo + ionoM;
4954
5554
  const v = psr - predicted;
4955
5555
  const h = new Array(dim).fill(0);
4956
5556
  h[0] = ux;
@@ -7386,6 +7986,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7386
7986
  computeStats,
7387
7987
  computeVisibility,
7388
7988
  connectToMountpoint,
7989
+ createGzipLineSink,
7389
7990
  createStationMeta,
7390
7991
  createStreamStats,
7391
7992
  crxDecompress,
@@ -7396,6 +7997,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7396
7997
  deg2dms,
7397
7998
  deg2rad,
7398
7999
  detrendIonoArcs,
8000
+ dopplerHz,
7399
8001
  ecefToAzEl,
7400
8002
  ecefToGeodetic,
7401
8003
  ephInfoToEphemeris,
@@ -7468,6 +8070,7 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7468
8070
  horizonDistance,
7469
8071
  ionoFree,
7470
8072
  keplerPosition,
8073
+ klobucharDelay,
7471
8074
  maskRadForAzimuth,
7472
8075
  msmEpochToDate,
7473
8076
  navTimesFromEph,
@@ -7482,11 +8085,13 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7482
8085
  parseRinexStream,
7483
8086
  parseSinexBiasDcb,
7484
8087
  parseSourcetable,
8088
+ parseSp3,
7485
8089
  phiAltBOC,
7486
8090
  phiBOCc,
7487
8091
  phiBOCs,
7488
8092
  phiBPSK,
7489
8093
  rad2deg,
8094
+ rangeRate,
7490
8095
  readString,
7491
8096
  reportDecodeError,
7492
8097
  resetGloFreqCache,
@@ -7497,6 +8102,8 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7497
8102
  setGloFreqNumber,
7498
8103
  setRtcm3DebugHandler,
7499
8104
  solveSpp,
8105
+ sp3Position,
8106
+ stationHeaderLines,
7500
8107
  systemCmp,
7501
8108
  systemName,
7502
8109
  transformFrame,
@@ -7504,5 +8111,9 @@ function computePsdDb(centerMHz, halfSpanChips, numPoints, psdFn, f0 = 1023e3, f
7504
8111
  updateStreamStats,
7505
8112
  verifyChecksum,
7506
8113
  vincenty,
7507
- writeRinexNav
8114
+ writeModernObsBlob,
8115
+ writeRinex2ObsBlob,
8116
+ writeRinex4ObsBlob,
8117
+ writeRinexNav,
8118
+ writeRinexObsBlob
7508
8119
  });