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.
@@ -9,6 +9,7 @@ import {
9
9
  START_GPS_TIME
10
10
  } from "./chunk-LEEU5OIO.js";
11
11
  import {
12
+ C_LIGHT,
12
13
  OMEGA_E
13
14
  } from "./chunk-FIEWO4J4.js";
14
15
 
@@ -27,6 +28,7 @@ function gmForSystem(sys) {
27
28
  if (sys === "C") return GM_BDS;
28
29
  return GM_GPS;
29
30
  }
31
+ var geoVelocityPass = false;
30
32
  function keplerPosition(eph, t) {
31
33
  const GM = gmForSystem(eph.system);
32
34
  const a = eph.sqrtA * eph.sqrtA;
@@ -54,8 +56,16 @@ function keplerPosition(eph, t) {
54
56
  const uk = phik + duk;
55
57
  const rk = a * (1 - eph.e * cosE) + drk;
56
58
  const ik = eph.i0 + dik + eph.idot * tk;
59
+ const EkDot = n / (1 - eph.e * cosE);
60
+ const vkDot = EkDot * Math.sqrt(1 - eph.e * eph.e) / (1 - eph.e * cosE);
61
+ const dukDot = 2 * vkDot * (eph.cus * cos2phi - eph.cuc * sin2phi);
62
+ const drkDot = a * eph.e * sinE * EkDot + 2 * vkDot * (eph.crs * cos2phi - eph.crc * sin2phi);
63
+ const dikDot = eph.idot + 2 * vkDot * (eph.cis * cos2phi - eph.cic * sin2phi);
64
+ const ukDot = vkDot + dukDot;
57
65
  const xp = rk * Math.cos(uk);
58
66
  const yp = rk * Math.sin(uk);
67
+ const xpDot = drkDot * Math.cos(uk) - rk * ukDot * Math.sin(uk);
68
+ const ypDot = drkDot * Math.sin(uk) + rk * ukDot * Math.cos(uk);
59
69
  const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < BDS_GEO_MAX_INCLINATION_RAD && a > BDS_GEO_MIN_SEMIMAJOR_AXIS_M;
60
70
  if (isBdsGeo) {
61
71
  const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
@@ -71,23 +81,43 @@ function keplerPosition(eph, t) {
71
81
  const sinPhi = Math.sin(phi);
72
82
  const COS5 = Math.cos(-5 * Math.PI / 180);
73
83
  const SIN5 = Math.sin(-5 * Math.PI / 180);
84
+ const px = xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5;
85
+ const py = -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5;
86
+ const pz = -yg * SIN5 + zg * COS5;
87
+ if (geoVelocityPass) {
88
+ return { prn: eph.prn, x: px, y: py, z: pz, vx: 0, vy: 0, vz: 0 };
89
+ }
90
+ geoVelocityPass = true;
91
+ const h = 0.5;
92
+ const pm = keplerPosition(eph, t - h);
93
+ const pp = keplerPosition(eph, t + h);
94
+ geoVelocityPass = false;
74
95
  return {
75
96
  prn: eph.prn,
76
- x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
77
- y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
78
- z: -yg * SIN5 + zg * COS5
97
+ x: px,
98
+ y: py,
99
+ z: pz,
100
+ vx: (pp.x - pm.x) / (2 * h),
101
+ vy: (pp.y - pm.y) / (2 * h),
102
+ vz: (pp.z - pm.z) / (2 * h)
79
103
  };
80
104
  }
81
105
  const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
106
+ const omegakDot = eph.omegaDot - OMEGA_E;
82
107
  const cosO = Math.cos(omegak);
83
108
  const sinO = Math.sin(omegak);
84
109
  const cosI = Math.cos(ik);
85
110
  const sinI = Math.sin(ik);
111
+ const x = xp * cosO - yp * cosI * sinO;
112
+ const y = xp * sinO + yp * cosI * cosO;
86
113
  return {
87
114
  prn: eph.prn,
88
- x: xp * cosO - yp * cosI * sinO,
89
- y: xp * sinO + yp * cosI * cosO,
90
- z: yp * sinI
115
+ x,
116
+ y,
117
+ z: yp * sinI,
118
+ vx: xpDot * cosO - ypDot * cosI * sinO + yp * sinI * sinO * dikDot - omegakDot * y,
119
+ vy: xpDot * sinO + ypDot * cosI * cosO - yp * sinI * cosO * dikDot + omegakDot * x,
120
+ vz: ypDot * sinI + yp * cosI * dikDot
91
121
  };
92
122
  }
93
123
  function gloDerivatives(state, acc) {
@@ -141,7 +171,25 @@ function glonassPosition(eph, tUtc) {
141
171
  if (Math.abs(remainder) > 1e-3) {
142
172
  state = rk4Step(state, acc, remainder);
143
173
  }
144
- return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
174
+ return {
175
+ prn: eph.prn,
176
+ x: state[0],
177
+ y: state[1],
178
+ z: state[2],
179
+ vx: state[3],
180
+ vy: state[4],
181
+ vz: state[5]
182
+ };
183
+ }
184
+ function rangeRate(sat, rx, rxVel = [0, 0, 0]) {
185
+ const dx = sat.x - rx[0];
186
+ const dy = sat.y - rx[1];
187
+ const dz = sat.z - rx[2];
188
+ const rho = Math.hypot(dx, dy, dz);
189
+ return ((sat.vx - rxVel[0]) * dx + (sat.vy - rxVel[1]) * dy + (sat.vz - rxVel[2]) * dz) / rho;
190
+ }
191
+ function dopplerHz(rangeRateMs, freqHz) {
192
+ return -rangeRateMs * freqHz / C_LIGHT;
145
193
  }
146
194
  function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
147
195
  const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
@@ -546,6 +594,8 @@ function computeVisibility(ephemerides, rxPos, startMs, endMs, stepSec = 300, el
546
594
  export {
547
595
  keplerPosition,
548
596
  glonassPosition,
597
+ rangeRate,
598
+ dopplerHz,
549
599
  ecefToAzEl,
550
600
  computeDop,
551
601
  selectEphemeris,
@@ -695,6 +695,496 @@ function parseIonex(text) {
695
695
  return { epochs, lats, lons, maps };
696
696
  }
697
697
 
698
+ // src/rinex/sp3.ts
699
+ var BAD_CLOCK = 999999.999999;
700
+ function parseSp3(text) {
701
+ const lines = text.split("\n");
702
+ const epochs = [];
703
+ const satellites = {};
704
+ let version = "";
705
+ let intervalSec = 0;
706
+ let timeSystem = "GPS";
707
+ let timeSystemSet = false;
708
+ let epochIdx = -1;
709
+ for (const line of lines) {
710
+ if (!version && line.startsWith("#") && line.charAt(1) !== "#") {
711
+ version = line.charAt(1);
712
+ }
713
+ if (line.startsWith("##")) {
714
+ const f = line.slice(2).trim().split(/\s+/);
715
+ intervalSec = Number(f[2]) || 0;
716
+ } else if (line.startsWith("%c") && !timeSystemSet) {
717
+ const sys = line.slice(9, 12).trim();
718
+ if (sys && !/^c+$/.test(sys)) {
719
+ timeSystem = sys;
720
+ timeSystemSet = true;
721
+ }
722
+ } else if (line.startsWith("*")) {
723
+ const f = line.slice(1).trim().split(/\s+/).map(Number);
724
+ if (f.length >= 6 && f.every(Number.isFinite)) {
725
+ const sec = f[5];
726
+ epochs.push(
727
+ Date.UTC(
728
+ f[0],
729
+ f[1] - 1,
730
+ f[2],
731
+ f[3],
732
+ f[4],
733
+ Math.floor(sec),
734
+ Math.round(sec % 1 * 1e3)
735
+ )
736
+ );
737
+ epochIdx++;
738
+ }
739
+ } else if (line.startsWith("P") && epochIdx >= 0) {
740
+ const prn = line.slice(1, 4).replace(/\s/g, "0");
741
+ const f = line.slice(4).trim().split(/\s+/).map(Number);
742
+ if (f.length < 3) continue;
743
+ const [xKm, yKm, zKm] = f;
744
+ let arr = satellites[prn];
745
+ if (!arr) satellites[prn] = arr = [];
746
+ while (arr.length < epochIdx) arr.push(null);
747
+ if (xKm === 0 && yKm === 0 && zKm === 0) {
748
+ arr.push(null);
749
+ continue;
750
+ }
751
+ const clkUs = f[3];
752
+ arr.push({
753
+ x: xKm * 1e3,
754
+ y: yKm * 1e3,
755
+ z: zKm * 1e3,
756
+ clk: clkUs !== void 0 && Math.abs(clkUs) < BAD_CLOCK ? clkUs * 1e-6 : null
757
+ });
758
+ }
759
+ }
760
+ for (const arr of Object.values(satellites)) {
761
+ while (arr.length < epochs.length) arr.push(null);
762
+ }
763
+ return { version, epochs, intervalSec, timeSystem, satellites };
764
+ }
765
+ function sp3Position(sp3, prn, tMs, order = 9) {
766
+ const samples = sp3.satellites[prn];
767
+ const { epochs } = sp3;
768
+ const n = epochs.length;
769
+ if (!samples || n < order) return null;
770
+ if (tMs < epochs[0] || tMs > epochs[n - 1]) return null;
771
+ let lo = 0;
772
+ let hi = n - 1;
773
+ while (lo < hi) {
774
+ const mid = lo + hi + 1 >> 1;
775
+ if (epochs[mid] <= tMs) lo = mid;
776
+ else hi = mid - 1;
777
+ }
778
+ let start = lo - (order >> 1) + (order % 2 === 0 ? 1 : 0);
779
+ start = Math.max(0, Math.min(n - order, start));
780
+ for (let i = start; i < start + order; i++) {
781
+ if (!samples[i]) return null;
782
+ }
783
+ const t0 = epochs[start];
784
+ const t = (tMs - t0) / 1e3;
785
+ let x = 0;
786
+ let y = 0;
787
+ let z = 0;
788
+ for (let i = 0; i < order; i++) {
789
+ const ti = (epochs[start + i] - t0) / 1e3;
790
+ let w = 1;
791
+ for (let j = 0; j < order; j++) {
792
+ if (j === i) continue;
793
+ const tj = (epochs[start + j] - t0) / 1e3;
794
+ w *= (t - tj) / (ti - tj);
795
+ }
796
+ const s = samples[start + i];
797
+ x += w * s.x;
798
+ y += w * s.y;
799
+ z += w * s.z;
800
+ }
801
+ let clk = null;
802
+ const a = samples[lo];
803
+ const b = samples[Math.min(lo + 1, n - 1)];
804
+ if (a?.clk != null && b?.clk != null) {
805
+ const span = epochs[Math.min(lo + 1, n - 1)] - epochs[lo];
806
+ const f = span > 0 ? (tMs - epochs[lo]) / span : 0;
807
+ clk = a.clk * (1 - f) + b.clk * f;
808
+ } else if (a?.clk != null) {
809
+ clk = a.clk;
810
+ }
811
+ return { x, y, z, clk };
812
+ }
813
+
814
+ // src/rinex/obs-writer-common.ts
815
+ function createGzipLineSink() {
816
+ const encoder = new TextEncoder();
817
+ const compressor = new CompressionStream("gzip");
818
+ const writer = compressor.writable.getWriter();
819
+ const compressedChunks = [];
820
+ const readerDone = (async () => {
821
+ const reader = compressor.readable.getReader();
822
+ for (; ; ) {
823
+ const { done, value } = await reader.read();
824
+ if (done) break;
825
+ compressedChunks.push(value);
826
+ }
827
+ })();
828
+ let batch = [];
829
+ async function flush() {
830
+ if (batch.length === 0) return;
831
+ await writer.write(encoder.encode(batch.join("\n") + "\n"));
832
+ batch = [];
833
+ }
834
+ return {
835
+ push(line) {
836
+ batch.push(line);
837
+ },
838
+ flush,
839
+ async finish() {
840
+ await flush();
841
+ await writer.close();
842
+ await readerDone;
843
+ return new Blob(compressedChunks, {
844
+ type: "application/gzip"
845
+ });
846
+ }
847
+ };
848
+ }
849
+ function stationHeaderLines(header) {
850
+ const pos = header.approxPosition ?? [0, 0, 0];
851
+ const delta = header.antDelta ?? [0, 0, 0];
852
+ return [
853
+ hdrLine(header.markerName || "UNKNOWN", "MARKER NAME"),
854
+ hdrLine("", "MARKER NUMBER"),
855
+ hdrLine(
856
+ `${padL(header.observer || "", 20)}${padL(header.agency || "", 40)}`,
857
+ "OBSERVER / AGENCY"
858
+ ),
859
+ hdrLine(
860
+ `${padL(header.receiverNumber || "", 20)}${padL(header.receiverType || "", 20)}${padL(header.receiverVersion || "", 20)}`,
861
+ "REC # / TYPE / VERS"
862
+ ),
863
+ hdrLine(
864
+ `${padL(header.antNumber || "", 20)}${padL(header.antType || "", 20)}`,
865
+ "ANT # / TYPE"
866
+ ),
867
+ hdrLine(
868
+ `${fmtF(pos[0], 14, 4)}${fmtF(pos[1], 14, 4)}${fmtF(pos[2], 14, 4)}`,
869
+ "APPROX POSITION XYZ"
870
+ ),
871
+ hdrLine(
872
+ `${fmtF(delta[0], 14, 4)}${fmtF(delta[1], 14, 4)}${fmtF(delta[2], 14, 4)}`,
873
+ "ANTENNA: DELTA H/E/N"
874
+ )
875
+ ];
876
+ }
877
+
878
+ // src/rinex/obs-writer.ts
879
+ function writeRinexObsBlob(header, epochs, obsTypes) {
880
+ return writeModernObsBlob(header, epochs, obsTypes, {
881
+ version: "3.04",
882
+ comment: "Merged from multiple RINEX files"
883
+ });
884
+ }
885
+ async function writeModernObsBlob(header, epochs, obsTypes, opts) {
886
+ if (epochs.length === 0) return new Blob([], { type: "application/gzip" });
887
+ const sink = createGzipLineSink();
888
+ const BATCH = 200;
889
+ const systems = [...obsTypes.keys()];
890
+ const sysChar = systems.length === 1 ? systems[0] : "M";
891
+ sink.push(
892
+ hdrLine(
893
+ ` ${opts.version} OBSERVATION DATA ${sysChar}`,
894
+ "RINEX VERSION / TYPE"
895
+ )
896
+ );
897
+ const now = /* @__PURE__ */ new Date();
898
+ 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`;
899
+ sink.push(
900
+ hdrLine(
901
+ `${padL("GNSSCalc", 20)}${padL("", 20)}${dateStr}`,
902
+ "PGM / RUN BY / DATE"
903
+ )
904
+ );
905
+ sink.push(hdrLine(opts.comment, "COMMENT"));
906
+ for (const line of stationHeaderLines(header)) sink.push(line);
907
+ for (const [sys, types] of obsTypes) {
908
+ for (let i = 0; i < types.length; i += 13) {
909
+ const chunk = types.slice(i, i + 13);
910
+ let content;
911
+ if (i === 0) {
912
+ content = `${sys} ${padR(String(types.length), 3)}`;
913
+ } else {
914
+ content = " ";
915
+ }
916
+ content += chunk.map((t) => ` ${padL(t, 3)}`).join("");
917
+ sink.push(hdrLine(content, "SYS / # / OBS TYPES"));
918
+ }
919
+ }
920
+ const timeSys = systems.includes("R") && systems.length === 1 ? "GLO" : "GPS";
921
+ 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}`;
922
+ sink.push(
923
+ hdrLine(obsTimeLine(new Date(epochs[0].time)), "TIME OF FIRST OBS")
924
+ );
925
+ sink.push(
926
+ hdrLine(
927
+ obsTimeLine(new Date(epochs[epochs.length - 1].time)),
928
+ "TIME OF LAST OBS"
929
+ )
930
+ );
931
+ if (epochs.length >= 2) {
932
+ const interval = (epochs[1].time - epochs[0].time) / 1e3;
933
+ if (interval > 0 && interval < 3600) {
934
+ sink.push(hdrLine(fmtF(interval, 10, 3), "INTERVAL"));
935
+ }
936
+ }
937
+ if (header.glonassSlots && Object.keys(header.glonassSlots).length > 0) {
938
+ const entries = Object.entries(header.glonassSlots).sort(
939
+ ([a], [b]) => Number(a) - Number(b)
940
+ );
941
+ for (let i = 0; i < entries.length; i += 8) {
942
+ const chunk = entries.slice(i, i + 8);
943
+ let content = i === 0 ? padR(String(entries.length), 3) + " " : " ";
944
+ for (const [slot, freq] of chunk) {
945
+ content += `R${padR(slot, 2)} ${padR(String(freq), 2)} `;
946
+ }
947
+ sink.push(hdrLine(content, "GLONASS SLOT / FRQ #"));
948
+ }
949
+ }
950
+ sink.push(hdrLine("", "END OF HEADER"));
951
+ await sink.flush();
952
+ let epochCount = 0;
953
+ for (const epoch of epochs) {
954
+ const t = new Date(epoch.time);
955
+ const sec = t.getUTCSeconds() + t.getUTCMilliseconds() / 1e3;
956
+ const prns = [...epoch.sats.keys()].sort();
957
+ sink.push(
958
+ `> ${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)}`
959
+ );
960
+ for (const prn of prns) {
961
+ const sys = prn[0];
962
+ const sysTypes = obsTypes.get(sys);
963
+ if (!sysTypes) continue;
964
+ const valArr = epoch.sats.get(prn);
965
+ let line = prn;
966
+ for (let i = 0; i < sysTypes.length; i++) {
967
+ const val = i < valArr.length ? valArr[i] : NaN;
968
+ if (!isNaN(val)) {
969
+ line += fmtF(val, 14, 3) + " ";
970
+ } else {
971
+ line += " ".repeat(16);
972
+ }
973
+ }
974
+ sink.push(line);
975
+ }
976
+ epochCount++;
977
+ if (epochCount % BATCH === 0) await sink.flush();
978
+ }
979
+ return sink.finish();
980
+ }
981
+
982
+ // src/rinex/obs-writer-v2.ts
983
+ var V3_TO_V2 = {
984
+ // GPS
985
+ C1C: "C1",
986
+ C1S: "C1",
987
+ C1L: "C1",
988
+ C1X: "C1",
989
+ C1W: "P1",
990
+ C1P: "P1",
991
+ C2C: "C2",
992
+ C2S: "P2",
993
+ C2L: "P2",
994
+ C2X: "P2",
995
+ C2W: "P2",
996
+ C2P: "P2",
997
+ C5I: "C5",
998
+ C5Q: "C5",
999
+ C5X: "C5",
1000
+ L1C: "L1",
1001
+ L1S: "L1",
1002
+ L1L: "L1",
1003
+ L1X: "L1",
1004
+ L1W: "L1",
1005
+ L1P: "L1",
1006
+ L2C: "L2",
1007
+ L2S: "L2",
1008
+ L2L: "L2",
1009
+ L2X: "L2",
1010
+ L2W: "L2",
1011
+ L2P: "L2",
1012
+ L5I: "L5",
1013
+ L5Q: "L5",
1014
+ L5X: "L5",
1015
+ D1C: "D1",
1016
+ D1S: "D1",
1017
+ D1L: "D1",
1018
+ D1X: "D1",
1019
+ D1W: "D1",
1020
+ D1P: "D1",
1021
+ D2C: "D2",
1022
+ D2S: "D2",
1023
+ D2L: "D2",
1024
+ D2X: "D2",
1025
+ D2W: "D2",
1026
+ D2P: "D2",
1027
+ D5I: "D5",
1028
+ D5Q: "D5",
1029
+ D5X: "D5",
1030
+ S1C: "S1",
1031
+ S1S: "S1",
1032
+ S1L: "S1",
1033
+ S1X: "S1",
1034
+ S1W: "S1",
1035
+ S1P: "S1",
1036
+ S2C: "S2",
1037
+ S2S: "S2",
1038
+ S2L: "S2",
1039
+ S2X: "S2",
1040
+ S2W: "S2",
1041
+ S2P: "S2",
1042
+ S5I: "S5",
1043
+ S5Q: "S5",
1044
+ S5X: "S5"
1045
+ };
1046
+ function v3ToV2Code(code3) {
1047
+ return V3_TO_V2[code3] ?? null;
1048
+ }
1049
+ async function writeRinex2ObsBlob(header, epochs, obsTypes) {
1050
+ if (epochs.length === 0) return new Blob([], { type: "application/gzip" });
1051
+ const gpsCodes = obsTypes.get("G") ?? obsTypes.get("R") ?? [];
1052
+ const v2Codes = [];
1053
+ const v2CodeSet = /* @__PURE__ */ new Set();
1054
+ const v3ToV2Map = /* @__PURE__ */ new Map();
1055
+ for (const code3 of gpsCodes) {
1056
+ const code2 = v3ToV2Code(code3);
1057
+ if (!code2) continue;
1058
+ if (!v2CodeSet.has(code2)) {
1059
+ v2CodeSet.add(code2);
1060
+ v3ToV2Map.set(code3, v2Codes.length);
1061
+ v2Codes.push(code2);
1062
+ } else {
1063
+ v3ToV2Map.set(code3, v2Codes.indexOf(code2));
1064
+ }
1065
+ }
1066
+ const gloCodes = obsTypes.get("R") ?? [];
1067
+ for (const code3 of gloCodes) {
1068
+ const code2 = v3ToV2Code(code3);
1069
+ if (!code2) continue;
1070
+ if (!v2CodeSet.has(code2)) {
1071
+ v2CodeSet.add(code2);
1072
+ v3ToV2Map.set(code3, v2Codes.length);
1073
+ v2Codes.push(code2);
1074
+ } else if (!v3ToV2Map.has(code3)) {
1075
+ v3ToV2Map.set(code3, v2Codes.indexOf(code2));
1076
+ }
1077
+ }
1078
+ const sink = createGzipLineSink();
1079
+ const BATCH = 200;
1080
+ const hasSystems = /* @__PURE__ */ new Set();
1081
+ for (const epoch of epochs) {
1082
+ for (const prn of epoch.sats.keys()) {
1083
+ const sys = prn[0];
1084
+ if (sys === "G" || sys === "R") hasSystems.add(sys);
1085
+ }
1086
+ }
1087
+ const sysChar = hasSystems.has("G") && hasSystems.has("R") ? "M" : hasSystems.has("R") ? "R" : "G";
1088
+ sink.push(
1089
+ hdrLine(
1090
+ ` 2.11 OBSERVATION DATA ${sysChar}`,
1091
+ "RINEX VERSION / TYPE"
1092
+ )
1093
+ );
1094
+ const now = /* @__PURE__ */ new Date();
1095
+ 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")}`;
1096
+ sink.push(
1097
+ hdrLine(
1098
+ `${padL("GNSSCalc", 20)}${padL("", 20)}${dateStr}`,
1099
+ "PGM / RUN BY / DATE"
1100
+ )
1101
+ );
1102
+ sink.push(hdrLine("Converted from RINEX 3 by GNSSCalc", "COMMENT"));
1103
+ for (const line of stationHeaderLines(header)) sink.push(line);
1104
+ sink.push(hdrLine(" 1 1", "WAVELENGTH FACT L1/2"));
1105
+ for (let i = 0; i < v2Codes.length; i += 9) {
1106
+ const chunk = v2Codes.slice(i, i + 9);
1107
+ let content;
1108
+ if (i === 0) {
1109
+ content = padR(String(v2Codes.length), 6);
1110
+ } else {
1111
+ content = " ";
1112
+ }
1113
+ content += chunk.map((c) => padR(c, 6)).join("");
1114
+ sink.push(hdrLine(content, "# / TYPES OF OBSERV"));
1115
+ }
1116
+ const first = new Date(epochs[0].time);
1117
+ const timeSys = sysChar === "R" ? "GLO" : "GPS";
1118
+ sink.push(
1119
+ hdrLine(
1120
+ ` ${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}`,
1121
+ "TIME OF FIRST OBS"
1122
+ )
1123
+ );
1124
+ sink.push(hdrLine("", "END OF HEADER"));
1125
+ await sink.flush();
1126
+ let epochCount = 0;
1127
+ for (const epoch of epochs) {
1128
+ const t = new Date(epoch.time);
1129
+ const sec = t.getUTCSeconds() + t.getUTCMilliseconds() / 1e3;
1130
+ const prns = [...epoch.sats.keys()].filter((p) => p[0] === "G" || p[0] === "R").sort();
1131
+ if (prns.length === 0) continue;
1132
+ const yy = t.getUTCFullYear() % 100;
1133
+ const yyS = padR(String(yy), 2);
1134
+ const moS = padR(String(t.getUTCMonth() + 1), 2);
1135
+ const ddS = padR(String(t.getUTCDate()), 2);
1136
+ const hhS = padR(String(t.getUTCHours()), 2);
1137
+ const mmS = padR(String(t.getUTCMinutes()), 2);
1138
+ const secS = fmtF(sec, 11, 7);
1139
+ const nSats = padR(String(prns.length), 3);
1140
+ let epochLine = ` ${yyS} ${moS} ${ddS} ${hhS} ${mmS}${secS} 0${nSats}`;
1141
+ for (let i = 0; i < prns.length; i++) {
1142
+ if (i > 0 && i % 12 === 0) {
1143
+ sink.push(epochLine);
1144
+ epochLine = " ".repeat(32);
1145
+ }
1146
+ epochLine += prns[i];
1147
+ }
1148
+ sink.push(epochLine);
1149
+ for (const prn of prns) {
1150
+ const sys = prn[0];
1151
+ const sysCodes3 = obsTypes.get(sys) ?? [];
1152
+ const valArr = epoch.sats.get(prn);
1153
+ const v2Vals = new Array(v2Codes.length).fill(NaN);
1154
+ for (let j = 0; j < sysCodes3.length; j++) {
1155
+ const v2Idx = v3ToV2Map.get(sysCodes3[j]);
1156
+ if (v2Idx != null && j < valArr.length && !isNaN(valArr[j])) {
1157
+ v2Vals[v2Idx] = valArr[j];
1158
+ }
1159
+ }
1160
+ let line = "";
1161
+ for (let j = 0; j < v2Codes.length; j++) {
1162
+ const val = v2Vals[j];
1163
+ if (!isNaN(val)) {
1164
+ line += fmtF(val, 14, 3) + " ";
1165
+ } else {
1166
+ line += " ".repeat(16);
1167
+ }
1168
+ if ((j + 1) % 5 === 0 || j === v2Codes.length - 1) {
1169
+ sink.push(line);
1170
+ line = "";
1171
+ }
1172
+ }
1173
+ }
1174
+ epochCount++;
1175
+ if (epochCount % BATCH === 0) await sink.flush();
1176
+ }
1177
+ return sink.finish();
1178
+ }
1179
+
1180
+ // src/rinex/obs-writer-v4.ts
1181
+ function writeRinex4ObsBlob(header, epochs, obsTypes) {
1182
+ return writeModernObsBlob(header, epochs, obsTypes, {
1183
+ version: "4.01",
1184
+ comment: "Converted to RINEX 4 by GNSSCalc"
1185
+ });
1186
+ }
1187
+
698
1188
  export {
699
1189
  padL,
700
1190
  padR,
@@ -704,5 +1194,13 @@ export {
704
1194
  WarningAccumulator,
705
1195
  parseNavFile,
706
1196
  writeRinexNav,
707
- parseIonex
1197
+ parseIonex,
1198
+ parseSp3,
1199
+ sp3Position,
1200
+ createGzipLineSink,
1201
+ stationHeaderLines,
1202
+ writeRinexObsBlob,
1203
+ writeModernObsBlob,
1204
+ writeRinex2ObsBlob,
1205
+ writeRinex4ObsBlob
708
1206
  };