You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

243 line
8.4 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorro - Compute The Next Ephemerides
  3. # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. from abc import ABC, abstractmethod
  18. from typing import Union
  19. from datetime import datetime
  20. from numpy import pi, arcsin
  21. from skyfield.api import Topos, Time
  22. from skyfield.vectorlib import VectorSum as SkfPlanet
  23. from .core import get_skf_objects, get_timescale
  24. from .i18n import _
  25. MOON_PHASES = {
  26. 'NEW_MOON': _('New Moon'),
  27. 'WAXING_CRESCENT': _('Waxing crescent'),
  28. 'FIRST_QUARTER': _('First Quarter'),
  29. 'WAXING_GIBBOUS': _('Waxing gibbous'),
  30. 'FULL_MOON': _('Full Moon'),
  31. 'WANING_GIBBOUS': _('Waning gibbous'),
  32. 'LAST_QUARTER': _('Last Quarter'),
  33. 'WANING_CRESCENT': _('Waning crescent')
  34. }
  35. EVENTS = {
  36. 'OPPOSITION': {'message': _('%s is in opposition')},
  37. 'CONJUNCTION': {'message': _('%s and %s are in conjunction')},
  38. 'OCCULTATION': {'message': _('%s occults %s')},
  39. 'MAXIMAL_ELONGATION': {'message': _("%s's largest elongation")}
  40. }
  41. class MoonPhase:
  42. def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]):
  43. if identifier not in MOON_PHASES.keys():
  44. raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()),
  45. identifier))
  46. self.identifier = identifier
  47. self.time = time
  48. self.next_phase_date = next_phase_date
  49. def get_phase(self):
  50. return MOON_PHASES[self.identifier]
  51. def get_next_phase(self):
  52. if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT':
  53. next_identifier = 'FIRST_QUARTER'
  54. elif self.identifier == 'FIRST_QUARTER' or self.identifier == 'WAXING_GIBBOUS':
  55. next_identifier = 'FULL_MOON'
  56. elif self.identifier == 'FULL_MOON' or self.identifier == 'WANING_GIBBOUS':
  57. next_identifier = 'LAST_QUARTER'
  58. else:
  59. next_identifier = 'NEW_MOON'
  60. return MOON_PHASES[next_identifier]
  61. class Position:
  62. def __init__(self, latitude: float, longitude: float):
  63. self.latitude = latitude
  64. self.longitude = longitude
  65. self.observation_planet = None
  66. self._topos = None
  67. def get_planet_topos(self) -> Topos:
  68. if self.observation_planet is None:
  69. raise TypeError('Observation planet must be set.')
  70. if self._topos is None:
  71. self._topos = self.observation_planet + Topos(latitude_degrees=self.latitude,
  72. longitude_degrees=self.longitude)
  73. return self._topos
  74. class AsterEphemerides:
  75. def __init__(self,
  76. rise_time: Union[datetime, None],
  77. culmination_time: Union[datetime, None],
  78. set_time: Union[datetime, None]):
  79. self.rise_time = rise_time
  80. self.culmination_time = culmination_time
  81. self.set_time = set_time
  82. class Object(ABC):
  83. """
  84. An astronomical object.
  85. """
  86. def __init__(self,
  87. name: str,
  88. skyfield_name: str,
  89. ephemerides: AsterEphemerides or None = None,
  90. radius: float = None):
  91. """
  92. Initialize an astronomical object
  93. :param str name: the official name of the object (may be internationalized)
  94. :param str skyfield_name: the internal name of the object in Skyfield library
  95. :param float radius: the radius (in km) of the object
  96. :param AsterEphemerides ephemerides: the ephemerides associated to the object
  97. """
  98. self.name = name
  99. self.skyfield_name = skyfield_name
  100. self.radius = radius
  101. self.ephemerides = ephemerides
  102. def get_skyfield_object(self) -> SkfPlanet:
  103. return get_skf_objects()[self.skyfield_name]
  104. @abstractmethod
  105. def get_type(self) -> str:
  106. pass
  107. def get_apparent_radius(self, time: Time, from_place) -> float:
  108. """
  109. Calculate the apparent radius, in degrees, of the object from the given place at a given time.
  110. :param time:
  111. :param from_place:
  112. :return:
  113. """
  114. if self.radius is None:
  115. raise ValueError('Missing radius for %s object' % self.name)
  116. return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km)
  117. class Star(Object):
  118. def get_type(self) -> str:
  119. return 'star'
  120. class Planet(Object):
  121. def get_type(self) -> str:
  122. return 'planet'
  123. class DwarfPlanet(Planet):
  124. def get_type(self) -> str:
  125. return 'dwarf_planet'
  126. class Satellite(Object):
  127. def get_type(self) -> str:
  128. return 'satellite'
  129. class Event:
  130. def __init__(self, event_type: str, objects: [Object], start_time: datetime,
  131. end_time: Union[datetime, None] = None, details: str = None):
  132. if event_type not in EVENTS.keys():
  133. accepted_types = ', '.join(EVENTS.keys())
  134. raise ValueError('event_type parameter must be one of the following: %s (got %s)' % (accepted_types,
  135. event_type))
  136. self.event_type = event_type
  137. self.objects = objects
  138. self.start_time = start_time
  139. self.end_time = end_time
  140. self.details = details
  141. def get_description(self, show_details: bool = True) -> str:
  142. description = EVENTS[self.event_type]['message'] % self._get_objects_name()
  143. if show_details and self.details is not None:
  144. description += ' ({:s})'.format(self.details)
  145. return description
  146. def _get_objects_name(self):
  147. if len(self.objects) == 1:
  148. return self.objects[0].name
  149. return tuple(object.name for object in self.objects)
  150. def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]:
  151. tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1)
  152. phases = list(MOON_PHASES.keys())
  153. current_phase = None
  154. current_phase_time = None
  155. next_phase_time = None
  156. i = 0
  157. if len(times) == 0:
  158. return None
  159. for i, time in enumerate(times):
  160. if now.utc_iso() <= time.utc_iso():
  161. if vals[i] in [0, 2, 4, 6]:
  162. if time.utc_datetime() < tomorrow.utc_datetime():
  163. current_phase_time = time
  164. current_phase = phases[vals[i]]
  165. else:
  166. i -= 1
  167. current_phase_time = None
  168. current_phase = phases[vals[i]]
  169. else:
  170. current_phase = phases[vals[i]]
  171. break
  172. for j in range(i + 1, len(times)):
  173. if vals[j] in [0, 2, 4, 6]:
  174. next_phase_time = times[j]
  175. break
  176. return MoonPhase(current_phase,
  177. current_phase_time.utc_datetime() if current_phase_time is not None else None,
  178. next_phase_time.utc_datetime() if next_phase_time is not None else None)
  179. MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
  180. ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
  181. Satellite(_('Moon'), 'MOON', radius=1737.4),
  182. Planet(_('Mercury'), 'MERCURY', radius=2439.7),
  183. Planet(_('Venus'), 'VENUS', radius=6051.8),
  184. Planet(_('Mars'), 'MARS', radius=3396.2),
  185. Planet(_('Jupiter'), 'JUPITER BARYCENTER', radius=71492),
  186. Planet(_('Saturn'), 'SATURN BARYCENTER', radius=60268),
  187. Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559),
  188. Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764),
  189. Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)]