@speleotica/frcsdata 4.2.0 → 4.3.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 (81) hide show
  1. package/FrcsPlotFile.d.ts +1 -0
  2. package/FrcsPlotFile.d.ts.map +1 -0
  3. package/FrcsPlotFile.js +2 -1
  4. package/FrcsPlotFile.js.map +1 -0
  5. package/FrcsPlotShot.d.ts +1 -0
  6. package/FrcsPlotShot.d.ts.map +1 -0
  7. package/FrcsPlotShot.js +2 -1
  8. package/FrcsPlotShot.js.map +1 -0
  9. package/FrcsShot.d.ts +1 -0
  10. package/FrcsShot.d.ts.map +1 -0
  11. package/FrcsShot.js +2 -1
  12. package/FrcsShot.js.map +1 -0
  13. package/FrcsSurveyFile.d.ts +20 -0
  14. package/FrcsSurveyFile.d.ts.map +1 -0
  15. package/FrcsSurveyFile.js +20 -1
  16. package/FrcsSurveyFile.js.map +1 -0
  17. package/FrcsTrip.d.ts +1 -0
  18. package/FrcsTrip.d.ts.map +1 -0
  19. package/FrcsTrip.js +2 -1
  20. package/FrcsTrip.js.map +1 -0
  21. package/FrcsTripSummary.d.ts +1 -0
  22. package/FrcsTripSummary.d.ts.map +1 -0
  23. package/FrcsTripSummary.js +2 -1
  24. package/FrcsTripSummary.js.map +1 -0
  25. package/FrcsTripSummaryFile.d.ts +1 -0
  26. package/FrcsTripSummaryFile.d.ts.map +1 -0
  27. package/FrcsTripSummaryFile.js +2 -1
  28. package/FrcsTripSummaryFile.js.map +1 -0
  29. package/formatFrcsShot.d.ts +8 -2
  30. package/formatFrcsShot.d.ts.map +1 -0
  31. package/formatFrcsShot.js +85 -62
  32. package/formatFrcsShot.js.map +1 -0
  33. package/formatFrcsSurveyFile.d.ts +3 -0
  34. package/formatFrcsSurveyFile.d.ts.map +1 -0
  35. package/formatFrcsSurveyFile.js +165 -0
  36. package/formatFrcsSurveyFile.js.map +1 -0
  37. package/index.d.ts +3 -1
  38. package/index.d.ts.map +1 -0
  39. package/index.js +9 -1
  40. package/index.js.map +1 -0
  41. package/node/index.d.ts +2 -1
  42. package/node/index.d.ts.map +1 -0
  43. package/node/index.js +6 -2
  44. package/node/index.js.map +1 -0
  45. package/package.json +6 -3
  46. package/parseFrcsPlotFile.d.ts +1 -0
  47. package/parseFrcsPlotFile.d.ts.map +1 -0
  48. package/parseFrcsPlotFile.js +2 -1
  49. package/parseFrcsPlotFile.js.map +1 -0
  50. package/parseFrcsSurveyFile.d.ts +50 -46
  51. package/parseFrcsSurveyFile.d.ts.map +1 -0
  52. package/parseFrcsSurveyFile.js +281 -172
  53. package/parseFrcsSurveyFile.js.map +1 -0
  54. package/parseFrcsTripSummaryFile.d.ts +1 -0
  55. package/parseFrcsTripSummaryFile.d.ts.map +1 -0
  56. package/parseFrcsTripSummaryFile.js +2 -1
  57. package/parseFrcsTripSummaryFile.js.map +1 -0
  58. package/src/FrcsPlotFile.ts +9 -0
  59. package/src/FrcsPlotShot.ts +18 -0
  60. package/src/FrcsShot.ts +56 -0
  61. package/src/FrcsSurveyFile.ts +47 -0
  62. package/src/FrcsTrip.ts +25 -0
  63. package/src/FrcsTripSummary.ts +14 -0
  64. package/src/FrcsTripSummaryFile.ts +7 -0
  65. package/src/formatFrcsShot.ts +168 -0
  66. package/src/formatFrcsSurveyFile.ts +97 -0
  67. package/src/index.ts +29 -0
  68. package/src/node/index.ts +20 -0
  69. package/src/parseFrcsPlotFile.ts +168 -0
  70. package/src/parseFrcsSurveyFile.ts +788 -0
  71. package/src/parseFrcsTripSummaryFile.ts +76 -0
  72. package/src/string/index.ts +21 -0
  73. package/src/web/index.ts +119 -0
  74. package/string/index.d.ts +2 -1
  75. package/string/index.d.ts.map +1 -0
  76. package/string/index.js +6 -2
  77. package/string/index.js.map +1 -0
  78. package/web/index.d.ts +5 -4
  79. package/web/index.d.ts.map +1 -0
  80. package/web/index.js +15 -6
  81. package/web/index.js.map +1 -0
@@ -0,0 +1,788 @@
1
+ import {
2
+ FrcsShotColumnConfig,
3
+ FrcsSurveyFile,
4
+ defaultFrcsShotColumnConfig,
5
+ } from './FrcsSurveyFile'
6
+ import { FrcsTrip, FrcsUnits } from './FrcsTrip'
7
+ import { Segment, SegmentParseError } from 'parse-segment'
8
+ import {
9
+ Angle,
10
+ Length,
11
+ Unit,
12
+ UnitizedNumber,
13
+ UnitType,
14
+ Unitize,
15
+ } from '@speleotica/unitized'
16
+ import { FrcsShot, FrcsShotKind } from './FrcsShot'
17
+
18
+ function parseNumber<T extends UnitType<T>>(
19
+ s: string,
20
+ unit: Unit<T>
21
+ ): UnitizedNumber<T> | null {
22
+ const value = parseFloat(s)
23
+ if (isNaN(value)) return null
24
+ return new UnitizedNumber(value, unit)
25
+ }
26
+
27
+ function parseAzimuth(
28
+ s: string,
29
+ unit: Unit<Angle>
30
+ ): UnitizedNumber<Angle> | null {
31
+ const parsed = parseNumber(s, unit)
32
+ return parsed?.get(Angle.degrees) === 360 ? Unitize.degrees(0) : parsed
33
+ }
34
+
35
+ function parseKind(kind: string): FrcsShotKind {
36
+ switch (kind) {
37
+ case 'H':
38
+ return FrcsShotKind.Horizontal
39
+ case 'D':
40
+ return FrcsShotKind.Diagonal
41
+ default:
42
+ return FrcsShotKind.Normal
43
+ }
44
+ }
45
+
46
+ function parseLengthUnit(unit: string): Unit<Length> | null {
47
+ switch (unit) {
48
+ case 'FI':
49
+ return Length.inches
50
+ case 'FF':
51
+ case 'FT':
52
+ return Length.feet
53
+ case 'MT':
54
+ case 'MM':
55
+ case 'M ':
56
+ return Length.meters
57
+ }
58
+ return null
59
+ }
60
+ function parseAngleUnit(unit: string): Unit<Angle> | null {
61
+ switch (unit) {
62
+ case 'D':
63
+ return Angle.degrees
64
+ case 'G':
65
+ return Angle.gradians
66
+ case 'M':
67
+ return Angle.milsNATO
68
+ }
69
+ return null
70
+ }
71
+
72
+ // determines if a cell contains a valid station name.
73
+ function isValidStation(s: string): boolean {
74
+ return /^\s*\S+\s*$/.test(s)
75
+ }
76
+ // determines if a cell contains a valid unsigned integer.
77
+ function isValidUInt(s: string): boolean {
78
+ return /^\s*[0-9]+\s*$/.test(s)
79
+ }
80
+ // determines if a cell contains a valid float.
81
+ function isValidFloat(s: string): boolean {
82
+ return /^\s*[-+]?([0-9]+(\.[0-9]*)?|\.[0-9]+)\s*$/.test(s)
83
+ }
84
+ // determines if a cell contains a valid unsigned float or whitespace.
85
+ function isValidOptFloat(s: string): boolean {
86
+ return /^\s*[-+]?[0-9]*(\.[0-9]*)?\s*$/.test(s)
87
+ }
88
+ // determines if a cell contains a valid unsigned float or whitespace.
89
+ function isValidOptUFloat(s: string): boolean {
90
+ return /^\s*[0-9]*(\.[0-9]*)?\s*$/.test(s)
91
+ }
92
+ // determines if a cell contains a valid unsigned float.
93
+ function isValidUFloat(s: string): boolean {
94
+ return /^\s*([0-9]+(\.[0-9]*)?|\.[0-9]+)\s*$/.test(s)
95
+ }
96
+ // determines if a cell contains a valid inclination or whitespace.
97
+ function isValidOptInclination(s: string): boolean {
98
+ return /^\s*[-+]?[0-9]*(\.[0-9]*)?\s*$/.test(s)
99
+ }
100
+ function parseLrud<T extends UnitType<T>>(
101
+ s: string,
102
+ unit: Unit<Length>
103
+ ): UnitizedNumber<Length> | null {
104
+ const value = parseFloat(s)
105
+ return !Number.isFinite(value) || value < 0
106
+ ? null
107
+ : new UnitizedNumber(value, unit)
108
+ }
109
+
110
+ function parseFromStationLruds(
111
+ line: string,
112
+ distanceUnit: Unit<Length>
113
+ ): [string, NonNullable<FrcsShot['fromLruds']>] | undefined {
114
+ const fromStr = line.substring(0, 5)
115
+ if (!/^\s*\S+$/.test(fromStr)) return undefined
116
+ const gap = line.substring(5, 40)
117
+ if (gap.trim()) return undefined
118
+ const lrudStr = line.substring(40, 52)
119
+ if (!/\d/.test(lrudStr)) return undefined
120
+ const lStr = line.substring(40, 43)
121
+ const rStr = line.substring(43, 46)
122
+ const uStr = line.substring(46, 49)
123
+ const dStr = line.substring(49, 52)
124
+ if (
125
+ !isValidOptFloat(lStr) ||
126
+ !isValidOptFloat(rStr) ||
127
+ !isValidOptFloat(uStr) ||
128
+ !isValidOptFloat(dStr)
129
+ ) {
130
+ return undefined
131
+ }
132
+ const up = parseLrud(uStr, distanceUnit)
133
+ const down = parseLrud(dStr, distanceUnit)
134
+ const left = parseLrud(lStr, distanceUnit)
135
+ const right = parseLrud(rStr, distanceUnit)
136
+ return [fromStr.trim(), { left, right, up, down }]
137
+ }
138
+
139
+ type ColumnRanges = {
140
+ toStation: [number, number]
141
+ fromStation: [number, number]
142
+ distance: [number, number]
143
+ distanceFeet: [number, number]
144
+ distanceInches: [number, number]
145
+ kind: [number, number]
146
+ exclude: [number, number]
147
+ frontsightAzimuth: [number, number]
148
+ backsightAzimuth: [number, number]
149
+ frontsightInclination: [number, number]
150
+ backsightInclination: [number, number]
151
+ left: [number, number]
152
+ right: [number, number]
153
+ up: [number, number]
154
+ down: [number, number]
155
+ }
156
+
157
+ function getColumnRanges(config: FrcsShotColumnConfig): ColumnRanges {
158
+ const result: ColumnRanges = {
159
+ toStation: [0, 0],
160
+ fromStation: [0, 0],
161
+ distance: [0, 0],
162
+ distanceFeet: [0, 0],
163
+ distanceInches: [0, 0],
164
+ kind: [0, 0],
165
+ exclude: [0, 0],
166
+ frontsightAzimuth: [0, 0],
167
+ backsightAzimuth: [0, 0],
168
+ frontsightInclination: [0, 0],
169
+ backsightInclination: [0, 0],
170
+ left: [0, 0],
171
+ right: [0, 0],
172
+ up: [0, 0],
173
+ down: [0, 0],
174
+ }
175
+
176
+ let c = 0
177
+ for (const [key, value] of Object.entries(config) as [
178
+ keyof FrcsShotColumnConfig,
179
+ FrcsShotColumnConfig[keyof FrcsShotColumnConfig]
180
+ ][]) {
181
+ if (key === 'distanceFeet' || key === 'distanceInches') continue
182
+ result[key][0] = c
183
+ result[key][1] = c + value
184
+ c += value
185
+ }
186
+ if (config.distanceFeet) {
187
+ result.distanceFeet[0] = result.distance[0]
188
+ result.distanceFeet[1] = result.distance[0] + config.distanceFeet
189
+ result.distanceInches[0] = result.distanceFeet[1]
190
+ result.distanceInches[1] = result.distanceFeet[1] + config.distanceInches
191
+ }
192
+ return result
193
+ }
194
+
195
+ export type ParseFrcsSurveyFileOptions = { columns?: FrcsShotColumnConfig }
196
+
197
+ /**
198
+ * Parses a raw cdata.fr survey file. These look like so:
199
+ *
200
+ <pre> Fisher Ridge Cave System, Hart Co., KY
201
+ ENTRANCE DROPS, JOE'S "I LOVE MY WIFE TRAVERSE", TRICKY TRAVERSE
202
+ PETER QUICK, KEITH ORTIZ - 2-15-81
203
+ This File has Crumps test connected. 11/20/12
204
+ *
205
+ FT C DD A
206
+ AE20 0 1 3 0 2
207
+ * %FS
208
+ * AE20 0 0 0 Bug-can't put before so put after-so can't make 2 fixed 10/28/12
209
+ AE19 AE20 9.3 60.0 60.0-36.0 2 12 0 20
210
+ AE18 AE19 24.5 0.0 0.0-90.0 6 10 25 0
211
+ AE17 AE18 8.0 350.5 350.5 17.0 3 5 0 0
212
+ AE16 AE17 6.7 0.0 0.0-90.0 3 5 6 1
213
+ AE15 AE16 12.6 70.5 71.0-18.0 4 0 2 1
214
+ AE14 AE15 10.0 21.5 20.0 6.0 5 5 0 3
215
+ AE13 AE14 26.8 288.0 286.0-50.0 0 7 20 5
216
+ *
217
+ *SHORT CANYON AT THE BASE OF THE SECOND DROP
218
+ AE12 AE13 20.7 236.0 236.0 34.0 3 5 4 4
219
+ AE11 AE12 12.4 210.0 210.0 35.0 7 4 5 1
220
+ AE10 AE13 25.7 40.0 40.0 -9.0 2 2 3 6
221
+ *
222
+ *AE10 AT JOE'S " I LOVE MY WIFE TRAVERSE "
223
+ AE9 AE10 17.8 32.5 31.0 23.0 4 5 20 15
224
+ AE1 AE9 13.7 82.0 82.0-13.0
225
+ A1 AE1 34.3 46.0 48.0-17.5
226
+ *
227
+ *SURVEY TO DOME NEAR THE ENTRANCE DOME (ABOVE THE SECOND DROP)
228
+ AD1 AE15 8.0 200.0 200.0 0.0 3 1 1 1
229
+ AD2 AD1 17.7 161.0 161.0 7.0 1 4 25 1
230
+ AD3 AD2 10.4 180.0 180.0 50.0 4 1 15 5
231
+ *
232
+ TRICKY TRAVERSE AND THEN FIRST SURVEY IN UPPER CROWLWAY
233
+ DAN CROWL, KEITH ORTIZ, CHIP HOPPER, PETER QUICK, LARRY BEAN 14 FEB 1981
234
+ *
235
+ FI B DD
236
+ A2 A1 48 10 292.0 110.0-42.0 5 10 35 5
237
+ A3 A2 12 5 333.5 153.5 35.0 3 1 15 5
238
+ A4 A3 4 2 0.0 0.0 90.0 3 1 10 10
239
+ ...</pre>
240
+ *
241
+ */
242
+ export default async function parseFrcsSurveyFile(
243
+ file: any, // eslint-disable-line @typescript-eslint/no-explicit-any
244
+ lines: AsyncIterable<string>,
245
+ { columns = defaultFrcsShotColumnConfig }: ParseFrcsSurveyFileOptions = {}
246
+ ): Promise<FrcsSurveyFile> {
247
+ const ranges = getColumnRanges(columns)
248
+ const maxRange = Math.max(...Object.values(ranges).map((r) => r[1]))
249
+
250
+ let cave: string | null = null
251
+ let location: string | null = null
252
+ const trips: Array<FrcsTrip> = []
253
+ const errors: Array<SegmentParseError> = []
254
+
255
+ let tripName: string | undefined
256
+ let tripTeam: string[] | undefined
257
+ let tripDate: Date | undefined
258
+ let inTripComment = true
259
+ let tripCommentStartLine = 1
260
+ let tripCommentEndLine = -1
261
+ const tripComment: Array<string> = []
262
+ const commentLines: Array<string> = []
263
+ let trip: FrcsTrip | null = null
264
+ let inBlockComment = false
265
+ let section
266
+ const commentFromStationLruds: Map<
267
+ string,
268
+ NonNullable<FrcsShot['fromLruds']>
269
+ > = new Map()
270
+
271
+ function addCommentLine(comment: string): void {
272
+ if (trip) {
273
+ const distanceUnit = trip.header.distanceUnit
274
+ const parsedFromStationLruds = parseFromStationLruds(
275
+ comment,
276
+ distanceUnit
277
+ )
278
+ if (parsedFromStationLruds) {
279
+ commentFromStationLruds.set(
280
+ parsedFromStationLruds[0],
281
+ parsedFromStationLruds[1]
282
+ )
283
+ return
284
+ }
285
+ }
286
+ if (commentLines) {
287
+ commentLines.push(comment)
288
+ }
289
+ }
290
+
291
+ function getComment(): string | null {
292
+ if (!commentLines?.length) return null
293
+ const comment = commentLines.join('\n').trim()
294
+ commentLines.length = 0
295
+ return comment || null
296
+ }
297
+
298
+ let unitsChanged = false
299
+ let alternateUnits: FrcsUnits | undefined
300
+ let nextShotUnits: FrcsUnits | undefined
301
+
302
+ let lineNumber = 0
303
+ let line: string
304
+
305
+ let errored = false
306
+
307
+ const error = (
308
+ message: string,
309
+ startColumn: number,
310
+ endColumn: number
311
+ ): void => {
312
+ errored = true
313
+ errors.push(
314
+ new SegmentParseError(
315
+ message,
316
+ new Segment({
317
+ value: line,
318
+ source: file,
319
+ startLine: lineNumber,
320
+ startCol: 0,
321
+ }).substring(startColumn, endColumn)
322
+ )
323
+ )
324
+ }
325
+
326
+ const parseUnits = (): FrcsUnits => {
327
+ // FT CC DD
328
+ // 01234567
329
+ let distanceUnit = parseLengthUnit(line.slice(0, 2))
330
+ if (!distanceUnit) {
331
+ distanceUnit = Length.feet
332
+ error('Invalid distance unit', 0, 2)
333
+ }
334
+ let azimuthUnit = parseAngleUnit(line[6])
335
+ if (!azimuthUnit) {
336
+ azimuthUnit = Angle.degrees
337
+ error('Invalid azimuth unit', 6, 7)
338
+ }
339
+ let inclinationUnit = parseAngleUnit(line[7])
340
+ if (!inclinationUnit) {
341
+ inclinationUnit = Angle.degrees
342
+ error('Invalid inclination unit', 7, 8)
343
+ }
344
+ const backsightAzimuthCorrected = line[3] === 'C'
345
+ const backsightInclinationCorrected = line[4] === 'C'
346
+ const hasBacksightAzimuth = line[3] !== ' ' && line[3] !== '-'
347
+ const hasBacksightInclination = line[4] !== ' ' && line[4] !== '-'
348
+
349
+ if (!/[-CB ]/.test(line[3])) {
350
+ error('Invalid backsight azimuth type', 3, 4)
351
+ }
352
+ if (!/[-CB ]/.test(line[4])) {
353
+ error('Invalid backsight inclination type', 4, 5)
354
+ }
355
+
356
+ return {
357
+ distanceUnit,
358
+ azimuthUnit,
359
+ inclinationUnit,
360
+ backsightAzimuthCorrected,
361
+ backsightInclinationCorrected,
362
+ hasBacksightAzimuth,
363
+ hasBacksightInclination,
364
+ }
365
+ }
366
+
367
+ const validate = (
368
+ startColumn: number,
369
+ endColumn: number,
370
+ fieldName: string,
371
+ validator: (value: string) => boolean
372
+ ): string => {
373
+ const field = line.substring(startColumn, endColumn)
374
+ if (!validator(field)) {
375
+ error(
376
+ (field.trim() ? 'Invalid ' : 'Missing ') + fieldName,
377
+ startColumn,
378
+ endColumn
379
+ )
380
+ }
381
+ return field
382
+ }
383
+
384
+ const addShot = (shot: FrcsShot) => {
385
+ if (!trip) return
386
+ if (alternateUnits) {
387
+ const recorded: FrcsShot & { units?: FrcsUnits } = shot
388
+ shot = {
389
+ ...shot,
390
+ recorded,
391
+ }
392
+ if (nextShotUnits) {
393
+ recorded.units = nextShotUnits
394
+ nextShotUnits = undefined
395
+ }
396
+ const {
397
+ backsightAzimuthCorrected,
398
+ backsightInclinationCorrected,
399
+ distanceUnit,
400
+ azimuthUnit,
401
+ inclinationUnit,
402
+ } = trip.header
403
+ if (
404
+ alternateUnits.backsightAzimuthCorrected !== backsightAzimuthCorrected
405
+ ) {
406
+ shot.backsightAzimuth = shot.backsightAzimuth
407
+ ? Angle.opposite(shot.backsightAzimuth)
408
+ : undefined
409
+ }
410
+ if (
411
+ alternateUnits.backsightInclinationCorrected !==
412
+ backsightInclinationCorrected
413
+ ) {
414
+ shot.backsightInclination = shot.backsightInclination?.negate()
415
+ }
416
+ if (distanceUnit !== alternateUnits.distanceUnit) {
417
+ shot.distance = shot.distance.in(distanceUnit)
418
+ if (shot.fromLruds) {
419
+ shot.fromLruds.left = shot.fromLruds.left?.in(distanceUnit)
420
+ shot.fromLruds.right = shot.fromLruds.right?.in(distanceUnit)
421
+ shot.fromLruds.up = shot.fromLruds.up?.in(distanceUnit)
422
+ shot.fromLruds.down = shot.fromLruds.down?.in(distanceUnit)
423
+ }
424
+ if (shot.toLruds) {
425
+ shot.toLruds.left = shot.toLruds.left?.in(distanceUnit)
426
+ shot.toLruds.right = shot.toLruds.right?.in(distanceUnit)
427
+ shot.toLruds.up = shot.toLruds.up?.in(distanceUnit)
428
+ shot.toLruds.down = shot.toLruds.down?.in(distanceUnit)
429
+ }
430
+ }
431
+ if (azimuthUnit !== alternateUnits.azimuthUnit) {
432
+ shot.frontsightAzimuth = shot.frontsightAzimuth?.in(azimuthUnit)
433
+ shot.backsightAzimuth = shot.backsightAzimuth?.in(azimuthUnit)
434
+ }
435
+ if (inclinationUnit !== alternateUnits.inclinationUnit) {
436
+ shot.frontsightInclination =
437
+ shot.frontsightInclination?.in(inclinationUnit)
438
+ shot.backsightInclination =
439
+ shot.backsightInclination?.in(inclinationUnit)
440
+ }
441
+ }
442
+ trip.shots.push(shot)
443
+ }
444
+
445
+ let began = false
446
+
447
+ for await (line of lines) {
448
+ errored = false
449
+
450
+ lineNumber++
451
+
452
+ if (!began) {
453
+ if (/^\s+\*/.test(line)) {
454
+ lineNumber++
455
+ continue
456
+ }
457
+ began = true
458
+ const match = /^\s*([^,]+)(,(.*))?/.exec(line)
459
+ if (match) {
460
+ cave = match[1].trim()
461
+ if (match[3]) {
462
+ location = match[3].trim()
463
+ }
464
+ }
465
+ }
466
+
467
+ if (unitsChanged) {
468
+ unitsChanged = false
469
+ alternateUnits = parseUnits()
470
+ nextShotUnits = alternateUnits
471
+ } else if (/^\s{1,8}\*/.test(line)) {
472
+ inTripComment = !inTripComment
473
+ alternateUnits = nextShotUnits = undefined
474
+ unitsChanged = false
475
+ if (inTripComment) {
476
+ section = undefined
477
+ tripTeam = undefined
478
+ tripDate = undefined
479
+ tripComment.length = 0
480
+ tripCommentStartLine = lineNumber
481
+ } else {
482
+ tripCommentEndLine = lineNumber
483
+ }
484
+ } else if (inTripComment) {
485
+ if (lineNumber === tripCommentStartLine + 1) {
486
+ tripName = line && line.trim()
487
+ } else if (lineNumber === tripCommentStartLine + 2) {
488
+ const match = /^(.+?,.+?)\s*(?:[-.](.*))?$/.exec(line && line.trim())
489
+ if (match) {
490
+ let k = 1
491
+ const team = match[k++]
492
+ tripTeam = team.split(
493
+ team.indexOf(';') >= 0 ? /\s*;\s*/g : /\s*,\s*/g
494
+ )
495
+ const dateMatch = /^(\d+)[-/](\d+)[-/](\d+)$/.exec(match[k++]?.trim())
496
+ if (dateMatch) {
497
+ const month = parseInt(dateMatch[1])
498
+ const day = parseInt(dateMatch[2])
499
+ const year = parseInt(dateMatch[3])
500
+ tripDate = new Date(year < 70 ? year + 2000 : year, month - 1, day)
501
+ }
502
+ }
503
+ } else if (lineNumber > 1) {
504
+ tripComment.push(line)
505
+ }
506
+ const match = /^\*\*\*([^*])\*\*\*/.exec(line)
507
+ if (match) {
508
+ section = match[1].trim()
509
+ }
510
+ } else if (/^(\s{9,}|)\*/.test(line)) {
511
+ if (/^\*\s*%NC(\b|$)/.test(line)) {
512
+ unitsChanged = true
513
+ }
514
+ if (/^\*\s*%/.test(line)) {
515
+ continue
516
+ }
517
+ if (/[^\s*]/.test(line)) {
518
+ addCommentLine(line.replace(/^\s*\*/, ''))
519
+ inBlockComment = false
520
+ } else {
521
+ inBlockComment = !inBlockComment
522
+ if (inBlockComment) commentLines.length = 0
523
+ }
524
+ } else if (inBlockComment) {
525
+ addCommentLine(line)
526
+ } else if (lineNumber === tripCommentEndLine + 1) {
527
+ trip = {
528
+ header: {
529
+ name: tripName || '',
530
+ comment: (tripComment && tripComment.join('\n')) || null,
531
+ section,
532
+ date: tripDate,
533
+ team: tripTeam,
534
+ ...parseUnits(),
535
+ },
536
+ shots: [],
537
+ }
538
+ trips.push(trip)
539
+ } else if (trip) {
540
+ let { distanceUnit } = alternateUnits || trip.header
541
+ const { azimuthUnit, inclinationUnit } = alternateUnits || trip.header
542
+
543
+ const inches = distanceUnit === Length.inches
544
+ if (inches) distanceUnit = Length.feet
545
+
546
+ // rigorously check the values in all the columns to make sure this
547
+ // is really a survey shot line, just in case any stray comments are
548
+ // not properly delimited.
549
+
550
+ // from station name
551
+ if (!/\S/.test(line.substring(...ranges.fromStation))) continue
552
+ const fromStr = validate(
553
+ ...ranges.fromStation,
554
+ 'from station',
555
+ isValidStation
556
+ )
557
+ const from = fromStr.trim()
558
+
559
+ // Sadly I have found negative LRUD values in Chip's format and apparently
560
+ // his program doesn't fail on them, so I have to accept them here
561
+ // isValidOptFloat instead of isValidOptUFloat
562
+ const lStr = validate(...ranges.left, 'left', isValidOptFloat)
563
+ const rStr = validate(...ranges.right, 'right', isValidOptFloat)
564
+ const uStr = validate(...ranges.up, 'up', isValidOptFloat)
565
+ const dStr = validate(...ranges.down, 'down', isValidOptFloat)
566
+
567
+ if (errored) continue
568
+
569
+ const up = parseLrud(uStr, distanceUnit)
570
+ const down = parseLrud(dStr, distanceUnit)
571
+ const left = parseLrud(lStr, distanceUnit)
572
+ const right = parseLrud(rStr, distanceUnit)
573
+
574
+ // to station name
575
+ const toStr = line.substring(...ranges.toStation)
576
+ if (!toStr.trim()) {
577
+ const shot: FrcsShot = {
578
+ from,
579
+ to: null,
580
+ kind: FrcsShotKind.Normal,
581
+ distance: new UnitizedNumber(0, distanceUnit),
582
+ frontsightAzimuth: null,
583
+ backsightAzimuth: null,
584
+ frontsightInclination: null,
585
+ backsightInclination: null,
586
+ fromLruds: {
587
+ left,
588
+ right,
589
+ up,
590
+ down,
591
+ },
592
+ excludeDistance: true,
593
+ comment: getComment(),
594
+ }
595
+ addShot(shot)
596
+ continue
597
+ }
598
+ if (!isValidStation(toStr)) {
599
+ error('Invalid station name', ...ranges.toStation)
600
+ }
601
+
602
+ let fromLruds = commentFromStationLruds.get(from)
603
+ if (fromLruds) {
604
+ commentFromStationLruds.delete(from)
605
+ } else {
606
+ const fromLrudMatch = new RegExp(
607
+ `^\\s+${fromStr
608
+ .trim()
609
+ .replace(
610
+ /[.*+?^${}()|[\]\\]/g,
611
+ '\\$&'
612
+ )}((\\s+(\\d+(\\.\\d*)?|\\.\\d+)){4})`
613
+ ).exec(line.substring(maxRange))
614
+ if (fromLrudMatch) {
615
+ const [left, right, up, down] = fromLrudMatch[1]
616
+ .trim()
617
+ .split(/\s+/g)
618
+ .map((s) => parseLrud(s, distanceUnit))
619
+ fromLruds = { left, right, up, down }
620
+ }
621
+ }
622
+
623
+ const comment = getComment()
624
+
625
+ // azimuth and inclination
626
+ const azmFsStr = validate(
627
+ ...ranges.frontsightAzimuth,
628
+ 'azimuth',
629
+ isValidOptUFloat
630
+ )
631
+ const azmBsStr = validate(
632
+ ...ranges.backsightAzimuth,
633
+ 'azimuth',
634
+ isValidOptUFloat
635
+ )
636
+ const incFsStr = line.substring(...ranges.frontsightInclination)
637
+ const incBsStr = line.substring(...ranges.backsightInclination)
638
+
639
+ if (errored) continue
640
+
641
+ let kind: FrcsShotKind
642
+ let distance: UnitizedNumber<Length>
643
+ let horizontalDistance: UnitizedNumber<Length> | undefined
644
+ let verticalDistance: UnitizedNumber<Length> | undefined
645
+ let frontsightInclination: UnitizedNumber<Angle> | null
646
+ let backsightInclination: UnitizedNumber<Angle> | null
647
+ let excludeDistance: boolean
648
+ let isSplay: boolean
649
+
650
+ // parse distance
651
+ if (inches) {
652
+ const feetStr = line.substring(...ranges.distanceFeet)
653
+ const inchesStr = line.substring(...ranges.distanceInches)
654
+ // feet and inches are not both optional
655
+ if (!isValidUInt(feetStr) && !isValidUInt(inchesStr)) {
656
+ const invalid = feetStr.trim() || inchesStr.trim()
657
+ error(
658
+ invalid ? 'Invalid distance' : 'Missing distance',
659
+ ranges.distanceFeet[0],
660
+ ranges.distanceInches[1]
661
+ )
662
+ continue
663
+ }
664
+
665
+ // sometimes inches are omitted, hence the || 0...I'm assuming it's possible
666
+ // for feet to be omitted as well
667
+ distance = Unitize.inches(parseFloat(inchesStr) || 0).add(
668
+ Unitize.feet(parseFloat(feetStr) || 0)
669
+ )
670
+
671
+ const offset =
672
+ ranges.kind[0] === ranges.distance[1]
673
+ ? ranges.distanceInches[1] - ranges.distance[1]
674
+ : 0
675
+ kind = parseKind(
676
+ line
677
+ .substring(ranges.kind[0] + offset, ranges.kind[1] + offset)
678
+ .trim()
679
+ )
680
+ const exclude = line
681
+ .substring(ranges.exclude[0] + offset, ranges.exclude[1] + offset)
682
+ .trim()
683
+ // NOTE there are two columns around here that can contain a *.
684
+ // I think they might represent different values, but thisis confused by
685
+ // the fact that for ft/in shots, if there is a D or H flag it occupies the
686
+ // first column that can contain a * for decimal feet shots
687
+ excludeDistance = exclude === '*' || exclude === 's'
688
+ isSplay = exclude === 's'
689
+ } else {
690
+ // decimal feet are not optional
691
+ const feetStr = validate(...ranges.distance, 'distance', isValidUFloat)
692
+ distance = new UnitizedNumber(parseFloat(feetStr), distanceUnit)
693
+ kind = parseKind(line.substring(...ranges.kind).trim())
694
+ const exclude = line.substring(...ranges.exclude).trim()
695
+ excludeDistance = exclude === '*' || exclude === 's'
696
+ isSplay = exclude === 's'
697
+ }
698
+
699
+ if (kind !== FrcsShotKind.Normal) {
700
+ validate(
701
+ ...ranges.frontsightInclination,
702
+ 'vertical-distance',
703
+ isValidFloat
704
+ )
705
+ }
706
+
707
+ // convert horizontal and diagonal shots to standard
708
+ // in this case incFs is the vertical offset between stations
709
+ // fortunately it appears we can always count on incFs being specified
710
+ // and incBs not being specified for these types of shots
711
+ if (kind === FrcsShotKind.Horizontal) {
712
+ // distance is horizontal offset and incFsStr is vertical offset
713
+ horizontalDistance = distance
714
+ const h = horizontalDistance.get(distanceUnit)
715
+ const v = parseFloat(incFsStr)
716
+ verticalDistance = new UnitizedNumber(v, distanceUnit)
717
+ distance = new UnitizedNumber(Math.sqrt(h * h + v * v), distanceUnit)
718
+ frontsightInclination = Angle.atan2(
719
+ verticalDistance,
720
+ horizontalDistance
721
+ )
722
+ backsightInclination = null
723
+ } else if (kind === FrcsShotKind.Diagonal) {
724
+ // distance is as usual, but incFsStr is vertical offset
725
+ const d = distance.get(distanceUnit)
726
+ const v = parseFloat(incFsStr)
727
+ verticalDistance = new UnitizedNumber(v, distanceUnit)
728
+ frontsightInclination = Angle.asin(v / d)
729
+ backsightInclination = null
730
+ } else {
731
+ // frontsight inclination
732
+ validate(
733
+ ...ranges.frontsightInclination,
734
+ 'inclination',
735
+ isValidOptInclination
736
+ )
737
+ // backsight inclination
738
+ validate(
739
+ ...ranges.backsightInclination,
740
+ 'inclination',
741
+ isValidOptInclination
742
+ )
743
+ frontsightInclination = parseNumber(incFsStr, inclinationUnit)
744
+ backsightInclination = parseNumber(incBsStr, inclinationUnit)
745
+ }
746
+ if (errored) continue
747
+
748
+ const frontsightAzimuth = parseAzimuth(azmFsStr, azimuthUnit)
749
+ const backsightAzimuth = parseAzimuth(azmBsStr, azimuthUnit)
750
+
751
+ if (!frontsightInclination && !backsightInclination) {
752
+ frontsightInclination = Unitize.degrees(0)
753
+ }
754
+
755
+ const shot: FrcsShot = {
756
+ from,
757
+ to: toStr.trim(),
758
+ kind,
759
+ distance,
760
+ frontsightAzimuth,
761
+ backsightAzimuth,
762
+ frontsightInclination,
763
+ backsightInclination,
764
+ toLruds: {
765
+ left,
766
+ right,
767
+ up,
768
+ down,
769
+ },
770
+ excludeDistance,
771
+ comment,
772
+ }
773
+ if (isSplay) shot.isSplay = true
774
+ if (fromLruds) shot.fromLruds = fromLruds
775
+ if (horizontalDistance) shot.horizontalDistance = horizontalDistance
776
+ if (verticalDistance) shot.verticalDistance = verticalDistance
777
+ addShot(shot)
778
+ }
779
+ }
780
+
781
+ return {
782
+ cave,
783
+ columns,
784
+ location,
785
+ trips,
786
+ errors,
787
+ }
788
+ }