mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:07:08 +00:00
208 lines
6.0 KiB
Python
208 lines
6.0 KiB
Python
|
|
#!/usr/bin/python3
|
||
|
|
|
||
|
|
"""
|
||
|
|
P1/90 parsing functions.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import math
|
||
|
|
import re
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
from parse_fwr import parse_fwr
|
||
|
|
|
||
|
|
|
||
|
|
def parse_p190_header (string):
|
||
|
|
"""Parse a generic P1/90 header record.
|
||
|
|
|
||
|
|
Returns a dictionary of fields.
|
||
|
|
"""
|
||
|
|
names = [ "record_type", "header_type", "header_type_modifier", "description", "data" ]
|
||
|
|
record = parse_fwr(string, [1, 2, 2, 27, 48])
|
||
|
|
return dict(zip(names, record))
|
||
|
|
|
||
|
|
def parse_p190_type1 (string):
|
||
|
|
"""Parse a P1/90 Type 1 non receiver group record."""
|
||
|
|
names = [ "record_type", "line_name", "spare1",
|
||
|
|
"vessel_id", "source_id", "tailbuoy_id", "point_number",
|
||
|
|
"latitude", "longitude", "easting", "northing", "water_depth",
|
||
|
|
"doy", "time", "spare2" ]
|
||
|
|
record = parse_fwr(string, [1, 12, 3, 1, 1, 1, 6, 10, 11, 9, 9, 6, 3, 6, 1])
|
||
|
|
return dict(zip(names, record))
|
||
|
|
|
||
|
|
def parse_p190_rcv_group (string):
|
||
|
|
"""Parse a P1/90 Type 1 receiver group record."""
|
||
|
|
names = [ "record_type",
|
||
|
|
"rcv_group_number1", "easting1", "northing1", "depth1",
|
||
|
|
"rcv_group_number2", "easting2", "northing2", "depth2",
|
||
|
|
"rcv_group_number3", "easting3", "northing3", "depth3",
|
||
|
|
"streamer_id" ]
|
||
|
|
record = parse_fwr(string, [1, 4, 9, 9, 4, 4, 9, 9, 4, 4, 9, 9, 4, 1])
|
||
|
|
return dict(zip(names, record))
|
||
|
|
|
||
|
|
def parse_line (string):
|
||
|
|
type = string[0]
|
||
|
|
if string[:3] == "EOF":
|
||
|
|
return
|
||
|
|
if type == "H":
|
||
|
|
return parse_p190_header(string)
|
||
|
|
elif type == "R":
|
||
|
|
return parse_p190_rcv_group(string)
|
||
|
|
else:
|
||
|
|
return parse_p190_type1(string)
|
||
|
|
|
||
|
|
|
||
|
|
def p190_type(type, records):
|
||
|
|
return [ r for r in records if r["record_type"] == type ]
|
||
|
|
|
||
|
|
def p190_header(code, records):
|
||
|
|
return [ h for h in p190_type("H", records) if h["header_type"]+h["header_type_modifier"] == code ]
|
||
|
|
|
||
|
|
def object_id(record):
|
||
|
|
return "".join([ record[k] for k in ["record_type", "vessel_id", "source_id", "tailbuoy_id", "rcv_group_number1", "rcv_group_number2", "rcv_group_number3"] if k in record])
|
||
|
|
|
||
|
|
def normalise_record(record):
|
||
|
|
for key in record:
|
||
|
|
# These are floats
|
||
|
|
if key in ("easting", "northing", "water_depth",
|
||
|
|
"easting1", "northing1", "depth1",
|
||
|
|
"easting2", "northing2", "depth2",
|
||
|
|
"easting3", "northing3", "depth3"):
|
||
|
|
try:
|
||
|
|
record[key] = float(record[key])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
# These are ints
|
||
|
|
elif key in ("point_number", "rcv_group_number1", "rcv_group_number2", "rcv_group_number3"):
|
||
|
|
try:
|
||
|
|
record[key] = int(record[key])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
# These are geographic coordinates in DMS,
|
||
|
|
# which we convert to decimal and return as a float
|
||
|
|
elif key in ("latitude", "longitude"):
|
||
|
|
try:
|
||
|
|
record[key] = dms(record[key])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
# These are probably strings
|
||
|
|
elif "strip" in dir(record[key]):
|
||
|
|
record[key] = record[key].strip()
|
||
|
|
|
||
|
|
return record
|
||
|
|
|
||
|
|
def normalise(records):
|
||
|
|
for record in records:
|
||
|
|
normalise_record(record)
|
||
|
|
|
||
|
|
return records
|
||
|
|
|
||
|
|
def from_file(path, only_records=None, shot_range=None, with_objrefs=False):
|
||
|
|
records = []
|
||
|
|
with open(path) as fd:
|
||
|
|
cnt = 0
|
||
|
|
line = fd.readline()
|
||
|
|
while line:
|
||
|
|
cnt = cnt + 1
|
||
|
|
|
||
|
|
if line == "EOF":
|
||
|
|
break
|
||
|
|
|
||
|
|
record = parse_line(line)
|
||
|
|
if record is not None:
|
||
|
|
if only_records:
|
||
|
|
if not record["record_type"] in only_records:
|
||
|
|
continue
|
||
|
|
if shot_range:
|
||
|
|
shot = int(record["point_number"])
|
||
|
|
if not (shot >= min(shot_range) and shot <= max(shot_range)):
|
||
|
|
continue
|
||
|
|
|
||
|
|
if with_objrefs:
|
||
|
|
record["objref"] = object_id(record)
|
||
|
|
|
||
|
|
records.append(record)
|
||
|
|
line = fd.readline()
|
||
|
|
|
||
|
|
return records
|
||
|
|
|
||
|
|
def apply_tstamps(recordset, tstamp=None, fix_bad_seconds=False):
|
||
|
|
#print("tstamp", tstamp, type(tstamp))
|
||
|
|
if type(tstamp) is int:
|
||
|
|
year = tstamp
|
||
|
|
elif type(tstamp) is str:
|
||
|
|
ts_rx = re.compile(tstamp)
|
||
|
|
date_value = next(iter(p190_header("0200", recordset)), {"data": ""})["data"].strip()
|
||
|
|
year = datetime.strptime(date_value, tstamp).year
|
||
|
|
#print("d,y", date_value, year)
|
||
|
|
elif tstamp is None:
|
||
|
|
# Take a guess. If "doy" of the first record is less than the current Julian day,
|
||
|
|
# use the present year, else go back one year.
|
||
|
|
year = datetime.utcnow().year
|
||
|
|
today = int(datetime.strftime(datetime.utcnow(), "%j"))
|
||
|
|
doy = next((int(r["doy"]) in r for r in recordset if "doy" in r), today)
|
||
|
|
if doy > today:
|
||
|
|
year -= 1
|
||
|
|
else:
|
||
|
|
raise ValueError("Cannot handle tstamp of type %s" % type(tstamp))
|
||
|
|
|
||
|
|
prev = dict()
|
||
|
|
for record in recordset:
|
||
|
|
if "doy" in record and "time" in record:
|
||
|
|
while True:
|
||
|
|
doy = record["doy"]
|
||
|
|
time = record["time"].replace(" ", "0") # Fix for some dodgy systems
|
||
|
|
if fix_bad_seconds and time.endswith("60"):
|
||
|
|
time = time[:4]+"59"
|
||
|
|
expr = "{} {} {} UTC".format(year, doy, time)
|
||
|
|
ts = datetime.strptime(expr, "%Y %j %H%M%S %Z").replace(tzinfo=timezone.utc)
|
||
|
|
if object_id(record) in prev and doy < prev[object_id(record)]:
|
||
|
|
# We have gone through 31 Dec and "doy" rolled back to 1
|
||
|
|
year += 1
|
||
|
|
else:
|
||
|
|
record["tstamp"] = ts
|
||
|
|
prev[object_id(record)] = doy
|
||
|
|
break
|
||
|
|
|
||
|
|
return recordset
|
||
|
|
|
||
|
|
def dms(value):
|
||
|
|
# 591544.61N
|
||
|
|
hemisphere = 1 if value[-1] in "NnEe" else -1
|
||
|
|
seconds = float(value[-6:-1])
|
||
|
|
minutes = int(value[-8:-6])
|
||
|
|
degrees = int(value[:-8])
|
||
|
|
|
||
|
|
return (degrees + minutes/60 + seconds/3600) * hemisphere
|
||
|
|
|
||
|
|
def tod(record):
|
||
|
|
if "tstamp" in record and record["tstamp"].timestamp:
|
||
|
|
return record["tstamp"].timestamp()
|
||
|
|
else:
|
||
|
|
time = record["time"]
|
||
|
|
d = int(record["doy"])
|
||
|
|
h = int(time[:2])
|
||
|
|
m = int(time[2:4])
|
||
|
|
s = float(time[4:])
|
||
|
|
return d*86400 + h*3600 + m*60 + s
|
||
|
|
|
||
|
|
def duration(record0, record1):
|
||
|
|
ts0 = tod(record0)
|
||
|
|
ts1 = tod(record1)
|
||
|
|
return timedelta(seconds=ts1-ts0)
|
||
|
|
|
||
|
|
def distance(record0, record1):
|
||
|
|
x0, y0 = float(record0["easting"]), float(record0["northing"])
|
||
|
|
x1, y1 = float(record1["easting"]), float(record1["northing"])
|
||
|
|
return math.sqrt((x1-x0)**2 + (y1-y0)**2)
|
||
|
|
|
||
|
|
def azimuth(record0, record1):
|
||
|
|
x0, y0 = float(record0["easting"]), float(record0["northing"])
|
||
|
|
x1, y1 = float(record1["easting"]), float(record1["northing"])
|
||
|
|
return math.degrees(math.atan2(x1-x0, y1-y0)) % 360
|
||
|
|
|
||
|
|
def speed(record0, record1, knots=False):
|
||
|
|
scale = 3600/1852 if knots else 1
|
||
|
|
t0 = tod(record0)
|
||
|
|
t1 = tod(record1)
|
||
|
|
return (distance(record0, record1) / math.fabs(t1-t0)) * scale
|
||
|
|
|