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.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)
 | 
			
		||||
 | 
			
		||||
        x = self.radius + r * math.cos(radians(wind_dir))
 | 
			
		||||
        y = self.radius + r * math.sin(radians(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']
 | 
			
		||||
 | 
			
		||||
            box_width  = max_x - min_x
 | 
			
		||||
            box_height = max_y - min_y
 | 
			
		||||
            box        = max(box_width, box_height)
 | 
			
		||||
 | 
			
		||||
            offset_x = self.width  * ((box - box_width)  / box) / 2
 | 
			
		||||
            offset_y = self.height * ((box - box_height) / box) / 2
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
            self.offset_x * self.width  + gx,
 | 
			
		||||
            self.offset_y * self.height + gy
 | 
			
		||||
                offset_x + ((x - min_x) / box) * self.width,
 | 
			
		||||
                offset_y + ((y - min_y) / box) * self.height
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    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.min_x > x:
 | 
			
		||||
                self.min_x = x
 | 
			
		||||
 | 
			
		||||
            if self.min_y > y:
 | 
			
		||||
                self.min_y = y
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
            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