From 66ca08c1ff3a3c6b7c241f93cd6b3a6f4b6cfb0c Mon Sep 17 00:00:00 2001 From: XANTRONIX Industrial Date: Mon, 3 Mar 2025 17:18:39 -0500 Subject: [PATCH] Nearly finished refactoring hodographs to zoom, scale automatically --- lib/xmet/hodograph.py | 172 +++++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 71 deletions(-) diff --git a/lib/xmet/hodograph.py b/lib/xmet/hodograph.py index d1c3c2b..963cd08 100644 --- a/lib/xmet/hodograph.py +++ b/lib/xmet/hodograph.py @@ -8,7 +8,7 @@ WIND_SPEED_MAX = 140 # knots WIND_SPEED_MIN = 10 WIND_SPEED_STEP = 10 -WIND_DIR_STEP = 90 # degrees +WIND_DIR_STEP = 45 # degrees def radians(degrees: float) -> float: return (degrees + 90) * (math.pi / 180.0) @@ -21,72 +21,54 @@ def knots(ms: float) -> float: class Hodograph(): def __init__(self, width, height): - self.zoom = 1.2 - self.offset_x = 0.1 # Percentage of the upper left quadrant rendered - self.offset_y = 0.9 # Percentage of the upper right quadrant rendered - - self.min_x = 0 - self.min_y = 0 - self.max_x = 0 - self.max_y = 0 - - self.width = min(width, height) - self.height = min(width, height) - self.radius = min(width, height) * self.zoom - - def sample_to_graph(self, wind_speed: dir, wind_dir: float) -> tuple: - r = self.radius * (wind_speed / WIND_SPEED_MAX) - - x = r * math.cos(radians(wind_dir)) - y = r * math.sin(radians(wind_dir)) - - return x, y + self.width = min(width, height) + self.height = min(width, height) + self.radius = min(width, height) / 2 + self.extents = None def sample_to_screen(self, wind_speed: dir, wind_dir: float) -> tuple: - gx, gy = self.sample_to_graph(wind_speed, wind_dir) + r = self.radius * (wind_speed / WIND_SPEED_MAX) - return ( - self.offset_x * self.width + gx, - self.offset_y * self.height + gy - ) + x = self.radius + r * math.cos(radians(wind_dir)) + y = self.radius + r * math.sin(radians(wind_dir)) - def find_extents(self, sounding: Sounding): - """ - Determine the boundaries of the sounding to set the center and zoom - appropriately. - """ - for sample in sounding.samples: - x, y = self.sample_to_graph(sample.wind_speed, sample.wind_dir) + if self.extents is None: + return x, y + else: + min_x, min_y = self.extents['min'] + max_x, max_y = self.extents['max'] - if self.min_x > x: - self.min_x = x + box_width = max_x - min_x + box_height = max_y - min_y + box = max(box_width, box_height) - if self.min_y > y: - self.min_y = y + offset_x = self.width * ((box - box_width) / box) / 2 + offset_y = self.height * ((box - box_height) / box) / 2 - if self.max_x < x: - self.max_x = x - - if self.max_y < y: - self.max_y = y - - self.offset_x = self.min_x / self.radius + 0.1 - self.offset_y = (self.radius - self.max_y) / self.radius - self.zoom = min(self.width, self.height) / max(self.max_x - self.min_x, - self.max_y - self.min_y) / 2 - self.radius = min(self.width, self.height) * self.zoom + return ( + offset_x + ((x - min_x) / box) * self.width, + offset_y + ((y - min_y) / box) * self.height + ) def draw_speed_lines(self, cr: cairo.Context, x, y): cr.save() cr.set_source_rgb(0.5, 0.5, 0.5) cr.set_line_width(0.5) - cr.set_dash([5, 5], 1) - for speed in range(WIND_SPEED_MIN, WIND_SPEED_MAX+1, WIND_SPEED_STEP): - cr.arc(x + self.offset_x * self.width, - y + self.offset_y * self.height, - self.radius * min(speed, WIND_SPEED_MAX) / WIND_SPEED_MAX, + cx, cy = self.sample_to_screen(0, 0) + + for speed in range(WIND_SPEED_MIN, 2*WIND_SPEED_MAX, WIND_SPEED_STEP): + if speed % (WIND_SPEED_STEP*2) == 0: + cr.set_dash([1, 1], 0) + else: + cr.set_dash([5, 5], 1) + + sx, sy = self.sample_to_screen(speed, 0) + + cr.arc(x + cx, + x + cy, + sy - self.radius, 0, 2*math.pi) @@ -98,10 +80,16 @@ class Hodograph(): cr.save() cr.set_source_rgb(0.5, 0.5, 0.5) + cr.set_line_width(0.5) for angle in range(0, 360+1, WIND_DIR_STEP): - sx1, sy1 = self.sample_to_screen(WIND_SPEED_MAX, angle) - sx2, sy2 = self.sample_to_screen(WIND_SPEED_MAX, angle + 180) + sx1, sy1 = self.sample_to_screen(2*WIND_SPEED_MAX, angle) + sx2, sy2 = self.sample_to_screen(2*WIND_SPEED_MAX, angle + 180) + + if angle % 90 == 0: + cr.set_dash([1, 1], 0) + else: + cr.set_dash([5, 5], 1) cr.move_to(x + sx1, y + sy1) cr.line_to(x + sx2, y + sy2) @@ -112,19 +100,14 @@ class Hodograph(): def draw_speed_legends(self, cr: cairo.Context, x, y): cr.save() - x_offset_scale = 0.01190476 - y_offset_scale = 0.01 - - x_offset = x_offset_scale * self.radius - y_offset = y_offset_scale * self.radius - for speed in range(WIND_SPEED_MIN, WIND_SPEED_MAX, WIND_SPEED_STEP): text = "%dkt" % speed extents = cr.text_extents(text) sx, sy = self.sample_to_screen(speed, 180) - cr.move_to(x + sx + x_offset, y + sy - y_offset) + cr.move_to(x + sx + extents.width * 0.25, + y + sy - extents.height * 1.5) cr.show_text(text) cr.stroke() @@ -142,6 +125,54 @@ class Hodograph(): if height < key: return self.COLORS[key] + def each_significant_sample(self, sounding: Sounding): + for sample in sounding.samples: + if sample.pressure < 0 or sample.pressure is None: + continue + + if sample.height is None: + continue + + if self.color(sample.height) is None: + break + + yield sample + + def find_extents(self, sounding: Sounding): + min_x, min_y = None, None + max_x, max_y = None, None + + first = True + + for sample in self.each_significant_sample(sounding): + sx, sy = self.sample_to_screen(knots(sample.wind_speed), + sample.wind_dir) + + if first: + min_x = sx + max_x = sx + min_y = sy + max_y = sy + + first = False + else: + if min_x > sx: + min_x = sx + + if min_y > sy: + min_y = sy + + if max_x < sx: + max_x = sx + + if max_y < sy: + max_y = sy + + return { + 'min': (min_x, min_y), + 'max': (max_x, max_y) + } + def draw_sounding(self, cr: cairo.Context, x, @@ -154,13 +185,12 @@ class Hodograph(): sx_last = None sy_last = None - for sample in sounding.samples: - if sample.pressure < 0 or sample.pressure is None: - continue + min_x, min_y = self.extents['min'] + max_x, max_y = self.extents['max'] - if sample.height is None: - continue + box = max(max_x - min_x, max_y - min_y) + for sample in self.each_significant_sample(sounding): color = self.color(sample.height) if color is None: @@ -233,17 +263,17 @@ class Hodograph(): offset += interval def draw(self, cr: cairo.Context, x, y, sounding: Sounding): - self.find_extents(sounding) + self.extents = self.find_extents(sounding) cr.rectangle(x, y, self.width, self.height) cr.clip() - self.draw_speed_lines(cr, x, y) self.draw_direction_lines(cr, x, y) - - self.draw_sounding(cr, x, y, sounding) + self.draw_speed_lines(cr, x, y) self.draw_speed_legends(cr, x, y) self.draw_height_legends(cr, x, y) + self.draw_sounding(cr, x, y, sounding) + cr.reset_clip()