xmet/lib/xmet/skew_t.py

128 lines
3.7 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_MAX = 60
TEMP_MIN = -40
TEMP_RANGE = TEMP_MAX - TEMP_MIN
TEMP_CENTER = 0 # degrees C
TEMP_STEP = 5
TEMP_STEP_COUNT = math.ceil(TEMP_RANGE / TEMP_STEP)
def clamp(value, lowest, highest):
if value < lowest:
return lowest
elif value > highest:
return highest
return value
class SkewTGraph():
__slots__ = 'width', 'height', 'temp_step_width',
def __init__(self, width: float, height: float):
self.width = width
self.height = height
self.temp_step_width = min(self.width, self.height) / TEMP_STEP_COUNT
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):
cr.set_source_rgb(0.5, 0.5, 0.5)
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 skew_t_to_graph(self, x: float, y: float):
return (x+y, y)
def sample_to_graph(self, temp: float, pressure: float):
x = (temp / TEMP_STEP) * self.temp_step_width
y = self.pressure_y(pressure)
return self.skew_t_to_graph(x, y)
def draw_isotherms(self, cr: cairo.Context, x: float, y: float):
cr.set_source_rgb(0.95, 0.95, 0.95)
for temp in range(-150, TEMP_MAX+1, TEMP_STEP):
x1, y1 = self.graph_to_screen(*self.sample_to_graph(temp, PRESSURE_MAX))
x2, y2 = self.graph_to_screen(*self.sample_to_graph(temp, PRESSURE_MIN))
cr.move_to(x + x1, y + y1)
cr.line_to(x + x2, y + y2)
cr.stroke()
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]):
cr.rectangle(x, y, self.width, self.height)
cr.clip()
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)