Compare commits
3 commits
902ae26b80
...
bbd07b7f3a
Author | SHA1 | Date | |
---|---|---|---|
bbd07b7f3a | |||
0c841ec4e9 | |||
0d1489d641 |
4 changed files with 205 additions and 43 deletions
|
@ -4,7 +4,11 @@ import argparse
|
|||
import cairo
|
||||
|
||||
from xmet.db import Database
|
||||
from xmet.spc import SPCOutlookParser, SPCOutlook, SPCOutlookMap
|
||||
|
||||
from xmet.spc import SPCOutlookParser, \
|
||||
SPCOutlook, \
|
||||
SPCOutlookMap, \
|
||||
SPCOutlookType
|
||||
|
||||
ASSETS = {
|
||||
'light': {
|
||||
|
@ -32,6 +36,7 @@ def render_categorical(conus: SPCOutlookMap,
|
|||
conus.draw_categories(cr, outlook)
|
||||
conus.draw_cities(cr, db)
|
||||
conus.draw_logo(cr, assets['logo'])
|
||||
conus.draw_legend(cr, SPCOutlookType.CATEGORICAL)
|
||||
|
||||
if args.dark:
|
||||
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_cities(cr, db)
|
||||
conus.draw_logo(cr, assets['logo'])
|
||||
conus.draw_legend(cr, SPCOutlookType.PROBABILISTIC)
|
||||
|
||||
if args.dark:
|
||||
cr.set_source_rgb(1, 1, 1)
|
||||
|
|
29
lib/xmet/draw.py
Normal file
29
lib/xmet/draw.py
Normal 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)
|
|
@ -88,9 +88,9 @@ class EquirectMap():
|
|||
elif city.population >= 500000:
|
||||
radius = 3
|
||||
elif city.population >= 100000:
|
||||
radius = 2
|
||||
radius = 2.5
|
||||
else:
|
||||
radius = 1
|
||||
radius = 1.3
|
||||
|
||||
extents = cr.text_extents(city.name)
|
||||
|
||||
|
|
207
lib/xmet/spc.py
207
lib/xmet/spc.py
|
@ -9,6 +9,7 @@ from xmet.coord import COORD_SYSTEM
|
|||
from xmet.city import City
|
||||
from xmet.map import EquirectMap, MAP_SCREEN_DIMENSIONS, MAP_BOUNDS
|
||||
from xmet.afos import MONTHS, TIMEZONES
|
||||
from xmet.draw import draw_rounded_rect
|
||||
|
||||
from pyiem.nws.products._outlook_util import (
|
||||
condition_segment,
|
||||
|
@ -64,57 +65,58 @@ RE_POINTS_START = re.compile(r'''
|
|||
RE_POINTS = re.compile(r'^(?:\s+\d{8}){1,6}$')
|
||||
|
||||
CITIES = {
|
||||
'WA': ('Seattle', 'Spokane'),
|
||||
'OR': ('Portland', 'Eugene', 'Medford'),
|
||||
'CA': (
|
||||
'Redding', 'Sacramento', 'San Francisco', 'Fresno', 'Santa Barbara',
|
||||
'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': (
|
||||
'Amarillo', 'Wichita Falls', 'Lubbock', 'Dallas', 'Abilene',
|
||||
'Midland', 'Waco', 'Austin', 'San Antonio', 'Houston',
|
||||
'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'),
|
||||
'GA': ('Atlanta', 'Columbus'),
|
||||
'FL': ('Tallahassee', 'Jacksonville', 'Orlando', 'Tampa', 'Miami'),
|
||||
'SC': ('Columbia', 'Charleston'),
|
||||
'NC': ('Charlotte', 'Raleigh', 'Wilmington'),
|
||||
'VA': ('Roanoke', 'Richmond'),
|
||||
'WV': ('Charleston',),
|
||||
'AR': ('Little Rock', 'Bentonville'),
|
||||
'AZ': ('Flagstaff', 'Phoenix', 'Tucson'),
|
||||
'CO': ('Denver', 'Grand Junction', 'Pueblo', 'Durango'),
|
||||
'CT': ('Hartford',),
|
||||
'DC': ('Washington',),
|
||||
'MD': ('Baltimore',),
|
||||
'OH': ('Columbus', 'Cincinnati'),
|
||||
'FL': ('Tallahassee', 'Jacksonville', 'Orlando', 'Tampa', 'Miami'),
|
||||
'GA': ('Atlanta', 'Columbus'),
|
||||
'IA': ('Des Moines',),
|
||||
'ID': ('Boise', 'Pocatello'),
|
||||
'IL': ('Chicago', 'Peoria', 'Springfield'),
|
||||
'IN': ('Fort Wayne', 'Indianapolis'),
|
||||
'PA': ('Philadelphia', 'Pittsburgh', 'Scranton'),
|
||||
'NY': ('Rochester', 'Buffalo', 'New York'),
|
||||
'VT': ('Burlington',),
|
||||
'KS': ('Wichita', 'Colby', 'Liberal', 'Garden City'),
|
||||
'KY': ('Louisville', 'Paducah', 'Bowling Green'),
|
||||
'LA': ('Shreveport', 'New Orleans', 'Alexandria',),
|
||||
'MA': ('Boston',),
|
||||
'MD': ('Baltimore',),
|
||||
'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):
|
||||
|
@ -542,13 +544,114 @@ class SPCOutlookParser():
|
|||
|
||||
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):
|
||||
TEXT_FONT = 'Muli'
|
||||
|
||||
LOGO_RATIO = 75.0 / 275.0
|
||||
LOGO_WIDTH = 360
|
||||
LOGO_HEIGHT = LOGO_RATIO * LOGO_WIDTH
|
||||
LOGO_MARGIN = 16
|
||||
|
||||
MARGIN = 16
|
||||
|
||||
__category_colors__ = {
|
||||
'TSTM': (212/255.0, 240/255.0, 213/255.0),
|
||||
|
@ -581,12 +684,36 @@ class SPCOutlookMap(EquirectMap):
|
|||
cr.line_to(4, 4)
|
||||
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):
|
||||
cr.save()
|
||||
|
||||
width = self.LOGO_WIDTH
|
||||
height = width * self.LOGO_RATIO
|
||||
margin = self.LOGO_MARGIN
|
||||
margin = self.MARGIN
|
||||
|
||||
x = margin
|
||||
y = self.height - height - margin
|
||||
|
|
Loading…
Add table
Reference in a new issue