Nearly finished refactoring hodographs to zoom, scale automatically
This commit is contained in:
parent
87c192d000
commit
66ca08c1ff
1 changed files with 101 additions and 71 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue