4 changed files with 1132 additions and 0 deletions
@ -0,0 +1,441 @@
|
||||
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'''<path id="hour-name-path" d="M {x1} {y1} a {radius} {radius} 15 0 1 {2 * delta_x} 0"></path>''' |
||||
|
||||
|
||||
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'''<g class="hour {season}" transform="rotate(-172.5, {image_width / 2}, {image_width / 2}) rotate({rotation}, {image_width / 2}, {image_width / 2})"> |
||||
<path |
||||
d="M {x1} {y1} a {outer_r} {outer_r} 15 0 1 {2 * delta_x} 0 l {s_delta_x} {s_delta_y} a {outer_r - ring_width} {outer_r - ring_width} 15 0 0 {i_delta_x} 0 z"></path> |
||||
<text |
||||
x="{image_width / 2}" |
||||
y="{hour_name_y}" |
||||
text-anchor="middle" |
||||
dominant-baseline="mathematical" |
||||
font-size="{hour_name_font_size}"><textPath xlink:href="#hour-name-path" startOffset="50%">{hour_name}</textPath></text> |
||||
<text |
||||
transform="rotate(-7.5, {image_width / 2}, {image_width / 2})" |
||||
class="utc" |
||||
x="{image_width / 2}" |
||||
y="{utc_hour_y}" |
||||
text-anchor="middle" |
||||
dominant-baseline="mathematical" |
||||
font-size="{utc_font_size}">U {hour:02d}</text> |
||||
</g>''' |
||||
|
||||
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'''<text |
||||
transform="rotate(180, {image_width / 2}, {image_width / 2}) rotate({rotation}, {image_width / 2}, {image_width / 2})" |
||||
class="local-hour" |
||||
x="{image_width / 2}" |
||||
y="{image_width / 2 - outer_r - font_size / 2}" |
||||
text-anchor="middle" |
||||
font-size="{font_size}">{hour}</text>''', |
||||
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'''<path |
||||
class="{range_name}" |
||||
d="M {image_width / 2} {image_width / 2} |
||||
L {image_width / 2 - start_delta_x} {image_width / 2 + radius - start_delta_y} |
||||
A {radius} {radius} {end_deg - start_deg} {large_arc_flag} 0 {image_width / 2 - end_delta_x} {image_width / 2 + radius - end_delta_y} |
||||
z"></path>''', |
||||
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'''<path |
||||
class="moon" |
||||
d="M {image_width / 2} {image_width / 2 - radius} |
||||
C {h1_x} {top_y}, {h1_x} {bottom_y}, {image_width / 2} {image_width / 2 + radius} |
||||
C {h2_x} {bottom_y}, {h2_x} {top_y}, {image_width / 2} {image_width / 2 - radius}"></path>''' |
||||
|
||||
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'''<svg width="{image_width}" height="{image_width}" xmlns:xlink="http://www.w3.org/1999/xlink"> |
||||
<style> |
||||
.hour path {{stroke: {hname_stroke_rgb}; stroke-width: {line_width};}} |
||||
.hour text {{stroke: none; fill: {line_rgb};}} |
||||
.hour text.utc {{stroke: none; fill: {utc_rgb};}} |
||||
.winter path {{fill: {winter_rgb};}} |
||||
.spring path {{fill: {spring_rgb};}} |
||||
.summer path {{fill: {summer_rgb};}} |
||||
.autumn path {{fill: {autumn_rgb};}} |
||||
.local-hour {{stroke: none; fill: {line_rgb};}} |
||||
.night-time {{stroke: none; fill: {night_rgb};}} |
||||
.blue-hour {{stroke: none; fill: {blue_rgb};}} |
||||
.golden-hour {{stroke: none; fill: {golden_rgb};}} |
||||
.day-time {{stroke: none; fill: {day_rgb};}} |
||||
.marker {{stroke: {night_rgb}; stroke-width: 2px; fill: none;}} |
||||
.moon-background {{stroke: {moon_rgb}; stroke-width: 2px; fill: {night_rgb};}} |
||||
.moon {{stroke: none; fill: {moon_rgb};}} |
||||
.sun {{stroke: none; fill: {line_rgb};}} |
||||
.dial {{stroke-width: 2px; stroke: {line_rgb};}} |
||||
</style> |
||||
<defs> |
||||
{hour_name_path(image_width, outer_r, ring_width)} |
||||
</defs> |
||||
<rect x="0" y="0" width="{image_width}" height="{image_width}" style="stroke: none; fill: {night_rgb};"></rect> |
||||
<g id="local-clock">\n''' |
||||
|
||||
for hour in range(24): |
||||
ret += local_hour( |
||||
image_width, outer_r, hour, hours[hour], local_hour_font_size, indent=8 |
||||
) |
||||
|
||||
ret += f''' </g> |
||||
<g id="seasonal-clock" transform="rotate({seconds_to_degrees(offset)}, {image_width / 2}, {image_width / 2})"> |
||||
''' |
||||
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''' <circle cx="{image_width / 2}" cy="{image_width / 2}" r="{marker_radius}" class="day-time"></circle>\n''' |
||||
ret += get_range_path( |
||||
image_width, |
||||
marker_radius, |
||||
'golden-hour', |
||||
morning_golden_end.time(), |
||||
evening_golden_start.time(), |
||||
) |
||||
sun_radius = 10 |
||||
ret += f''' <circle cx="{image_width / 2}" cy="{image_width / 2 + outer_r / 2 + sun_radius}" r="{sun_radius}" class="sun" transform="rotate({time_to_degrees(local_time.astimezone(UTC))}, {image_width / 2}, {image_width / 2})"></circle>\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''' <circle cx="{image_width / 2}" cy="{image_width / 2}" r="{marker_radius}" class="marker"></circle> |
||||
</g> |
||||
<g>\n''' |
||||
|
||||
moon_radius = 50 |
||||
ret += f''' <circle cx="{image_width / 2}" cy="{image_width / 2}" r="{moon_radius}" class="moon-background"></circle>\n''' |
||||
|
||||
ret += get_moon_path(image_width, moon_radius, moon_phase, indent=8) |
||||
ret += f''' </g> |
||||
<line |
||||
style="stroke: red;" |
||||
transform="rotate({time_to_degrees(midnight.astimezone(local_tz))}, {image_width / 2}, {image_width / 2})" |
||||
x1="{image_width / 2}" |
||||
y1="{image_width / 2 + marker_radius - sun_radius}" |
||||
x2="{image_width / 2}" |
||||
y2="{image_width / 2 + marker_radius}"></line> |
||||
<line |
||||
style="stroke: red;" |
||||
transform="rotate({time_to_degrees(noon.astimezone(local_tz))}, {image_width / 2}, {image_width / 2})" |
||||
x1="{image_width / 2}" |
||||
y1="{image_width / 2 + marker_radius - sun_radius}" |
||||
x2="{image_width / 2}" |
||||
y2="{image_width / 2 + marker_radius}"></line> |
||||
<line |
||||
id="dial" |
||||
transform="rotate({time_to_degrees(local_time)}, {image_width / 2}, {image_width / 2})" |
||||
class="dial" |
||||
x1="{image_width / 2}" |
||||
y1="{image_width / 2 + outer_r * 0.5}" |
||||
x2="{image_width / 2}" |
||||
y2="{image_width / 2 + outer_r - ring_width + hour_name_font_size}"></line> |
||||
</svg> |
||||
''' |
||||
|
||||
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)) |
Loading…
Reference in new issue