@packet-net/pdn-aprs 0.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +661 -0
  3. package/README.md +192 -0
  4. package/THIRD-PARTY-NOTICES.md +5 -0
  5. package/dist/builder.d.ts +232 -0
  6. package/dist/builder.d.ts.map +1 -0
  7. package/dist/builder.js +508 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/bytes.d.ts +19 -0
  10. package/dist/bytes.d.ts.map +1 -0
  11. package/dist/bytes.js +120 -0
  12. package/dist/bytes.js.map +1 -0
  13. package/dist/codes.d.ts +13 -0
  14. package/dist/codes.d.ts.map +1 -0
  15. package/dist/codes.js +112 -0
  16. package/dist/codes.js.map +1 -0
  17. package/dist/decode/context.d.ts +33 -0
  18. package/dist/decode/context.d.ts.map +1 -0
  19. package/dist/decode/context.js +65 -0
  20. package/dist/decode/context.js.map +1 -0
  21. package/dist/decode/index.d.ts +10 -0
  22. package/dist/decode/index.d.ts.map +1 -0
  23. package/dist/decode/index.js +121 -0
  24. package/dist/decode/index.js.map +1 -0
  25. package/dist/decode/message.d.ts +5 -0
  26. package/dist/decode/message.d.ts.map +1 -0
  27. package/dist/decode/message.js +190 -0
  28. package/dist/decode/message.js.map +1 -0
  29. package/dist/decode/other.d.ts +25 -0
  30. package/dist/decode/other.d.ts.map +1 -0
  31. package/dist/decode/other.js +326 -0
  32. package/dist/decode/other.js.map +1 -0
  33. package/dist/decode/positioned.d.ts +132 -0
  34. package/dist/decode/positioned.d.ts.map +1 -0
  35. package/dist/decode/positioned.js +1063 -0
  36. package/dist/decode/positioned.js.map +1 -0
  37. package/dist/decode/reports.d.ts +13 -0
  38. package/dist/decode/reports.d.ts.map +1 -0
  39. package/dist/decode/reports.js +375 -0
  40. package/dist/decode/reports.js.map +1 -0
  41. package/dist/decoder.d.ts +23 -0
  42. package/dist/decoder.d.ts.map +1 -0
  43. package/dist/decoder.js +75 -0
  44. package/dist/decoder.js.map +1 -0
  45. package/dist/deviceid-data.d.ts +33 -0
  46. package/dist/deviceid-data.d.ts.map +1 -0
  47. package/dist/deviceid-data.js +2941 -0
  48. package/dist/deviceid-data.js.map +1 -0
  49. package/dist/deviceid.d.ts +12 -0
  50. package/dist/deviceid.d.ts.map +1 -0
  51. package/dist/deviceid.js +86 -0
  52. package/dist/deviceid.js.map +1 -0
  53. package/dist/diagnostics.d.ts +48 -0
  54. package/dist/diagnostics.d.ts.map +1 -0
  55. package/dist/diagnostics.js +65 -0
  56. package/dist/diagnostics.js.map +1 -0
  57. package/dist/encode/common.d.ts +19 -0
  58. package/dist/encode/common.d.ts.map +1 -0
  59. package/dist/encode/common.js +84 -0
  60. package/dist/encode/common.js.map +1 -0
  61. package/dist/encode/index.d.ts +35 -0
  62. package/dist/encode/index.d.ts.map +1 -0
  63. package/dist/encode/index.js +248 -0
  64. package/dist/encode/index.js.map +1 -0
  65. package/dist/encode/other.d.ts +21 -0
  66. package/dist/encode/other.d.ts.map +1 -0
  67. package/dist/encode/other.js +253 -0
  68. package/dist/encode/other.js.map +1 -0
  69. package/dist/encode/position.d.ts +31 -0
  70. package/dist/encode/position.d.ts.map +1 -0
  71. package/dist/encode/position.js +729 -0
  72. package/dist/encode/position.js.map +1 -0
  73. package/dist/header.d.ts +42 -0
  74. package/dist/header.d.ts.map +1 -0
  75. package/dist/header.js +206 -0
  76. package/dist/header.js.map +1 -0
  77. package/dist/index.d.ts +13 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +14 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/neutral.d.ts +22 -0
  82. package/dist/neutral.d.ts.map +1 -0
  83. package/dist/neutral.js +593 -0
  84. package/dist/neutral.js.map +1 -0
  85. package/dist/symbols-data.d.ts +832 -0
  86. package/dist/symbols-data.d.ts.map +1 -0
  87. package/dist/symbols-data.js +173 -0
  88. package/dist/symbols-data.js.map +1 -0
  89. package/dist/symbols.d.ts +35 -0
  90. package/dist/symbols.d.ts.map +1 -0
  91. package/dist/symbols.js +71 -0
  92. package/dist/symbols.js.map +1 -0
  93. package/dist/types.d.ts +458 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +4 -0
  96. package/dist/types.js.map +1 -0
  97. package/package.json +68 -0
  98. package/src/builder.ts +617 -0
  99. package/src/bytes.ts +118 -0
  100. package/src/codes.ts +218 -0
  101. package/src/decode/context.ts +63 -0
  102. package/src/decode/index.ts +135 -0
  103. package/src/decode/message.ts +194 -0
  104. package/src/decode/other.ts +307 -0
  105. package/src/decode/positioned.ts +1161 -0
  106. package/src/decode/reports.ts +378 -0
  107. package/src/decoder.ts +76 -0
  108. package/src/deviceid-data.ts +2954 -0
  109. package/src/deviceid.ts +83 -0
  110. package/src/diagnostics.ts +86 -0
  111. package/src/encode/common.ts +87 -0
  112. package/src/encode/index.ts +280 -0
  113. package/src/encode/other.ts +247 -0
  114. package/src/encode/position.ts +656 -0
  115. package/src/header.ts +221 -0
  116. package/src/index.ts +57 -0
  117. package/src/neutral.ts +595 -0
  118. package/src/symbols-data.ts +181 -0
  119. package/src/symbols.ts +85 -0
  120. package/src/types.ts +571 -0
@@ -0,0 +1,1063 @@
1
+ // Positions, and everything that can follow one: data extensions, weather data, and the
2
+ // structured elements lifted out of a comment (APRS12c ch. 6-9, 12, 13, 18).
3
+ // ---- small helpers
4
+ export function isDigit(c) {
5
+ return c !== undefined && c >= '0' && c <= '9';
6
+ }
7
+ export function isBase91(c) {
8
+ if (c === undefined || c.length === 0)
9
+ return false;
10
+ const code = c.charCodeAt(0);
11
+ return code >= 33 && code <= 123;
12
+ }
13
+ export function base91Value(s) {
14
+ let v = 0;
15
+ for (let i = 0; i < s.length; i++)
16
+ v = v * 91 + (s.charCodeAt(i) - 33);
17
+ return v;
18
+ }
19
+ function isPrintableSymbolCode(c) {
20
+ if (c === undefined)
21
+ return false;
22
+ const code = c.charCodeAt(0);
23
+ return code >= 0x21 && code <= 0x7e;
24
+ }
25
+ /** Whether `c` can be an uncompressed symbol table identifier: `/`, `\`, `0`-`9`, `A`-`Z`. */
26
+ export function isSymbolTable(c) {
27
+ return c === '/' || c === '\\' || isDigit(c) || (c !== undefined && c >= 'A' && c <= 'Z');
28
+ }
29
+ function isCompressedTable(c) {
30
+ return c === '/' || c === '\\' || (c !== undefined && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'j')));
31
+ }
32
+ export const KNOTS_TO_MPH = 1.150779448;
33
+ export const METRES_PER_FOOT = 0.3048;
34
+ // ---- timestamps
35
+ /** A 7-byte timestamp shaped `ddhhmmz`, `ddhhmm/` or `hhmmssh` at `i`, without checking ranges. */
36
+ export function timestampAt(s, i) {
37
+ if (s.length < i + 7)
38
+ return undefined;
39
+ for (let k = 0; k < 6; k++)
40
+ if (!isDigit(s[i + k]))
41
+ return undefined;
42
+ const a = Number(s.substr(i, 2));
43
+ const b = Number(s.substr(i + 2, 2));
44
+ const c = Number(s.substr(i + 4, 2));
45
+ switch (s[i + 6]) {
46
+ case 'z':
47
+ return { kind: 'dhm-zulu', day: a, hour: b, minute: c };
48
+ case '/':
49
+ return { kind: 'dhm-local', day: a, hour: b, minute: c };
50
+ case 'h':
51
+ return { kind: 'hms', hour: a, minute: b, second: c };
52
+ default:
53
+ return undefined;
54
+ }
55
+ }
56
+ /** Whether a timestamp's fields are in range. */
57
+ export function timestampValid(t) {
58
+ switch (t.kind) {
59
+ case 'dhm-zulu':
60
+ case 'dhm-local':
61
+ return t.day >= 1 && t.day <= 31 && t.hour <= 23 && t.minute <= 59;
62
+ case 'hms':
63
+ return t.hour <= 23 && t.minute <= 59 && t.second <= 59;
64
+ case 'mdhm':
65
+ return t.month >= 1 && t.month <= 12 && t.day >= 1 && t.day <= 31 && t.hour <= 23 && t.minute <= 59;
66
+ }
67
+ }
68
+ function ambiguityDigits(chars) {
69
+ // chars are the four blankable digits, most significant first; blanks must trail.
70
+ let blanks = 0;
71
+ for (let k = chars.length - 1; k >= 0; k--) {
72
+ if (chars[k] === ' ')
73
+ blanks++;
74
+ else
75
+ break;
76
+ }
77
+ for (let k = 0; k < chars.length - blanks; k++)
78
+ if (!isDigit(chars[k]))
79
+ return undefined;
80
+ return blanks;
81
+ }
82
+ function ambiguousMinutes(tens, units, tenths, hundredths, ambiguity) {
83
+ const d = (c) => (isDigit(c) ? c.charCodeAt(0) - 48 : 0);
84
+ switch (ambiguity) {
85
+ case 0:
86
+ return d(tens) * 10 + d(units) + d(tenths) / 10 + d(hundredths) / 100;
87
+ case 1:
88
+ return d(tens) * 10 + d(units) + d(tenths) / 10 + 0.05;
89
+ case 2:
90
+ return d(tens) * 10 + d(units) + 0.5;
91
+ case 3:
92
+ return d(tens) * 10 + 5;
93
+ default:
94
+ return 30;
95
+ }
96
+ }
97
+ /** Parses an uncompressed position (`ddmm.hhN/dddmm.hhW$`) at `i`; 19 bytes. */
98
+ export function parseUncompressed(ctx, s, i) {
99
+ if (s.length < i + 19)
100
+ ctx.fail('truncated');
101
+ // Latitude
102
+ const lat = s.substr(i, 8);
103
+ if (!isDigit(lat[0]) || !isDigit(lat[1]) || lat[4] !== '.')
104
+ ctx.fail('invalid-latitude');
105
+ const latAmbiguity = ambiguityDigits([lat[2], lat[3], lat[5], lat[6]]);
106
+ if (latAmbiguity === undefined)
107
+ ctx.fail('invalid-latitude');
108
+ const latDegrees = Number(lat.substr(0, 2));
109
+ const latMinutes = ambiguousMinutes(lat[2], lat[3], lat[5], lat[6], latAmbiguity);
110
+ const rawLatMinutes = ambiguousMinutes(lat[2], lat[3], lat[5], lat[6], 0);
111
+ if (rawLatMinutes >= 60 || latDegrees > 90 || (latDegrees === 90 && rawLatMinutes > 0))
112
+ ctx.fail('invalid-latitude');
113
+ let ns = lat[7];
114
+ if (ns === 'n' || ns === 's') {
115
+ ctx.tolerate('lowercase-hemisphere');
116
+ ns = ns.toUpperCase();
117
+ }
118
+ if (ns !== 'N' && ns !== 'S')
119
+ ctx.fail('invalid-latitude');
120
+ // Symbol table
121
+ const table = s[i + 8];
122
+ if (!isSymbolTable(table))
123
+ ctx.fail('invalid-symbol-table');
124
+ // Longitude
125
+ const lon = s.substr(i + 9, 9);
126
+ if (!isDigit(lon[0]) || !isDigit(lon[1]) || !isDigit(lon[2]) || lon[5] !== '.')
127
+ ctx.fail('invalid-longitude');
128
+ const lonBlanks = ambiguityDigits([lon[3], lon[4], lon[6], lon[7]]);
129
+ if (lonBlanks === undefined || lonBlanks > latAmbiguity)
130
+ ctx.fail('invalid-longitude');
131
+ const lonDegrees = Number(lon.substr(0, 3));
132
+ const lonMinutes = ambiguousMinutes(lon[3], lon[4], lon[6], lon[7], latAmbiguity);
133
+ const rawLonMinutes = ambiguousMinutes(lon[3], lon[4], lon[6], lon[7], 0);
134
+ if (rawLonMinutes >= 60 || lonDegrees > 180 || (lonDegrees === 180 && rawLonMinutes > 0))
135
+ ctx.fail('invalid-longitude');
136
+ let ew = lon[8];
137
+ if (ew === 'e' || ew === 'w') {
138
+ ctx.tolerate('lowercase-hemisphere');
139
+ ew = ew.toUpperCase();
140
+ }
141
+ if (ew !== 'E' && ew !== 'W')
142
+ ctx.fail('invalid-longitude');
143
+ const code = s[i + 18];
144
+ if (!isPrintableSymbolCode(code))
145
+ ctx.fail('invalid-symbol-code');
146
+ const latitude = (ns === 'S' ? -1 : 1) * (latDegrees + latMinutes / 60);
147
+ const longitude = (ew === 'W' ? -1 : 1) * (lonDegrees + lonMinutes / 60);
148
+ return {
149
+ latDegrees,
150
+ latMinutes,
151
+ latSouth: ns === 'S',
152
+ lonDegrees,
153
+ lonMinutes,
154
+ lonWest: ew === 'W',
155
+ // A zero coordinate keeps its hemisphere as the sign of zero, so it is written back the same.
156
+ latitude,
157
+ longitude,
158
+ ambiguity: latAmbiguity,
159
+ symbol: { table, code },
160
+ compressed: false,
161
+ csBlank: true,
162
+ length: 19,
163
+ };
164
+ }
165
+ const COMPRESSION_SOURCES = ['other', 'gll', 'gga', 'rmc'];
166
+ const COMPRESSION_ORIGINS = [
167
+ 'compressed',
168
+ 'tnc-beacon-text',
169
+ 'software',
170
+ 'reserved3',
171
+ 'kpc3',
172
+ 'pico',
173
+ 'other-tracker',
174
+ 'digipeater-conversion',
175
+ ];
176
+ /** Decodes a compression type byte's value (0-63). */
177
+ export function compressionTypeOf(t) {
178
+ return {
179
+ fix: (t & 0x20) !== 0 ? 'current' : 'old',
180
+ source: COMPRESSION_SOURCES[(t >> 3) & 3],
181
+ origin: COMPRESSION_ORIGINS[t & 7],
182
+ };
183
+ }
184
+ /** Parses a compressed position (`/YYYYXXXX$csT`) at `i`; 13 bytes. */
185
+ export function parseCompressed(ctx, s, i) {
186
+ if (s.length < i + 13)
187
+ ctx.fail('truncated');
188
+ let table = s[i];
189
+ if (table >= 'a' && table <= 'j')
190
+ table = String.fromCharCode(table.charCodeAt(0) - 49); // a-j -> 0-9
191
+ for (let k = 1; k <= 8; k++)
192
+ if (!isBase91(s[i + k]))
193
+ ctx.fail('invalid-compressed-position');
194
+ const latitude = 90 - base91Value(s.substr(i + 1, 4)) / 380926;
195
+ const longitude = -180 + base91Value(s.substr(i + 5, 4)) / 190463;
196
+ if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180)
197
+ ctx.fail('invalid-compressed-position');
198
+ const code = s[i + 9];
199
+ if (!isPrintableSymbolCode(code))
200
+ ctx.fail('invalid-symbol-code');
201
+ const c = s[i + 10];
202
+ const sByte = s[i + 11];
203
+ const tByte = s[i + 12];
204
+ const pos = {
205
+ latDegrees: 0,
206
+ latMinutes: 0,
207
+ latSouth: false,
208
+ lonDegrees: 0,
209
+ lonMinutes: 0,
210
+ lonWest: false,
211
+ latitude,
212
+ longitude,
213
+ ambiguity: 0,
214
+ symbol: { table, code },
215
+ compressed: true,
216
+ csBlank: c === ' ',
217
+ length: 13,
218
+ };
219
+ if (c === ' ')
220
+ return pos;
221
+ if (!isBase91(c) || !isBase91(sByte) || !isBase91(tByte))
222
+ ctx.fail('invalid-compressed-position');
223
+ let t = tByte.charCodeAt(0) - 33;
224
+ if ((t & 0xc0) !== 0) {
225
+ ctx.tolerate('compression-type-reserved-bits');
226
+ t &= 0x3f;
227
+ }
228
+ pos.compression = compressionTypeOf(t);
229
+ const cv = c.charCodeAt(0) - 33;
230
+ const sv = sByte.charCodeAt(0) - 33;
231
+ if (pos.compression.source === 'gga') {
232
+ pos.csAltitudeFeet = Math.pow(1.002, cv * 91 + sv);
233
+ }
234
+ else if (cv <= 89) {
235
+ pos.csCourseRaw = cv * 4;
236
+ pos.csCourse = cv === 0 ? 360 : cv * 4;
237
+ pos.csSpeedKnots = Math.pow(1.08, sv) - 1;
238
+ }
239
+ else {
240
+ pos.csRangeMiles = 2 * Math.pow(1.08, sv);
241
+ }
242
+ return pos;
243
+ }
244
+ /** Parses a position at `i`, uncompressed or compressed by its first byte. */
245
+ export function parsePosition(ctx, s, i) {
246
+ const c = s[i];
247
+ if (c === undefined)
248
+ ctx.fail('truncated');
249
+ if (isDigit(c))
250
+ return parseUncompressed(ctx, s, i);
251
+ if (isCompressedTable(c))
252
+ return parseCompressed(ctx, s, i);
253
+ return ctx.fail('invalid-position');
254
+ }
255
+ /** Whether a position decodes at `i`, without reporting anything. */
256
+ export function positionDecodesAt(ctx, s, i) {
257
+ const probe = Object.create(ctx);
258
+ // A throwaway context: diagnostics go nowhere and every defect is tolerated silently.
259
+ Object.defineProperty(probe, 'diagnostics', { value: [] });
260
+ try {
261
+ parsePosition(probe, s, i);
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
268
+ function daoAt(s, p) {
269
+ if (s[p] !== '!' || s[p + 4] !== '!')
270
+ return undefined;
271
+ const d = s[p + 1];
272
+ const a = s[p + 2];
273
+ const o = s[p + 3];
274
+ if (d >= 'A' && d <= 'Z') {
275
+ if (a === ' ' && o === ' ')
276
+ return { start: p, dao: { datum: d, precision: 'none' }, latAdd: 0, lonAdd: 0 };
277
+ if (isDigit(a) && isDigit(o))
278
+ return {
279
+ start: p,
280
+ dao: { datum: d, precision: 'thousandths' },
281
+ latAdd: (a.charCodeAt(0) - 48) / 1000,
282
+ lonAdd: (o.charCodeAt(0) - 48) / 1000,
283
+ };
284
+ return undefined;
285
+ }
286
+ if (d >= 'a' && d <= 'z') {
287
+ const datum = d.toUpperCase();
288
+ if (a === ' ' && o === ' ')
289
+ return { start: p, dao: { datum, precision: 'none' }, latAdd: 0, lonAdd: 0 };
290
+ if (isBase91(a) && isBase91(o))
291
+ return {
292
+ start: p,
293
+ dao: { datum, precision: 'base91' },
294
+ latAdd: (a.charCodeAt(0) - 33) / 91 / 100,
295
+ lonAdd: (o.charCodeAt(0) - 33) / 91 / 100,
296
+ };
297
+ return undefined;
298
+ }
299
+ return undefined;
300
+ }
301
+ function findCommentTelemetry(s) {
302
+ const last = s.lastIndexOf('|');
303
+ if (last <= 0)
304
+ return undefined;
305
+ const prev = s.lastIndexOf('|', last - 1);
306
+ if (prev < 0)
307
+ return undefined;
308
+ const body = s.slice(prev + 1, last);
309
+ if (body.length < 4 || body.length > 14 || body.length % 2 !== 0)
310
+ return undefined;
311
+ for (let k = 0; k < body.length; k++)
312
+ if (!isBase91(body[k]))
313
+ return undefined;
314
+ const values = [];
315
+ for (let k = 0; k < body.length; k += 2)
316
+ values.push(base91Value(body.substr(k, 2)));
317
+ const sequence = values[0];
318
+ const rest = values.slice(1);
319
+ const telemetry = rest.length === 6 ? { sequence, analog: rest.slice(0, 5), digital: rest[5] } : { sequence, analog: rest };
320
+ return { start: prev, end: last + 1, telemetry };
321
+ }
322
+ // ---- data extensions
323
+ const AREA_SHAPES = [
324
+ 'open-circle',
325
+ 'line-down-right',
326
+ 'open-ellipse',
327
+ 'open-triangle',
328
+ 'open-box',
329
+ 'filled-circle',
330
+ 'line-down-left',
331
+ 'filled-ellipse',
332
+ 'filled-triangle',
333
+ 'filled-box',
334
+ ];
335
+ const AREA_COLORS = [
336
+ 'black',
337
+ 'blue',
338
+ 'green',
339
+ 'cyan',
340
+ 'red',
341
+ 'violet',
342
+ 'yellow',
343
+ 'gray',
344
+ 'black-low',
345
+ 'blue-low',
346
+ 'green-low',
347
+ 'cyan-low',
348
+ 'red-low',
349
+ 'violet-low',
350
+ 'yellow-low',
351
+ 'gray-low',
352
+ ];
353
+ export { AREA_SHAPES, AREA_COLORS };
354
+ const PHG_RE = /^PHG([0-9])([0-~])([0-9])([0-9])(?:([1-9A-Z])\/)?/;
355
+ const RNG_RE = /^RNG([0-9]{4})/;
356
+ const DFS_RE = /^DFS([0-9])([0-~])([0-9])([0-9])/;
357
+ const CSE_SPD_RE = /^([0-9]{3}|\.{3}| {3})\/([0-9]{3}|\.{3}| {3})/;
358
+ function rateValue(c) {
359
+ return isDigit(c) ? c.charCodeAt(0) - 48 : c.charCodeAt(0) - 55;
360
+ }
361
+ function extensionAt(s, i) {
362
+ const t = s.slice(i, i + 9);
363
+ let m = PHG_RE.exec(t);
364
+ if (m) {
365
+ const phg = {
366
+ power: Number(m[1]),
367
+ height: m[2].charCodeAt(0) - 48,
368
+ gain: Number(m[3]),
369
+ directivity: Number(m[4]),
370
+ ...(m[5] !== undefined ? { beaconsPerHour: rateValue(m[5]) } : {}),
371
+ };
372
+ return { length: m[0].length, phg };
373
+ }
374
+ m = RNG_RE.exec(t);
375
+ if (m)
376
+ return { length: 7, rangeMiles: Number(m[1]) };
377
+ m = DFS_RE.exec(t);
378
+ if (m)
379
+ return {
380
+ length: 7,
381
+ dfs: { strength: Number(m[1]), height: m[2].charCodeAt(0) - 48, gain: Number(m[3]), directivity: Number(m[4]) },
382
+ };
383
+ return undefined;
384
+ }
385
+ function findLaterExtension(s) {
386
+ for (const key of ['PHG', 'RNG', 'DFS']) {
387
+ let from = 0;
388
+ for (;;) {
389
+ const p = s.indexOf(key, from);
390
+ if (p < 0)
391
+ break;
392
+ const ext = extensionAt(s, p);
393
+ if (ext)
394
+ return { start: p, ext };
395
+ from = p + 1;
396
+ }
397
+ }
398
+ return undefined;
399
+ }
400
+ function threeDigits(v) {
401
+ return /^[0-9]{3}$/.test(v) ? Number(v) : undefined;
402
+ }
403
+ // ---- voice frequency (APRS12c ch. 18)
404
+ const MICROWAVE_BASE = {
405
+ A: 1200,
406
+ B: 2300,
407
+ C: 2400,
408
+ D: 3400,
409
+ E: 5600,
410
+ F: 5700,
411
+ G: 5800,
412
+ H: 10100,
413
+ I: 10200,
414
+ J: 10300,
415
+ K: 10400,
416
+ L: 10500,
417
+ M: 24000,
418
+ N: 24100,
419
+ O: 24200,
420
+ };
421
+ export { MICROWAVE_BASE };
422
+ const FREQ_KHZ_RE = /^([0-9]{3}|[A-O][0-9]{2})\.([0-9]{3})MHz/i;
423
+ const FREQ_10KHZ_RE = /^([0-9]{3}|[A-O][0-9]{2})\.([0-9]{2}) MHz/i;
424
+ function frequencyMhz(whole, fraction) {
425
+ const first = whole[0];
426
+ const base = isDigit(first) ? Number(whole) : MICROWAVE_BASE[first] + Number(whole.slice(1));
427
+ return base + Number(fraction) / Math.pow(10, fraction.length);
428
+ }
429
+ /** Parses a voice frequency and its tone, offset and range fields at the start of `s`. */
430
+ function frequencyAt(s) {
431
+ let m = FREQ_KHZ_RE.exec(s);
432
+ let tenKhz = false;
433
+ if (!m) {
434
+ m = FREQ_10KHZ_RE.exec(s);
435
+ tenKhz = true;
436
+ }
437
+ if (!m)
438
+ return undefined;
439
+ let freq = { mhz: frequencyMhz(m[1], m[2]) };
440
+ if (tenKhz)
441
+ freq.tenKhzResolution = true;
442
+ let p = m[0].length;
443
+ const fieldAt = (re) => {
444
+ if (s[p] !== ' ')
445
+ return undefined;
446
+ const r = re.exec(s.slice(p + 1));
447
+ if (!r)
448
+ return undefined;
449
+ const after = s[p + 1 + r[0].length];
450
+ if (after !== undefined && after !== ' ')
451
+ return undefined;
452
+ return r;
453
+ };
454
+ let r = fieldAt(/^(?:([TtCcDd])([0-9]{3})|([Tt])off|(1|l)750)/);
455
+ if (r) {
456
+ if (r[1] !== undefined) {
457
+ const kind = r[1].toLowerCase() === 't' ? 'tone' : r[1].toLowerCase() === 'c' ? 'ctcss' : 'dcs';
458
+ freq = { ...freq, tone: kind, toneValue: Number(r[2]) };
459
+ if (r[1] >= 'a')
460
+ freq.narrow = true;
461
+ }
462
+ else if (r[3] !== undefined) {
463
+ freq = { ...freq, tone: 'off' };
464
+ if (r[3] === 't')
465
+ freq.narrow = true;
466
+ }
467
+ else {
468
+ freq = { ...freq, tone: 'tone-burst' };
469
+ if (r[4] === 'l')
470
+ freq.narrow = true;
471
+ }
472
+ p += 1 + r[0].length;
473
+ }
474
+ r = fieldAt(/^([+-])([0-9]{3})/);
475
+ if (r) {
476
+ freq.offsetKhz = (r[1] === '-' ? -1 : 1) * Number(r[2]) * 10;
477
+ p += 1 + r[0].length;
478
+ }
479
+ r = fieldAt(/^R([0-9]{2})([mk])/);
480
+ if (r) {
481
+ freq.range = Number(r[1]);
482
+ if (r[2] === 'k')
483
+ freq.rangeKm = true;
484
+ p += 1 + r[0].length;
485
+ }
486
+ // One space after the frequency and its fields is a separator.
487
+ if (s[p] === ' ')
488
+ p++;
489
+ return { frequency: sortFrequency(freq), length: p };
490
+ }
491
+ function sortFrequency(f) {
492
+ const out = { mhz: f.mhz };
493
+ if (f.tone !== undefined)
494
+ out.tone = f.tone;
495
+ if (f.toneValue !== undefined)
496
+ out.toneValue = f.toneValue;
497
+ if (f.offsetKhz !== undefined)
498
+ out.offsetKhz = f.offsetKhz;
499
+ if (f.range !== undefined)
500
+ out.range = f.range;
501
+ if (f.rangeKm)
502
+ out.rangeKm = true;
503
+ if (f.narrow)
504
+ out.narrow = true;
505
+ if (f.tenKhzResolution)
506
+ out.tenKhzResolution = true;
507
+ return out;
508
+ }
509
+ function removeRanges(s, ranges) {
510
+ const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
511
+ let out = '';
512
+ let from = 0;
513
+ for (const [a, b] of sorted) {
514
+ out += s.slice(from, a);
515
+ from = b;
516
+ }
517
+ return out + s.slice(from);
518
+ }
519
+ /** Lifts base-91 telemetry and the last `!DAO!` outside it. */
520
+ function liftTelemetryAndDao(s) {
521
+ const tel = findCommentTelemetry(s);
522
+ let dao;
523
+ for (let p = s.length - 5; p >= 0; p--) {
524
+ if (tel && p + 5 > tel.start && p < tel.end)
525
+ continue;
526
+ const found = daoAt(s, p);
527
+ if (found) {
528
+ dao = found;
529
+ break;
530
+ }
531
+ }
532
+ const ranges = [];
533
+ if (tel)
534
+ ranges.push([tel.start, tel.end]);
535
+ if (dao)
536
+ ranges.push([dao.start, dao.start + 5]);
537
+ const out = { rest: removeRanges(s, ranges) };
538
+ if (tel)
539
+ out.telemetry = tel.telemetry;
540
+ if (dao)
541
+ out.dao = dao;
542
+ return out;
543
+ }
544
+ const ALTITUDE_RE = /\/A=(-[0-9]{5}|[0-9]{6})/;
545
+ /** Lifts the structured elements out of a comment, in the order the vectors' README gives. */
546
+ function liftComment(ctx, text, options) {
547
+ const first = liftTelemetryAndDao(text);
548
+ const lifted = { rest: first.rest };
549
+ if (first.telemetry)
550
+ lifted.telemetry = first.telemetry;
551
+ if (first.dao)
552
+ lifted.dao = first.dao;
553
+ let s = first.rest;
554
+ let extensionFound = options.extensionFound;
555
+ if (options.extensionAtStart) {
556
+ const ext = extensionAt(s, 0);
557
+ if (ext) {
558
+ applyExtension(lifted, ext);
559
+ s = s.slice(ext.length);
560
+ extensionFound = true;
561
+ }
562
+ }
563
+ // Altitude anywhere.
564
+ const alt = ALTITUDE_RE.exec(s);
565
+ if (alt) {
566
+ lifted.altitudeFeet = Number(alt[1]);
567
+ s = s.slice(0, alt.index) + s.slice(alt.index + alt[0].length);
568
+ }
569
+ // Signpost or corridor braces.
570
+ if (isSignpost(options.symbol) || options.areaLine) {
571
+ const brace = /\{([^{}]{1,3})\}/.exec(s);
572
+ if (brace) {
573
+ if (isSignpost(options.symbol)) {
574
+ lifted.signpost = brace[1];
575
+ s = s.slice(0, brace.index) + s.slice(brace.index + brace[0].length);
576
+ }
577
+ else if (/^[0-9]{1,3}$/.test(brace[1])) {
578
+ lifted.corridorWidthMiles = Number(brace[1]);
579
+ s = s.slice(0, brace.index) + s.slice(brace.index + brace[0].length);
580
+ }
581
+ }
582
+ }
583
+ // A data extension later in the text, only when none came straight after the symbol.
584
+ if (!extensionFound) {
585
+ const later = findLaterExtension(s);
586
+ if (later && ctx.allows('data-extension-in-comment')) {
587
+ applyExtension(lifted, later.ext);
588
+ s = s.slice(0, later.start) + s.slice(later.start + later.ext.length);
589
+ }
590
+ }
591
+ // A voice frequency at the start, after at most one space or /.
592
+ let freq = frequencyAt(s);
593
+ let skip = 0;
594
+ if (!freq && (s[0] === ' ' || s[0] === '/')) {
595
+ freq = frequencyAt(s.slice(1));
596
+ skip = 1;
597
+ }
598
+ if (freq) {
599
+ lifted.frequency = freq.frequency;
600
+ s = s.slice(skip + freq.length);
601
+ }
602
+ // One leading delimiter.
603
+ if (s[0] === ' ' || s[0] === '/')
604
+ s = s.slice(1);
605
+ lifted.rest = s;
606
+ return lifted;
607
+ }
608
+ function applyExtension(lifted, ext) {
609
+ if (ext.phg)
610
+ lifted.phg = ext.phg;
611
+ if (ext.rangeMiles !== undefined)
612
+ lifted.rangeMiles = ext.rangeMiles;
613
+ if (ext.dfs)
614
+ lifted.dfs = ext.dfs;
615
+ }
616
+ function isSignpost(symbol) {
617
+ return symbol.table === '\\' && symbol.code === 'm';
618
+ }
619
+ // ---- weather data (APRS12c ch. 12)
620
+ const WEATHER_WIDTH = {
621
+ c: 3,
622
+ s: 3,
623
+ g: 3,
624
+ t: 3,
625
+ r: 3,
626
+ p: 3,
627
+ P: 3,
628
+ h: 2,
629
+ b: 5,
630
+ L: 3,
631
+ l: 3,
632
+ '#': 3,
633
+ };
634
+ /**
635
+ * A weather field's value: a run of dots (unknown) at most `width` long, exactly `width` spaces
636
+ * (unknown), or digits.
637
+ */
638
+ function weatherValueRun(s, j, width, signed) {
639
+ if (s[j] === ' ')
640
+ return s.slice(j, j + width) === ' '.repeat(width) ? s.slice(j, j + width) : '';
641
+ if (s[j] === '.') {
642
+ let k = j;
643
+ while (s[k] === '.' && k - j < width)
644
+ k++;
645
+ return s.slice(j, k);
646
+ }
647
+ let k = j;
648
+ if (signed && s[k] === '-')
649
+ k++;
650
+ const digitsFrom = k;
651
+ while (isDigit(s[k]))
652
+ k++;
653
+ if (k === digitsFrom)
654
+ return '';
655
+ return s.slice(j, k);
656
+ }
657
+ function isLetter(c) {
658
+ return c !== undefined && ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
659
+ }
660
+ /** Parses the run of weather fields from `j`. */
661
+ function parseWeatherFields(ctx, s, j, options) {
662
+ const w = {};
663
+ if (options.wind.direction !== undefined)
664
+ w.windDirectionDegrees = options.wind.direction;
665
+ if (options.wind.speedMph !== undefined)
666
+ w.windSpeedMph = options.wind.speedMph;
667
+ const seen = new Set();
668
+ let windKnown = options.wind.present;
669
+ let windFromFields = false;
670
+ let warnedWindFields = false;
671
+ const extra = [];
672
+ for (;;) {
673
+ const letter = s[j];
674
+ if (letter === undefined)
675
+ break;
676
+ let key = letter;
677
+ const width = WEATHER_WIDTH[letter];
678
+ if (letter === 's') {
679
+ // s is the wind speed after c (or first thing in a positionless report), else snowfall.
680
+ const windSlot = !seen.has('s:wind') && (seen.has('c') || (options.positionless && !windKnown));
681
+ key = windSlot ? 's:wind' : 's:snow';
682
+ }
683
+ if (width !== undefined) {
684
+ if (seen.has(key))
685
+ break;
686
+ if (letter === 'c' && windKnown)
687
+ break;
688
+ if (key === 's:snow') {
689
+ const v = s.slice(j + 1, j + 4);
690
+ const dots = /^\.+/.exec(s.slice(j + 1))?.[0];
691
+ if (dots !== undefined) {
692
+ if (dots.length !== 3)
693
+ ctx.tolerate('non-standard-weather-field-width');
694
+ seen.add(key);
695
+ j += 1 + dots.length;
696
+ continue;
697
+ }
698
+ if (!/^[0-9.]{3}$/.test(v) || !/[0-9]/.test(v))
699
+ break;
700
+ w.snow24hIn = Number(v);
701
+ seen.add(key);
702
+ j += 4;
703
+ continue;
704
+ }
705
+ let run = weatherValueRun(s, j + 1, width, letter === 't');
706
+ if (run.length === 0)
707
+ break;
708
+ if (run.length > width + 1)
709
+ run = run.slice(0, width);
710
+ if (run.length !== width)
711
+ ctx.tolerate('non-standard-weather-field-width');
712
+ seen.add(key);
713
+ j += 1 + run.length;
714
+ const unknown = /^(?:\.+| +)$/.test(run);
715
+ if (letter === 'c' || key === 's:wind') {
716
+ windFromFields = true;
717
+ if (!warnedWindFields && !options.positionless) {
718
+ warnedWindFields = true;
719
+ ctx.tolerate('wind-fields-instead-of-extension');
720
+ }
721
+ }
722
+ if (unknown)
723
+ continue;
724
+ const v = Number(run);
725
+ switch (key) {
726
+ case 'c':
727
+ if (v > 360)
728
+ ctx.tolerate('out-of-range-value');
729
+ else
730
+ w.windDirectionDegrees = v;
731
+ break;
732
+ case 's:wind':
733
+ w.windSpeedMph = v;
734
+ break;
735
+ case 'g':
736
+ w.windGustMph = v;
737
+ break;
738
+ case 't':
739
+ w.temperatureF = v;
740
+ break;
741
+ case 'r':
742
+ w.rain1hIn = v / 100;
743
+ break;
744
+ case 'p':
745
+ w.rain24hIn = v / 100;
746
+ break;
747
+ case 'P':
748
+ w.rainMidnightIn = v / 100;
749
+ break;
750
+ case 'h':
751
+ if (v > 100)
752
+ ctx.tolerate('out-of-range-value');
753
+ else
754
+ w.humidityPercent = v === 0 ? 100 : v;
755
+ break;
756
+ case 'b':
757
+ w.pressureMbar = v / 10;
758
+ break;
759
+ case 'L':
760
+ w.luminosityWM2 = v;
761
+ break;
762
+ case 'l':
763
+ w.luminosityWM2 = v + 1000;
764
+ break;
765
+ case '#':
766
+ w.rainRaw = v;
767
+ break;
768
+ }
769
+ continue;
770
+ }
771
+ if (isLetter(letter)) {
772
+ const m = /^[0-9.-]{2,}/.exec(s.slice(j + 1));
773
+ if (m && isDigit(m[0][m[0].length - 1])) {
774
+ extra.push({ letter, value: m[0] });
775
+ j += 1 + m[0].length;
776
+ continue;
777
+ }
778
+ }
779
+ break;
780
+ }
781
+ if (seen.has('c') || seen.has('s:wind'))
782
+ windKnown = true;
783
+ if (extra.length > 0)
784
+ w.extra = extra;
785
+ // Completeness: wind, and gust and temperature.
786
+ if (options.positionless) {
787
+ if (!seen.has('c') || !seen.has('s:wind') || !seen.has('g') || !seen.has('t'))
788
+ ctx.tolerate('incomplete-weather');
789
+ }
790
+ else {
791
+ // An uncompressed report's wind (the DDD/SSS extension, or c/s fields), then gust and temperature.
792
+ if (!windKnown && !options.compressed)
793
+ ctx.tolerate('incomplete-weather');
794
+ if (!seen.has('g') || !seen.has('t'))
795
+ ctx.tolerate('incomplete-weather');
796
+ }
797
+ return { weather: w, end: j, windFromFields };
798
+ }
799
+ const SOFTWARE_UNIT_RE = /^([A-Za-z])([A-Za-z0-9_-]{2,4})$/;
800
+ /**
801
+ * The software type and unit after the weather fields, or the weather comment. One leading
802
+ * delimiter is dropped from a position's comment, not from a positionless report's.
803
+ */
804
+ function weatherTail(ctx, w, rest, dropDelimiter = true) {
805
+ const lifted = liftTelemetryAndDao(rest);
806
+ const out = {};
807
+ if (lifted.telemetry)
808
+ out.telemetry = lifted.telemetry;
809
+ if (lifted.dao)
810
+ out.dao = lifted.dao;
811
+ let text = lifted.rest;
812
+ if (text.length === 0)
813
+ return out;
814
+ const m = SOFTWARE_UNIT_RE.exec(text);
815
+ if (m && !/^[0-9]+$/.test(m[2])) {
816
+ w.software = m[1];
817
+ w.unit = m[2];
818
+ return out;
819
+ }
820
+ ctx.tolerate('weather-comment');
821
+ if (dropDelimiter && (text[0] === ' ' || text[0] === '/'))
822
+ text = text.slice(1);
823
+ if (text.length > 0)
824
+ out.comment = text;
825
+ return out;
826
+ }
827
+ /** Parses a positionless weather report's fields and tail from `j`. */
828
+ export function parsePositionlessWeather(ctx, s, j) {
829
+ const parsed = parseWeatherFields(ctx, s, j, { wind: { present: false }, positionless: true });
830
+ const tail = weatherTail(ctx, parsed.weather, s.slice(parsed.end), false);
831
+ const out = { weather: parsed.weather };
832
+ if (tail.comment !== undefined)
833
+ out.comment = ctx.text(tail.comment);
834
+ return out;
835
+ }
836
+ // ---- after the position: extension, weather or comment
837
+ /** What follows a position, decoded into the fields. */
838
+ export function decodeAfterPosition(ctx, s, i, pos) {
839
+ const f = { latitude: pos.latitude, longitude: pos.longitude, symbol: pos.symbol };
840
+ if (pos.ambiguity > 0)
841
+ f.ambiguity = pos.ambiguity;
842
+ if (pos.compressed) {
843
+ f.compressed = true;
844
+ if (pos.compression)
845
+ f.compression = pos.compression;
846
+ }
847
+ let dao;
848
+ if (pos.symbol.code === '_') {
849
+ dao = decodeWeatherAfterPosition(ctx, s, i, pos, f);
850
+ }
851
+ else {
852
+ dao = decodeExtensionAndComment(ctx, s, i, pos, f);
853
+ }
854
+ applyDao(ctx, f, pos, dao);
855
+ return f;
856
+ }
857
+ /** Applies a `!DAO!` to the fields. */
858
+ export function applyDao(ctx, f, pos, dao) {
859
+ if (!dao)
860
+ return;
861
+ f.dao = dao.dao;
862
+ if (pos.compressed)
863
+ return;
864
+ if (pos.ambiguity > 0) {
865
+ ctx.tolerate('dao-with-ambiguity');
866
+ return;
867
+ }
868
+ const lat = (pos.latSouth ? -1 : 1) * (pos.latDegrees + (pos.latMinutes + dao.latAdd) / 60);
869
+ const lon = (pos.lonWest ? -1 : 1) * (pos.lonDegrees + (pos.lonMinutes + dao.lonAdd) / 60);
870
+ f.latitude = lat;
871
+ f.longitude = lon;
872
+ }
873
+ function decodeExtensionAndComment(ctx, s, i, pos, f) {
874
+ let extensionFound = false;
875
+ if (pos.compressed) {
876
+ if (pos.csCourse !== undefined) {
877
+ f.courseDegrees = pos.csCourse;
878
+ f.speedKnots = pos.csSpeedKnots;
879
+ }
880
+ else if (pos.csRangeMiles !== undefined) {
881
+ f.rangeMiles = pos.csRangeMiles;
882
+ extensionFound = true;
883
+ }
884
+ else if (pos.csAltitudeFeet !== undefined) {
885
+ f.altitudeFeet = pos.csAltitudeFeet;
886
+ }
887
+ }
888
+ else {
889
+ const isArea = pos.symbol.table === '\\' && pos.symbol.code === 'l';
890
+ const t = s.slice(i, i + 7);
891
+ const cs = CSE_SPD_RE.exec(t);
892
+ let area = null;
893
+ if (isArea)
894
+ area = /^([0-9])([0-9]{2})(\/[0-9]|1[0-5])([0-9]{2})/.exec(t);
895
+ if (area) {
896
+ const colorCode = area[3].startsWith('/') ? Number(area[3][1]) : Number(area[3]);
897
+ f.area = {
898
+ shape: AREA_SHAPES[Number(area[1])],
899
+ color: AREA_COLORS[colorCode],
900
+ latOffset: Number(area[2]),
901
+ lonOffset: Number(area[4]),
902
+ };
903
+ i += 7;
904
+ }
905
+ else if (cs) {
906
+ const course = threeDigits(cs[1]);
907
+ const speed = threeDigits(cs[2]);
908
+ if (course !== undefined) {
909
+ if (course > 360)
910
+ ctx.tolerate('out-of-range-value');
911
+ else
912
+ f.courseDegrees = course;
913
+ }
914
+ if (speed !== undefined)
915
+ f.speedKnots = speed;
916
+ i += 7;
917
+ if (pos.symbol.table === '/' && pos.symbol.code === '\\') {
918
+ const df = /^\/([0-9]{3})\/([0-9])([0-9])([0-9])/.exec(s.slice(i, i + 8));
919
+ if (df) {
920
+ f.dfBearing = {
921
+ bearingDegrees: Number(df[1]),
922
+ number: Number(df[2]),
923
+ range: Number(df[3]),
924
+ quality: Number(df[4]),
925
+ };
926
+ i += 8;
927
+ }
928
+ }
929
+ if (pos.symbol.code === '@' && (pos.symbol.table === '/' || pos.symbol.table === '\\')) {
930
+ const storm = parseStorm(s.slice(i));
931
+ if (storm) {
932
+ f.storm = storm.storm;
933
+ i += storm.length;
934
+ }
935
+ }
936
+ }
937
+ else {
938
+ const ext = extensionAt(s, i);
939
+ if (ext) {
940
+ if (ext.phg)
941
+ f.phg = ext.phg;
942
+ if (ext.rangeMiles !== undefined)
943
+ f.rangeMiles = ext.rangeMiles;
944
+ if (ext.dfs)
945
+ f.dfs = ext.dfs;
946
+ i += ext.length;
947
+ extensionFound = true;
948
+ }
949
+ }
950
+ }
951
+ const areaLine = f.area !== undefined && (f.area.shape === 'line-down-right' || f.area.shape === 'line-down-left');
952
+ const lifted = liftComment(ctx, s.slice(i), { extensionFound, symbol: pos.symbol, areaLine });
953
+ applyLifted(ctx, f, lifted);
954
+ return lifted.dao;
955
+ }
956
+ function applyLifted(ctx, f, lifted) {
957
+ if (lifted.altitudeFeet !== undefined)
958
+ f.altitudeFeet = lifted.altitudeFeet;
959
+ if (lifted.phg)
960
+ f.phg = lifted.phg;
961
+ if (lifted.rangeMiles !== undefined)
962
+ f.rangeMiles = lifted.rangeMiles;
963
+ if (lifted.dfs)
964
+ f.dfs = lifted.dfs;
965
+ if (lifted.corridorWidthMiles !== undefined && f.area)
966
+ f.area = { ...f.area, corridorWidthMiles: lifted.corridorWidthMiles };
967
+ if (lifted.telemetry)
968
+ f.telemetry = lifted.telemetry;
969
+ if (lifted.frequency)
970
+ f.frequency = lifted.frequency;
971
+ if (lifted.signpost !== undefined)
972
+ f.signpost = ctx.text(lifted.signpost);
973
+ if (lifted.rest.length > 0) {
974
+ const text = ctx.text(lifted.rest);
975
+ if (text.length > 0)
976
+ f.comment = text;
977
+ }
978
+ }
979
+ const STORM_TYPES = { TS: 'tropical-storm', HC: 'hurricane', TD: 'tropical-depression' };
980
+ function parseStorm(s) {
981
+ const m = /^\/(TS|HC|TD)\/([0-9 .]{3})\^([0-9 .]{3})\/([0-9 .]{4})>([0-9 .]{3})&([0-9 .]{3})(?:%([0-9 .]{3}))?/.exec(s);
982
+ if (!m)
983
+ return undefined;
984
+ const num = (v) => (v !== undefined && /^[0-9]+$/.test(v) ? Number(v) : undefined);
985
+ const storm = { type: STORM_TYPES[m[1]] };
986
+ const set = (key, v) => {
987
+ const n = num(v);
988
+ if (n !== undefined)
989
+ storm[key] = n;
990
+ };
991
+ set('sustainedWindKnots', m[2]);
992
+ set('gustKnots', m[3]);
993
+ set('centralPressureMbar', m[4]);
994
+ set('hurricaneRadiusNm', m[5]);
995
+ set('tropicalStormRadiusNm', m[6]);
996
+ set('wholeGaleRadiusNm', m[7]);
997
+ return { storm, length: m[0].length };
998
+ }
999
+ const DEFAULT_COMPRESSION = { fix: 'current', source: 'other', origin: 'software' };
1000
+ function decodeWeatherAfterPosition(ctx, s, i, pos, f) {
1001
+ const wind = { present: false };
1002
+ let windFromCs = false;
1003
+ if (pos.compressed && pos.csCourseRaw !== undefined) {
1004
+ wind.present = true;
1005
+ windFromCs = true;
1006
+ wind.direction = pos.csCourseRaw;
1007
+ wind.speedMph = pos.csSpeedKnots * KNOTS_TO_MPH;
1008
+ }
1009
+ else if (pos.compressed && pos.csAltitudeFeet !== undefined) {
1010
+ f.altitudeFeet = pos.csAltitudeFeet;
1011
+ }
1012
+ else if (pos.compressed && pos.csRangeMiles !== undefined) {
1013
+ f.rangeMiles = pos.csRangeMiles;
1014
+ }
1015
+ const ext = CSE_SPD_RE.exec(s.slice(i, i + 7));
1016
+ let windFromExtension = false;
1017
+ if (ext) {
1018
+ if (pos.compressed)
1019
+ ctx.tolerate('wind-extension-after-compressed');
1020
+ wind.present = true;
1021
+ windFromExtension = true;
1022
+ delete wind.direction;
1023
+ delete wind.speedMph;
1024
+ const dir = threeDigits(ext[1]);
1025
+ const spd = threeDigits(ext[2]);
1026
+ if (dir !== undefined) {
1027
+ if (dir > 360)
1028
+ ctx.tolerate('out-of-range-value');
1029
+ else
1030
+ wind.direction = dir;
1031
+ }
1032
+ if (spd !== undefined)
1033
+ wind.speedMph = spd;
1034
+ i += 7;
1035
+ }
1036
+ const parsed = parseWeatherFields(ctx, s, i, { wind, positionless: false, compressed: pos.compressed });
1037
+ const tail = weatherTail(ctx, parsed.weather, s.slice(parsed.end));
1038
+ f.weather = parsed.weather;
1039
+ if (pos.compressed && pos.csBlank && !windFromCs) {
1040
+ const hasWind = parsed.weather.windDirectionDegrees !== undefined || parsed.weather.windSpeedMph !== undefined;
1041
+ if (hasWind && (windFromExtension || parsed.windFromFields))
1042
+ f.compression = DEFAULT_COMPRESSION;
1043
+ }
1044
+ if (tail.telemetry)
1045
+ f.telemetry = tail.telemetry;
1046
+ if (tail.comment !== undefined) {
1047
+ const text = ctx.text(tail.comment);
1048
+ if (text.length > 0)
1049
+ f.comment = text;
1050
+ }
1051
+ return tail.dao;
1052
+ }
1053
+ // ---- Mic-E status text
1054
+ /** Lifts the elements out of Mic-E status text after its type code, altitude and locator. */
1055
+ export function liftMicEComment(ctx, text, symbol) {
1056
+ const lifted = liftComment(ctx, text, { extensionFound: false, extensionAtStart: true, symbol });
1057
+ return { lifted };
1058
+ }
1059
+ /** Applies lifted comment elements to fields (Mic-E). */
1060
+ export function applyLiftedTo(ctx, f, lifted) {
1061
+ applyLifted(ctx, f, lifted);
1062
+ }
1063
+ //# sourceMappingURL=positioned.js.map