from datetime import datetime, time from math import ceil, cos, pi, radians, sin from typing import Union from astral import LocationInfo, moon from pytz import UTC, timezone from .config import load_config from .times import collect_day_parts 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) return f'rgb({r}, {g}, {b})' 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 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', 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) 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') 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(), ) 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))