Compare commits

...

3 commits

4 changed files with 205 additions and 43 deletions

View file

@ -4,7 +4,11 @@ import argparse
import cairo import cairo
from xmet.db import Database from xmet.db import Database
from xmet.spc import SPCOutlookParser, SPCOutlook, SPCOutlookMap
from xmet.spc import SPCOutlookParser, \
SPCOutlook, \
SPCOutlookMap, \
SPCOutlookType
ASSETS = { ASSETS = {
'light': { 'light': {
@ -32,6 +36,7 @@ def render_categorical(conus: SPCOutlookMap,
conus.draw_categories(cr, outlook) conus.draw_categories(cr, outlook)
conus.draw_cities(cr, db) conus.draw_cities(cr, db)
conus.draw_logo(cr, assets['logo']) conus.draw_logo(cr, assets['logo'])
conus.draw_legend(cr, SPCOutlookType.CATEGORICAL)
if args.dark: if args.dark:
cr.set_source_rgb(1, 1, 1) cr.set_source_rgb(1, 1, 1)
@ -54,6 +59,7 @@ def render_probabilistic(conus: SPCOutlookMap,
conus.draw_probabilities(cr, outlook, hazard.upper()) conus.draw_probabilities(cr, outlook, hazard.upper())
conus.draw_cities(cr, db) conus.draw_cities(cr, db)
conus.draw_logo(cr, assets['logo']) conus.draw_logo(cr, assets['logo'])
conus.draw_legend(cr, SPCOutlookType.PROBABILISTIC)
if args.dark: if args.dark:
cr.set_source_rgb(1, 1, 1) cr.set_source_rgb(1, 1, 1)

29
lib/xmet/draw.py Normal file
View file

@ -0,0 +1,29 @@
import math
import cairo
def draw_rounded_rect(cr: cairo.Context,
x: float,
y: float,
width: float,
height: float,
radius: float):
rect_width = width - 2 * radius
rect_height = height - 2 * radius
# Upper right
cr.arc(x + radius + rect_width, y + radius, radius,
-math.pi / 2, 0)
# Lower right
cr.arc(x + radius + rect_width, y + radius + rect_height, radius,
0, math.pi / 2)
# Lower left
cr.arc(x + radius, y + radius + rect_height, radius,
math.pi / 2, math.pi)
# Upper right
cr.arc(x + radius, y + radius, radius,
math.pi, math.pi * 1.5)
cr.line_to(x + radius + rect_width, y)

View file

@ -88,9 +88,9 @@ class EquirectMap():
elif city.population >= 500000: elif city.population >= 500000:
radius = 3 radius = 3
elif city.population >= 100000: elif city.population >= 100000:
radius = 2 radius = 2.5
else: else:
radius = 1 radius = 1.3
extents = cr.text_extents(city.name) extents = cr.text_extents(city.name)

View file

@ -9,6 +9,7 @@ from xmet.coord import COORD_SYSTEM
from xmet.city import City from xmet.city import City
from xmet.map import EquirectMap, MAP_SCREEN_DIMENSIONS, MAP_BOUNDS from xmet.map import EquirectMap, MAP_SCREEN_DIMENSIONS, MAP_BOUNDS
from xmet.afos import MONTHS, TIMEZONES from xmet.afos import MONTHS, TIMEZONES
from xmet.draw import draw_rounded_rect
from pyiem.nws.products._outlook_util import ( from pyiem.nws.products._outlook_util import (
condition_segment, condition_segment,
@ -64,57 +65,58 @@ RE_POINTS_START = re.compile(r'''
RE_POINTS = re.compile(r'^(?:\s+\d{8}){1,6}$') RE_POINTS = re.compile(r'^(?:\s+\d{8}){1,6}$')
CITIES = { CITIES = {
'WA': ('Seattle', 'Spokane'),
'OR': ('Portland', 'Eugene', 'Medford'),
'CA': ( 'CA': (
'Redding', 'Sacramento', 'San Francisco', 'Fresno', 'Santa Barbara', 'Redding', 'Sacramento', 'San Francisco', 'Fresno', 'Santa Barbara',
'Los Angeles', 'San Diego' 'Los Angeles', 'San Diego'
), ),
'ID': ('Boise', 'Pocatello'),
'NV': ('Elko', 'Reno', 'Las Vegas'),
'UT': ('Salt Lake City', 'Cedar City'),
'AZ': ('Flagstaff', 'Phoenix', 'Tucson'),
'MT': ('Great Falls', 'Missoula', 'Butte', 'Billings'),
'WY': ('Sheridan', 'Jackson', 'Casper', 'Cheyenne'),
'CO': ('Denver', 'Grand Junction', 'Pueblo', 'Durango'),
'NM': ('Santa Fe', 'Albuquerque', 'Las Cruces'),
'ND': ('Minot', 'Bismarck', 'Fargo', 'Grand Forks'),
'SD': ('Aberdeen', 'Pierre', 'Rapid City', 'Sioux Falls'),
'NE': ('Omaha', 'Lincoln', 'McCook', 'Norfolk'),
'KS': ('Wichita', 'Colby', 'Liberal', 'Garden City'),
'OK': ('Woodward', 'Tulsa', 'Oklahoma City', 'Norman', 'Altus'),
'TX': ( 'TX': (
'Amarillo', 'Wichita Falls', 'Lubbock', 'Dallas', 'Abilene', 'Amarillo', 'Wichita Falls', 'Lubbock', 'Dallas', 'Abilene',
'Midland', 'Waco', 'Austin', 'San Antonio', 'Houston', 'Midland', 'Waco', 'Austin', 'San Antonio', 'Houston',
'Corpus Christi', 'Brownsville' 'Corpus Christi', 'Brownsville'
), ),
'LA': ('Shreveport', 'New Orleans', 'Alexandria',),
'AR': ('Little Rock', 'Bentonville'),
'MO': ('Jefferson City', 'Kansas City'),
'IA': ('Des Moines',),
'MN': ('Minneapolis', 'Duluth'),
'MI': ('Marquette', 'Detroit'),
'WI': ('Green Bay', 'Milwaukee'),
'IL': ('Chicago', 'Peoria', 'Springfield'),
'KY': ('Louisville', 'Paducah', 'Bowling Green'),
'TN': ('Nashville', 'Jackson', 'Memphis'),
'MS': ('Jackson',),
'AL': ('Tuscaloosa', 'Mobile'), 'AL': ('Tuscaloosa', 'Mobile'),
'GA': ('Atlanta', 'Columbus'), 'AR': ('Little Rock', 'Bentonville'),
'FL': ('Tallahassee', 'Jacksonville', 'Orlando', 'Tampa', 'Miami'), 'AZ': ('Flagstaff', 'Phoenix', 'Tucson'),
'SC': ('Columbia', 'Charleston'), 'CO': ('Denver', 'Grand Junction', 'Pueblo', 'Durango'),
'NC': ('Charlotte', 'Raleigh', 'Wilmington'), 'CT': ('Hartford',),
'VA': ('Roanoke', 'Richmond'),
'WV': ('Charleston',),
'DC': ('Washington',), 'DC': ('Washington',),
'MD': ('Baltimore',), 'FL': ('Tallahassee', 'Jacksonville', 'Orlando', 'Tampa', 'Miami'),
'OH': ('Columbus', 'Cincinnati'), 'GA': ('Atlanta', 'Columbus'),
'IA': ('Des Moines',),
'ID': ('Boise', 'Pocatello'),
'IL': ('Chicago', 'Peoria', 'Springfield'),
'IN': ('Fort Wayne', 'Indianapolis'), 'IN': ('Fort Wayne', 'Indianapolis'),
'PA': ('Philadelphia', 'Pittsburgh', 'Scranton'), 'KS': ('Wichita', 'Colby', 'Liberal', 'Garden City'),
'NY': ('Rochester', 'Buffalo', 'New York'), 'KY': ('Louisville', 'Paducah', 'Bowling Green'),
'VT': ('Burlington',), 'LA': ('Shreveport', 'New Orleans', 'Alexandria',),
'MA': ('Boston',),
'MD': ('Baltimore',),
'ME': ('Portland',), 'ME': ('Portland',),
'MA': ('Boston',) 'MI': ('Marquette', 'Detroit'),
'MN': ('Minneapolis', 'Duluth'),
'MO': ('Jefferson City', 'Kansas City'),
'MS': ('Jackson',),
'MT': ('Great Falls', 'Missoula', 'Butte', 'Billings'),
'NC': ('Charlotte', 'Raleigh', 'Wilmington'),
'ND': ('Minot', 'Bismarck', 'Fargo', 'Grand Forks'),
'NE': ('Omaha', 'Lincoln', 'McCook', 'Norfolk'),
'NM': ('Santa Fe', 'Albuquerque', 'Las Cruces'),
'NV': ('Elko', 'Reno', 'Las Vegas'),
'NY': ('Rochester', 'Buffalo', 'New York'),
'OH': ('Columbus', 'Cincinnati'),
'OK': ('Woodward', 'Tulsa', 'Oklahoma City', 'Norman', 'Altus'),
'OR': ('Portland', 'Eugene', 'Medford'),
'PA': ('Philadelphia', 'Pittsburgh', 'Scranton'),
'SC': ('Columbia', 'Charleston'),
'SD': ('Aberdeen', 'Pierre', 'Rapid City', 'Sioux Falls'),
'TN': ('Nashville', 'Jackson', 'Memphis'),
'UT': ('Salt Lake City', 'Cedar City'),
'VA': ('Roanoke', 'Richmond'),
'VT': ('Burlington',),
'WA': ('Seattle', 'Spokane'),
'WI': ('Green Bay', 'Milwaukee'),
'WV': ('Charleston',),
'WY': ('Sheridan', 'Jackson', 'Casper', 'Cheyenne'),
} }
class SPCOutlookParserException(Exception): class SPCOutlookParserException(Exception):
@ -542,13 +544,114 @@ class SPCOutlookParser():
return self.outlook return self.outlook
class SPCOutlookType(enum.Enum):
CATEGORICAL = 1
PROBABILISTIC = 2
class SPCOutlookLegend():
MARGIN = 16
RADIUS = 16
COLOR_WIDTH = 32
COLOR_HEIGHT = 32
FONT_FACE = 'Muli'
FONT_SIZE = 16
def __init__(self, colors: dict[str, tuple[float, float, float]]):
self.colors = colors
self.text_width = 0.0
self.width = None
self.height = None
def find_size(self, cr: cairo.Context) -> tuple[float, float]:
cr.save()
count = 0
cr.select_font_face(self.FONT_FACE,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(self.FONT_SIZE)
for key in self.colors:
if key is None:
continue
extents = cr.text_extents(str(key))
if self.text_width < extents.width:
self.text_width = extents.width
count += 1
self.width = 3 * self.MARGIN + self.COLOR_WIDTH + self.text_width
self.height = self.MARGIN + count * (self.COLOR_HEIGHT + self.MARGIN)
cr.restore()
return self.width, self.height
def draw_item(self, cr: cairo.Context, key: str, x: float, y: float):
cr.save()
cr.set_source_rgb(*self.colors[key])
cr.rectangle(x, y, self.COLOR_WIDTH, self.COLOR_HEIGHT)
cr.fill()
cr.set_source_rgb(0, 0, 0)
cr.rectangle(x, y, self.COLOR_WIDTH, self.COLOR_HEIGHT)
cr.stroke()
text = str(key)
extents = cr.text_extents(text)
cr.move_to(x + self.COLOR_WIDTH + self.MARGIN,
y + self.COLOR_WIDTH / 2 + extents.height / 2)
cr.show_text(text)
cr.restore()
def draw(self, cr: cairo.Context, x: float, y: float):
cr.save()
cr.select_font_face(self.FONT_FACE,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cr.set_font_size(self.FONT_SIZE)
cr.set_source_rgba(0.2, 0.2, 0.2, 0.5)
draw_rounded_rect(cr, x, y, self.width, self.height, self.RADIUS)
cr.fill()
i = 0
for key in self.colors:
if key is None:
continue
item_y = y + self.MARGIN + i * (self.COLOR_HEIGHT + self.MARGIN)
self.draw_item(cr, key, x + self.MARGIN, item_y)
i += 1
cr.restore()
class SPCOutlookMap(EquirectMap): class SPCOutlookMap(EquirectMap):
TEXT_FONT = 'Muli' TEXT_FONT = 'Muli'
LOGO_RATIO = 75.0 / 275.0 LOGO_RATIO = 75.0 / 275.0
LOGO_WIDTH = 360 LOGO_WIDTH = 360
LOGO_HEIGHT = LOGO_RATIO * LOGO_WIDTH LOGO_HEIGHT = LOGO_RATIO * LOGO_WIDTH
LOGO_MARGIN = 16
MARGIN = 16
__category_colors__ = { __category_colors__ = {
'TSTM': (212/255.0, 240/255.0, 213/255.0), 'TSTM': (212/255.0, 240/255.0, 213/255.0),
@ -581,12 +684,36 @@ class SPCOutlookMap(EquirectMap):
cr.line_to(4, 4) cr.line_to(4, 4)
cr.stroke() cr.stroke()
def draw_legend(self,
cr: cairo.Context,
kind: SPCOutlookType):
if kind is SPCOutlookType.CATEGORICAL:
colors = self.__category_colors__
elif kind is SPCOutlookType.PROBABILISTIC:
colors = dict()
for key in self.__probability_colors__:
if key is None:
continue
text = "%d%%" % int(key * 100)
colors[text] = self.__probability_colors__[key]
legend = SPCOutlookLegend(colors)
size = legend.find_size(cr)
x = self.width - 3 * self.MARGIN - size[0]
y = self.height - 6 * self.MARGIN - size[1]
legend.draw(cr, x, y)
def draw_logo(self, cr: cairo.Context, path: str): def draw_logo(self, cr: cairo.Context, path: str):
cr.save() cr.save()
width = self.LOGO_WIDTH width = self.LOGO_WIDTH
height = width * self.LOGO_RATIO height = width * self.LOGO_RATIO
margin = self.LOGO_MARGIN margin = self.MARGIN
x = margin x = margin
y = self.height - height - margin y = self.height - height - margin