Initial commit of hodograph.py
This commit is contained in:
		
							parent
							
								
									5e615b5d59
								
							
						
					
					
						commit
						f4d066d5db
					
				
					 1 changed files with 168 additions and 0 deletions
				
			
		
							
								
								
									
										168
									
								
								lib/xmet/hodograph.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								lib/xmet/hodograph.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,168 @@ | |||
| import math | ||||
| import cairo | ||||
| 
 | ||||
| from typing import Iterable | ||||
| 
 | ||||
| from xmet.igra     import IGRAReader | ||||
| from xmet.sounding import Sounding, SoundingSample | ||||
| 
 | ||||
| IMAGE_WIDTH = 800 | ||||
| IMAGE_HEIGHT = 800 | ||||
| 
 | ||||
| GRAPH_WIDTH  = IMAGE_WIDTH  - 128 | ||||
| GRAPH_HEIGHT = IMAGE_HEIGHT - 128 | ||||
| 
 | ||||
| WIND_SPEED_MAX  = 90 # knots | ||||
| WIND_SPEED_MIN  = 10 | ||||
| WIND_SPEED_STEP = 10 | ||||
| 
 | ||||
| WIND_DIR_STEP = 45 # degrees | ||||
| 
 | ||||
| PRESSURE_MIN = 100 | ||||
| 
 | ||||
| def radians(degrees: float) -> float: | ||||
|     return (degrees + 90) * (math.pi / 180.0) | ||||
| 
 | ||||
| def degrees(radians: float) -> float: | ||||
|     return (radians * (180.0 / math.pi)) - 90 | ||||
| 
 | ||||
| class Hodograph(): | ||||
|     def __init__(self, width, height): | ||||
|         self.width  = min(width, height) | ||||
|         self.height = min(width, height) | ||||
|         self.radius = min(width, height) / 2 | ||||
| 
 | ||||
|     def sample_to_screen(self, wind_speed: dir, wind_dir: float) -> tuple: | ||||
|         r = self.radius * min(wind_speed, WIND_SPEED_MAX) / WIND_SPEED_MAX | ||||
| 
 | ||||
|         return ( | ||||
|             self.width  / 2 + r * math.cos(radians(wind_dir)), | ||||
|             self.height / 2 + r * math.sin(radians(wind_dir)) | ||||
|         ) | ||||
| 
 | ||||
|     def draw_speed_lines(self, cr: cairo.Context, x, y): | ||||
|         for speed in range(WIND_SPEED_MIN, WIND_SPEED_MAX+1, WIND_SPEED_STEP): | ||||
|             cr.arc(x + self.width  / 2, | ||||
|                    y + self.height / 2, | ||||
|                    self.radius * min(speed, WIND_SPEED_MAX) / WIND_SPEED_MAX, | ||||
|                    0, | ||||
|                    2*math.pi) | ||||
| 
 | ||||
|             cr.stroke() | ||||
| 
 | ||||
|     def draw_direction_lines(self, cr: cairo.Context, x, y): | ||||
|          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) | ||||
| 
 | ||||
|             cr.move_to(x + sx1, y + sy1) | ||||
|             cr.line_to(x + sx2, y + sy2) | ||||
|             cr.stroke() | ||||
| 
 | ||||
|     def draw_direction_legends(self, cr: cairo.Context, x, y): | ||||
|         cr.save() | ||||
| 
 | ||||
|         cr.select_font_face('Sans', | ||||
|                             cairo.FONT_SLANT_NORMAL, | ||||
|                             cairo.FONT_WEIGHT_BOLD) | ||||
| 
 | ||||
|         cr.set_font_size(16) | ||||
| 
 | ||||
|         for angle in range(0, 360, WIND_DIR_STEP): | ||||
|             text    = "%d°" % angle | ||||
|             extents = cr.text_extents(text) | ||||
|             r       = self.radius + ((extents.width + extents.height) / 2) | ||||
| 
 | ||||
|             sx = self.width  / 2 + r * math.cos(radians(angle)) | ||||
|             sy = self.height / 2 + r * math.sin(radians(angle)) | ||||
| 
 | ||||
|             cr.move_to(x + sx - extents.width / 2, y + sy + extents.height / 2) | ||||
|             cr.show_text(text) | ||||
|             cr.stroke() | ||||
| 
 | ||||
|         cr.restore() | ||||
| 
 | ||||
|     def draw_speed_legends(self, cr: cairo.Context, x, y): | ||||
|         cr.save() | ||||
| 
 | ||||
|         x_offset_scale = 0.01190476 | ||||
|         y_offset_scale = 0.02380952 | ||||
| 
 | ||||
|         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.show_text(text) | ||||
|             cr.stroke() | ||||
| 
 | ||||
|         cr.restore() | ||||
| 
 | ||||
|     def color(self, height: float): | ||||
|         if height <= 500: | ||||
|             return (1, 0, 1) | ||||
|         elif height <= 3000: | ||||
|             return (1, 0, 0) | ||||
|         elif height <= 7000: | ||||
|             return (0, 1, 0) | ||||
|         elif height <= 10000: | ||||
|             return (0, 0, 1) | ||||
| 
 | ||||
|     def draw_samples(self, | ||||
|                      cr: cairo.Context, | ||||
|                      x, | ||||
|                      y, | ||||
|                      samples: Iterable[SoundingSample]): | ||||
|         cr.save() | ||||
| 
 | ||||
|         first      = True | ||||
|         color_last = None | ||||
|         sx_last    = None | ||||
|         sy_last    = None | ||||
| 
 | ||||
|         for sample in samples: | ||||
|             if sample.pressure < 0 or sample.pressure is None: | ||||
|                 continue | ||||
| 
 | ||||
|             if sample.pressure < PRESSURE_MIN: | ||||
|                 break | ||||
| 
 | ||||
|             color = self.color(sample.height) | ||||
| 
 | ||||
|             if color is None: | ||||
|                 break | ||||
|             elif color is not color_last: | ||||
|                 cr.stroke() | ||||
|                 cr.set_source_rgb(*color) | ||||
| 
 | ||||
|                 if sx_last is not None and sy_last is not None: | ||||
|                     cr.move_to(x + sx_last, x + sy_last) | ||||
| 
 | ||||
|             sx, sy = self.sample_to_screen(sample.wind_speed, sample.wind_dir) | ||||
| 
 | ||||
|             if first: | ||||
|                 cr.move_to(x + sx, y + sy) | ||||
|                 first = False | ||||
|             else: | ||||
|                 cr.line_to(x + sx, y + sy) | ||||
| 
 | ||||
|             color_last = color | ||||
|             sx_last    = sx | ||||
|             sy_last    = sy | ||||
| 
 | ||||
|         cr.stroke() | ||||
|         cr.restore() | ||||
| 
 | ||||
|     def draw(self, cr: cairo.Context, x, y, samples: Iterable[SoundingSample]): | ||||
|         self.draw_speed_lines(cr, x, y) | ||||
|         self.draw_direction_lines(cr, x, y) | ||||
| 
 | ||||
|         self.draw_samples(cr, x, y, samples) | ||||
| 
 | ||||
|         self.draw_speed_legends(cr, x, y) | ||||
|         self.draw_direction_legends(cr, x, y) | ||||
		Loading…
	
	Add table
		
		Reference in a new issue