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_MIN  =  10
 | 
				
			||||||
WIND_SPEED_STEP =  10
 | 
					WIND_SPEED_STEP =  10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WIND_DIR_STEP = 90 # degrees
 | 
					WIND_DIR_STEP = 45 # degrees
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def radians(degrees: float) -> float:
 | 
					def radians(degrees: float) -> float:
 | 
				
			||||||
    return (degrees + 90) * (math.pi / 180.0)
 | 
					    return (degrees + 90) * (math.pi / 180.0)
 | 
				
			||||||
| 
						 | 
					@ -21,72 +21,54 @@ def knots(ms: float) -> float:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Hodograph():
 | 
					class Hodograph():
 | 
				
			||||||
    def __init__(self, width, height):
 | 
					    def __init__(self, width, height):
 | 
				
			||||||
        self.zoom     = 1.2
 | 
					        self.width   = min(width, height)
 | 
				
			||||||
        self.offset_x = 0.1 # Percentage of the upper left quadrant rendered
 | 
					        self.height  = min(width, height)
 | 
				
			||||||
        self.offset_y = 0.9 # Percentage of the upper right quadrant rendered
 | 
					        self.radius  = min(width, height) / 2
 | 
				
			||||||
 | 
					        self.extents = None
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def sample_to_screen(self, wind_speed: dir, wind_dir: float) -> tuple:
 | 
					    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 (
 | 
					        x = self.radius + r * math.cos(radians(wind_dir))
 | 
				
			||||||
            self.offset_x * self.width  + gx,
 | 
					        y = self.radius + r * math.sin(radians(wind_dir))
 | 
				
			||||||
            self.offset_y * self.height + gy
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def find_extents(self, sounding: Sounding):
 | 
					        if self.extents is None:
 | 
				
			||||||
        """
 | 
					            return x, y
 | 
				
			||||||
        Determine the boundaries of the sounding to set the center and zoom
 | 
					        else:
 | 
				
			||||||
        appropriately.
 | 
					            min_x, min_y = self.extents['min']
 | 
				
			||||||
        """
 | 
					            max_x, max_y = self.extents['max']
 | 
				
			||||||
        for sample in sounding.samples:
 | 
					 | 
				
			||||||
            x, y = self.sample_to_graph(sample.wind_speed, sample.wind_dir)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.min_x > x:
 | 
					            box_width  = max_x - min_x
 | 
				
			||||||
                self.min_x = x
 | 
					            box_height = max_y - min_y
 | 
				
			||||||
 | 
					            box        = max(box_width, box_height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.min_y > y:
 | 
					            offset_x = self.width  * ((box - box_width)  / box) / 2
 | 
				
			||||||
                self.min_y = y
 | 
					            offset_y = self.height * ((box - box_height) / box) / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.max_x < x:
 | 
					            return (
 | 
				
			||||||
                self.max_x = x
 | 
					                offset_x + ((x - min_x) / box) * self.width,
 | 
				
			||||||
 | 
					                offset_y + ((y - min_y) / box) * self.height
 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def draw_speed_lines(self, cr: cairo.Context, x, y):
 | 
					    def draw_speed_lines(self, cr: cairo.Context, x, y):
 | 
				
			||||||
        cr.save()
 | 
					        cr.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cr.set_source_rgb(0.5, 0.5, 0.5)
 | 
					        cr.set_source_rgb(0.5, 0.5, 0.5)
 | 
				
			||||||
        cr.set_line_width(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):
 | 
					        cx, cy = self.sample_to_screen(0, 0)
 | 
				
			||||||
            cr.arc(x + self.offset_x * self.width,
 | 
					
 | 
				
			||||||
                   y + self.offset_y * self.height,
 | 
					        for speed in range(WIND_SPEED_MIN, 2*WIND_SPEED_MAX, WIND_SPEED_STEP):
 | 
				
			||||||
                   self.radius * min(speed, WIND_SPEED_MAX) / WIND_SPEED_MAX,
 | 
					            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,
 | 
					                   0,
 | 
				
			||||||
                   2*math.pi)
 | 
					                   2*math.pi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,10 +80,16 @@ class Hodograph():
 | 
				
			||||||
        cr.save()
 | 
					        cr.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cr.set_source_rgb(0.5, 0.5, 0.5)
 | 
					        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):
 | 
					        for angle in range(0, 360+1, WIND_DIR_STEP):
 | 
				
			||||||
            sx1, sy1 = self.sample_to_screen(WIND_SPEED_MAX, angle)
 | 
					            sx1, sy1 = self.sample_to_screen(2*WIND_SPEED_MAX, angle)
 | 
				
			||||||
            sx2, sy2 = self.sample_to_screen(WIND_SPEED_MAX, angle + 180)
 | 
					            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.move_to(x + sx1, y + sy1)
 | 
				
			||||||
            cr.line_to(x + sx2, y + sy2)
 | 
					            cr.line_to(x + sx2, y + sy2)
 | 
				
			||||||
| 
						 | 
					@ -112,19 +100,14 @@ class Hodograph():
 | 
				
			||||||
    def draw_speed_legends(self, cr: cairo.Context, x, y):
 | 
					    def draw_speed_legends(self, cr: cairo.Context, x, y):
 | 
				
			||||||
        cr.save()
 | 
					        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):
 | 
					        for speed in range(WIND_SPEED_MIN, WIND_SPEED_MAX, WIND_SPEED_STEP):
 | 
				
			||||||
            text    = "%dkt" % speed
 | 
					            text    = "%dkt" % speed
 | 
				
			||||||
            extents = cr.text_extents(text)
 | 
					            extents = cr.text_extents(text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            sx, sy = self.sample_to_screen(speed, 180)
 | 
					            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.show_text(text)
 | 
				
			||||||
            cr.stroke()
 | 
					            cr.stroke()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -142,6 +125,54 @@ class Hodograph():
 | 
				
			||||||
            if height < key:
 | 
					            if height < key:
 | 
				
			||||||
                return self.COLORS[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,
 | 
					    def draw_sounding(self,
 | 
				
			||||||
                      cr: cairo.Context,
 | 
					                      cr: cairo.Context,
 | 
				
			||||||
                      x,
 | 
					                      x,
 | 
				
			||||||
| 
						 | 
					@ -154,13 +185,12 @@ class Hodograph():
 | 
				
			||||||
        sx_last    = None
 | 
					        sx_last    = None
 | 
				
			||||||
        sy_last    = None
 | 
					        sy_last    = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for sample in sounding.samples:
 | 
					        min_x, min_y = self.extents['min']
 | 
				
			||||||
            if sample.pressure < 0 or sample.pressure is None:
 | 
					        max_x, max_y = self.extents['max']
 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if sample.height is None:
 | 
					        box = max(max_x - min_x, max_y - min_y)
 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for sample in self.each_significant_sample(sounding):
 | 
				
			||||||
            color = self.color(sample.height)
 | 
					            color = self.color(sample.height)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if color is None:
 | 
					            if color is None:
 | 
				
			||||||
| 
						 | 
					@ -233,17 +263,17 @@ class Hodograph():
 | 
				
			||||||
            offset += interval
 | 
					            offset += interval
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def draw(self, cr: cairo.Context, x, y, sounding: Sounding):
 | 
					    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.rectangle(x, y, self.width, self.height)
 | 
				
			||||||
        cr.clip()
 | 
					        cr.clip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.draw_speed_lines(cr, x, y)
 | 
					 | 
				
			||||||
        self.draw_direction_lines(cr, x, y)
 | 
					        self.draw_direction_lines(cr, x, y)
 | 
				
			||||||
 | 
					        self.draw_speed_lines(cr, x, y)
 | 
				
			||||||
        self.draw_sounding(cr, x, y, sounding)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.draw_speed_legends(cr, x, y)
 | 
					        self.draw_speed_legends(cr, x, y)
 | 
				
			||||||
        self.draw_height_legends(cr, x, y)
 | 
					        self.draw_height_legends(cr, x, y)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.draw_sounding(cr, x, y, sounding)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cr.reset_clip()
 | 
					        cr.reset_clip()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue