@saber-usa/node-common 1.7.35 → 1.7.36

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.36",
4
4
  "description": "Common node functions for Saber",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/astro.js CHANGED
@@ -48,6 +48,7 @@ import {DEG2RAD,
48
48
  import TLEValidator from "./tle/TLEValidator.js";
49
49
  import MultiLineTLEValidator from "./tle/MultiLineTLEValidator.js";
50
50
  import {TLE_ERRORS} from "./tle/tleErrors.js";
51
+ import {TleParseUtils} from "./tle/TleParseUtils.js";
51
52
 
52
53
  // Solar Terminator
53
54
  // Returns sun latitude and longitude as -180/180
@@ -145,24 +146,24 @@ const validateTle = (line1, line2) => {
145
146
  * @return {boolean} true if the propagation output is valid, false otherwise
146
147
  */
147
148
  const isPropagateValid = (out) => {
148
- if (out === null || out === undefined) {
149
+ if (!isDefined(out)) {
149
150
  return false;
150
151
  }
151
152
 
152
153
  if (out.position) {
153
154
  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)) {
155
+ if (!isDefined(pos.x) || Number.isNaN(pos.x)
156
+ || !isDefined(pos.y) || Number.isNaN(pos.y)
157
+ || !isDefined(pos.z) || Number.isNaN(pos.z)) {
157
158
  return false;
158
159
  }
159
160
  }
160
161
 
161
162
  if (out.velocity) {
162
163
  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)) {
164
+ if (!isDefined(vel.x) || Number.isNaN(vel.x)
165
+ || !isDefined(vel.y) || Number.isNaN(vel.y)
166
+ || !isDefined(vel.z) || Number.isNaN(vel.z)) {
166
167
  return false;
167
168
  }
168
169
  }
@@ -1391,7 +1392,7 @@ const GetElsetUdlFromTle = (
1391
1392
  // and we may be unsure of entering true or false for any reason.
1392
1393
  if (isBoolean(isUct)) {
1393
1394
  elset.uct = isUct;
1394
- } else if (isUct === null || isUct === undefined) {
1395
+ } else if (!isDefined(isUct)) {
1395
1396
  // Do nothing, do not populate nor put null.
1396
1397
  } else {
1397
1398
  throw new Error("Input uct flag is not of type boolean.");
@@ -1423,7 +1424,7 @@ const GetElsetUdlFromTle = (
1423
1424
  const date = julianToGregorian(satrec.jdsatepoch);
1424
1425
  elset.epoch = date.toISOString();
1425
1426
 
1426
- elset.satNo = Number.parseInt(satrec.satnum);
1427
+ elset.satNo = TleParseUtils.getSatnoFromTle(satrec.satnum);
1427
1428
 
1428
1429
  // Eccentricity
1429
1430
  elset.eccentricity = satrec.ecco;
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
@@ -10,7 +10,7 @@ import {
10
10
  raDecToGeodetic,
11
11
  estimateSlantRange,
12
12
  } from "./astro.js";
13
- import {lowerCaseObjectKeys} from "./transform.js";
13
+ import {lowerCaseObjectKeys, jsonArrayOrNull} from "./transform.js";
14
14
  import {isDefined} from "./utils.js";
15
15
  import _ from "lodash";
16
16
 
@@ -335,7 +335,7 @@ const udlToNpsState = (udlRow) => {
335
335
  const Xvel = _.get(udlRow, "xvel", null); // kilometers/second
336
336
  const Yvel = _.get(udlRow, "yvel", null);
337
337
  const Zvel = _.get(udlRow, "zvel", null);
338
- const cov = _.get(udlRow, "cov", null );
338
+ const rawCov = _.get(udlRow, "cov", null );
339
339
  const covRefFrame = _.get(udlRow, "covReferenceFrame", null );
340
340
  let keplerians = {a: null, e: null, i: null, raan: null, w: null, f: null};
341
341
 
@@ -368,7 +368,10 @@ const udlToNpsState = (udlRow) => {
368
368
  Xvel: Xvel,
369
369
  Yvel: Yvel,
370
370
  Zvel: Zvel,
371
- Covariance: JSON.stringify(cov),
371
+ // Guard against providers returning a non-array `cov` (e.g. the string "null" or
372
+ // "OTHER" in place of a real covariance matrix), and write a true SQL NULL rather
373
+ // than the string "null" when there is no covariance at all.
374
+ Covariance: jsonArrayOrNull(rawCov),
372
375
  CovarianceRefFrame: covRefFrame,
373
376
  SemiMajorAxis: keplerians.a,
374
377
  Eccentricity: keplerians.e,