135 lines
4 KiB
Python
135 lines
4 KiB
Python
import math
|
|
import cairo
|
|
|
|
from typing import Iterable, Callable
|
|
|
|
from xmet.sounding import SoundingSample
|
|
|
|
PRESSURE_MAX = 1050 # millibar
|
|
PRESSURE_MIN = 100
|
|
PRESSURE_STEP = 50
|
|
|
|
PRESSURE_LOG_MAX = math.log(PRESSURE_MAX)
|
|
PRESSURE_LOG_MIN = math.log(PRESSURE_MIN)
|
|
PRESSURE_LOG_RANGE = PRESSURE_LOG_MAX - PRESSURE_LOG_MIN
|
|
|
|
TEMP_CENTER = 0 # degrees C
|
|
TEMP_STEP = 5
|
|
|
|
def clamp(value, lowest, highest):
|
|
if value < lowest:
|
|
return lowest
|
|
elif value > highest:
|
|
return highest
|
|
|
|
return value
|
|
|
|
class SkewT():
|
|
__slots__ = 'width', 'height', 'temp_step_width',
|
|
|
|
ISOTHERM_LINES = (
|
|
(0, 0, 9, 9), (1, 0, 9, 8), (2, 0, 9, 7), (3, 0, 9, 6),
|
|
(4, 0, 9, 5), (5, 0, 9, 4), (6, 0, 9, 3), (7, 0, 9, 2),
|
|
(8, 0, 9, 1), (9, 0, 9, 0),
|
|
|
|
(-1, 0, 9, 10), (-2, 0, 8, 10), (-3, 0, 7, 10), (-4, 0, 6, 10),
|
|
(-5, 0, 5, 10), (-6, 0, 4, 10), (-7, 0, 3, 10), (-8, 0, 2, 10),
|
|
(-9, 0, 1, 10),
|
|
|
|
(-9, 1, 0, 10), (-9, 2, -1, 10), (-9, 3, -2, 10), (-9, 4, -3, 10),
|
|
(-9, 5, -4, 10), (-9, 6, -5, 10), (-9, 7, -6, 10), (-9, 8, -7, 10),
|
|
(-9, 9, -8, 10),
|
|
)
|
|
|
|
def __init__(self, width: float, height: float):
|
|
self.width = width
|
|
self.height = height
|
|
|
|
self.temp_step_width = self.height / 10.0
|
|
|
|
def graph_to_screen(self, x, y) -> tuple:
|
|
return (self.width / 2) + x, self.height - y
|
|
|
|
def pressure_y(self, pressure: float) -> float:
|
|
log_p = math.log(clamp(pressure, PRESSURE_MIN, PRESSURE_MAX))
|
|
factor = (PRESSURE_LOG_MAX - log_p) / PRESSURE_LOG_RANGE
|
|
|
|
return factor * self.height
|
|
|
|
def draw_isobars(self, cr: cairo.Context, x: float, y: float):
|
|
for pressure in range(PRESSURE_MIN, PRESSURE_MAX+1, PRESSURE_STEP):
|
|
coord = self.graph_to_screen(-self.width / 2,
|
|
self.pressure_y(pressure))
|
|
|
|
cr.set_source_rgba(0, 0, 0, 0.5)
|
|
cr.move_to(x + coord[0], y + coord[1])
|
|
cr.rel_line_to(self.width, 0)
|
|
cr.stroke()
|
|
|
|
def draw_isotherms(self, cr: cairo.Context, x: float, y: float):
|
|
cr.set_source_rgba(0.1, 0.5, 0.1, 0.8)
|
|
|
|
for line in self.ISOTHERM_LINES:
|
|
x1 = line[0] * self.temp_step_width
|
|
y1 = line[1] * self.temp_step_width
|
|
x2 = line[2] * self.temp_step_width
|
|
y2 = line[3] * self.temp_step_width
|
|
|
|
(screen_x1, screen_y1) = self.graph_to_screen(x1, y1)
|
|
(screen_x2, screen_y2) = self.graph_to_screen(x2, y2)
|
|
|
|
cr.move_to(x + screen_x1, y + screen_y1)
|
|
cr.line_to(x + screen_x2, y + screen_y2)
|
|
cr.stroke()
|
|
|
|
def sample_to_graph(self, temp: float, pressure: float):
|
|
x = (temp / TEMP_STEP) * self.temp_step_width
|
|
y = self.pressure_y(pressure)
|
|
|
|
return x + y, y
|
|
|
|
def draw_samples(self,
|
|
cr: cairo.Context,
|
|
x: float,
|
|
y: float,
|
|
samples: Iterable[SoundingSample],
|
|
fn: Callable):
|
|
first = True
|
|
|
|
for sample in samples:
|
|
if sample.pressure < PRESSURE_MIN:
|
|
break
|
|
|
|
#
|
|
# Temperature may possibly be dewpoint, depending on the
|
|
# return value of the callback.
|
|
#
|
|
temp = fn(sample)
|
|
|
|
if temp is None:
|
|
continue
|
|
|
|
gx, gy = self.sample_to_graph(temp, sample.pressure)
|
|
sx, sy = self.graph_to_screen(gx, gy)
|
|
|
|
if first:
|
|
cr.move_to(x + sx, y + sy)
|
|
first = False
|
|
else:
|
|
cr.line_to(x + sx, y + sy)
|
|
|
|
cr.stroke()
|
|
|
|
def draw(self,
|
|
cr: cairo.Context,
|
|
x: float,
|
|
y: float,
|
|
samples: Iterable[SoundingSample]):
|
|
self.draw_isotherms(cr, x, y)
|
|
self.draw_isobars(cr, x, y)
|
|
|
|
cr.set_source_rgb(1, 0, 0)
|
|
self.draw_samples(cr, x, y, samples, lambda s: s.temp)
|
|
|
|
cr.set_source_rgb(0, 1, 0)
|
|
self.draw_samples(cr, x, y, samples, lambda s: s.dewpoint)
|