from datetime import datetime, time from math import ceil, cos, pi, radians, sin from typing import Optional, Union from astral import LocationInfo, moon from pytz import UTC, timezone from .config import load_config from .times import collect_day_parts, get_rahukaalam_times HOURS_AMPM = ( 'Midnight', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a', '10a', '11a', 'Noon', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p', ) HOURS_24 = ( 'Midnight', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', 'Noon', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', ) HOUR_NAMES = ( 'Candle', 'Ice', 'Comet', 'Thimble', 'Root', 'Mist', 'Sprout', 'Rainbow', 'Worm', 'Bud', 'Blossom', 'Ladybug', 'Geese', 'Dust', 'Peach', 'Fog', 'Acorn', 'Gourd', 'Soup', 'Crow', 'Mushroom', 'Thunder', 'Frost', 'Lantern', ) SEASONS = ('winter', 'spring', 'summer', 'autumn') def hex_to_rgb(hex_color: str) -> str: r = int(hex_color[1:3], 16) g = int(hex_color[3:5], 16) b = int(hex_color[5:7], 16) ret = f'rgb({r}, {g}, {b})' return ret def get_utc_offset(utc_time: datetime, local_time: datetime): assert utc_time.tzinfo assert local_time.tzinfo utc = utc_time.replace(tzinfo=None) local = utc_time.replace(tzinfo=None) return (utc - local).total_seconds() def seconds_to_degrees(seconds: float) -> float: one_second = 360 / 86400 return seconds * one_second def indent_lines(string: str, indentation: int = 8) -> str: return '\n'.join((' ' * indentation) + line for line in string.split('\n')) + '\n' def time_to_degrees(timestamp: Union[datetime, time]) -> float: return seconds_to_degrees( timestamp.hour * 3600 + timestamp.minute * 60 + timestamp.second ) def hour_name_path(image_width: int, outer_r: float, ring_width: float) -> str: radius = outer_r - ring_width / 2 delta_x = radius * sin(radians(15) / 2) delta_y = radius * (1 - cos(radians(15) / 2)) x1 = image_width / 2 - delta_x y1 = (image_width / 2 - radius) + delta_y return f'''''' def hour_marker( hour: int, hour_name: str, image_width: int, outer_r: float, ring_width: float, hour_name_font_size: float, utc_font_size: float, indent: int = 8, ) -> str: season = SEASONS[ceil((hour + 1) / 6) - 1] rotation = hour * 15 delta_x = outer_r * sin(radians(15) / 2) delta_y = outer_r * (1 - cos(radians(15) / 2)) s_delta_x = 0 - ring_width * sin(radians(15) / 2) s_delta_y = ring_width * cos(radians(15) / 2) i_delta_x = -2 * (outer_r - ring_width) * sin(radians(15) / 2) x1 = image_width / 2 - delta_x y1 = (image_width / 2 - outer_r) + delta_y hour_name_y = image_width / 2 - outer_r + ring_width / 2 + hour_name_font_size / 4 utc_hour_y = image_width / 2 - outer_r + ring_width + utc_font_size ret = f''' {hour_name} U {hour:02d} ''' return indent_lines(ret, indent) def local_hour( image_width: int, outer_r: float, hour_num: int, hour: str, font_size: float, indent: int = 8, ) -> str: rotation = hour_num * 15 return indent_lines( f'''{hour}''', indent, ) def get_range_path( image_width: int, radius: float, range_name: str, start_time: time, end_time: time, outer: bool = True, indent: int = 8, ) -> str: start_deg = time_to_degrees(start_time) end_deg = time_to_degrees(end_time) start_delta_x = radius * sin(radians(start_deg)) start_delta_y = radius * (1 - cos(radians(start_deg))) end_delta_x = radius * sin(radians(end_deg)) end_delta_y = radius * (1 - cos(radians(end_deg))) deg_diff = end_deg - start_deg large_arc_flag = 0 if abs(deg_diff) >= 180 else 1 return indent_lines( f'''''', indent, ) def get_moon_path(image_width: int, radius: float, moon_phase: float, indent: int = 8): handle_x_pos = radius * 1.34 handle_y_pos = radius * 0.88 min_x = image_width / 2 - handle_x_pos max_x = min_x + 2 * handle_x_pos top_y = image_width / 2 - handle_y_pos bottom_y = image_width / 2 + handle_y_pos if moon_phase < 14: h1_x = min_x + 2 * handle_x_pos * (1 - moon_phase / 14) h2_x = max_x else: h1_x = min_x h2_x = max_x + 2 * handle_x_pos * (1 - moon_phase / 14) ret = f'''''' return indent_lines(ret, indent) def draw_rahukaala( image_width: float, rahukaala_radius: float, rahukaala_width: float, sun_radius: float, start: datetime, end: datetime, indent: int = 8, ) -> str: start_deg = time_to_degrees(start) end_deg = time_to_degrees(end) alpha = radians(end_deg - start_deg) delta_x = -rahukaala_radius * sin(alpha) delta_y = -rahukaala_radius * (1 - cos(alpha)) inner_delta_x = -(rahukaala_radius - rahukaala_width) * sin(alpha) inner_delta_y = -(rahukaala_radius - rahukaala_width) * (1 - cos(alpha)) s_delta_x = rahukaala_width * sin(alpha) s_delta_y = -rahukaala_width * cos(alpha) i_delta_x = -2 * (rahukaala_radius - rahukaala_width) * sin(alpha) x1 = image_width / 2 y1 = image_width / 2 + rahukaala_radius x2 = x1 + delta_x y2 = y1 + delta_y x4 = x1 y4 = y1 - rahukaala_width x3 = x4 + inner_delta_x y3 = y4 + inner_delta_y ret = f'''''' return indent_lines(ret, indent) def get_svg_data( local_time: datetime, image_width: int = 700, line_width: str = '2px', line_color: str = '#eebb55', utc_color: str = '#5b4426', hname_stroke_color: str = '#000000', winter_color: str = '#463e6c', spring_color: str = '#375737', summer_color: str = '#715c2b', autumn_color: str = '#6c442c', night_color: str = '#13111e', blue_color: str = '#090177', golden_color: str = '#aa842c', day_color: str = '#7dc5f0', moon_color: str = '#aaaaaa', rahukaala_color: str = '#ff7777', rahukaala_alpha: Optional[int] = None, local_hour_font_size: float = 16.5, hour_name_font_size: float = 13.37699, utc_hour_font_size: float = 15.0234, hour_24: bool = True, ) -> str: line_rgb = hex_to_rgb(line_color) hname_stroke_rgb = hex_to_rgb(hname_stroke_color) utc_rgb = hex_to_rgb(utc_color) winter_rgb = hex_to_rgb(winter_color) spring_rgb = hex_to_rgb(spring_color) summer_rgb = hex_to_rgb(summer_color) autumn_rgb = hex_to_rgb(autumn_color) night_rgb = hex_to_rgb(night_color) blue_rgb = hex_to_rgb(blue_color) golden_rgb = hex_to_rgb(golden_color) day_rgb = hex_to_rgb(day_color) moon_rgb = hex_to_rgb(moon_color) rahukaala_rgb = hex_to_rgb(rahukaala_color) hours = HOURS_24 if hour_24 else HOURS_AMPM outer_r = image_width / 2 - 3 * hour_name_font_size ring_width = hour_name_font_size * 3 utc_time = local_time.astimezone(UTC) utc_naive = utc_time.replace(tzinfo=None) local_naive = local_time.replace(tzinfo=None) offset = (local_naive - utc_naive).total_seconds() config = load_config() location = LocationInfo( config['city'], config['country'], config['timezone'], config['latitude'], config['longitude'], ) local_tz = location.tzinfo day_parts = collect_day_parts(location.observer, local_time) day_parts_dict = dict(part[0:2] for part in day_parts) morning_blue_start = day_parts_dict.pop('morning_blue_start') morning_blue_end = day_parts_dict.pop('morning_golden_start') morning_golden_end = day_parts_dict.pop('morning_golden_end') evening_golden_start = day_parts_dict.pop('evening_golden_start') evening_blue_start = day_parts_dict.pop('evening_blue_start') evening_blue_end = day_parts_dict.pop('evening_blue_end') moon_phase = moon.phase(local_time) noon = day_parts_dict.pop('noon') midnight = day_parts_dict.pop('midnight') if rahukaala_alpha is not None: value = rahukaala_alpha / value rahukaala_opacity = f' fill-opacity: {value:.2f};' else: rahukaala_opacity = '' ret = f''' {hour_name_path(image_width, outer_r, ring_width)} \n''' for hour in range(24): ret += local_hour( image_width, outer_r, hour, hours[hour], local_hour_font_size, indent=8 ) ret += f''' ''' for hour in range(24): ret += hour_marker( hour, HOUR_NAMES[hour], image_width, outer_r, ring_width, hour_name_font_size, utc_hour_font_size, indent=8, ) marker_radius = outer_r - ring_width - 2 * utc_hour_font_size ret += f''' \n''' ret += get_range_path( image_width, marker_radius, 'golden-hour', morning_golden_end.time(), evening_golden_start.time(), ) sun_radius = 10 ret += f''' \n''' ret += get_range_path( image_width, marker_radius, 'blue-hour', morning_blue_end.time(), evening_blue_start.time(), ) ret += get_range_path( image_width, marker_radius, 'night-time', morning_blue_start.time(), evening_blue_end.time(), ) rahukaala_radius = outer_r / 2 + 2 * sun_radius rahukaala_width = 2 * sun_radius for start, end in get_rahukaalam_times(location.observer, local_time): ret += draw_rahukaala(image_width, rahukaala_radius, rahukaala_width, sun_radius, start, end) ret += f''' \n''' moon_radius = 50 ret += f''' \n''' ret += get_moon_path(image_width, moon_radius, moon_phase, indent=8) ret += f''' ''' return ret def print_svg(): utc_now = datetime.utcnow().replace(tzinfo=UTC) local_now = utc_now.astimezone(timezone('Europe/Budapest')) print(get_svg_data(local_now))