import { defaultFrcsShotColumnConfig } from './FrcsSurveyFile'
import type {
ParseFrcsSurveyFileOptions,
FrcsSurveyFile,
FrcsTrip,
FrcsUnits,
FrcsShot,
InvalidFrcsSurveyFile,
InvalidFrcsUnits,
InvalidFrcsTrip,
InvalidFrcsShot,
FrcsTripHeader,
} from './FrcsSurveyFile'
import { Angle, Length, UnitizedNumber, Unitize } from '@speleotica/unitized'
import { ParseIssue, ParseIssueSeverity } from '../ParseIssue'
import { chunksToLines } from '../chunksToLines'
import {
isValidStation,
isValidOptFloat,
isValidOptUFloat,
isValidUInt,
isValidUFloat,
isValidFloat,
} from './validators'
import { getColumnRanges } from './getColumnRanges'
import {
parseFromStationLruds,
parseLengthUnit,
parseAngleUnit,
parseMonth,
parseLrud,
parseSpecialKind,
parseNumber,
parseAzimuth,
} from './parsers'
import { normalizeTeamMemberName } from './normalizeTeamMemberName'
import { unwrapInvalid } from '../unwrapInvalid'
import { SourceLoc } from '../SourceLoc'
/**
* Parses a raw cdata.fr survey file. These look like so:
*
Fisher Ridge Cave System, Hart Co., KY
ENTRANCE DROPS, JOE'S "I LOVE MY WIFE TRAVERSE", TRICKY TRAVERSE
PETER QUICK, KEITH ORTIZ - 2-15-81
This File has Crumps test connected. 11/20/12
*
FT C DD A
AE20 0 1 3 0 2
* %FS
* AE20 0 0 0 Bug-can't put before so put after-so can't make 2 fixed 10/28/12
AE19 AE20 9.3 60.0 60.0-36.0 2 12 0 20
AE18 AE19 24.5 0.0 0.0-90.0 6 10 25 0
AE17 AE18 8.0 350.5 350.5 17.0 3 5 0 0
AE16 AE17 6.7 0.0 0.0-90.0 3 5 6 1
AE15 AE16 12.6 70.5 71.0-18.0 4 0 2 1
AE14 AE15 10.0 21.5 20.0 6.0 5 5 0 3
AE13 AE14 26.8 288.0 286.0-50.0 0 7 20 5
*
*SHORT CANYON AT THE BASE OF THE SECOND DROP
AE12 AE13 20.7 236.0 236.0 34.0 3 5 4 4
AE11 AE12 12.4 210.0 210.0 35.0 7 4 5 1
AE10 AE13 25.7 40.0 40.0 -9.0 2 2 3 6
*
*AE10 AT JOE'S " I LOVE MY WIFE TRAVERSE "
AE9 AE10 17.8 32.5 31.0 23.0 4 5 20 15
AE1 AE9 13.7 82.0 82.0-13.0
A1 AE1 34.3 46.0 48.0-17.5
*
*SURVEY TO DOME NEAR THE ENTRANCE DOME (ABOVE THE SECOND DROP)
AD1 AE15 8.0 200.0 200.0 0.0 3 1 1 1
AD2 AD1 17.7 161.0 161.0 7.0 1 4 25 1
AD3 AD2 10.4 180.0 180.0 50.0 4 1 15 5
*
TRICKY TRAVERSE AND THEN FIRST SURVEY IN UPPER CROWLWAY
DAN CROWL, KEITH ORTIZ, CHIP HOPPER, PETER QUICK, LARRY BEAN 14 FEB 1981
*
FI B DD
A2 A1 48 10 292.0 110.0-42.0 5 10 35 5
A3 A2 12 5 333.5 153.5 35.0 3 1 15 5
A4 A3 4 2 0.0 0.0 90.0 3 1 10 10
...
*
*/
export default async function parseFrcsSurveyFile(
file: any, // eslint-disable-line @typescript-eslint/no-explicit-any
chunks: Iterable | AsyncIterable,
{
columns = defaultFrcsShotColumnConfig,
outputColumns = false,
normalizeNames = true,
suppressWarnings,
includeLocs,
}: ParseFrcsSurveyFileOptions = {}
): Promise {
const columnRanges = getColumnRanges(columns)
const maxRange = Math.max(
...Object.values(columnRanges.decimal).map((r) => r[1])
)
let cave: string | undefined = undefined
let location: string | undefined = undefined
const trips: (FrcsTrip | InvalidFrcsTrip)[] = []
const rootLocs: FrcsSurveyFile['locs'] = {}
const issues: ParseIssue[] = []
let tripName: string | undefined
let tripNameLoc: SourceLoc | undefined
let tripTeam: string[] | undefined
let tripTeamLoc: SourceLoc[] | undefined
let tripDate: Date | undefined
let tripDateLoc: SourceLoc | undefined
let inTripComment = true
let tripCommentStartLine = 1
let tripCommentEndLine = -1
const tripComment: string[] = []
const commentLines: string[] = []
let trip: FrcsTrip | InvalidFrcsTrip | undefined
let lastShot: FrcsShot | InvalidFrcsShot | undefined
let inBlockComment = false
let section
let sectionLoc: SourceLoc | undefined
const commentFromStationLruds = new Map<
string,
NonNullable
>()
let unitsChanged = false
let alternateUnits: FrcsUnits | InvalidFrcsUnits | undefined
let nextShotUnits: FrcsUnits | InvalidFrcsUnits | undefined
let lineNumber = 0
let line: string
let lineStartIndex = 0
let lineIssues: number[] = []
let tripIssues: number[] = []
let began = false
for await ({ line, startIndex: lineStartIndex } of chunksToLines(chunks, {
includeStartIndex: true,
})) {
if (lineIssues.length) lineIssues = []
lineNumber++
if (!began) {
began = true
if (/^\s+\*/.test(line)) {
continue
}
const match = /^\s*([^,]+)(,(.*))?/d.exec(line)
if (match) {
cave = match[1].trim()
if (includeLocs && match.indices) {
rootLocs.cave = makeLineLoc(
lineNumber,
lineStartIndex,
...match.indices[1]
)
}
if (match[3]) {
location = match[3].trim()
if (includeLocs && match.indices) {
rootLocs.cave = makeLineLoc(
lineNumber,
lineStartIndex,
...match.indices[3]
)
}
}
}
}
if (unitsChanged) {
unitsChanged = false
alternateUnits = parseUnits()
nextShotUnits = alternateUnits
} else if (/^\s{1,8}\*(?!\*)/.test(line)) {
inTripComment = !inTripComment
alternateUnits = nextShotUnits = undefined
unitsChanged = false
if (inTripComment) {
const trailingComment = getComment()
if (trailingComment) attachComment(lastShot, trailingComment)
tripName = undefined
tripNameLoc = undefined
section = undefined
sectionLoc = undefined
tripTeam = undefined
tripTeamLoc = undefined
tripDate = undefined
tripDateLoc = undefined
tripComment.length = 0
tripCommentStartLine = lineNumber
} else {
tripCommentEndLine = lineNumber
}
} else if (inTripComment) {
if (lineNumber === tripCommentStartLine + 1) {
tripName = line.trim()
const startColumn = /\S/.exec(line)?.index ?? 0
const endColumn = startColumn + tripName.length
tripNameLoc = makeLineLoc(
lineNumber,
lineStartIndex,
startColumn,
endColumn
)
} else if (lineNumber === tripCommentStartLine + 2) {
const dateMatch =
/(?:[-.]\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})))/di.exec(
line
)
if (dateMatch) {
const team = line.substring(0, dateMatch.index).replace(/\.?\s*$/, '')
tripTeam = []
tripTeamLoc = []
for (const match of team.matchAll(
team.includes(';')
? /\s*([^;]+)\s*/dg
: team.includes(',')
? /\s*([^,]+)\s*/dg
: team.includes('&')
? /\s*([^&]+)\s*/dg
: /\S {2,}\S/.test(team)
? /(\S+( \S+)*)/dg
: /(\S+( \S\.? )? \S{2,})/dg
)) {
const name = match[1].trim()
tripTeam.push(name)
if (includeLocs && match.indices) {
const index = match.indices[1][0]
tripTeamLoc.push(
makeLineLoc(
lineNumber,
lineStartIndex,
index,
index + name.length
)
)
}
}
if (!tripTeam.length) {
addIssue(
'warning',
'missingTripTeam',
'Missing team',
0,
dateMatch.index,
tripIssues
)
}
if (normalizeNames) tripTeam = tripTeam.map(normalizeTeamMemberName)
let month, day, year
if (dateMatch[2]) {
month = parseInt(dateMatch[2])
day = parseInt(dateMatch[3])
year = parseInt(dateMatch[4])
} else if (dateMatch[6]) {
day = parseInt(dateMatch[6])
month = parseMonth(dateMatch[7])
year = parseInt(dateMatch[8])
} else {
month = parseMonth(dateMatch[10])
day = parseInt(dateMatch[11])
year = parseInt(dateMatch[12])
}
tripDate = new Date(year < 60 ? year + 2000 : year, month - 1, day)
if (includeLocs && dateMatch.indices) {
tripDateLoc = makeLineLoc(
lineNumber,
lineStartIndex,
...dateMatch.indices[1]
)
}
} else {
addIssue(
'warning',
'missingTripDate',
'Missing date',
line.length,
line.length,
tripIssues
)
}
} else if (lineNumber > 1) {
tripComment.push(line)
}
const match = /^\*\*\*([^*]+)\*\*\*/.exec(line)
if (match) {
section = match[1].trim()
}
} else if (/^(\s{9,}|)\*(?!\*)/.test(line)) {
if (/^\*\s*%NC(\b|$)/.test(line)) {
unitsChanged = true
}
let match: RegExpMatchArray | null
if ((match = /^\*\s*%T([ITM])(\b|$)/.exec(line))) {
const currentUnits = alternateUnits || unwrapInvalid(trip)?.units
if (currentUnits && !('INVALID' in currentUnits)) {
alternateUnits = {
...currentUnits,
distanceUnit:
match[1] === 'I'
? Length.inches
: match[1] === 'M'
? Length.meters
: Length.feet,
}
nextShotUnits = alternateUnits
}
}
if (/^\*\s*%/.test(line)) {
inBlockComment = false
continue
}
if (/[^\s*]/.test(line)) {
addCommentLine(line.replace(/^\s*\*/, ''))
inBlockComment = false
} else {
inBlockComment = !inBlockComment
if (inBlockComment) {
const trailingComment = getComment()
if (trailingComment) attachComment(lastShot, trailingComment)
}
}
} else if (inBlockComment) {
addCommentLine(line)
} else if (lineNumber === tripCommentEndLine + 1) {
if (trip) trips.push(trip)
const comment = tripComment.join('\n') || undefined
const headerLocs: FrcsTripHeader['locs'] =
includeLocs && tripNameLoc
? {
name: tripNameLoc,
date: tripDateLoc,
section: sectionLoc,
team: tripTeamLoc,
}
: undefined
const header = {
name: tripName || '',
comment,
section,
date: tripDate,
team: tripTeam,
...(headerLocs && { locs: headerLocs }),
}
const units = parseUnits()
if (
'INVALID' in units ||
tripIssues.some((i) => issues[i]?.type === 'error')
) {
trip = {
INVALID: {
header,
units,
shots: [],
},
...(tripIssues.length ? { issues: tripIssues } : {}),
}
} else {
trip = {
tripNumber: 1,
header,
units,
shots: [],
}
}
tripIssues = []
} else if (trip) {
let distanceUnit =
unwrapInvalid(alternateUnits)?.distanceUnit ||
unwrapInvalid(unwrapInvalid(trip).units).distanceUnit ||
Length.feet
const azimuthUnit =
unwrapInvalid(alternateUnits)?.azimuthUnit ||
unwrapInvalid(unwrapInvalid(trip).units).azimuthUnit ||
Angle.degrees
const inclinationUnit =
unwrapInvalid(alternateUnits)?.inclinationUnit ||
unwrapInvalid(unwrapInvalid(trip).units).inclinationUnit ||
Angle.degrees
const inches = distanceUnit === Length.inches
if (inches) distanceUnit = Length.feet
const ranges = inches ? columnRanges.feetAndInches : columnRanges.decimal
// from station name
if (!/\S/.test(line.substring(...ranges.fromStation))) continue
const fromStr = validate(
...ranges.fromStation,
'from station',
isValidStation
)
const from = fromStr.trim()
// Sadly I have found negative LRUD values in Chip's format and apparently
// his program doesn't fail on them, so I have to accept them here
// isValidOptFloat instead of isValidOptUFloat
const lStr = validate(...ranges.left, 'left', isValidOptFloat)
const rStr = validate(...ranges.right, 'right', isValidOptFloat)
const uStr = validate(...ranges.up, 'up', isValidOptFloat)
const dStr = validate(...ranges.down, 'down', isValidOptFloat)
const up = parseLrud(uStr, distanceUnit)
const down = parseLrud(dStr, distanceUnit)
const left = parseLrud(lStr, distanceUnit)
const right = parseLrud(rStr, distanceUnit)
// to station name
const toStr = line.substring(...ranges.toStation)
if (!toStr.trim()) {
const shot: FrcsShot = {
from,
to: undefined,
distance: new UnitizedNumber(0, distanceUnit),
frontsightAzimuth: undefined,
backsightAzimuth: undefined,
frontsightInclination: undefined,
backsightInclination: undefined,
fromLruds: {
left,
right,
up,
down,
},
comment: getComment(),
}
addShot(
lineIssues.length ? { INVALID: shot, issues: lineIssues } : shot
)
continue
}
if (!isValidStation(toStr)) {
addIssue(
'error',
'invalidStationName',
'Invalid station name',
...ranges.toStation
)
}
let fromLruds = commentFromStationLruds.get(from)
if (fromLruds) {
commentFromStationLruds.delete(from)
} else {
const fromLrudMatch = new RegExp(
`^\\s+${fromStr
.trim()
.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
)}((\\s+(\\d+(\\.\\d*)?|\\.\\d+)){4})`
).exec(line.substring(maxRange))
if (fromLrudMatch) {
const [left, right, up, down] = fromLrudMatch[1]
.trim()
.split(/\s+/g)
.map((s) => parseLrud(s, distanceUnit))
fromLruds = { left, right, up, down }
}
}
const comment = getComment()
// azimuth and inclination
const azmFsStr = validate(
ranges.frontsightAzimuth[0],
ranges.frontsightAzimuth[1],
'azimuth',
isValidOptUFloat
)
const azmBsStr = validate(
...ranges.backsightAzimuth,
'azimuth',
isValidOptUFloat
)
const incFsStr = line.substring(...ranges.frontsightInclination)
const incBsStr = line.substring(...ranges.backsightInclination)
let distance: UnitizedNumber | undefined
let horizontalDistance: UnitizedNumber | undefined
let verticalDistance: UnitizedNumber | undefined
let frontsightInclination: UnitizedNumber | undefined
let backsightInclination: UnitizedNumber | undefined
// parse distance
if (inches) {
const feetStr = validate(
...ranges.distanceFeet,
'feet',
isValidOptUFloat
)
const inchesStr = validate(
...ranges.distanceInches,
'inches',
isValidOptUFloat
)
if (!/\S/.test(feetStr) && !/\S/.test(inchesStr)) {
addIssue(
'error',
'missingDistance',
'Missing distance',
ranges.distanceFeet[0],
ranges.distanceInches[1]
)
}
// sometimes inches are omitted, hence the || 0...I'm assuming it's possible
// for feet to be omitted as well
else if (
(isValidUInt(feetStr) && isValidOptUFloat(inchesStr)) ||
(isValidOptUFloat(feetStr) && isValidUInt(inchesStr))
) {
distance = Unitize.inches(parseFloat(inchesStr) || 0).add(
Unitize.feet(parseFloat(feetStr) || 0)
)
}
// NOTE there are two columns around here that can contain a *.
// I think they might represent different values, but thisis confused by
// the fact that for ft/in shots, if there is a D or H flag it occupies the
// first column that can contain a * for decimal feet shots
} else {
const distStr = validate(...ranges.distance, 'distance', isValidUFloat)
const distNum = parseFloat(distStr)
distance = Number.isFinite(distNum)
? new UnitizedNumber(distNum, distanceUnit)
: undefined
}
const specialKindStr = line.substring(...ranges.kind).trim()
const exclude = line.substring(...ranges.exclude).trim()
const specialKind = parseSpecialKind(specialKindStr)
if (specialKindStr && !specialKind) {
addIssue(
'error',
'invalidShotType',
'Invalid shot type',
ranges.kind[0],
ranges.kind[1]
)
}
const excludeDistance = exclude === '*' || exclude === 's'
const isSplay = exclude === 's'
if (exclude && !excludeDistance) {
addIssue(
'error',
'invalidShotFlag',
'Invalid shot flag',
ranges.exclude[0],
ranges.exclude[1]
)
}
if (specialKind) {
validate(
...ranges.frontsightInclination,
'vertical-distance',
isValidFloat
)
}
// convert horizontal and diagonal shots to standard
// in this case incFs is the vertical offset between stations
// fortunately it appears we can always count on incFs being specified
// and incBs not being specified for these types of shots
if (specialKind === 'horizontal') {
// distance is horizontal offset and incFsStr is vertical offset
horizontalDistance = distance
const h = horizontalDistance?.get(distanceUnit) ?? NaN
const v = parseFloat(incFsStr)
verticalDistance = Number.isFinite(v)
? new UnitizedNumber(v, distanceUnit)
: undefined
distance = new UnitizedNumber(Math.sqrt(h * h + v * v), distanceUnit)
frontsightInclination =
verticalDistance && horizontalDistance
? Angle.atan2(verticalDistance, horizontalDistance).in(
inclinationUnit
)
: undefined
backsightInclination = undefined
} else if (specialKind === 'diagonal') {
// distance is as usual, but incFsStr is vertical offset
const d = distance?.get(distanceUnit) ?? NaN
const v = parseFloat(incFsStr)
verticalDistance = Number.isFinite(v)
? new UnitizedNumber(v, distanceUnit)
: undefined
frontsightInclination = Angle.asin(v / d).in(inclinationUnit)
backsightInclination = undefined
} else {
// frontsight inclination
validate(
...ranges.frontsightInclination,
'inclination',
isValidOptFloat
)
// backsight inclination
validate(...ranges.backsightInclination, 'inclination', isValidOptFloat)
frontsightInclination = parseNumber(incFsStr, inclinationUnit)
backsightInclination = parseNumber(incBsStr, inclinationUnit)
validateInclinationRange(
frontsightInclination,
...ranges.frontsightInclination
)
validateInclinationRange(
backsightInclination,
...ranges.backsightInclination
)
}
const frontsightAzimuth = parseAzimuth(azmFsStr, azimuthUnit)
const backsightAzimuth = parseAzimuth(azmBsStr, azimuthUnit)
validateAzimuthRange(frontsightAzimuth, ...ranges.frontsightAzimuth)
validateAzimuthRange(backsightAzimuth, ...ranges.backsightAzimuth)
if (!/\S/.test(incFsStr) && !/\S/.test(incBsStr)) {
frontsightInclination = Unitize.degrees(0)
}
if (from && distance && !lineIssues.length) {
const shot: FrcsShot = {
from,
to: toStr.trim(),
specialKind,
distance,
frontsightAzimuth,
backsightAzimuth,
frontsightInclination,
backsightInclination,
toLruds: {
left,
right,
up,
down,
},
excludeDistance,
comment,
...(includeLocs && {
loc: makeLineLoc(lineNumber, lineStartIndex, 0, line.length),
}),
}
if (isSplay) shot.isSplay = true
if (fromLruds) shot.fromLruds = fromLruds
if (horizontalDistance) shot.horizontalDistance = horizontalDistance
if (verticalDistance) shot.verticalDistance = verticalDistance
addShot(shot)
} else {
const shot: InvalidFrcsShot['INVALID'] = {
from,
to: toStr.trim(),
specialKind,
distance,
frontsightAzimuth,
backsightAzimuth,
frontsightInclination,
backsightInclination,
toLruds: {
left,
right,
up,
down,
},
excludeDistance,
comment,
...(includeLocs && {
loc: makeLineLoc(lineNumber, lineStartIndex, 0, line.length),
}),
}
if (isSplay) shot.isSplay = true
if (fromLruds) shot.fromLruds = fromLruds
if (horizontalDistance) shot.horizontalDistance = horizontalDistance
if (verticalDistance) shot.verticalDistance = verticalDistance
addShot({ INVALID: shot, issues: lineIssues })
}
}
}
if (trip) trips.push(trip)
trips.forEach((trip, index) => (unwrapInvalid(trip).tripNumber = index + 1))
if (
!issues.some((i) => i.type === 'error') &&
trips.every((t): t is FrcsTrip => !('INVALID' in t))
) {
return {
cave,
columns: outputColumns ? columns : undefined,
location,
trips,
...(issues.length ? { issues } : undefined),
...(includeLocs && { locs: rootLocs }),
}
}
return {
INVALID: {
cave,
columns: outputColumns ? columns : undefined,
location,
trips,
...(includeLocs && { locs: rootLocs }),
},
issues,
}
////////////////////////////////////////////////////////////////////////////////////////////
function getComment(): string | undefined {
if (!commentLines.length) return undefined
const comment = commentLines.join('\n').trim()
commentLines.length = 0
return comment || undefined
}
function attachComment(
shot: FrcsShot | InvalidFrcsShot | undefined,
comment: string
) {
const unwrapped = unwrapInvalid(shot)
if (!unwrapped) return
if (unwrapped.comment) unwrapped.comment += '\n' + comment
else unwrapped.comment = comment
}
function addCommentLine(comment: string): void {
if (trip) {
const distanceUnit =
unwrapInvalid(alternateUnits)?.distanceUnit ||
unwrapInvalid(unwrapInvalid(trip).units).distanceUnit ||
Length.feet
const parsedFromStationLruds = parseFromStationLruds(
comment,
distanceUnit
)
if (parsedFromStationLruds) {
commentFromStationLruds.set(
parsedFromStationLruds[0],
parsedFromStationLruds[1]
)
return
}
}
if (commentLines) {
commentLines.push(comment)
}
}
function addIssue(
type: ParseIssueSeverity,
code: string,
message: string,
startColumn: number,
endColumn: number,
indicesArray?: number[]
) {
if (
suppressWarnings &&
type === 'warning' &&
(suppressWarnings === true ||
('code' in suppressWarnings &&
suppressWarnings[code as keyof typeof suppressWarnings] === true))
) {
return
}
issues.push({
type,
code,
message,
loc: {
start: {
line: lineNumber,
column: startColumn,
index: lineStartIndex + startColumn,
},
end: {
line: lineNumber,
column: endColumn,
index: lineStartIndex + endColumn,
},
},
})
if (!lineIssues) lineIssues = []
lineIssues.push(issues.length - 1)
indicesArray?.push(issues.length - 1)
}
function parseUnits(): FrcsUnits | InvalidFrcsUnits {
// FT CC DD
// 01234567
const distanceUnit = parseLengthUnit(line.slice(0, 2))
if (!distanceUnit) {
addIssue('error', 'invalidDistanceUnit', 'Invalid distance unit', 0, 2)
}
const azimuthUnit = parseAngleUnit(line[6])
if (!azimuthUnit) {
addIssue('error', 'invalidAzimuthUnit', 'Invalid azimuth unit', 6, 7)
}
const inclinationUnit = parseAngleUnit(line[7])
if (!inclinationUnit) {
addIssue(
'error',
'invalidInclinationUnit',
'Invalid inclination unit',
7,
8
)
}
const backsightAzimuthCorrected = line[3] === 'C'
const backsightInclinationCorrected = line[4] === 'C'
const hasBacksightAzimuth = line[3] !== ' ' && line[3] !== '-'
const hasBacksightInclination = line[4] !== ' ' && line[4] !== '-'
if (!/[-CB ]/.test(line[3])) {
addIssue(
'error',
'invalidBacksightAzimuthType',
'Invalid backsight azimuth type',
3,
4
)
}
if (!/[-CB ]/.test(line[4])) {
addIssue(
'error',
'invalidBacksightInclinationType',
'Invalid backsight inclination type',
4,
5
)
}
if (!distanceUnit || !azimuthUnit || !inclinationUnit) {
return {
INVALID: {
distanceUnit,
azimuthUnit,
inclinationUnit,
backsightAzimuthCorrected,
backsightInclinationCorrected,
hasBacksightAzimuth,
hasBacksightInclination,
},
issues: lineIssues,
}
}
return {
distanceUnit,
azimuthUnit,
inclinationUnit,
backsightAzimuthCorrected,
backsightInclinationCorrected,
hasBacksightAzimuth,
hasBacksightInclination,
}
}
function validate(
startColumn: number,
endColumn: number,
fieldName: string,
validator: (value: string) => boolean
): string {
const field = line.substring(startColumn, endColumn)
if (!validator(field)) {
addIssue(
'error',
`${
field.trim() ? 'invalid' : 'missing'
}${fieldName[0].toUpperCase()}${fieldName.substring(1)}`,
(field.trim() ? 'Invalid ' : 'Missing ') + fieldName,
startColumn,
endColumn
)
}
return field
}
function validateAzimuthRange(
azimuth: UnitizedNumber | undefined,
startColumn: number,
endColumn: number
) {
if (!azimuth) return
const rawValue = azimuth.get(azimuth.unit)
let max = Infinity
switch (azimuth.unit) {
case Angle.degrees:
max = 360
break
case Angle.gradians:
max = 400
break
case Angle.radians:
max = Math.PI * 2
break
case Angle.milsNATO:
max = 6400
break
}
if (rawValue < 0 || rawValue >= max) {
addIssue(
'error',
'azimuthOutOfRange',
'Azimuth out of range',
startColumn,
endColumn,
lineIssues
)
}
}
function validateInclinationRange(
inclination: UnitizedNumber | undefined,
startColumn: number,
endColumn: number
) {
if (!inclination) return
const rawValue = inclination.get(inclination.unit)
let max = Infinity
switch (inclination.unit) {
case Angle.degrees:
max = 90
break
case Angle.gradians:
max = 100
break
case Angle.radians:
max = Math.PI / 2
break
case Angle.milsNATO:
max = 1600
break
}
if (rawValue < -max || rawValue > max) {
addIssue(
'error',
'inclinationOutOfRange',
'Inclination out of range',
startColumn,
endColumn,
lineIssues
)
}
}
function addShot(shot: FrcsShot | InvalidFrcsShot) {
lastShot = shot
if (!trip) return
if (alternateUnits) {
const recorded:
| FrcsShot['recorded']
| InvalidFrcsShot['INVALID']['recorded'] = shot
if ('INVALID' in shot) {
shot = {
INVALID: {
...shot.INVALID,
recorded: shot,
},
issues: shot.issues,
}
} else {
shot = { ...shot, recorded: shot }
}
if (nextShotUnits) {
unwrapInvalid(recorded).units = nextShotUnits
nextShotUnits = undefined
}
const {
backsightAzimuthCorrected,
backsightInclinationCorrected,
distanceUnit,
azimuthUnit,
inclinationUnit,
} = unwrapInvalid(unwrapInvalid(trip).units)
const unwrappedAlternateUnits = unwrapInvalid(alternateUnits)
const unwrappedShot = unwrapInvalid(shot)
{
const alternateUnits = unwrappedAlternateUnits
const shot = unwrappedShot
if (
alternateUnits.backsightAzimuthCorrected !== backsightAzimuthCorrected
) {
shot.backsightAzimuth = shot.backsightAzimuth
? Angle.opposite(shot.backsightAzimuth)
: undefined
}
if (
alternateUnits.backsightInclinationCorrected !==
backsightInclinationCorrected
) {
shot.backsightInclination = shot.backsightInclination?.negate()
}
if (distanceUnit && distanceUnit !== alternateUnits.distanceUnit) {
shot.distance = shot.distance?.in(distanceUnit)
if (shot.fromLruds) {
shot.fromLruds = { ...shot.fromLruds }
shot.fromLruds.left = shot.fromLruds.left?.in(distanceUnit)
shot.fromLruds.right = shot.fromLruds.right?.in(distanceUnit)
shot.fromLruds.up = shot.fromLruds.up?.in(distanceUnit)
shot.fromLruds.down = shot.fromLruds.down?.in(distanceUnit)
}
if (shot.toLruds) {
shot.toLruds = { ...shot.toLruds }
shot.toLruds.left = shot.toLruds.left?.in(distanceUnit)
shot.toLruds.right = shot.toLruds.right?.in(distanceUnit)
shot.toLruds.up = shot.toLruds.up?.in(distanceUnit)
shot.toLruds.down = shot.toLruds.down?.in(distanceUnit)
}
}
if (azimuthUnit && azimuthUnit !== alternateUnits.azimuthUnit) {
shot.frontsightAzimuth = shot.frontsightAzimuth?.in(azimuthUnit)
shot.backsightAzimuth = shot.backsightAzimuth?.in(azimuthUnit)
}
if (
inclinationUnit &&
inclinationUnit !== alternateUnits.inclinationUnit
) {
shot.frontsightInclination =
shot.frontsightInclination?.in(inclinationUnit)
shot.backsightInclination =
shot.backsightInclination?.in(inclinationUnit)
}
}
}
if ('INVALID' in trip) {
trip.INVALID.shots.push(shot)
} else if ('INVALID' in shot) {
trip = { INVALID: trip }
trip.INVALID.shots.push(shot)
} else {
trip.shots.push(shot)
}
}
}
function makeLineLoc(
line: number,
lineStartIndex: number,
startColumn: number,
endColumn: number
): SourceLoc {
return {
start: {
index: lineStartIndex + startColumn,
line,
column: startColumn,
},
end: {
index: lineStartIndex + endColumn,
line,
column: endColumn,
},
}
}