From 8755d4d899923fa585b34262231fd97e4a185f8a Mon Sep 17 00:00:00 2001 From: XANTRONIX Industrial Date: Tue, 25 Mar 2025 14:41:20 -0400 Subject: [PATCH] Implement SPCOutlookMap class --- lib/xmet/spc.py | 104 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/lib/xmet/spc.py b/lib/xmet/spc.py index 4cad492..47ec1a4 100644 --- a/lib/xmet/spc.py +++ b/lib/xmet/spc.py @@ -2,9 +2,11 @@ import re import enum import shapely import datetime +import cairo from xmet.db import DatabaseTable from xmet.coord import COORD_SYSTEM +from xmet.map import EquirectMap, MAP_SCREEN_DIMENSIONS, MAP_BOUNDS from xmet.afos import MONTHS, TIMEZONES from pyiem.nws.products._outlook_util import ( @@ -114,7 +116,7 @@ def each_poly(parts: list[str]): polygons, interiors, linestrings = convert_segments(segments) # - # we do our winding logic now + # We do our winding logic now # polygons.extend(winding_logic(linestrings)) @@ -123,10 +125,12 @@ def each_poly(parts: list[str]): # for interior in interiors: for i, polygon in enumerate(polygons): - if not polygon.intersection(interior).is_empty: - current = list(polygon.interiors) - current.append(interior) - polygons[i] = shapely.Polygon(polygon.exterior, current) + if polygon.intersection(interior).is_empty: + continue + + current = list(polygon.interiors) + current.append(interior) + polygons[i] = shapely.Polygon(polygon.exterior, current) # # Buffer zero any invalid polygons @@ -165,6 +169,9 @@ class SPCOutlookArea(DatabaseTable): self.outlook_id = None self.poly = None + def sort_value(self): + raise NotImplementedError + class SPCOutlookProbabilityArea(SPCOutlookArea): __slots__ = ( 'hazard', 'probability', 'sig', @@ -177,6 +184,12 @@ class SPCOutlookProbabilityArea(SPCOutlookArea): 'id', 'outlook_id', 'hazard', 'probability', 'sig', 'poly' ) + def sort_value(self): + if self.probability == 'SIGN': + return 1.0 + + return float(self.probability) + class SPCOutlookCategoryArea(SPCOutlookArea): __slots__ = ( 'category' @@ -189,6 +202,18 @@ class SPCOutlookCategoryArea(SPCOutlookArea): 'id', 'outlook_id', 'category', 'poly' ) + __category_levels__ = { + 'TSTM': 0, + 'MRGL': 1, + 'SLGT': 2, + 'ENH': 3, + 'MDT': 4, + 'HIGH': 5 + } + + def sort_value(self): + return self.__category_levels__[self.category] + class SPCOutlook(): __slots__ = ( 'id', 'timestamp_issued', 'timestamp_start', 'timestamp_end', 'day', @@ -216,6 +241,12 @@ class SPCOutlook(): self.probabilities = list() self.categories = list() + def sorted_probabilities(self) -> list[SPCOutlookProbabilityArea]: + return sorted(self.probabilities, key=lambda p: p.sort_value()) + + def sorted_categories(self) -> list[SPCOutlookCategoryArea]: + return sorted(self.categories, key=lambda p: p.sort_value()) + class SPCOutlookParserState(enum.Enum): HEADER = 1 OFFICE = enum.auto() @@ -455,3 +486,66 @@ class SPCOutlookParser(): self.parse_body(line) return self.outlook + +class SPCOutlookMap(EquirectMap): + TEXT_FONT = 'Muli' + + LOGO_RATIO = 75.0 / 275.0 + LOGO_WIDTH = 360 + LOGO_HEIGHT = LOGO_RATIO * LOGO_WIDTH + LOGO_MARGIN = 16 + + __category_colors__ = { + 'TSTM': (212, 240, 213), + 'MRGL': ( 80, 201, 134), + 'SLGT': (255, 255, 81), + 'ENH': (255, 192, 108), + 'MDT': (255, 80, 80), + 'HIGH': (255, 80, 255) + } + + def __init__(self): + super().__init__(*MAP_SCREEN_DIMENSIONS, MAP_BOUNDS) + + def draw_logo(self, cr: cairo.Context, path: str): + width = self.LOGO_WIDTH + height = self.LOGO_HEIGHT + margin = self.LOGO_MARGIN + + x = margin + y = self.height - height - margin + + self.draw_from_file(cr, path, x, y, width, height) + + def draw_annotation(self, cr: cairo.Context, text: str): + cr.select_font_face('Muli') + cr.set_font_size(28) + + extents = cr.text_extents(text) + + cr.move_to(self.width - extents.width - 48, + self.height - extents.height - 16) + + cr.show_text(text) + + def draw_categories(self, + cr: cairo.Context, + outlook: SPCOutlook): + cr.save() + + for category in outlook.sorted_categories(): + color = self.__category_colors__[category.category] + + r = color[0] / 255.0 + g = color[1] / 255.0 + b = color[2] / 255.0 + + cr.set_source_rgba(r, g, b, 0.35) + self.draw_polygon(cr, category.poly) + cr.fill() + + cr.set_source_rgba(r*0.75, g*0.75, b*0.75, 0.8) + self.draw_polygon(cr, category.poly) + cr.stroke() + + cr.restore()