From 392b2dd7dcd1df5e166e45e2ac8a5d5f970c8ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Fri, 1 Nov 2019 16:36:39 +0100 Subject: [PATCH] Refactorization of the computation functions --- .github/workflows/pythonapp.yml | 2 +- .pylintrc | 5 +- README.md | 25 ++-- ephemeris.py | 226 ----------------------------- kosmorro.py | 51 ++++--- kosmorrolib/__init__.py | 17 +++ kosmorrolib/core.py | 46 ++++++ kosmorrolib/data.py | 92 ++++++++++++ dumper.py => kosmorrolib/dumper.py | 44 ++++-- kosmorrolib/ephemerides.py | 149 +++++++++++++++++++ 10 files changed, 381 insertions(+), 276 deletions(-) delete mode 100644 ephemeris.py create mode 100644 kosmorrolib/__init__.py create mode 100644 kosmorrolib/core.py create mode 100644 kosmorrolib/data.py rename dumper.py => kosmorrolib/dumper.py (50%) create mode 100644 kosmorrolib/ephemerides.py diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index b3c9694..03f1129 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -19,4 +19,4 @@ jobs: pipenv sync -d - name: Lint run: | - pipenv run pylint *.py + pipenv run pylint *.py kosmorrolib/*.py diff --git a/.pylintrc b/.pylintrc index 9208a70..a8bf859 100644 --- a/.pylintrc +++ b/.pylintrc @@ -30,7 +30,7 @@ limit-inference-results=100 # usually to register additional checkers. load-plugins=pylintfileheader -file-header=#!/usr/bin/env python3\n\n# Kosmorro - Compute The Next Ephemeris\n# Copyright \(C\) 2019 Jérôme Deuchnord \n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or \(at your option\) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see .\n\n +file-header=#!/usr/bin/env python3\n\n# Kosmorro - Compute The Next Ephemerides\n# Copyright \(C\) 2019 Jérôme Deuchnord \n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as\n# published by the Free Software Foundation, either version 3 of the\n# License, or \(at your option\) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see .\n # Pickle collected data for later comparisons. persistent=yes @@ -144,7 +144,8 @@ disable=print-statement, missing-docstring, too-many-locals, too-many-branches, - too-few-public-methods + too-few-public-methods, + protected-access # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/README.md b/README.md index c32a525..c5d4be8 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,20 @@ For instance, if you want the ephemeris of October 31th, 2019 in Paris, France: ```console $ python kosmorro.py --latitude 48.8032 --longitude 2.3511 -m 10 -d 31 2019 -Planet Rise time Maximum time Set time --------- ----------- -------------- ---------- -SUN 06:35 - 16:32 -MERCURY 08:44 13:01 16:59 -VENUS 08:35 13:01 17:18 -MARS 04:48 10:20 15:51 -JUPITER 10:40 15:01 18:46 -SATURN 12:12 16:20 20:26 -URANUS 16:23 - 06:22 -NEPTUNE 14:53 20:23 01:56 -PLUTO 12:36 17:01 20:50 +Planet Rise time Culmination time Set time +-------- ----------- ------------------ ---------- +Sun 06:35 11:33 16:32 +Moon 10:21 14:41 19:01 +Mercury 08:37 12:51 17:04 +Venus 08:28 12:56 17:23 +Mars 04:42 10:19 15:55 +Jupiter 10:33 14:42 18:52 +Saturn 12:05 16:18 20:31 +Uranus 16:17 - 06:27 +Neptune 14:47 20:21 02:00 +Pluto 12:29 16:42 20:55 Moon phase: New Moon + +Note: All the hours are given in UTC. ``` \ No newline at end of file diff --git a/ephemeris.py b/ephemeris.py deleted file mode 100644 index 1343c7e..0000000 --- a/ephemeris.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemeris -# Copyright (C) 2019 Jérôme Deuchnord -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from multiprocessing import Pool as ThreadPool -from skyfield.api import Loader, Topos -from skyfield import almanac - - -class Ephemeris: - MONTH = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] - PLANETS = ['MERCURY', 'VENUS', 'MARS', 'JUPITER BARYCENTER', 'SATURN BARYCENTER', 'URANUS BARYCENTER', - 'NEPTUNE BARYCENTER', 'PLUTO BARYCENTER'] - position = None - latitude = None - longitude = None - timescale = None - planets = None - - def __init__(self, position): - load = Loader('./cache') - self.timescale = load.timescale() - self.planets = load('de421.bsp') - self.latitude = position['lat'] - self.longitude = position['lon'] - self.position = Topos(latitude_degrees=position['lat'], longitude_degrees=position['lon']) - - def get_sun(self, start_time, end_time) -> dict: - times, is_risen = almanac.find_discrete(start_time, - end_time, - almanac.sunrise_sunset(self.planets, self.position)) - - sunrise = times[0] if is_risen[0] else times[1] - sunset = times[1] if not is_risen[1] else times[0] - - return {'rise': sunrise, 'set': sunset} - - def get_moon(self, year, month, day) -> dict: - time1 = self.timescale.utc(year, month, day - 10) - time2 = self.timescale.utc(year, month, day) - - _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(self.planets)) - - return {'phase': moon_phase[-1]} - - def get_planets(self, year: int, month: int, day: int) -> dict: - args = [] - - for planet_name in self.PLANETS: - args.append({'planet': planet_name, - 'observer': {'latitude': self.latitude, 'longitude': self.longitude}, - 'year': year, 'month': month, 'day': day}) - - with ThreadPool() as pool: - planets = pool.map(Ephemeris.get_planet, args) - - obj = {} - - for planet in planets: - obj[planet['name'].split(' ')[0]] = {'rise': planet['rise'], 'maximum': planet['maximum'], - 'set': planet['set']} - - return obj - - @staticmethod - def get_planet(p_obj) -> dict: - load = Loader('./cache') - planets = load('de421.bsp') - timescale = load.timescale() - position = Topos(latitude_degrees=p_obj['observer']['latitude'], - longitude_degrees=p_obj['observer']['longitude']) - observer = planets['earth'] + position - planet = planets[p_obj['planet']] - rise_time = maximum_time = set_time = None - is_risen = None - is_elevating = None - last_is_elevating = None - last_position = None - - for hours in range(0, 24): - time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours) - position = observer.at(time).observe(planet).apparent().altaz()[0].degrees - - if is_risen is None: - is_risen = position > 0 - if last_position is not None: - is_elevating = last_position < position - - if rise_time is None and not is_risen and is_elevating and position > 0: - # Planet has risen in the last hour, let's look for a more precise time! - for minutes in range(0, 60): - time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) - position = observer.at(time).observe(planet).apparent().altaz()[0].degrees - - if position > 0: - # Planet has just risen! - rise_time = time - is_risen = True - break - - if set_time is None and is_risen and not is_elevating and position < 0: - # Planet has set in the last hour, let's look for a more precise time! - for minutes in range(0, 60): - time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) - position = observer.at(time).observe(planet).apparent().altaz()[0].degrees - - if position < 0: - # Planet has just set! - set_time = time - is_risen = False - break - - if not is_elevating and last_is_elevating: - # Planet has reached its azimuth in the last hour, let's look for a more precise time! - for minutes in range(0, 60): - time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes) - position = observer.at(time).observe(planet).apparent().altaz()[0].degrees - - maximum_time = time - - if last_position > position: - # Planet has reached its azimuth! - is_elevating = False - break - - last_position = position - - last_position = position - last_is_elevating = is_elevating - - if rise_time is not None and set_time is not None and maximum_time is not None: - return { - 'name': p_obj['planet'], - 'rise': rise_time, - 'maximum': maximum_time, - 'set': set_time - } - - return { - 'name': p_obj['planet'], - 'rise': rise_time if rise_time is not None else None, - 'maximum': maximum_time if maximum_time is not None else None, - 'set': set_time if set_time is not None else None - } - - def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: - ephemeris = {} - time1 = self.timescale.utc(year, month, day, 2) - time2 = self.timescale.utc(year, month, day + 1, 2) - - # Compute sunrise and sunset - ephemeris['sun'] = self.get_sun(time1, time2) - ephemeris['moon'] = self.get_moon(year, month, day) - ephemeris['planets'] = self.get_planets(year, month, day) - - return ephemeris - - def compute_ephemerides_for_month(self, year: int, month: int) -> list: - if month == 2: - is_leap_year = (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) - max_day = 29 if is_leap_year else 28 - elif month < 8: - max_day = 30 if month % 2 == 0 else 31 - else: - max_day = 31 if month % 2 == 0 else 30 - - ephemerides = [] - - for day in range(1, max_day + 1): - ephemerides.append(self.compute_ephemerides_for_day(year, month, day)) - - return ephemerides - - def compute_ephemerides_for_year(self, year: int) -> dict: - ephemerides = {} - for month in range(0, 12): - ephemerides[self.MONTH[month]] = self.compute_ephemerides_for_month(year, month + 1) - - ephemerides['seasons'] = self.get_seasons(year) - - return ephemerides - - def get_seasons(self, year: int) -> dict: - start_time = self.timescale.utc(year, 1, 1) - end_time = self.timescale.utc(year, 12, 31) - times, almanac_seasons = almanac.find_discrete(start_time, end_time, almanac.seasons(self.planets)) - - seasons = {} - for time, almanac_season in zip(times, almanac_seasons): - if almanac_season == 0: - season = 'MARCH' - elif almanac_season == 1: - season = 'JUNE' - elif almanac_season == 2: - season = 'SEPTEMBER' - elif almanac_season == 3: - season = 'DECEMBER' - else: - raise AssertionError - - seasons[season] = time.utc_iso() - - return seasons - - def compute_ephemeris(self, year: int, month: int, day: int): - if day is not None: - return self.compute_ephemerides_for_day(year, month, day) - - if month is not None: - return self.compute_ephemerides_for_month(year, month) - - return self.compute_ephemerides_for_year(year) diff --git a/kosmorro.py b/kosmorro.py index 91d965e..f478abe 100644 --- a/kosmorro.py +++ b/kosmorro.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Kosmorro - Compute The Next Ephemeris +# Kosmorro - Compute The Next Ephemerides # Copyright (C) 2019 Jérôme Deuchnord # # This program is free software: you can redistribute it and/or modify @@ -19,8 +19,8 @@ import argparse from datetime import date import numpy -import dumper -from ephemeris import Ephemeris +from kosmorrolib import dumper +from kosmorrolib.ephemerides import EphemeridesComputer, Position # Fixes the "TypeError: Object of type int64 is not JSON serializable" @@ -35,35 +35,42 @@ def main(): args = get_args() year = args.year month = args.month - day = args.date - position = {'lat': args.latitude, 'lon': args.longitude, 'alt': args.altitude} + day = args.day if day is not None and month is None: month = date.today().month - ephemeris = Ephemeris(position) - ephemerides = ephemeris.compute_ephemeris(year, month, day) + ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) + ephemerides = ephemeris.compute_ephemerides(year, month, day) dump = dumper.TextDumper(ephemerides) print(dump.to_string()) def get_args(): - parser = argparse.ArgumentParser(description='Compute the ephemeris for a given day/month/year.', - epilog='By default, the observer will be set at position (0,0) with an altitude' - ' of 0. You will more likely want to change that.') - parser.add_argument('--latitude', '-lat', type=float, default=0., help="The observer's position on Earth" - " (latitude)") - parser.add_argument('--longitude', '-lon', type=float, default=0., help="The observer's position on Earth" - " (longitude)") - parser.add_argument('--altitude', '-alt', type=float, default=0., help="The observer's position on Earth" - " (altitude)") - parser.add_argument('--date', '-d', type=int, help='A number between 1 and 28, 29, 30 or 31 (depending on the' - ' month). The date you want to compute the ephemeris for') - parser.add_argument('--month', '-m', type=int, help='A number between 1 and 12. The month you want to compute the' - ' ephemeris for (defaults to the current month if the day is' - ' defined)') - parser.add_argument('year', type=int, help='The year you want to compute the ephemeris for') + today = date.today() + + parser = argparse.ArgumentParser(description='Compute the ephemerides for a given date, at a given position' + ' on Earth.', + epilog='By default, the ephemerides will be computed for today (%s) for an' + ' observer positioned at coordinates (0,0), with an altitude of 0.' + % today.strftime('%a %b %d, %Y')) + + parser.add_argument('--latitude', '-lat', type=float, default=0., + help="The observer's latitude on Earth") + parser.add_argument('--longitude', '-lon', type=float, default=0., + help="The observer's longitude on Earth") + parser.add_argument('--altitude', '-alt', type=float, default=0., + help="The observer's altitude on Earth") + parser.add_argument('--day', '-d', type=int, default=today.day, + help='A number between 1 and 28, 29, 30 or 31 (depending on the month). The day you want to ' + ' compute the ephemerides for. Defaults to %d (the current day).' % today.day) + parser.add_argument('--month', '-m', type=int, default=today.month, + help='A number between 1 and 12. The month you want to compute the ephemerides for. Defaults to' + ' %d (the current month).' % today.month) + parser.add_argument('--year', '-y', type=int, default=today.year, + help='The year you want to compute the ephemerides for.' + ' Defaults to %d (the current year).' % today.year) return parser.parse_args() diff --git a/kosmorrolib/__init__.py b/kosmorrolib/__init__.py new file mode 100644 index 0000000..239afac --- /dev/null +++ b/kosmorrolib/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . diff --git a/kosmorrolib/core.py b/kosmorrolib/core.py new file mode 100644 index 0000000..4229337 --- /dev/null +++ b/kosmorrolib/core.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from skyfield.api import Loader + +from .data import Star, Planet, Satellite + +MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] + +ASTERS = [Star('Sun', 'SUN'), + Satellite('Moon', 'MOON'), + Planet('Mercury', 'MERCURY'), + Planet('Venus', 'VENUS'), + Planet('Mars', 'MARS'), + Planet('Jupiter', 'JUPITER BARYCENTER'), + Planet('Saturn', 'SATURN BARYCENTER'), + Planet('Uranus', 'URANUS BARYCENTER'), + Planet('Neptune', 'NEPTUNE BARYCENTER'), + Planet('Pluto', 'PLUTO BARYCENTER')] + + +def get_loader(): + return Loader('./cache') + + +def get_timescale(): + return get_loader().timescale() + + +def get_skf_objects(): + return get_loader()('de421.bsp') diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py new file mode 100644 index 0000000..89695a4 --- /dev/null +++ b/kosmorrolib/data.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from abc import ABC, abstractmethod +from typing import Union + +from skyfield.api import Topos +from skyfield.timelib import Time + + +class Position: + def __init__(self, latitude: float, longitude: float, altitude: float = 0): + self.latitude = latitude + self.longitude = longitude + self.altitude = altitude + self.observation_planet = None + + def get_planet_topos(self) -> Topos: + if self.observation_planet is None: + raise TypeError('Observation planet must be set.') + + return self.observation_planet + Topos(latitude_degrees=self.latitude, longitude_degrees=self.longitude) + + +class AsterEphemerides: + def __init__(self, + rise_time: Union[Time, None], + culmination_time: Union[Time, None], + set_time: Union[Time, None]): + self.rise_time = rise_time + self.maximum_time = culmination_time + self.set_time = set_time + + +class Object(ABC): + """ + An astronomical object. + """ + + def __init__(self, + name: str, + skyfield_name: str, + ephemerides: AsterEphemerides or None = None): + """ + Initialize an astronomical object + + :param str name: the official name of the object (may be internationalized) + :param str skyfield_name: the internal name of the object in Skyfield library + :param AsterEphemerides ephemerides: the ephemerides associated to the object + """ + self.name = name + self.skyfield_name = skyfield_name + self.ephemerides = ephemerides + + @abstractmethod + def get_type(self) -> str: + pass + + +class Star(Object): + def get_type(self) -> str: + return 'star' + + +class Planet(Object): + def get_type(self) -> str: + return 'planet' + + +class DwarfPlanet(Planet): + def get_type(self) -> str: + return 'dwarf_planet' + + +class Satellite(Object): + def get_type(self) -> str: + return 'satellite' diff --git a/dumper.py b/kosmorrolib/dumper.py similarity index 50% rename from dumper.py rename to kosmorrolib/dumper.py index 650f8bd..b5f039c 100644 --- a/dumper.py +++ b/kosmorrolib/dumper.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Kosmorro - Compute The Next Ephemeris +# Kosmorro - Compute The Next Ephemerides # Copyright (C) 2019 Jérôme Deuchnord # # This program is free software: you can redistribute it and/or modify @@ -17,13 +17,16 @@ # along with this program. If not, see . from abc import ABC, abstractmethod +import datetime from tabulate import tabulate from skyfield import almanac +from .data import Object class Dumper(ABC): - def __init__(self, ephemeris): + def __init__(self, ephemeris, date: datetime.date = datetime.date.today()): self.ephemeris = ephemeris + self.date = date @abstractmethod def to_string(self): @@ -32,23 +35,36 @@ class Dumper(ABC): class TextDumper(Dumper): def to_string(self): - return '\n\n'.join([self.get_planets(self.ephemeris['planets'], self.ephemeris['sun']), - self.get_moon(self.ephemeris['moon'])]) + return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), + self.get_asters(self.ephemeris['planets']), + self.get_moon(self.ephemeris['moon_phase']), + 'Note: All the hours are given in UTC.']) @staticmethod - def get_planets(planets, sun): - data = [['SUN', sun['rise'].utc_strftime('%H:%M'), '-', sun['set'].utc_strftime('%H:%M')]] - for planet in planets: - name = planet - planet_data = planets[planet] - planet_rise = planet_data['rise'].utc_strftime('%H:%M') if planet_data['rise'] is not None else ' -' - planet_maximum = planet_data['maximum'].utc_strftime('%H:%M') if planet_data['maximum'] is not None\ - else ' -' - planet_set = planet_data['set'].utc_strftime('%H:%M') if planet_data['set'] is not None else ' -' + def get_asters(asters: [Object]) -> str: + data = [] + + for aster in asters: + name = aster.name + + if aster.ephemerides.rise_time is not None: + planet_rise = aster.ephemerides.rise_time.utc_strftime('%H:%M') + else: + planet_rise = '-' + + if aster.ephemerides.maximum_time is not None: + planet_maximum = aster.ephemerides.maximum_time.utc_strftime('%H:%M') + else: + planet_maximum = '-' + + if aster.ephemerides.set_time is not None: + planet_set = aster.ephemerides.set_time.utc_strftime('%H:%M') + else: + planet_set = '-' data.append([name, planet_rise, planet_maximum, planet_set]) - return tabulate(data, headers=['Planet', 'Rise time', 'Maximum time', 'Set time'], tablefmt='simple', + return tabulate(data, headers=['Planet', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple', stralign='center', colalign=('left',)) @staticmethod diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py new file mode 100644 index 0000000..93b5e35 --- /dev/null +++ b/kosmorrolib/ephemerides.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import datetime +from skyfield import almanac +from skyfield.timelib import Time + +from .data import Object, Position, AsterEphemerides +from .core import get_skf_objects, get_timescale, ASTERS, MONTHS + +RISEN_ANGLE = -0.8333 + + +class EphemeridesComputer: + def __init__(self, position: Position): + position.observation_planet = get_skf_objects()['earth'] + self.position = position + + def get_sun(self, start_time, end_time) -> dict: + times, is_risen = almanac.find_discrete(start_time, + end_time, + almanac.sunrise_sunset(get_skf_objects(), self.position)) + + sunrise = times[0] if is_risen[0] else times[1] + sunset = times[1] if not is_risen[1] else times[0] + + return {'rise': sunrise, 'set': sunset} + + @staticmethod + def get_moon(year, month, day) -> dict: + time1 = get_timescale().utc(year, month, day - 10) + time2 = get_timescale().utc(year, month, day) + + _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) + + return {'phase': moon_phase[-1]} + + @staticmethod + def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: + skyfield_aster = get_skf_objects()[aster.skyfield_name] + + def get_angle(time: Time) -> float: + return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees + + def is_risen(time: Time) -> bool: + return get_angle(time) > RISEN_ANGLE + + get_angle.rough_period = 1.0 + is_risen.rough_period = 0.5 + + start_time = get_timescale().utc(date.year, date.month, date.day) + end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) + + rise_times, arr = almanac.find_discrete(start_time, end_time, is_risen) + try: + culmination_time, _ = almanac._find_maxima(start_time, end_time, get_angle, epsilon=1./3600/24) + except ValueError: + culmination_time = None + + if len(rise_times) == 2: + rise_time = rise_times[0 if arr[0] else 1] + set_time = rise_times[0 if not arr[1] else 0] + else: + rise_time = rise_times[0] if arr[0] else None + set_time = rise_times[0] if not arr[0] else None + + culmination_time = culmination_time[0] if culmination_time is not None else None + + aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time) + return aster + + @staticmethod + def is_leap_year(year: int) -> bool: + return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) + + def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: + return {'moon_phase': self.get_moon(year, month, day), + 'planets': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) + for aster in ASTERS]} + + def compute_ephemerides_for_month(self, year: int, month: int) -> [dict]: + if month == 2: + max_day = 29 if self.is_leap_year(year) else 28 + elif month < 8: + max_day = 30 if month % 2 == 0 else 31 + else: + max_day = 31 if month % 2 == 0 else 30 + + ephemerides = [] + + for day in range(1, max_day + 1): + ephemerides.append(self.compute_ephemerides_for_day(year, month, day)) + + return ephemerides + + def compute_ephemerides_for_year(self, year: int) -> [dict]: + ephemerides = {'seasons': self.get_seasons(year)} + + for month in range(0, 12): + ephemerides[MONTHS[month]] = self.compute_ephemerides_for_month(year, month + 1) + + return ephemerides + + @staticmethod + def get_seasons(year: int) -> dict: + start_time = get_timescale().utc(year, 1, 1) + end_time = get_timescale().utc(year, 12, 31) + times, almanac_seasons = almanac.find_discrete(start_time, end_time, almanac.seasons(get_skf_objects())) + + seasons = {} + for time, almanac_season in zip(times, almanac_seasons): + if almanac_season == 0: + season = 'MARCH' + elif almanac_season == 1: + season = 'JUNE' + elif almanac_season == 2: + season = 'SEPTEMBER' + elif almanac_season == 3: + season = 'DECEMBER' + else: + raise AssertionError + + seasons[season] = time.utc_iso() + + return seasons + + def compute_ephemerides(self, year: int, month: int, day: int): + if day is not None: + return self.compute_ephemerides_for_day(year, month, day) + + if month is not None: + return self.compute_ephemerides_for_month(year, month) + + return self.compute_ephemerides_for_year(year)