@speleotica/frcsdata 4.3.1 → 5.0.0-beta.2

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