diff --git a/doc/vtec-test.txt b/doc/afos-test.txt similarity index 100% rename from doc/vtec-test.txt rename to doc/afos-test.txt diff --git a/lib/nexrad/afos.py b/lib/nexrad/afos.py index 418a046..2101036 100644 --- a/lib/nexrad/afos.py +++ b/lib/nexrad/afos.py @@ -30,6 +30,8 @@ RE_PRODUCT = re.compile(r'^(?P[A-Z]{3})(?P[A-Z]{3})$') RE_POLY = re.compile(r'^LAT\.\.\.LON (?P\d+(?: \d+)+)') +RE_POLY_CONT = re.compile(r'^\s+(?P\d+(?: \d+)+)') + RE_MOTION = re.compile(r''' ^ TIME \.\.\. MOT @@ -58,37 +60,30 @@ TIMEZONES = { def parse_lon(text: str): size = len(text) - return 0 - float(text[0:size-2]) + (float(text[size-2:size]) / 100) + + return 0 - float(text[0:size-2] + '.' + text[size-2:size]) def parse_lat(text: str): size = len(text) - return float(text[0:size-2]) + (float(text[size-2:size]) / 100) + return float(text[0:size-2] + '.' + text[size-2:size]) def parse_location(lon: str, lat: str): return shapely.Point(parse_lon(lon), parse_lat(lat)) -def parse_poly(text: str): - points = list() - coords = text.split(' ') +def parse_poly_coords(text: str) -> list: + coords = list() + items = text.split(' ') - for i in range(0, len(coords), 2): - lat = coords[i] - lon = coords[i+1] + for i in range(0, len(items), 2): + lat = items[i] + lon = items[i+1] - points.append([parse_lon(lon), parse_lat(lat)]) + coords.append([parse_lon(lon), parse_lat(lat)]) - points.append([parse_lon(coords[1]), parse_lat(coords[0])]) + return coords - return shapely.Polygon(points) - -class AFOSMessageParserState(enum.Enum): - NONE = 0 - SERIAL = enum.auto() - ISSUANCE = enum.auto() - PRODUCT = enum.auto() - BODY = enum.auto() - TAGS = enum.auto() - FOOTER = enum.auto() +def poly_from_coords(coords: list) -> shapely.Polygon: + return shapely.Polygon([*coords, [coords[0][0], coords[0][1]]]) class AFOSMessage(DatabaseTable): __table__ = 'nexrad_afos_message' @@ -171,14 +166,32 @@ class AFOSMessage(DatabaseTable): def is_warning(self): return self.sig is not None and self.sig == 'W' +class AFOSMessageParserState(enum.Enum): + NONE = 0 + SERIAL = enum.auto() + ISSUANCE = enum.auto() + PRODUCT = enum.auto() + BODY = enum.auto() + TAGS = enum.auto() + TAGS_POLY = enum.auto() + FOOTER = enum.auto() + class AFOSMessageParser(): - __slots__ = 'message', 'state', 'issuance', 'timestamp', + __slots__ = ( + 'message', 'state', 'issuance', 'timestamp', 'poly_coords' + ) + + message: AFOSMessage + state: AFOSMessageParserState + timestamp: datetime.datetime + poly_coords: list def __init__(self): - self.message = None - self.state = None - self.issuance = None - self.timestamp = None + self.message = None + self.state = None + self.issuance = None + self.timestamp = None + self.poly_coords = None def parse_vtec(self, line: str): vtec = VTECEvent.parse(line) @@ -241,14 +254,25 @@ class AFOSMessageParser(): def parse_tags(self, line: str): if line == '$$': + if len(self.poly_coords) > 0: + self.message.poly = shapely.Polygon(self.poly_coords) + self.state = AFOSMessageParserState.FOOTER return + # + # Parsing for "LAT...LON" + # match = RE_POLY.match(line) if match is not None: - self.message.poly = parse_poly(match['coords']) + self.poly_coords.extend(parse_poly_coords(match['coords'])) + self.state = AFOSMessageParserState.TAGS_POLY + return + # + # Parsing for "TIME...MOT...LOC" + # match = RE_MOTION.match(line) if match is not None: @@ -256,6 +280,15 @@ class AFOSMessageParser(): self.message.speed = int(match['speed']) self.message.location = parse_location(match['lon'], match['lat']) + def parse_tags_poly(self, line: str): + match = RE_POLY_CONT.match(line) + + if match is None: + self.state = AFOSMessageParserState.TAGS + return self.parse_tags(line) + + self.poly_coords.extend(parse_poly_coords(match['coords'])) + def parse_footer(self, line: str): self.message.forecaster = line @@ -280,6 +313,8 @@ class AFOSMessageParser(): self.parse_body(line) elif self.state == AFOSMessageParserState.TAGS: self.parse_tags(line) + elif self.state == AFOSMessageParserState.TAGS_POLY: + self.parse_tags_poly(line) elif self.state == AFOSMessageParserState.FOOTER: self.parse_footer(line) @@ -301,10 +336,13 @@ class AFOSMessageParser(): ) def parse(self, text: str): - self.message = AFOSMessage() - self.state = AFOSMessageParserState.SERIAL - self.issuance = None - self.timestamp = None + self.message = AFOSMessage() + self.message.text_raw = text + + self.poly_coords = list() + self.state = AFOSMessageParserState.SERIAL + self.issuance = None + self.timestamp = None for line in text.split('\n'): self.parse_line(line.rstrip())