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/antex.d.cts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANTEX 1.4 (Antenna Exchange Format) parser.
|
|
3
|
+
* Parses receiver and satellite antenna calibration files containing
|
|
4
|
+
* Phase Center Offsets (PCO) and Phase Center Variations (PCV).
|
|
5
|
+
*/
|
|
6
|
+
interface FrequencyData {
|
|
7
|
+
/** Frequency code, e.g. "G01", "E05" */
|
|
8
|
+
frequency: string;
|
|
9
|
+
/** Phase Center Offset — North/X component (mm) */
|
|
10
|
+
pcoN: number;
|
|
11
|
+
/** Phase Center Offset — East/Y component (mm) */
|
|
12
|
+
pcoE: number;
|
|
13
|
+
/** Phase Center Offset — Up/Z component (mm) */
|
|
14
|
+
pcoU: number;
|
|
15
|
+
/** Non-azimuth-dependent PCV values (mm), length = numZen */
|
|
16
|
+
pcvNoazi: number[];
|
|
17
|
+
/**
|
|
18
|
+
* Azimuth-dependent PCV grid (mm).
|
|
19
|
+
* Rows = azimuths (0..360 step dazi), cols = zenith angles.
|
|
20
|
+
* Empty when dazi === 0 (no azimuth dependence).
|
|
21
|
+
*/
|
|
22
|
+
pcv: number[][];
|
|
23
|
+
}
|
|
24
|
+
interface AntennaEntry {
|
|
25
|
+
/** Antenna type name (20 chars, e.g. "LEIAR20 LEIM") */
|
|
26
|
+
type: string;
|
|
27
|
+
/** Serial number or satellite code (e.g. "G05", or blank for type-mean) */
|
|
28
|
+
serialNo: string;
|
|
29
|
+
/** SVN code (satellites only, optional) */
|
|
30
|
+
svnCode: string;
|
|
31
|
+
/** COSPAR ID (satellites only, optional) */
|
|
32
|
+
cosparId: string;
|
|
33
|
+
/** Whether this is a satellite antenna (vs receiver) */
|
|
34
|
+
isSatellite: boolean;
|
|
35
|
+
/** Calibration method */
|
|
36
|
+
method: string;
|
|
37
|
+
/** Agency that performed calibration */
|
|
38
|
+
agency: string;
|
|
39
|
+
/** Number of individual antennas calibrated */
|
|
40
|
+
numCalibrated: number;
|
|
41
|
+
/** Calibration date string */
|
|
42
|
+
date: string;
|
|
43
|
+
/** Azimuth increment (degrees). 0 = no azimuth dependence. */
|
|
44
|
+
dazi: number;
|
|
45
|
+
/** Zenith/nadir start angle (degrees) */
|
|
46
|
+
zen1: number;
|
|
47
|
+
/** Zenith/nadir end angle (degrees) */
|
|
48
|
+
zen2: number;
|
|
49
|
+
/** Zenith/nadir increment (degrees) */
|
|
50
|
+
dzen: number;
|
|
51
|
+
/** SINEX code (optional) */
|
|
52
|
+
sinexCode: string;
|
|
53
|
+
/** Valid-from date (optional) */
|
|
54
|
+
validFrom: string;
|
|
55
|
+
/** Valid-until date (optional) */
|
|
56
|
+
validUntil: string;
|
|
57
|
+
/** Per-frequency calibration data */
|
|
58
|
+
frequencies: FrequencyData[];
|
|
59
|
+
}
|
|
60
|
+
interface AntexFile {
|
|
61
|
+
/** Format version (e.g. 1.4) */
|
|
62
|
+
version: number;
|
|
63
|
+
/** Satellite system flag ('G','R','E','C','J','S','M') */
|
|
64
|
+
system: string;
|
|
65
|
+
/** PCV type: 'A' (absolute) or 'R' (relative) */
|
|
66
|
+
pcvType: string;
|
|
67
|
+
/** Reference antenna type (for relative values) */
|
|
68
|
+
refAntenna: string;
|
|
69
|
+
/** Header comments */
|
|
70
|
+
comments: string[];
|
|
71
|
+
/** All antenna entries in the file */
|
|
72
|
+
antennas: AntennaEntry[];
|
|
73
|
+
}
|
|
74
|
+
declare function parseAntex(text: string): AntexFile;
|
|
75
|
+
declare function frequencyLabel(code: string): string;
|
|
76
|
+
|
|
77
|
+
export { type AntennaEntry, type AntexFile, type FrequencyData, frequencyLabel, parseAntex };
|
package/dist/antex.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANTEX 1.4 (Antenna Exchange Format) parser.
|
|
3
|
+
* Parses receiver and satellite antenna calibration files containing
|
|
4
|
+
* Phase Center Offsets (PCO) and Phase Center Variations (PCV).
|
|
5
|
+
*/
|
|
6
|
+
interface FrequencyData {
|
|
7
|
+
/** Frequency code, e.g. "G01", "E05" */
|
|
8
|
+
frequency: string;
|
|
9
|
+
/** Phase Center Offset — North/X component (mm) */
|
|
10
|
+
pcoN: number;
|
|
11
|
+
/** Phase Center Offset — East/Y component (mm) */
|
|
12
|
+
pcoE: number;
|
|
13
|
+
/** Phase Center Offset — Up/Z component (mm) */
|
|
14
|
+
pcoU: number;
|
|
15
|
+
/** Non-azimuth-dependent PCV values (mm), length = numZen */
|
|
16
|
+
pcvNoazi: number[];
|
|
17
|
+
/**
|
|
18
|
+
* Azimuth-dependent PCV grid (mm).
|
|
19
|
+
* Rows = azimuths (0..360 step dazi), cols = zenith angles.
|
|
20
|
+
* Empty when dazi === 0 (no azimuth dependence).
|
|
21
|
+
*/
|
|
22
|
+
pcv: number[][];
|
|
23
|
+
}
|
|
24
|
+
interface AntennaEntry {
|
|
25
|
+
/** Antenna type name (20 chars, e.g. "LEIAR20 LEIM") */
|
|
26
|
+
type: string;
|
|
27
|
+
/** Serial number or satellite code (e.g. "G05", or blank for type-mean) */
|
|
28
|
+
serialNo: string;
|
|
29
|
+
/** SVN code (satellites only, optional) */
|
|
30
|
+
svnCode: string;
|
|
31
|
+
/** COSPAR ID (satellites only, optional) */
|
|
32
|
+
cosparId: string;
|
|
33
|
+
/** Whether this is a satellite antenna (vs receiver) */
|
|
34
|
+
isSatellite: boolean;
|
|
35
|
+
/** Calibration method */
|
|
36
|
+
method: string;
|
|
37
|
+
/** Agency that performed calibration */
|
|
38
|
+
agency: string;
|
|
39
|
+
/** Number of individual antennas calibrated */
|
|
40
|
+
numCalibrated: number;
|
|
41
|
+
/** Calibration date string */
|
|
42
|
+
date: string;
|
|
43
|
+
/** Azimuth increment (degrees). 0 = no azimuth dependence. */
|
|
44
|
+
dazi: number;
|
|
45
|
+
/** Zenith/nadir start angle (degrees) */
|
|
46
|
+
zen1: number;
|
|
47
|
+
/** Zenith/nadir end angle (degrees) */
|
|
48
|
+
zen2: number;
|
|
49
|
+
/** Zenith/nadir increment (degrees) */
|
|
50
|
+
dzen: number;
|
|
51
|
+
/** SINEX code (optional) */
|
|
52
|
+
sinexCode: string;
|
|
53
|
+
/** Valid-from date (optional) */
|
|
54
|
+
validFrom: string;
|
|
55
|
+
/** Valid-until date (optional) */
|
|
56
|
+
validUntil: string;
|
|
57
|
+
/** Per-frequency calibration data */
|
|
58
|
+
frequencies: FrequencyData[];
|
|
59
|
+
}
|
|
60
|
+
interface AntexFile {
|
|
61
|
+
/** Format version (e.g. 1.4) */
|
|
62
|
+
version: number;
|
|
63
|
+
/** Satellite system flag ('G','R','E','C','J','S','M') */
|
|
64
|
+
system: string;
|
|
65
|
+
/** PCV type: 'A' (absolute) or 'R' (relative) */
|
|
66
|
+
pcvType: string;
|
|
67
|
+
/** Reference antenna type (for relative values) */
|
|
68
|
+
refAntenna: string;
|
|
69
|
+
/** Header comments */
|
|
70
|
+
comments: string[];
|
|
71
|
+
/** All antenna entries in the file */
|
|
72
|
+
antennas: AntennaEntry[];
|
|
73
|
+
}
|
|
74
|
+
declare function parseAntex(text: string): AntexFile;
|
|
75
|
+
declare function frequencyLabel(code: string): string;
|
|
76
|
+
|
|
77
|
+
export { type AntennaEntry, type AntexFile, type FrequencyData, frequencyLabel, parseAntex };
|
package/dist/antex.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WGS84_ECCENTRICITY_SQUARED,
|
|
3
|
+
WGS84_SEMI_MAJOR_AXIS
|
|
4
|
+
} from "./chunk-6FAL6P4G.js";
|
|
5
|
+
|
|
6
|
+
// src/coordinates/ecef.ts
|
|
7
|
+
function clampUnit(x) {
|
|
8
|
+
return x < -1 ? -1 : x > 1 ? 1 : x;
|
|
9
|
+
}
|
|
10
|
+
function geodeticToEcef(lat, lon, h) {
|
|
11
|
+
const N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * Math.sin(lat) ** 2);
|
|
12
|
+
const x = (N + h) * Math.cos(lat) * Math.cos(lon);
|
|
13
|
+
const y = (N + h) * Math.cos(lat) * Math.sin(lon);
|
|
14
|
+
const z = ((1 - WGS84_ECCENTRICITY_SQUARED) * N + h) * Math.sin(lat);
|
|
15
|
+
return [x, y, z];
|
|
16
|
+
}
|
|
17
|
+
function ecefToGeodetic(x, y, z) {
|
|
18
|
+
const lon = Math.atan2(y, x);
|
|
19
|
+
const p = Math.sqrt(x * x + y * y);
|
|
20
|
+
if (p < 1e-10) {
|
|
21
|
+
const b = WGS84_SEMI_MAJOR_AXIS * Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED);
|
|
22
|
+
const lat2 = z >= 0 ? Math.PI / 2 : -Math.PI / 2;
|
|
23
|
+
const alt2 = Math.abs(z) - b;
|
|
24
|
+
return [lat2, lon, alt2];
|
|
25
|
+
}
|
|
26
|
+
let lat = Math.atan2(z, p * (1 - WGS84_ECCENTRICITY_SQUARED));
|
|
27
|
+
let N;
|
|
28
|
+
for (let i = 0; i < 10; i++) {
|
|
29
|
+
const sinLat2 = Math.sin(lat);
|
|
30
|
+
N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * sinLat2 * sinLat2);
|
|
31
|
+
const prevLat = lat;
|
|
32
|
+
lat = Math.atan2(z + WGS84_ECCENTRICITY_SQUARED * N * sinLat2, p);
|
|
33
|
+
if (Math.abs(lat - prevLat) < 1e-15) break;
|
|
34
|
+
}
|
|
35
|
+
const sinLat = Math.sin(lat);
|
|
36
|
+
N = WGS84_SEMI_MAJOR_AXIS / Math.sqrt(1 - WGS84_ECCENTRICITY_SQUARED * sinLat * sinLat);
|
|
37
|
+
const alt = p / Math.cos(lat) - N;
|
|
38
|
+
return [lat, lon, alt];
|
|
39
|
+
}
|
|
40
|
+
function getEnuDifference(x, y, z, xRef, yRef, zRef) {
|
|
41
|
+
const [latRef, lonRef] = ecefToGeodetic(xRef, yRef, zRef);
|
|
42
|
+
const deltaX = x - xRef;
|
|
43
|
+
const deltaY = y - yRef;
|
|
44
|
+
const deltaZ = z - zRef;
|
|
45
|
+
const deltaE = -Math.sin(lonRef) * deltaX + Math.cos(lonRef) * deltaY;
|
|
46
|
+
const deltaN = -Math.cos(lonRef) * Math.sin(latRef) * deltaX - Math.sin(lonRef) * Math.sin(latRef) * deltaY + Math.cos(latRef) * deltaZ;
|
|
47
|
+
const deltaU = Math.cos(lonRef) * Math.cos(latRef) * deltaX + Math.sin(lonRef) * Math.cos(latRef) * deltaY + Math.sin(latRef) * deltaZ;
|
|
48
|
+
return [deltaE, deltaN, deltaU];
|
|
49
|
+
}
|
|
50
|
+
function getAer(x, y, z, xRef, yRef, zRef) {
|
|
51
|
+
const slant = Math.sqrt((x - xRef) ** 2 + (y - yRef) ** 2 + (z - zRef) ** 2);
|
|
52
|
+
if (!slant) return [0, 0, 0];
|
|
53
|
+
const [deltaE, deltaN, deltaU] = getEnuDifference(x, y, z, xRef, yRef, zRef);
|
|
54
|
+
const elevation = Math.asin(clampUnit(deltaU / slant));
|
|
55
|
+
const azimuth = Math.atan2(deltaE, deltaN);
|
|
56
|
+
return [elevation, azimuth, slant];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
clampUnit,
|
|
61
|
+
geodeticToEcef,
|
|
62
|
+
ecefToGeodetic,
|
|
63
|
+
getEnuDifference,
|
|
64
|
+
getAer
|
|
65
|
+
};
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ecefToGeodetic
|
|
3
|
+
} from "./chunk-37QNKGTC.js";
|
|
4
|
+
|
|
5
|
+
// src/orbit/index.ts
|
|
6
|
+
var GM_GPS = 3986005e8;
|
|
7
|
+
var GM_GAL = 3986004418e5;
|
|
8
|
+
var GM_BDS = 3986004418e5;
|
|
9
|
+
var GM_GLO = 39860044e7;
|
|
10
|
+
var OMEGA_E = 72921151467e-15;
|
|
11
|
+
var AE_GLO = 6378136;
|
|
12
|
+
var J2_GLO = 108263e-8;
|
|
13
|
+
var TWO_PI = 2 * Math.PI;
|
|
14
|
+
function gmForSystem(sys) {
|
|
15
|
+
if (sys === "E") return GM_GAL;
|
|
16
|
+
if (sys === "C") return GM_BDS;
|
|
17
|
+
return GM_GPS;
|
|
18
|
+
}
|
|
19
|
+
function keplerPosition(eph, t) {
|
|
20
|
+
const GM = gmForSystem(eph.system);
|
|
21
|
+
const a = eph.sqrtA * eph.sqrtA;
|
|
22
|
+
const n0 = Math.sqrt(GM / (a * a * a));
|
|
23
|
+
const n = n0 + eph.deltaN;
|
|
24
|
+
let tk = t - eph.toe;
|
|
25
|
+
if (tk > 302400) tk -= 604800;
|
|
26
|
+
if (tk < -302400) tk += 604800;
|
|
27
|
+
const Mk = eph.m0 + n * tk;
|
|
28
|
+
let Ek = Mk;
|
|
29
|
+
for (let i = 0; i < 10; i++) {
|
|
30
|
+
const dE = (Mk - (Ek - eph.e * Math.sin(Ek))) / (1 - eph.e * Math.cos(Ek));
|
|
31
|
+
Ek += dE;
|
|
32
|
+
if (Math.abs(dE) < 1e-12) break;
|
|
33
|
+
}
|
|
34
|
+
const sinE = Math.sin(Ek);
|
|
35
|
+
const cosE = Math.cos(Ek);
|
|
36
|
+
const vk = Math.atan2(Math.sqrt(1 - eph.e * eph.e) * sinE, cosE - eph.e);
|
|
37
|
+
const phik = vk + eph.omega;
|
|
38
|
+
const sin2phi = Math.sin(2 * phik);
|
|
39
|
+
const cos2phi = Math.cos(2 * phik);
|
|
40
|
+
const duk = eph.cus * sin2phi + eph.cuc * cos2phi;
|
|
41
|
+
const drk = eph.crs * sin2phi + eph.crc * cos2phi;
|
|
42
|
+
const dik = eph.cis * sin2phi + eph.cic * cos2phi;
|
|
43
|
+
const uk = phik + duk;
|
|
44
|
+
const rk = a * (1 - eph.e * cosE) + drk;
|
|
45
|
+
const ik = eph.i0 + dik + eph.idot * tk;
|
|
46
|
+
const xp = rk * Math.cos(uk);
|
|
47
|
+
const yp = rk * Math.sin(uk);
|
|
48
|
+
const isBdsGeo = eph.system === "C" && Math.abs(eph.i0) < 0.1 && a > 4e7;
|
|
49
|
+
if (isBdsGeo) {
|
|
50
|
+
const omegak2 = eph.omega0 + eph.omegaDot * tk - OMEGA_E * eph.toe;
|
|
51
|
+
const cosO2 = Math.cos(omegak2);
|
|
52
|
+
const sinO2 = Math.sin(omegak2);
|
|
53
|
+
const cosI2 = Math.cos(ik);
|
|
54
|
+
const sinI2 = Math.sin(ik);
|
|
55
|
+
const xg = xp * cosO2 - yp * cosI2 * sinO2;
|
|
56
|
+
const yg = xp * sinO2 + yp * cosI2 * cosO2;
|
|
57
|
+
const zg = yp * sinI2;
|
|
58
|
+
const phi = OMEGA_E * tk;
|
|
59
|
+
const cosPhi = Math.cos(phi);
|
|
60
|
+
const sinPhi = Math.sin(phi);
|
|
61
|
+
const COS5 = Math.cos(-5 * Math.PI / 180);
|
|
62
|
+
const SIN5 = Math.sin(-5 * Math.PI / 180);
|
|
63
|
+
return {
|
|
64
|
+
prn: eph.prn,
|
|
65
|
+
x: xg * cosPhi + yg * sinPhi * COS5 + zg * sinPhi * SIN5,
|
|
66
|
+
y: -xg * sinPhi + yg * cosPhi * COS5 + zg * cosPhi * SIN5,
|
|
67
|
+
z: -yg * SIN5 + zg * COS5
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const omegak = eph.omega0 + (eph.omegaDot - OMEGA_E) * tk - OMEGA_E * eph.toe;
|
|
71
|
+
const cosO = Math.cos(omegak);
|
|
72
|
+
const sinO = Math.sin(omegak);
|
|
73
|
+
const cosI = Math.cos(ik);
|
|
74
|
+
const sinI = Math.sin(ik);
|
|
75
|
+
return {
|
|
76
|
+
prn: eph.prn,
|
|
77
|
+
x: xp * cosO - yp * cosI * sinO,
|
|
78
|
+
y: xp * sinO + yp * cosI * cosO,
|
|
79
|
+
z: yp * sinI
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function gloDerivatives(state, acc) {
|
|
83
|
+
const [x, y, z, vx, vy, vz] = state;
|
|
84
|
+
const [ax, ay, az] = acc;
|
|
85
|
+
const r = Math.sqrt(x * x + y * y + z * z);
|
|
86
|
+
const r2 = r * r;
|
|
87
|
+
const r3 = r2 * r;
|
|
88
|
+
const r5 = r2 * r3;
|
|
89
|
+
const mu_r3 = GM_GLO / r3;
|
|
90
|
+
const j2_term = 1.5 * J2_GLO * AE_GLO * AE_GLO / r5;
|
|
91
|
+
const dvx = -mu_r3 * x + j2_term * x * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * x + 2 * OMEGA_E * vy + ax;
|
|
92
|
+
const dvy = -mu_r3 * y + j2_term * y * (5 * z * z / r2 - 1) + OMEGA_E * OMEGA_E * y - 2 * OMEGA_E * vx + ay;
|
|
93
|
+
const dvz = -mu_r3 * z + j2_term * z * (5 * z * z / r2 - 3) + az;
|
|
94
|
+
return [vx, vy, vz, dvx, dvy, dvz];
|
|
95
|
+
}
|
|
96
|
+
function rk4Step(state, acc, dt) {
|
|
97
|
+
const k1 = gloDerivatives(state, acc);
|
|
98
|
+
const s2 = state.map((v, i) => v + k1[i] * dt / 2);
|
|
99
|
+
const k2 = gloDerivatives(s2, acc);
|
|
100
|
+
const s3 = state.map((v, i) => v + k2[i] * dt / 2);
|
|
101
|
+
const k3 = gloDerivatives(s3, acc);
|
|
102
|
+
const s4 = state.map((v, i) => v + k3[i] * dt);
|
|
103
|
+
const k4 = gloDerivatives(s4, acc);
|
|
104
|
+
return state.map(
|
|
105
|
+
(v, i) => v + dt / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function glonassPosition(eph, tUtc) {
|
|
109
|
+
const ephTime = eph.tocDate.getTime() / 1e3;
|
|
110
|
+
const dt = tUtc - ephTime;
|
|
111
|
+
let state = [
|
|
112
|
+
eph.x * 1e3,
|
|
113
|
+
eph.y * 1e3,
|
|
114
|
+
eph.z * 1e3,
|
|
115
|
+
eph.xDot * 1e3,
|
|
116
|
+
eph.yDot * 1e3,
|
|
117
|
+
eph.zDot * 1e3
|
|
118
|
+
];
|
|
119
|
+
const acc = [
|
|
120
|
+
eph.xAcc * 1e3,
|
|
121
|
+
eph.yAcc * 1e3,
|
|
122
|
+
eph.zAcc * 1e3
|
|
123
|
+
];
|
|
124
|
+
const step = dt > 0 ? 60 : -60;
|
|
125
|
+
const nSteps = Math.floor(Math.abs(dt) / 60);
|
|
126
|
+
const remainder = dt - nSteps * step;
|
|
127
|
+
for (let i = 0; i < nSteps; i++) {
|
|
128
|
+
state = rk4Step(state, acc, step);
|
|
129
|
+
}
|
|
130
|
+
if (Math.abs(remainder) > 1e-3) {
|
|
131
|
+
state = rk4Step(state, acc, remainder);
|
|
132
|
+
}
|
|
133
|
+
return { prn: eph.prn, x: state[0], y: state[1], z: state[2] };
|
|
134
|
+
}
|
|
135
|
+
function ecefToAzEl(rxX, rxY, rxZ, satX, satY, satZ) {
|
|
136
|
+
const [lat, lon] = ecefToGeodetic(rxX, rxY, rxZ);
|
|
137
|
+
const dx = satX - rxX;
|
|
138
|
+
const dy = satY - rxY;
|
|
139
|
+
const dz = satZ - rxZ;
|
|
140
|
+
const sinLat = Math.sin(lat);
|
|
141
|
+
const cosLat = Math.cos(lat);
|
|
142
|
+
const sinLon = Math.sin(lon);
|
|
143
|
+
const cosLon = Math.cos(lon);
|
|
144
|
+
const east = -sinLon * dx + cosLon * dy;
|
|
145
|
+
const north = -sinLat * cosLon * dx - sinLat * sinLon * dy + cosLat * dz;
|
|
146
|
+
const up = cosLat * cosLon * dx + cosLat * sinLon * dy + sinLat * dz;
|
|
147
|
+
const az = Math.atan2(east, north);
|
|
148
|
+
const el = Math.atan2(up, Math.sqrt(east * east + north * north));
|
|
149
|
+
return { az: (az + TWO_PI) % TWO_PI, el };
|
|
150
|
+
}
|
|
151
|
+
function computeDop(satAzEls) {
|
|
152
|
+
const n = satAzEls.length;
|
|
153
|
+
if (n < 4) return null;
|
|
154
|
+
const H = [];
|
|
155
|
+
for (const { az, el } of satAzEls) {
|
|
156
|
+
const cosEl = Math.cos(el);
|
|
157
|
+
H.push([cosEl * Math.sin(az), cosEl * Math.cos(az), Math.sin(el), 1]);
|
|
158
|
+
}
|
|
159
|
+
const HtH = Array.from({ length: 4 }, () => new Array(4).fill(0));
|
|
160
|
+
for (let i = 0; i < 4; i++) {
|
|
161
|
+
for (let j = 0; j < 4; j++) {
|
|
162
|
+
let sum = 0;
|
|
163
|
+
for (let k = 0; k < n; k++) {
|
|
164
|
+
sum += H[k][i] * H[k][j];
|
|
165
|
+
}
|
|
166
|
+
HtH[i][j] = sum;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const Q = invert4x4(HtH);
|
|
170
|
+
if (!Q) return null;
|
|
171
|
+
const gdop = Math.sqrt(Q[0][0] + Q[1][1] + Q[2][2] + Q[3][3]);
|
|
172
|
+
const pdop = Math.sqrt(Q[0][0] + Q[1][1] + Q[2][2]);
|
|
173
|
+
const hdop = Math.sqrt(Q[0][0] + Q[1][1]);
|
|
174
|
+
const vdop = Math.sqrt(Q[2][2]);
|
|
175
|
+
return { gdop, pdop, hdop, vdop };
|
|
176
|
+
}
|
|
177
|
+
function invert4x4(m) {
|
|
178
|
+
const a = m.flat();
|
|
179
|
+
const inv = new Array(16).fill(0);
|
|
180
|
+
inv[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
|
|
181
|
+
inv[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
|
|
182
|
+
inv[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
|
|
183
|
+
inv[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
|
|
184
|
+
inv[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
|
|
185
|
+
inv[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
|
|
186
|
+
inv[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
|
|
187
|
+
inv[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
|
|
188
|
+
inv[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
|
|
189
|
+
inv[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
|
|
190
|
+
inv[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
|
|
191
|
+
inv[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
|
|
192
|
+
inv[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
|
|
193
|
+
inv[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
|
|
194
|
+
inv[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
|
|
195
|
+
inv[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
|
|
196
|
+
let det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12];
|
|
197
|
+
if (Math.abs(det) < 1e-20) return null;
|
|
198
|
+
det = 1 / det;
|
|
199
|
+
const result = Array.from(
|
|
200
|
+
{ length: 4 },
|
|
201
|
+
() => new Array(4).fill(0)
|
|
202
|
+
);
|
|
203
|
+
for (let i = 0; i < 4; i++) {
|
|
204
|
+
for (let j = 0; j < 4; j++) {
|
|
205
|
+
result[i][j] = inv[i * 4 + j] * det;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
function selectEphemeris(ephemerides, prn, timeMs) {
|
|
211
|
+
let best = null;
|
|
212
|
+
let bestDt = Infinity;
|
|
213
|
+
for (const eph of ephemerides) {
|
|
214
|
+
if (eph.prn !== prn) continue;
|
|
215
|
+
const ephTime = eph.tocDate.getTime();
|
|
216
|
+
const dt = Math.abs(timeMs - ephTime);
|
|
217
|
+
if (dt > 4 * 3600 * 1e3) continue;
|
|
218
|
+
if (dt < bestDt) {
|
|
219
|
+
bestDt = dt;
|
|
220
|
+
best = eph;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return best;
|
|
224
|
+
}
|
|
225
|
+
function computeSatPosition(eph, timeMs) {
|
|
226
|
+
if (eph.system === "R" || eph.system === "S") {
|
|
227
|
+
return glonassPosition(eph, timeMs / 1e3);
|
|
228
|
+
}
|
|
229
|
+
const GPS_EPOCH = Date.UTC(1980, 0, 6);
|
|
230
|
+
const gpsSeconds = (timeMs - GPS_EPOCH) / 1e3;
|
|
231
|
+
const tow = gpsSeconds % (7 * 86400);
|
|
232
|
+
return keplerPosition(eph, tow);
|
|
233
|
+
}
|
|
234
|
+
function navTimesFromEph(ephs) {
|
|
235
|
+
let minT = Infinity, maxT = -Infinity;
|
|
236
|
+
for (const eph of ephs) {
|
|
237
|
+
const t = eph.tocDate.getTime();
|
|
238
|
+
if (t < minT) minT = t;
|
|
239
|
+
if (t > maxT) maxT = t;
|
|
240
|
+
}
|
|
241
|
+
const span = maxT - minT;
|
|
242
|
+
const step = Math.max(span / 500, 3e4);
|
|
243
|
+
const times = [];
|
|
244
|
+
for (let t = minT; t <= maxT; t += step) times.push(t);
|
|
245
|
+
if (times.length === 0 && isFinite(minT)) times.push(minT);
|
|
246
|
+
return times;
|
|
247
|
+
}
|
|
248
|
+
function computeAllPositions(ephemerides, times, rxPos) {
|
|
249
|
+
const ephByPrn = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const eph of ephemerides) {
|
|
251
|
+
let arr = ephByPrn.get(eph.prn);
|
|
252
|
+
if (!arr) {
|
|
253
|
+
arr = [];
|
|
254
|
+
ephByPrn.set(eph.prn, arr);
|
|
255
|
+
}
|
|
256
|
+
arr.push(eph);
|
|
257
|
+
}
|
|
258
|
+
const prns = [...ephByPrn.keys()].sort();
|
|
259
|
+
const positions = {};
|
|
260
|
+
for (const prn of prns) positions[prn] = [];
|
|
261
|
+
let sinLat = 0, cosLat = 1, sinLon = 0, cosLon = 1;
|
|
262
|
+
let hasRx = false;
|
|
263
|
+
if (rxPos) {
|
|
264
|
+
const [rxX, rxY, rxZ] = rxPos;
|
|
265
|
+
if (rxX !== 0 || rxY !== 0 || rxZ !== 0) {
|
|
266
|
+
hasRx = true;
|
|
267
|
+
const [gLat, gLon] = ecefToGeodetic(rxX, rxY, rxZ);
|
|
268
|
+
sinLat = Math.sin(gLat);
|
|
269
|
+
cosLat = Math.cos(gLat);
|
|
270
|
+
sinLon = Math.sin(gLon);
|
|
271
|
+
cosLon = Math.cos(gLon);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const t of times) {
|
|
275
|
+
for (const prn of prns) {
|
|
276
|
+
const eph = selectBest(ephByPrn.get(prn), t);
|
|
277
|
+
if (!eph) {
|
|
278
|
+
positions[prn].push(null);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const pos = computeSatPosition(eph, t);
|
|
282
|
+
const [geoLat, geoLon] = ecefToGeodetic(pos.x, pos.y, pos.z);
|
|
283
|
+
let az = 0, el = 0;
|
|
284
|
+
if (hasRx) {
|
|
285
|
+
const dx = pos.x - rxPos[0];
|
|
286
|
+
const dy = pos.y - rxPos[1];
|
|
287
|
+
const dz = pos.z - rxPos[2];
|
|
288
|
+
const east = -sinLon * dx + cosLon * dy;
|
|
289
|
+
const north = -sinLat * cosLon * dx - sinLat * sinLon * dy + cosLat * dz;
|
|
290
|
+
const up = cosLat * cosLon * dx + cosLat * sinLon * dy + sinLat * dz;
|
|
291
|
+
az = (Math.atan2(east, north) + TWO_PI) % TWO_PI;
|
|
292
|
+
el = Math.atan2(up, Math.sqrt(east * east + north * north));
|
|
293
|
+
}
|
|
294
|
+
positions[prn].push({ lat: geoLat, lon: geoLon, az, el });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { prns, times, positions };
|
|
298
|
+
}
|
|
299
|
+
function selectBest(ephs, timeMs) {
|
|
300
|
+
let best = null;
|
|
301
|
+
let bestDt = Infinity;
|
|
302
|
+
for (const eph of ephs) {
|
|
303
|
+
const dt = Math.abs(timeMs - eph.tocDate.getTime());
|
|
304
|
+
const maxAge = eph.system === "R" || eph.system === "S" ? 18e5 : 4 * 36e5;
|
|
305
|
+
if (dt > maxAge) continue;
|
|
306
|
+
if (dt < bestDt) {
|
|
307
|
+
bestDt = dt;
|
|
308
|
+
best = eph;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return best;
|
|
312
|
+
}
|
|
313
|
+
var GPS_EPOCH_MS = Date.UTC(1980, 0, 6);
|
|
314
|
+
var BDS_EPOCH_MS = Date.UTC(2006, 0, 1);
|
|
315
|
+
var GAL_EPOCH_MS = GPS_EPOCH_MS;
|
|
316
|
+
function ephInfoToEphemeris(info) {
|
|
317
|
+
const sys = info.prn.charAt(0);
|
|
318
|
+
if (sys === "R") {
|
|
319
|
+
if (info.x === void 0 || info.y === void 0 || info.z === void 0)
|
|
320
|
+
return null;
|
|
321
|
+
if (info.vx === void 0 || info.vy === void 0 || info.vz === void 0)
|
|
322
|
+
return null;
|
|
323
|
+
const now = new Date(info.lastReceived);
|
|
324
|
+
const utcMidnight = Date.UTC(
|
|
325
|
+
now.getUTCFullYear(),
|
|
326
|
+
now.getUTCMonth(),
|
|
327
|
+
now.getUTCDate()
|
|
328
|
+
);
|
|
329
|
+
const tbSec = (info.tb ?? 0) * 900;
|
|
330
|
+
const moscowOffset = 3 * 3600;
|
|
331
|
+
const tocMs = utcMidnight + (tbSec - moscowOffset) * 1e3;
|
|
332
|
+
return {
|
|
333
|
+
system: "R",
|
|
334
|
+
prn: info.prn,
|
|
335
|
+
tocDate: new Date(tocMs),
|
|
336
|
+
tauN: -(info.af0 ?? 0),
|
|
337
|
+
// RTCM stores -tauN
|
|
338
|
+
gammaN: info.gammaN ?? 0,
|
|
339
|
+
messageFrameTime: 0,
|
|
340
|
+
x: info.x,
|
|
341
|
+
xDot: info.vx,
|
|
342
|
+
xAcc: info.ax ?? 0,
|
|
343
|
+
y: info.y,
|
|
344
|
+
yDot: info.vy,
|
|
345
|
+
yAcc: info.ay ?? 0,
|
|
346
|
+
z: info.z,
|
|
347
|
+
zDot: info.vz,
|
|
348
|
+
zAcc: info.az ?? 0,
|
|
349
|
+
health: info.health,
|
|
350
|
+
freqNum: info.freqChannel ?? 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (info.sqrtA === void 0 || info.eccentricity === void 0 || info.inclination === void 0 || info.omega0 === void 0 || info.argPerigee === void 0 || info.meanAnomaly === void 0 || info.toe === void 0 || info.week === void 0)
|
|
354
|
+
return null;
|
|
355
|
+
let epochMs;
|
|
356
|
+
const tocSec = info.toc ?? info.toe;
|
|
357
|
+
if (sys === "C") {
|
|
358
|
+
epochMs = BDS_EPOCH_MS;
|
|
359
|
+
} else if (sys === "E") {
|
|
360
|
+
epochMs = GAL_EPOCH_MS;
|
|
361
|
+
} else {
|
|
362
|
+
epochMs = GPS_EPOCH_MS;
|
|
363
|
+
}
|
|
364
|
+
const tocDate = new Date(epochMs + info.week * 7 * 864e5 + tocSec * 1e3);
|
|
365
|
+
return {
|
|
366
|
+
system: sys,
|
|
367
|
+
prn: info.prn,
|
|
368
|
+
toc: tocSec,
|
|
369
|
+
tocDate,
|
|
370
|
+
af0: info.af0 ?? 0,
|
|
371
|
+
af1: info.af1 ?? 0,
|
|
372
|
+
af2: info.af2 ?? 0,
|
|
373
|
+
iode: info.iode ?? 0,
|
|
374
|
+
crs: info.crs ?? 0,
|
|
375
|
+
deltaN: info.deltaN ?? 0,
|
|
376
|
+
m0: info.meanAnomaly,
|
|
377
|
+
cuc: info.cuc ?? 0,
|
|
378
|
+
e: info.eccentricity,
|
|
379
|
+
cus: info.cus ?? 0,
|
|
380
|
+
sqrtA: info.sqrtA,
|
|
381
|
+
toe: info.toe,
|
|
382
|
+
cic: info.cic ?? 0,
|
|
383
|
+
omega0: info.omega0,
|
|
384
|
+
cis: info.cis ?? 0,
|
|
385
|
+
i0: info.inclination,
|
|
386
|
+
crc: info.crc ?? 0,
|
|
387
|
+
omega: info.argPerigee,
|
|
388
|
+
omegaDot: info.omegaDot ?? 0,
|
|
389
|
+
idot: info.idot ?? 0,
|
|
390
|
+
week: info.week,
|
|
391
|
+
svHealth: info.health,
|
|
392
|
+
tgd: 0
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function computeLiveSkyPositions(ephemerides, rxPos, cn0Map) {
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
const result = [];
|
|
398
|
+
const [rxX, rxY, rxZ] = rxPos;
|
|
399
|
+
const [rxLat, rxLon] = ecefToGeodetic(rxX, rxY, rxZ);
|
|
400
|
+
const sinRxLat = Math.sin(rxLat), cosRxLat = Math.cos(rxLat);
|
|
401
|
+
const sinRxLon = Math.sin(rxLon), cosRxLon = Math.cos(rxLon);
|
|
402
|
+
for (const info of ephemerides.values()) {
|
|
403
|
+
const eph = ephInfoToEphemeris(info);
|
|
404
|
+
if (!eph) continue;
|
|
405
|
+
try {
|
|
406
|
+
const pos = computeSatPosition(eph, now);
|
|
407
|
+
const [satLat, satLon] = ecefToGeodetic(pos.x, pos.y, pos.z);
|
|
408
|
+
const dx = pos.x - rxX, dy = pos.y - rxY, dz = pos.z - rxZ;
|
|
409
|
+
const east = -sinRxLon * dx + cosRxLon * dy;
|
|
410
|
+
const north = -sinRxLat * cosRxLon * dx - sinRxLat * sinRxLon * dy + cosRxLat * dz;
|
|
411
|
+
const up = cosRxLat * cosRxLon * dx + cosRxLat * sinRxLon * dy + sinRxLat * dz;
|
|
412
|
+
const az = (Math.atan2(east, north) + TWO_PI) % TWO_PI;
|
|
413
|
+
const el = Math.atan2(up, Math.sqrt(east * east + north * north));
|
|
414
|
+
if (el > -0.05) {
|
|
415
|
+
result.push({
|
|
416
|
+
prn: info.prn,
|
|
417
|
+
az,
|
|
418
|
+
el,
|
|
419
|
+
lat: satLat,
|
|
420
|
+
lon: satLon,
|
|
421
|
+
cn0: cn0Map?.get(info.prn)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export {
|
|
431
|
+
keplerPosition,
|
|
432
|
+
glonassPosition,
|
|
433
|
+
ecefToAzEl,
|
|
434
|
+
computeDop,
|
|
435
|
+
selectEphemeris,
|
|
436
|
+
computeSatPosition,
|
|
437
|
+
navTimesFromEph,
|
|
438
|
+
computeAllPositions,
|
|
439
|
+
computeLiveSkyPositions
|
|
440
|
+
};
|