///|
fn system_ok(s : String) -> Bool {
["G", "R", "E", "C", "J", "I", "S"].contains(s)
}
///|
fn satellite_ok(s : String) -> Bool {
s.length() == 3 &&
system_ok(part(s, 0, 1)) &&
s[1] >= '0' &&
s[1] <= '9' &&
s[2] >= '0' &&
s[2] <= '9' &&
part(s, 1, 3) != "00"
}
///|
fn Checker::check_continuation(self : Checker) -> Unit raise CheckError {
if self.continuation is Some(sys) {
if self.types[sys].length() != self.expected_types[sys] {
self.reject(
"observation-types-count", "unfinished SYS / # / OBS TYPES continuation",
)
}
self.continuation = None
}
}
///|
fn Checker::header_line(self : Checker, s : String) -> Unit raise CheckError {
if self.line > 4096 {
self.reject("capacity", "more than 4096 header lines")
}
if s.length() < 60 || s.length() > 80 {
self.reject(
"header-width", "header must retain columns 1..60 with label in 61..80",
)
}
let label = trimmed(s, 60, 80)
if self.line == 1 && label != "RINEX VERSION / TYPE" {
self.reject(
"version", "first line must be ordinary RINEX, not compressed data",
)
}
if label != "SYS / # / OBS TYPES" {
self.check_continuation()
}
if [
"RINEX VERSION / TYPE", "MARKER NAME", "TIME OF FIRST OBS", "TIME OF LAST OBS",
"INTERVAL", "# OF SATELLITES", "RCV CLOCK OFFS APPL", "DOI", "LICENSE OF USE",
"STATION INFORMATION",
].contains(label) {
if self.headers_seen.contains(label) {
self.reject("duplicate-header", label)
}
self.headers_seen[label] = self.line
}
match label {
"RINEX VERSION / TYPE" => {
self.version = trimmed(s, 0, 9)
if !["3.03", "3.04", "3.05", "4.00", "4.01"].contains(self.version) ||
part(s, 20, 21) != "O" {
self.reject("unsupported-version", "only listed RINEX 3/4 OBS versions")
}
self.file_system = trimmed(s, 40, 41)
if self.file_system != "M" && !system_ok(self.file_system) {
self.reject("system", "invalid file satellite system")
}
}
"MARKER NAME" => self.marker = trimmed(s, 0, 60)
"DOI" => self.doi = trimmed(s, 0, 60)
"LICENSE OF USE" => self.license = trimmed(s, 0, 60)
"STATION INFORMATION" => self.station = trimmed(s, 0, 60)
"TIME OF FIRST OBS" | "TIME OF LAST OBS" => {
let t = timestamp(
part(s, 0, 6),
part(s, 6, 12),
part(s, 12, 18),
part(s, 18, 24),
part(s, 24, 30),
part(s, 30, 43),
)
let scale = trimmed(s, 48, 51)
if label == "TIME OF FIRST OBS" {
self.header_first = Some(t)
self.time_system = scale
} else {
self.header_last = Some(t)
self.last_time_system = scale
}
}
"INTERVAL" => {
let t = seconds_ticks(part(s, 0, 10))
if t <= 0L || t > 864000000000L {
self.reject("interval", "interval must be positive and <=1 day")
}
self.interval = Some(t)
}
"RCV CLOCK OFFS APPL" => {
let n = digits(part(s, 0, 6))
if n > 1 {
self.reject("clock-applied", "expected 0 or 1")
}
self.clock_applied = Some(n)
}
"# OF SATELLITES" => {
let n = digits(part(s, 0, 6))
if n > 693 {
self.reject(
"satellite-count", "at most 7 systems of 99 satellite identifiers",
)
}
self.declared_satellites = Some(n)
}
"PRN / # OF OBS" => {
let id = trimmed(s, 3, 6)
if !id.is_empty() {
if !satellite_ok(id) || self.prn_headers.contains(id) {
self.reject("prn-header", "invalid or repeated satellite header")
}
self.prn_headers[id] = true
}
self.other_labels[label] = true // observation-total values not interpreted in this version
}
"SYS / # / OBS TYPES" => {
for
index in [
1, 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 59,
] {
if s[index] != ' ' {
self.reject(
"observation-types-layout", "reserved type-list columns must be blank",
)
}
}
let first = trimmed(s, 0, 1)
let sys = if first.is_empty() {
match self.continuation {
Some(v) => v
None => {
self.reject("observation-types", "orphan continuation")
""
}
}
} else {
self.check_continuation()
if !system_ok(first) || self.types.contains(first) {
self.reject(
"observation-types", "invalid or repeated system declaration",
)
}
let n = digits(part(s, 3, 6))
if n < 1 || n > 96 {
self.reject("capacity", "1..96 observation types per system")
}
self.types[first] = []
self.expected_types[first] = n
self.continuation = Some(first)
first
}
if first.is_empty() && !trimmed(s, 1, 6).is_empty() {
self.reject("observation-types", "continuation prefix must be blank")
}
let codes = self.types[sys]
let mut ended = false
for i in 0..<13 {
let code = trimmed(s, 7 + i * 4, 10 + i * 4)
if !code.is_empty() {
if ended {
self.reject("observation-code", "gap inside observation type list")
}
if code.length() != 3 ||
!["C", "L", "D", "S"].contains(part(code, 0, 1)) ||
code[1] < '1' ||
code[1] > '9' ||
code[2] < 'A' ||
code[2] > 'Z' ||
codes.contains(code) ||
codes.length() >= self.expected_types[sys] {
self.reject(
"observation-code",
"invalid/duplicate/extra observation type " + code,
)
}
codes.push(code)
} else {
ended = true
}
}
}
"END OF HEADER" => {
if self.header_first is None ||
self.types.is_empty() ||
self.marker.is_empty() {
self.reject(
"required-header", "marker, first observation and system type declarations required",
)
}
if self.time_system.is_empty() && self.file_system == "G" {
self.time_system = "GPS"
}
if !self.last_time_system.is_empty() &&
self.last_time_system != self.time_system {
self.reject("time-system", "first/last time systems disagree")
}
if self.time_system != "GPS" {
self.reject(
"unsupported-time-system", "only explicit GPS time (or pure-G default) is currently inspected",
)
}
if self.file_system != "M" &&
(self.types.length() != 1 || !self.types.contains(self.file_system)) {
self.reject(
"system", "single-system header and type declarations disagree",
)
}
for sys, codes in self.types {
self.counts[sys] = codes.map(code => {
code,
nonempty_fields: 0,
blank_fields: 0,
zero_values: 0,
lli_nonzero: 0,
lli_bit0: 0,
lli_bit1: 0,
lli_bit2: 0,
})
self.system_records[sys] = 0
}
if self.window is Some(_) {
if self.interval != Some(300000000L) {
self.note(
"profile-interval", "EPN daily/hourly subset requires INTERVAL=30",
)
}
if self.license.is_empty() {
self.note("profile-license", "no LICENSE OF USE header", error=false)
}
if self.doi.is_empty() {
self.note("profile-doi", "no DOI header", error=false)
}
if self.station.is_empty() {
self.note(
"profile-station-link",
"no STATION INFORMATION header",
error=false,
)
}
}
self.body = true
}
"" => self.reject("header-label", "missing header label")
_ => self.other_labels[label] = true
}
}