@saber-usa/node-common 1.7.35 → 1.7.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber-usa/node-common",
3
- "version": "1.7.35",
3
+ "version": "1.7.37",
4
4
  "description": "Common node functions for Saber",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -23,13 +23,13 @@
23
23
  "author": "Saber USA",
24
24
  "license": "ISC",
25
25
  "dependencies": {
26
- "@aws-sdk/client-s3": "^3.1092.0",
26
+ "@aws-sdk/client-s3": "^3.1119.0",
27
27
  "date-fns": "^4.4.0",
28
28
  "lodash": "4.18.1",
29
29
  "mathjs": "^15.2.0",
30
30
  "pious-squid": "^2.3.0",
31
31
  "plotly": "^1.0.6",
32
- "satellite.js": "^7.0.1",
32
+ "satellite.js": "^7.1.0",
33
33
  "solar-calculator": "^0.3.0",
34
34
  "three": "^0.185.1",
35
35
  "winston": "3.19.0"
@@ -41,16 +41,17 @@
41
41
  "@jest/globals": "^30.4.1",
42
42
  "@sonar/scan": "^5.0.0",
43
43
  "cross-env": "^10.1.0",
44
- "eslint": "^10.7.0",
45
- "eslint-plugin-jest": "^29.15.5",
46
- "globals": "^17.7.0",
44
+ "eslint": "^10.9.1",
45
+ "eslint-plugin-jest": "^29.16.2",
46
+ "globals": "^17.11.0",
47
47
  "jest": "^30.4.2",
48
48
  "jest-diff": "^30.4.1",
49
49
  "nodemon": "3.1.14"
50
50
  },
51
51
  "overrides": {
52
52
  "braces": "3.0.3",
53
- "brace-expansion": "2.1.2",
53
+ "brace-expansion": "2.1.4",
54
+ "js-yaml": "3.15.2",
54
55
  "lodash": "4.18.1"
55
56
  }
56
57
  }
package/src/astro.js CHANGED
@@ -40,6 +40,7 @@ import {DEG2RAD,
40
40
  REGIMES,
41
41
  GRAV_CONST,
42
42
  EARTH_MASS,
43
+ MU,
43
44
  WGS72_EARTH_EQUATORIAL_RADIUS_KM,
44
45
  WGS84_EARTH_EQUATORIAL_RADIUS_KM,
45
46
  MILLIS_PER_DAY,
@@ -48,6 +49,8 @@ import {DEG2RAD,
48
49
  import TLEValidator from "./tle/TLEValidator.js";
49
50
  import MultiLineTLEValidator from "./tle/MultiLineTLEValidator.js";
50
51
  import {TLE_ERRORS} from "./tle/tleErrors.js";
52
+ import {TleParseUtils} from "./tle/TleParseUtils.js";
53
+ import {OrbitUtils} from "./OrbitUtils.js";
51
54
 
52
55
  // Solar Terminator
53
56
  // Returns sun latitude and longitude as -180/180
@@ -145,24 +148,24 @@ const validateTle = (line1, line2) => {
145
148
  * @return {boolean} true if the propagation output is valid, false otherwise
146
149
  */
147
150
  const isPropagateValid = (out) => {
148
- if (out === null || out === undefined) {
151
+ if (!isDefined(out)) {
149
152
  return false;
150
153
  }
151
154
 
152
155
  if (out.position) {
153
156
  const pos = out.position;
154
- if (pos.x === null || pos.x === undefined || Number.isNaN(pos.x)
155
- || pos.y === null || pos.y === undefined || Number.isNaN(pos.y)
156
- || pos.z === null || pos.z === undefined || Number.isNaN(pos.z)) {
157
+ if (!isDefined(pos.x) || Number.isNaN(pos.x)
158
+ || !isDefined(pos.y) || Number.isNaN(pos.y)
159
+ || !isDefined(pos.z) || Number.isNaN(pos.z)) {
157
160
  return false;
158
161
  }
159
162
  }
160
163
 
161
164
  if (out.velocity) {
162
165
  const vel = out.velocity;
163
- if (vel.x === null || vel.x === undefined || Number.isNaN(vel.x)
164
- || vel.y === null || vel.y === undefined || Number.isNaN(vel.y)
165
- || vel.z === null || vel.z === undefined || Number.isNaN(vel.z)) {
166
+ if (!isDefined(vel.x) || Number.isNaN(vel.x)
167
+ || !isDefined(vel.y) || Number.isNaN(vel.y)
168
+ || !isDefined(vel.z) || Number.isNaN(vel.z)) {
166
169
  return false;
167
170
  }
168
171
  }
@@ -308,6 +311,28 @@ const getLonAndDrift = (line1, line2, datetime) => {
308
311
  }
309
312
  };
310
313
 
314
+ /**
315
+ * Orbital period in minutes from semi-major axis and gravitational parameter.
316
+ * @param {number} aKm Semi-major axis (km)
317
+ * @param {number} muKm3s2 Standard gravitational parameter (km³/s²)
318
+ * @return {number|null} Period in minutes, or null if a ≤ 0
319
+ */
320
+ const orbitalPeriodMinutes = (aKm, muKm3s2) => {
321
+ const periodSec = OrbitUtils.getPeriod(aKm, muKm3s2);
322
+ return periodSec === null ? null : periodSec / 60;
323
+ };
324
+
325
+ /**
326
+ * Mean motion in revolutions per day from semi-major axis and gravitational parameter.
327
+ * @param {number} aKm Semi-major axis (km)
328
+ * @param {number} muKm3s2 Standard gravitational parameter (km³/s²)
329
+ * @return {number} Mean motion in rev/day
330
+ */
331
+ const orbitalMeanMotionRevPerDay = (aKm, muKm3s2) => {
332
+ const nRadS = OrbitUtils.getMeanMotion(aKm, muKm3s2);
333
+ return nRadS * 86400 / (2 * Math.PI);
334
+ };
335
+
311
336
  /**
312
337
  * Given eccentricity, inclination, meanmotion, and period
313
338
  * Calculates & returns type of orbit to include LEO, MEO, HEO, GEO Drifter, GEO Inclined,GEO Stationary, and undetermined if applicable
@@ -1391,7 +1416,7 @@ const GetElsetUdlFromTle = (
1391
1416
  // and we may be unsure of entering true or false for any reason.
1392
1417
  if (isBoolean(isUct)) {
1393
1418
  elset.uct = isUct;
1394
- } else if (isUct === null || isUct === undefined) {
1419
+ } else if (!isDefined(isUct)) {
1395
1420
  // Do nothing, do not populate nor put null.
1396
1421
  } else {
1397
1422
  throw new Error("Input uct flag is not of type boolean.");
@@ -1423,7 +1448,7 @@ const GetElsetUdlFromTle = (
1423
1448
  const date = julianToGregorian(satrec.jdsatepoch);
1424
1449
  elset.epoch = date.toISOString();
1425
1450
 
1426
- elset.satNo = Number.parseInt(satrec.satnum);
1451
+ elset.satNo = TleParseUtils.getSatnoFromTle(satrec.satnum);
1427
1452
 
1428
1453
  // Eccentricity
1429
1454
  elset.eccentricity = satrec.ecco;
@@ -1747,11 +1772,9 @@ const cartesianToElsetElements = (pv, epoch) => {
1747
1772
  ArgOfPerigee: kepl.w,
1748
1773
  };
1749
1774
 
1750
- // Mean motion in radians per second
1751
- const mu = 3.986004418e14; // (m^3)/(s^2) WGS-84 Earth Mu
1752
- const meanMotion = Math.sqrt(mu/Math.pow((elset.SemiMajorAxis*1000), 3));
1753
-
1754
- elset.MeanMotion = meanMotion / (2*Math.PI) * 60 * 60 * 24; // rads/s to revs per day
1775
+ const muKm3s2 = MU / 1e9;
1776
+ elset.MeanMotion = orbitalMeanMotionRevPerDay(elset.SemiMajorAxis, muKm3s2);
1777
+ elset.Period = orbitalPeriodMinutes(elset.SemiMajorAxis, muKm3s2);
1755
1778
 
1756
1779
  const trueAnomaly = kepl.f;
1757
1780
 
@@ -1762,7 +1785,6 @@ const cartesianToElsetElements = (pv, epoch) => {
1762
1785
  // Compute Mean Anomaly from Eccentric Anomaly and Eccentricity
1763
1786
  elset.MeanAnomaly = (E - (elset.Eccentricity)*Math.sin(E)) * RAD2DEG;
1764
1787
 
1765
- elset.Period = 2*Math.PI/meanMotion / 60; // period in minutes
1766
1788
  elset.Apogee = elset.SemiMajorAxis * (1 + elset.Eccentricity); // km
1767
1789
  elset.Perigee = elset.SemiMajorAxis * (1 - elset.Eccentricity); // km
1768
1790
 
@@ -3589,6 +3611,8 @@ export {
3589
3611
  } from "satellite.js";
3590
3612
  export {
3591
3613
  calcRegime,
3614
+ orbitalPeriodMinutes,
3615
+ orbitalMeanMotionRevPerDay,
3592
3616
  altToRegime,
3593
3617
  cartesianToRIC,
3594
3618
  angleBetween3DCoords,
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ export * from "./OrbitUtils.js";
11
11
  export * from "./PropagateUtils.js";
12
12
  export * from "./ballisticPropagator.js";
13
13
  export * from "./NodeVector3D.js";
14
+ export * from "./tle/TleParseUtils.js";
14
15
 
15
16
  // UDL exports are grouped; re-export individually and as namespace if needed
16
17
  import * as udl from "./udl.js";
@@ -30,6 +31,7 @@ import * as OrbitUtilsNS from "./OrbitUtils.js";
30
31
  import * as PropagateUtilsNS from "./PropagateUtils.js";
31
32
  import * as ballisticPropagatorNS from "./ballisticPropagator.js";
32
33
  import * as NodeVector3DNS from "./NodeVector3D.js";
34
+ import * as TleParseUtilsNS from "./tle/TleParseUtils.js";
33
35
 
34
36
  const aggregate = {
35
37
  ...loggerFactoryNS,
@@ -44,6 +46,7 @@ const aggregate = {
44
46
  ...PropagateUtilsNS,
45
47
  ...ballisticPropagatorNS,
46
48
  ...NodeVector3DNS,
49
+ ...TleParseUtilsNS,
47
50
  ...udl,
48
51
  };
49
52
 
@@ -1,12 +1,13 @@
1
1
  import {formatChecksumError, formatLengthError, TLE_ERRORS} from "./tleErrors.js";
2
+ import {TleParseUtils} from "./TleParseUtils.js";
2
3
 
3
4
  /**
4
5
  * NORAD TLE line-pair validator.
5
6
  * Logic derived from whatswrongwithmytle.com (Dr. TS Kelso's field guide).
7
+ * Catalog-number grammar is owned by TleParseUtils (Alpha-5 aware).
6
8
  */
7
9
  class TLEValidator {
8
10
  constructor() {
9
- this.lineOneRegexCatalogNumber = /[0-Z]{5}[CSU]/;
10
11
  this.lineOneRegexInternationalDesignator = / (\d{5}[A-Z][ A-Z]{2}|\d{5}[ A-Z]{2}[A-Z]| {8})/;
11
12
  this.lineOneRegexEpoch = / \d{5}\.\d{8}/;
12
13
  this.lineOneRegexDMeanMotion = / [ +-]\.\d{8}/;
@@ -15,7 +16,6 @@ class TLEValidator {
15
16
  this.lineOneRegexEphemType = / [0-5]/;
16
17
  this.lineOneRegexSetNumber = / [ \d]{3}\d/;
17
18
 
18
- this.lineTwoRegexCatalogNumber = /[0-Z]{5}/;
19
19
  this.lineTwoRegexInclination = / [ \d]{3}\.\d{4}/;
20
20
  this.lineTwoRegexRAAN = / [ \d]{3}\.\d{4}/;
21
21
  this.lineTwoRegexEccentricity = / \d{7}/;
@@ -57,7 +57,8 @@ class TLEValidator {
57
57
  if (line.length < 69) {
58
58
  return formatLengthError(TLE_ERRORS.LINE1_LENGTH, line.length);
59
59
  }
60
- if (!this.lineOneRegexCatalogNumber.test(line.slice(2, 8))) {
60
+ if (!TleParseUtils.isValidSatelliteNumberField(line.slice(2, 7))
61
+ || !/[CSU]/.test(line.slice(7, 8))) {
61
62
  return TLE_ERRORS.LINE1_CATALOG_NUMBER;
62
63
  }
63
64
  if (!this.lineOneRegexInternationalDesignator.test(line.slice(8, 17))) {
@@ -96,7 +97,7 @@ class TLEValidator {
96
97
  if (line.length < 69) {
97
98
  return formatLengthError(TLE_ERRORS.LINE2_LENGTH, line.length);
98
99
  }
99
- if (!this.lineTwoRegexCatalogNumber.test(line.slice(2, 7))) {
100
+ if (!TleParseUtils.isValidSatelliteNumberField(line.slice(2, 7))) {
100
101
  return TLE_ERRORS.LINE2_CATALOG_NUMBER;
101
102
  }
102
103
  if (!this.lineTwoRegexInclination.test(line.slice(7, 16))) {
@@ -0,0 +1,168 @@
1
+ import {isDefined} from "../utils.js";
2
+
3
+ /**
4
+ * Utility class for TLE parsing, including Alpha-5 TLE satellite IDs.
5
+ * Ported from org.orekit.propagation.analytical.tle.ParseUtils / TLE.
6
+ *
7
+ * Alpha-5 extends the range of existing 5-digit TLE satellite numbers
8
+ * by allowing the first digit to be an upper case letter, ignoring 'I'
9
+ * and 'O' to avoid confusion with numbers '1' and '0'.
10
+ *
11
+ * @see https://www.space-track.org/documentation#tle-alpha5
12
+ */
13
+ export class TleParseUtils {
14
+ /** Maximum satellite number representable with 5 numeric digits. */
15
+ static MAX_NUMERIC_SATNUM = 99999;
16
+
17
+ /** Scaling factor for Alpha-5 numbers. */
18
+ static ALPHA5_SCALING = 10000;
19
+
20
+ /** Letter → number map for Alpha-5 satellite numbers. */
21
+ static ALPHA5_NUMBERS = new Map();
22
+
23
+ /** Number → letter map for Alpha-5 satellite numbers. */
24
+ static ALPHA5_LETTERS = new Map();
25
+
26
+ static {
27
+ const alpha5Letters = [
28
+ "A", "B", "C", "D", "E", "F", "G", "H", "J",
29
+ "K", "L", "M", "N", "P", "Q", "R", "S", "T",
30
+ "U", "V", "W", "X", "Y", "Z",
31
+ ];
32
+ for (let i = 0; i < alpha5Letters.length; ++i) {
33
+ TleParseUtils.ALPHA5_NUMBERS.set(alpha5Letters[i], i + 10);
34
+ TleParseUtils.ALPHA5_LETTERS.set(i + 10, alpha5Letters[i]);
35
+ }
36
+ }
37
+
38
+ /** Private constructor for a utility class. */
39
+ constructor() {
40
+ throw new Error("TleParseUtils is a utility class and cannot be instantiated");
41
+ }
42
+
43
+ /**
44
+ * True if token is a valid 5-char TLE catalog field:
45
+ * five digits, or Alpha-5 letter (no I/O) plus four digits.
46
+ * @param {string} token
47
+ * @return {boolean}
48
+ */
49
+ static isValidSatelliteNumberField(token) {
50
+ if (typeof token !== "string" || token.length !== 5) {
51
+ return false;
52
+ }
53
+ if (/^\d{5}$/.test(token)) {
54
+ return true;
55
+ }
56
+ if (!TleParseUtils.ALPHA5_NUMBERS.has(token.charAt(0))) {
57
+ return false;
58
+ }
59
+ return /^\d{4}$/.test(token.substring(1));
60
+ }
61
+
62
+ /**
63
+ * Stricter int parser: accepts an optional sign plus digits only, else throws.
64
+ * Unlike Number.parseInt, rejects partial parses (e.g. "12abc").
65
+ * @param {string} string
66
+ * @return {number}
67
+ */
68
+ static #strictParseInt(string) {
69
+ if (!/^[+-]?\d+$/.test(string)) {
70
+ throw new Error(`Invalid integer: "${string}"`);
71
+ }
72
+ return Number.parseInt(string, 10);
73
+ }
74
+
75
+ /**
76
+ * Add padding characters to a string.
77
+ * Port of ParseUtils.addPadding.
78
+ * @param {string} name parameter name
79
+ * @param {string} string string to pad
80
+ * @param {string} c padding character
81
+ * @param {number} size desired size
82
+ * @param {boolean} rightJustified if true, pad on the left
83
+ * @param {number} satelliteNumber satellite number (for error context)
84
+ * @return {string} padded string
85
+ */
86
+ static addPadding(name, string, c, size, rightJustified, satelliteNumber) {
87
+ if (string.length > size) {
88
+ throw new Error(
89
+ `TLE invalid parameter: satelliteNumber=${satelliteNumber}, name=${name}, value=${string}`,
90
+ );
91
+ }
92
+
93
+ const padding = c.repeat(size);
94
+
95
+ if (rightJustified) {
96
+ const concatenated = padding + string;
97
+ const l = concatenated.length;
98
+ return concatenated.substring(l - size, l);
99
+ }
100
+
101
+ return (string + padding).substring(0, size);
102
+ }
103
+
104
+ /**
105
+ * Build an Alpha-5 (or numeric) satellite number field.
106
+ * Port of ParseUtils.buildSatelliteNumber.
107
+ * @param {number} satelliteNumber satellite number, that may exceed the 99999 limit
108
+ * @param {string} name parameter name (used in error messages)
109
+ * @return {string} satellite number in alpha5 / 5-digit representation
110
+ */
111
+ static buildSatelliteNumber(satelliteNumber, name) {
112
+ if (satelliteNumber > TleParseUtils.MAX_NUMERIC_SATNUM) {
113
+ const highDigits = Math.trunc(satelliteNumber / TleParseUtils.ALPHA5_SCALING);
114
+ const lowDigits = satelliteNumber - highDigits * TleParseUtils.ALPHA5_SCALING;
115
+
116
+ const alpha = TleParseUtils.ALPHA5_LETTERS.get(highDigits);
117
+ if (alpha === undefined) {
118
+ throw new Error(
119
+ `TLE invalid parameter: satelliteNumber=${satelliteNumber}, name=${name}, value=null`,
120
+ );
121
+ }
122
+ return alpha + TleParseUtils.addPadding(
123
+ name, String(lowDigits), "0", 4, true, satelliteNumber,
124
+ );
125
+ }
126
+ return TleParseUtils.addPadding(
127
+ name, String(satelliteNumber), "0", 5, true, satelliteNumber,
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Parse a satellite number from a string field (supports Alpha-5).
133
+ * Port of TLE.parseSatelliteNumber / ParseUtils.parseSatelliteNumber.
134
+ * @param {string} satNumberString the string to parse (e.g., "25544" or "A0001")
135
+ * @return {number} the satellite number as an integer
136
+ */
137
+ static getSatnoFromTle(satNumberString) {
138
+ if (!TleParseUtils.isValidSatelliteNumberField(satNumberString)) {
139
+ throw new Error(`Invalid satellite number field: "${satNumberString}"`);
140
+ }
141
+
142
+ const alpha = TleParseUtils.ALPHA5_NUMBERS.get(satNumberString.charAt(0));
143
+ if (alpha !== undefined) {
144
+ return (alpha * TleParseUtils.ALPHA5_SCALING)
145
+ + TleParseUtils.#strictParseInt(satNumberString.substring(1));
146
+ }
147
+ return TleParseUtils.#strictParseInt(satNumberString);
148
+ }
149
+
150
+ /**
151
+ * Lenient catalog NORAD ID decode: 5-char TLE field (digits or Alpha-5),
152
+ * or an unpadded plain integer string (e.g. "16", "900").
153
+ * Returns null for nullish / empty / non-numeric values instead of throwing.
154
+ * @param {string|number|null|undefined} value
155
+ * @return {number|null}
156
+ */
157
+ static parseCatalogSatno(value) {
158
+ if (!isDefined(value) || value === "") {
159
+ return null;
160
+ }
161
+ const token = String(value).trim();
162
+ if (TleParseUtils.isValidSatelliteNumberField(token)) {
163
+ return TleParseUtils.getSatnoFromTle(token);
164
+ }
165
+ const n = Number.parseInt(token, 10);
166
+ return Number.isNaN(n) ? null : n;
167
+ }
168
+ }
package/src/transform.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import _ from "lodash";
2
+ import {isDefined} from "./utils.js";
2
3
 
3
4
  const transformObjectKeys = (transform, object, deep = true) => _.cond([
4
5
  [
@@ -32,4 +33,15 @@ const pascalCaseObjectKeys = (transformItem, deep) => transformObjectKeys(
32
33
  deep,
33
34
  );
34
35
 
35
- export {transformObjectKeys, lowerCaseObjectKeys, pascalCaseObjectKeys, pascalCase};
36
+ // Write a true SQL NULL instead of the *string* "null" (JSON.stringify(null) === "null")
37
+ // when a value is absent, so nullable JSON/TEXT columns stay unambiguously empty.
38
+ const jsonOrNull = (value) => (isDefined(value) ? JSON.stringify(value) : null);
39
+
40
+ // As jsonOrNull, but additionally rejects non-array values (e.g. a provider returning the
41
+ // string "OTHER" in place of a real array), coercing them to null rather than serializing them.
42
+ const jsonArrayOrNull = (value) => (Array.isArray(value) ? JSON.stringify(value) : null);
43
+
44
+ export {
45
+ transformObjectKeys, lowerCaseObjectKeys, pascalCaseObjectKeys, pascalCase,
46
+ jsonOrNull, jsonArrayOrNull,
47
+ };
package/src/udl.js CHANGED
@@ -5,12 +5,15 @@ import {
5
5
  getElsetUdlFromTle,
6
6
  getLonAndDrift,
7
7
  getRaanPrecession,
8
+ orbitalMeanMotionRevPerDay,
9
+ orbitalPeriodMinutes,
8
10
  raDecToAzEl,
9
11
  azElToRaDec,
10
12
  raDecToGeodetic,
11
13
  estimateSlantRange,
12
14
  } from "./astro.js";
13
- import {lowerCaseObjectKeys} from "./transform.js";
15
+ import {REGIMES, MU} from "./constants.js";
16
+ import {lowerCaseObjectKeys, jsonArrayOrNull} from "./transform.js";
14
17
  import {isDefined} from "./utils.js";
15
18
  import _ from "lodash";
16
19
 
@@ -327,6 +330,28 @@ const getSVSatno = (udlRow) => {
327
330
  return null;
328
331
  };
329
332
 
333
+ const computeStateRegimeFromKeplerians = (keplerians) => {
334
+ const {a, e, i} = keplerians;
335
+ if (!isDefined(a) || a <= 0 || !isDefined(e) || e >= 1) {
336
+ return REGIMES.Undetermined;
337
+ }
338
+
339
+ const muKm3s2 = MU / 1e9;
340
+ const period = orbitalPeriodMinutes(a, muKm3s2);
341
+ const meanmotion = orbitalMeanMotionRevPerDay(a, muKm3s2);
342
+
343
+ if (period === null) {
344
+ return REGIMES.Undetermined;
345
+ }
346
+
347
+ return calcRegime({
348
+ eccentricity: e,
349
+ inclination: i,
350
+ meanmotion,
351
+ period,
352
+ });
353
+ };
354
+
330
355
  const udlToNpsState = (udlRow) => {
331
356
  const Xpos = _.get(udlRow, "xpos", null); // kilometers
332
357
  const Ypos = _.get(udlRow, "ypos", null);
@@ -335,7 +360,7 @@ const udlToNpsState = (udlRow) => {
335
360
  const Xvel = _.get(udlRow, "xvel", null); // kilometers/second
336
361
  const Yvel = _.get(udlRow, "yvel", null);
337
362
  const Zvel = _.get(udlRow, "zvel", null);
338
- const cov = _.get(udlRow, "cov", null );
363
+ const rawCov = _.get(udlRow, "cov", null );
339
364
  const covRefFrame = _.get(udlRow, "covReferenceFrame", null );
340
365
  let keplerians = {a: null, e: null, i: null, raan: null, w: null, f: null};
341
366
 
@@ -351,6 +376,8 @@ const udlToNpsState = (udlRow) => {
351
376
  keplerians = cartesianToKeplerian(pos, vel);
352
377
  }
353
378
 
379
+ const regime = computeStateRegimeFromKeplerians(keplerians);
380
+
354
381
  const npsState = lowerCaseObjectKeys({
355
382
  satno: getSVSatno(udlRow),
356
383
  Epoch: fixDate(_.get(udlRow, "epoch", null)),
@@ -368,7 +395,10 @@ const udlToNpsState = (udlRow) => {
368
395
  Xvel: Xvel,
369
396
  Yvel: Yvel,
370
397
  Zvel: Zvel,
371
- Covariance: JSON.stringify(cov),
398
+ // Guard against providers returning a non-array `cov` (e.g. the string "null" or
399
+ // "OTHER" in place of a real covariance matrix), and write a true SQL NULL rather
400
+ // than the string "null" when there is no covariance at all.
401
+ Covariance: jsonArrayOrNull(rawCov),
372
402
  CovarianceRefFrame: covRefFrame,
373
403
  SemiMajorAxis: keplerians.a,
374
404
  Eccentricity: keplerians.e,
@@ -385,7 +415,7 @@ const udlToNpsState = (udlRow) => {
385
415
  Longitude: null,
386
416
  LonDriftDegreesPerDay: null,
387
417
  RaanPrecessionDegreesPerDay: null,
388
- Regime: 1,
418
+ Regime: regime,
389
419
  });
390
420
  return npsState;
391
421
  };