| @@ -4,3 +4,4 @@ source = | |||||
| kosmorrolib | kosmorrolib | ||||
| omit = | omit = | ||||
| test/* | test/* | ||||
| kosmorrolib/gui/* | |||||
| @@ -1,6 +1,8 @@ | |||||
| __pycache__ | __pycache__ | ||||
| /build | |||||
| /build/* | |||||
| !/build/distrib | |||||
| /dist | /dist | ||||
| kosmorro.egg-info | kosmorro.egg-info | ||||
| .coverage | .coverage | ||||
| coverage.xml | coverage.xml | ||||
| @@ -28,7 +28,7 @@ function canRun() { | |||||
| return 0 | return 0 | ||||
| } | } | ||||
| # Asserts that command $1 has finished with sucess | |||||
| # Asserts that command $1 has finished with success | |||||
| # $1: the command to run | # $1: the command to run | ||||
| function assertSuccess() { | function assertSuccess() { | ||||
| if ! canRun "$2"; then | if ! canRun "$2"; then | ||||
| @@ -48,7 +48,7 @@ function assertSuccess() { | |||||
| echo -n '.' | echo -n '.' | ||||
| } | } | ||||
| # Asserts that command $1 has finished with sucess | |||||
| # Asserts that command $1 has finished with failure | |||||
| # $1: the command to run | # $1: the command to run | ||||
| function assertFailure() { | function assertFailure() { | ||||
| if ! canRun "$2"; then | if ! canRun "$2"; then | ||||
| @@ -6,6 +6,7 @@ test: | |||||
| unset KOSMORRO_TIMEZONE; \ | unset KOSMORRO_TIMEZONE; \ | ||||
| LANG=C pipenv run python3 -m coverage run -m unittest test | LANG=C pipenv run python3 -m coverage run -m unittest test | ||||
| .PHONY: build | |||||
| build: i18n | build: i18n | ||||
| python3 setup.py sdist bdist_wheel | python3 setup.py sdist bdist_wheel | ||||
| @@ -54,3 +55,30 @@ finish-release: env | |||||
| @echo | @echo | ||||
| @echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!" | @echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!" | ||||
| @echo -e "Invoke \e[33mgit push origin master features v$$RELEASE_NUMBER\e[39m to finish." | @echo -e "Invoke \e[33mgit push origin master features v$$RELEASE_NUMBER\e[39m to finish." | ||||
| distapp = dist/Kosmorro.app | |||||
| distrib-mac: env | |||||
| @if [ -e $(distapp) ]; then echo "Deleting the existing app."; rm -rf $(distapp); fi | |||||
| mkdir -p "$(distapp)/Contents/MacOS" "$(distapp)/Contents/Resources" | |||||
| # Add application files | |||||
| cp "kosmorro" "$(distapp)/Contents/MacOS/kosmorro" | |||||
| cp -r "kosmorrolib" "$(distapp)/Contents/MacOS/kosmorrolib" | |||||
| cp "Pipfile" "$(distapp)/Contents/MacOS/Pipfile" | |||||
| cp "Pipfile.lock" "$(distapp)/Contents/MacOS/Pipfile.lock" | |||||
| # Install dependencies | |||||
| cd $(distapp)/Contents/MacOS && PIPENV_VENV_IN_PROJECT=1 pipenv sync | |||||
| cd $(distapp)/Contents/MacOS && source .venv/bin/activate && pip install wxPython | |||||
| # Add Mac-specific files | |||||
| cp "build/distrib/darwin/Info.plist" "$(distapp)/Contents/Info.plist" | |||||
| cp "build/distrib/darwin/launch-kosmorro.sh" "$(distapp)/Contents/MacOS/launch-kosmorro" | |||||
| cp "build/distrib/darwin/icon.icns" "$(distapp)/Contents/Resources/icon.icns" | |||||
| sed "s/{{app_version}}/$$RELEASE_NUMBER/" "build/distrib/darwin/Info.plist" > "$(distapp)/Contents/Info.plist" | |||||
| chmod +x "$(distapp)/Contents/MacOS/launch-kosmorro" | |||||
| echo "Application created." | |||||
| @@ -270,11 +270,11 @@ | |||||
| }, | }, | ||||
| "coveralls": { | "coveralls": { | ||||
| "hashes": [ | "hashes": [ | ||||
| "sha256:3726d35c0f93a28631a003880e2aa6cc93c401d62bc6919c5cb497217ba30c55", | |||||
| "sha256:afe359cd5b350e1b3895372bda32af8f0260638c7c4a31a5c0f15aa6a96f40d9" | |||||
| "sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6", | |||||
| "sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1" | |||||
| ], | ], | ||||
| "index": "pypi", | "index": "pypi", | ||||
| "version": "==2.1.1" | |||||
| "version": "==2.1.2" | |||||
| }, | }, | ||||
| "docopt": { | "docopt": { | ||||
| "hashes": [ | "hashes": [ | ||||
| @@ -292,11 +292,11 @@ | |||||
| }, | }, | ||||
| "isort": { | "isort": { | ||||
| "hashes": [ | "hashes": [ | ||||
| "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", | |||||
| "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" | |||||
| "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750", | |||||
| "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8" | |||||
| ], | ], | ||||
| "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", | |||||
| "version": "==4.3.21" | |||||
| "markers": "python_version >= '3.6' and python_version < '4.0'", | |||||
| "version": "==5.4.2" | |||||
| }, | }, | ||||
| "lazy-object-proxy": { | "lazy-object-proxy": { | ||||
| "hashes": [ | "hashes": [ | ||||
| @@ -334,11 +334,11 @@ | |||||
| }, | }, | ||||
| "pylint": { | "pylint": { | ||||
| "hashes": [ | "hashes": [ | ||||
| "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", | |||||
| "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" | |||||
| "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", | |||||
| "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" | |||||
| ], | ], | ||||
| "index": "pypi", | "index": "pypi", | ||||
| "version": "==2.5.3" | |||||
| "version": "==2.6.0" | |||||
| }, | }, | ||||
| "pylintfileheader": { | "pylintfileheader": { | ||||
| "hashes": [ | "hashes": [ | ||||
| @@ -77,3 +77,16 @@ Kosmorro can export the computation results to PDF files, but this feature requi | |||||
| These dependencies are not installed by default, because they take a lot of place and are not necessary if you are not interested in generating PDF files. | These dependencies are not installed by default, because they take a lot of place and are not necessary if you are not interested in generating PDF files. | ||||
| The first time you ask Kosmorro to create a PDF, it may be a little verbose. You can ignore its blahblah. | The first time you ask Kosmorro to create a PDF, it may be a little verbose. You can ignore its blahblah. | ||||
| ### Opening the GUI | |||||
|  | |||||
| Kosmorro provides a graphical user interface that can be easily launched by invoking the `kosmorro --gui` command. | |||||
| To run it, you will have to install the `wxpython` package: | |||||
| - **On Linux**, wxPython is most likely provided by your packages manager. | |||||
| - **On Mac**, install wxPython by invoking `pip3 install wxPython`. | |||||
| Alternatively, you may download the DMG file on the release page (note that you won't be able to use the command | |||||
| line in this case). | |||||
| @@ -0,0 +1,22 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <key>CFBundleDisplayName</key> | |||||
| <string>Kosmorro</string> | |||||
| <key>CFBundleExecutable</key> | |||||
| <string>MacOS/launch-kosmorro</string> | |||||
| <key>CFBundleIconFile</key> | |||||
| <string>icon.icns</string> | |||||
| <key>CFBundleIdentifier</key> | |||||
| <string>space.kosmorro.app</string> | |||||
| <key>CFBundleInfoDictionaryVersion</key> | |||||
| <string>6.0</string> | |||||
| <key>CFBundleName</key> | |||||
| <string>Kosmorro</string> | |||||
| <key>CFBundlePackageType</key> | |||||
| <string>APPL</string> | |||||
| <key>CFBundleShortVersionString</key> | |||||
| <string>{{app_version}}</string> | |||||
| </dict> | |||||
| </plist> | |||||
| @@ -0,0 +1,7 @@ | |||||
| #!/bin/sh | |||||
| # Move to the MacOS folder | |||||
| cd $(dirname "$0") | |||||
| source .venv/bin/activate | |||||
| .venv/bin/python kosmorro --gui | |||||
| @@ -6,7 +6,7 @@ rules: | |||||
| applicable: always | applicable: always | ||||
| value: 0 | value: 0 | ||||
| level: 1 | level: 1 | ||||
| header-max-length: | |||||
| header-max-length: | |||||
| applicable: always | applicable: always | ||||
| value: 120 | value: 120 | ||||
| level: 2 | level: 2 | ||||
| @@ -14,4 +14,5 @@ rules: | |||||
| value: lower-case | value: lower-case | ||||
| scope-empty: | scope-empty: | ||||
| level: 0 | level: 0 | ||||
| subject-case: | |||||
| level: 0 | |||||
| @@ -0,0 +1,10 @@ | |||||
| [Desktop Entry] | |||||
| Name=Kosmorro | |||||
| Comment=Ephemerides computer | |||||
| Comment[fr]=Calculateur d'éphémérides | |||||
| Comment[de]=Ephemeriden-Rechner | |||||
| Exec=kosmorro --gui | |||||
| Icon=kosmorro | |||||
| Terminal=false | |||||
| Categories=Astronomy;Science; | |||||
| keywords=ephemerides; | |||||
| @@ -98,7 +98,9 @@ def get_date(date_arg: str) -> date: | |||||
| try: | try: | ||||
| return date.fromisoformat(date_arg) | return date.fromisoformat(date_arg) | ||||
| except ValueError as error: | except ValueError as error: | ||||
| raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, error=error.args[0])) | |||||
| raise ValueError(_('The date {date} is not valid: {error}').format( | |||||
| date=date_arg, error=error.args[0] | |||||
| )) from error | |||||
| elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg): | elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg): | ||||
| def get_offset(date_arg: str, signifier: str): | def get_offset(date_arg: str, signifier: str): | ||||
| if re.search(r'([0-9]+)' + signifier, date_arg): | if re.search(r'([0-9]+)' + signifier, date_arg): | ||||
| @@ -272,7 +272,7 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||||
| class Position: | class Position: | ||||
| def __init__(self, latitude: float, longitude: float, aster: Object): | |||||
| def __init__(self, latitude: float, longitude: float, aster: Object = EARTH): | |||||
| self.latitude = latitude | self.latitude = latitude | ||||
| self.longitude = longitude | self.longitude = longitude | ||||
| self.aster = aster | self.aster = aster | ||||
| @@ -287,3 +287,6 @@ class Position: | |||||
| longitude_degrees=self.longitude) | longitude_degrees=self.longitude) | ||||
| return self._topos | return self._topos | ||||
| def __str__(self): | |||||
| return 'lat. %.3f, lon. %.3f' % (self.latitude, self.longitude) | |||||
| @@ -348,10 +348,10 @@ class PdfDumper(Dumper): | |||||
| date=self.date, timezone=self.timezone, with_colors=self.with_colors, | date=self.date, timezone=self.timezone, with_colors=self.with_colors, | ||||
| show_graph=self.show_graph) | show_graph=self.show_graph) | ||||
| return self._compile(latex_dumper.to_string()) | return self._compile(latex_dumper.to_string()) | ||||
| except RuntimeError: | |||||
| except RuntimeError as error: | |||||
| raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not" | raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not" | ||||
| " installed.\nPlease look at the documentation at http://kosmorro.space " | " installed.\nPlease look at the documentation at http://kosmorro.space " | ||||
| "for more information.")) | |||||
| "for more information.")) from error | |||||
| @staticmethod | @staticmethod | ||||
| def is_file_output_needed() -> bool: | def is_file_output_needed() -> bool: | ||||
| @@ -58,7 +58,7 @@ def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase: | |||||
| start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12) | start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12) | ||||
| end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | ||||
| raise OutOfRangeDateError(start, end) | |||||
| raise OutOfRangeDateError(start, end) from error | |||||
| return skyfield_to_moon_phase(times, phase, today) | return skyfield_to_moon_phase(times, phase, today) | ||||
| @@ -119,6 +119,6 @@ def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) | |||||
| start = datetime.date(start.year, start.month, start.day + 1) | start = datetime.date(start.year, start.month, start.day + 1) | ||||
| end = datetime.date(end.year, end.month, end.day - 1) | end = datetime.date(end.year, end.month, end.day - 1) | ||||
| raise OutOfRangeDateError(start, end) | |||||
| raise OutOfRangeDateError(start, end) from error | |||||
| return ephemerides | return ephemerides | ||||
| @@ -152,4 +152,4 @@ def search_events(date: date_type, timezone: int = 0) -> [Event]: | |||||
| start_date = date_type(start_date.year, start_date.month, start_date.day) | start_date = date_type(start_date.year, start_date.month, start_date.day) | ||||
| end_date = date_type(end_date.year, end_date.month, end_date.day) | end_date = date_type(end_date.year, end_date.month, end_date.day) | ||||
| raise OutOfRangeDateError(start_date, end_date) | |||||
| raise OutOfRangeDateError(start_date, end_date) from error | |||||
| @@ -22,13 +22,13 @@ from .i18n import _, SHORT_DATE_FORMAT | |||||
| class UnavailableFeatureError(RuntimeError): | class UnavailableFeatureError(RuntimeError): | ||||
| def __init__(self, msg: str): | def __init__(self, msg: str): | ||||
| super(UnavailableFeatureError, self).__init__() | |||||
| super().__init__() | |||||
| self.msg = msg | self.msg = msg | ||||
| class OutOfRangeDateError(RuntimeError): | class OutOfRangeDateError(RuntimeError): | ||||
| def __init__(self, min_date: date, max_date: date): | def __init__(self, min_date: date, max_date: date): | ||||
| super(OutOfRangeDateError, self).__init__() | |||||
| super().__init__() | |||||
| self.min_date = min_date | self.min_date = min_date | ||||
| self.max_date = max_date | self.max_date = max_date | ||||
| self.msg = _('The date must be between {minimum_date}' | self.msg = _('The date must be between {minimum_date}' | ||||
| @@ -0,0 +1,297 @@ | |||||
| import wx | |||||
| import platform | |||||
| from typing import Union | |||||
| import webbrowser | |||||
| from threading import Thread | |||||
| from datetime import date | |||||
| from . import panel | |||||
| from ..data import Position, AsterEphemerides, Event, MoonPhase | |||||
| from ..exceptions import UnavailableFeatureError | |||||
| from ..ephemerides import get_ephemerides, get_moon_phase | |||||
| from ..events import search_events | |||||
| from ..version import VERSION | |||||
| from ..dumper import PdfDumper | |||||
| from ..i18n import _ | |||||
| MIN_SIZE = wx.Size(700, 0) | |||||
| # Events definitions | |||||
| myEVT_EPHEMERIDES_COMPUTED = wx.NewEventType() | |||||
| EVT_EPHEMERIDES_COMPUTED = wx.PyEventBinder(myEVT_EPHEMERIDES_COMPUTED, 1) | |||||
| class EphemeridesComputedEvent(wx.PyCommandEvent): | |||||
| def __init__(self, etype, eid, value=None): | |||||
| super(EphemeridesComputedEvent, self).__init__(etype, eid) | |||||
| self.value = value | |||||
| myEVT_MOON_PHASE_COMPUTED = wx.NewEventType() | |||||
| EVT_MOON_PHASE_COMPUTED = wx.PyEventBinder(myEVT_MOON_PHASE_COMPUTED, 1) | |||||
| class MoonPhaseComputedEvent(wx.PyCommandEvent): | |||||
| def __init__(self, etype, eid, value=None): | |||||
| super(MoonPhaseComputedEvent, self).__init__(etype, eid) | |||||
| self.value = value | |||||
| myEVT_EVENTS_COMPUTED = wx.NewEventType() | |||||
| EVT_EVENTS_COMPUTED = wx.PyEventBinder(myEVT_EVENTS_COMPUTED, 1) | |||||
| class EventsComputedEvent(wx.PyCommandEvent): | |||||
| def __init__(self, etype, eid, value=None): | |||||
| super(EventsComputedEvent, self).__init__(etype, eid) | |||||
| self.value = value | |||||
| # Computing threads | |||||
| class MoonPhaseComputer(Thread): | |||||
| def __init__(self, parent, compute_date: date): | |||||
| super(MoonPhaseComputer, self).__init__() | |||||
| self._parent = parent | |||||
| self.compute_date = compute_date | |||||
| self.moon_phase = None | |||||
| def run(self): | |||||
| self.moon_phase = get_moon_phase(self.compute_date) | |||||
| wx.PostEvent(self._parent, EphemeridesComputedEvent(myEVT_MOON_PHASE_COMPUTED, -1, self.moon_phase)) | |||||
| class EphemeridesComputer(Thread): | |||||
| def __init__(self, parent, compute_date: date, position: Position, timezone: int): | |||||
| super(EphemeridesComputer, self).__init__() | |||||
| self._parent = parent | |||||
| self.compute_date = compute_date | |||||
| self.position = position | |||||
| self.timezone = timezone | |||||
| self.ephemerides = [] | |||||
| def run(self): | |||||
| self.ephemerides = get_ephemerides(self.compute_date, self.position, self.timezone) | |||||
| wx.PostEvent(self._parent, EphemeridesComputedEvent(myEVT_EPHEMERIDES_COMPUTED, -1, self.ephemerides)) | |||||
| class EventsComputer(Thread): | |||||
| def __init__(self, parent, compute_date: date, timezone: int): | |||||
| super(EventsComputer, self).__init__() | |||||
| self._parent = parent | |||||
| self.compute_date = compute_date | |||||
| self.timezone = timezone | |||||
| self.events = [] | |||||
| def run(self): | |||||
| self.events = search_events(self.compute_date, self.timezone) | |||||
| wx.PostEvent(self._parent, EventsComputedEvent(myEVT_EVENTS_COMPUTED, -1, self.events)) | |||||
| # Main window | |||||
| class MainWindow(wx.Frame): | |||||
| ephemerides: [AsterEphemerides] | |||||
| events: [Event] | |||||
| moon_phase: Union[None, MoonPhase] | |||||
| def __init__(self): | |||||
| super(MainWindow, self).__init__(None, title='Kosmorro', | |||||
| style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) | |||||
| self.export_path = None | |||||
| self.config_panel = panel.ConfigPanel(self) | |||||
| self.Bind(panel.EVT_COMPUTE_BUTTON, self.compute) | |||||
| self.Bind(panel.EVT_EXPORT_BUTTON, self.export) | |||||
| self.result_presenter = panel.ResultPanel(self) | |||||
| self.sizer = wx.BoxSizer(wx.VERTICAL) | |||||
| self.sizer.AddMany([(self.config_panel, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (self.result_presenter, 0, wx.EXPAND | wx.ALL, 5)]) | |||||
| self.SetSizer(self.sizer) | |||||
| self.make_menu_bar() | |||||
| status_bar = self.CreateStatusBar() | |||||
| status_bar.SetFieldsCount(2) | |||||
| status_bar.SetStatusWidths([-2, -1]) | |||||
| self.progress_bar = wx.Gauge(status_bar, -1, style=wx.GA_HORIZONTAL | wx.GA_SMOOTH) | |||||
| rect = status_bar.GetFieldRect(1) | |||||
| self.progress_bar.SetPosition((rect.x + 2, rect.y + 2)) | |||||
| self.progress_bar.SetSize((rect.width - 4, rect.height - 4)) | |||||
| self.result_presenter.Hide() | |||||
| self.SetMinSize(MIN_SIZE) | |||||
| self.Fit() | |||||
| self.Center() | |||||
| self.progress_bar.Hide() | |||||
| def resize(self, width: int, height: int, center: bool = False): | |||||
| self.resize(wx.Size(width, height), center) | |||||
| def resize(self, size: wx.Size, center: bool = False): | |||||
| self.Size = size | |||||
| if center: | |||||
| self.Center() | |||||
| def resize(self, center: bool = False): | |||||
| self.Fit() | |||||
| if center: | |||||
| self.Center() | |||||
| def make_menu_bar(self): | |||||
| menu_bar = wx.MenuBar() | |||||
| if platform.system() != 'Darwin': | |||||
| # "Application" menu | |||||
| app_menu = wx.Menu() | |||||
| exit_item = app_menu.Append(wx.ID_EXIT) | |||||
| # "Help" menu | |||||
| help_menu = wx.Menu() | |||||
| about_item = help_menu.Append(wx.ID_ABOUT) | |||||
| menu_bar.Append(app_menu, _('&Application')) | |||||
| menu_bar.Append(help_menu, _('&Help')) | |||||
| self.Bind(wx.EVT_MENU, self.on_exit, exit_item) | |||||
| else: | |||||
| # On macOS, put the menu items to the place they belong to: the application name menu | |||||
| apple_menu = menu_bar.OSXGetAppleMenu() | |||||
| about_item = apple_menu.Insert(0, wx.ID_ABOUT) | |||||
| self.Bind(wx.EVT_MENU, self.on_about, about_item) | |||||
| self.SetMenuBar(menu_bar) | |||||
| def on_exit(self, event): | |||||
| self.Close(True) | |||||
| def on_about(self, event): | |||||
| message = wx.MessageDialog(caption='Kosmorro version %s' % VERSION, | |||||
| message='© Jérôme Deuchnord - 2020\n\n' + | |||||
| _('This is a free software licensed under the ' | |||||
| 'GNU Affero General Public License.'), | |||||
| style=wx.OK | wx.HELP | wx.ICON_INFORMATION, | |||||
| parent=self) | |||||
| message.SetOKLabel(_('Close')) | |||||
| message.SetHelpLabel(_('Website')) | |||||
| btn = message.ShowModal() | |||||
| if btn == wx.ID_HELP: | |||||
| webbrowser.open('http://kosmorro.space') | |||||
| def compute(self, __=None): | |||||
| # Reset data | |||||
| self.moon_phase = None | |||||
| self.ephemerides = None | |||||
| self.events = None | |||||
| self.config_panel.disable_buttons() | |||||
| self.SetCursor(wx.Cursor(wx.CURSOR_WAIT)) | |||||
| self.SetStatusText(_('Computing…')) | |||||
| self.progress_bar.SetValue(0) | |||||
| self.progress_bar.Show() | |||||
| thread_moon_phase = MoonPhaseComputer(self, self.config_panel.compute_date) | |||||
| thread_ephemerides = EphemeridesComputer(self, self.config_panel.compute_date, self.config_panel.position, | |||||
| self.config_panel.timezone) | |||||
| thread_events = EventsComputer(self, self.config_panel.compute_date, self.config_panel.timezone) | |||||
| self.Bind(EVT_MOON_PHASE_COMPUTED, self.on_moon_phase_computed) | |||||
| self.Bind(EVT_EPHEMERIDES_COMPUTED, self.on_ephemerides_computed) | |||||
| self.Bind(EVT_EVENTS_COMPUTED, self.on_events_computed) | |||||
| thread_moon_phase.start() | |||||
| if self.config_panel.activate_position and self.config_panel.position is not None: | |||||
| thread_ephemerides.start() | |||||
| else: | |||||
| self.ephemerides = [] | |||||
| self.progress_bar.SetValue(50) | |||||
| thread_events.start() | |||||
| def export(self, __): | |||||
| self.export_path = self.get_export_file_path() | |||||
| if self.export_path is None: | |||||
| self.SetStatusText('Aborted.') | |||||
| return | |||||
| self.compute() | |||||
| def on_moon_phase_computed(self, event): | |||||
| self.moon_phase = event.value | |||||
| self.progress_bar.SetValue(self.progress_bar.GetValue() + 10) | |||||
| if self.is_all_computed(): | |||||
| self.show_results() | |||||
| def on_ephemerides_computed(self, event): | |||||
| self.ephemerides = event.value | |||||
| self.progress_bar.SetValue(self.progress_bar.GetValue() + 50) | |||||
| if self.is_all_computed(): | |||||
| self.show_results() | |||||
| def on_events_computed(self, event): | |||||
| self.events = event.value | |||||
| self.progress_bar.SetValue(self.progress_bar.GetValue() + 40) | |||||
| if self.is_all_computed(): | |||||
| self.config_panel.enable_buttons() | |||||
| self.SetStatusText('') | |||||
| self.progress_bar.Hide() | |||||
| self.SetCursor(wx.Cursor(wx.CURSOR_ARROW)) | |||||
| if self.export_path is not None: | |||||
| ephemerides = self.ephemerides if len(self.ephemerides) > 0 else None | |||||
| dumper = PdfDumper(date=self.config_panel.compute_date, ephemerides=ephemerides, | |||||
| events=self.events, moon_phase=self.moon_phase, timezone=self.config_panel.timezone, | |||||
| show_graph=True) | |||||
| try: | |||||
| with open(self.export_path, 'wb') as file: | |||||
| file.write(dumper.to_string()) | |||||
| self.SetStatusText(_('PDF export saved in "{path}"!').format(path=self.export_path)) | |||||
| except UnavailableFeatureError as error: | |||||
| wx.MessageDialog(self, error.msg, caption=_('Error while exporting your document'), | |||||
| style=wx.OK | wx.ICON_ERROR).ShowModal() | |||||
| finally: | |||||
| self.export_path = None | |||||
| return | |||||
| self.show_results() | |||||
| self.result_presenter.Show() | |||||
| self.resize(center=True) | |||||
| def is_all_computed(self) -> bool: | |||||
| return self.moon_phase is not None and self.ephemerides is not None and self.events is not None | |||||
| def show_results(self): | |||||
| self.result_presenter.moon_phase = self.moon_phase | |||||
| self.result_presenter.ephemerides = self.ephemerides | |||||
| self.result_presenter.events = self.events | |||||
| self.result_presenter.render() | |||||
| def get_export_file_path(self) -> Union[None, str]: | |||||
| dialog = wx.FileDialog(parent=self, message=_('Please select an export file location'), | |||||
| wildcard='*.pdf', style=wx.FD_SAVE) | |||||
| if dialog.ShowModal() == wx.ID_OK: | |||||
| return dialog.GetPath() | |||||
| else: | |||||
| return None | |||||
| def start() -> bool: | |||||
| app = wx.App() | |||||
| window = MainWindow() | |||||
| window.Show() | |||||
| return app.MainLoop() == 0 | |||||
| @@ -0,0 +1,241 @@ | |||||
| import os | |||||
| from typing import Union | |||||
| import wx | |||||
| import wx.adv | |||||
| import wx.grid | |||||
| import wx.richtext | |||||
| import wx.lib.newevent | |||||
| from .positionwindow import PositionWindow | |||||
| from ..data import Position, AsterEphemerides, Event, MoonPhase | |||||
| from ..i18n import _, TIME_FORMAT, FULL_DATE_FORMAT | |||||
| from datetime import date | |||||
| FPB_HORIZONTAL = 0x4 | |||||
| ComputeButtonEvent, EVT_COMPUTE_BUTTON = wx.lib.newevent.NewEvent() | |||||
| ExportButtonEvent, EVT_EXPORT_BUTTON = wx.lib.newevent.NewEvent() | |||||
| class ConfigPanel(wx.Panel): | |||||
| activate_position: bool | |||||
| position: Union[None, Position] | |||||
| compute_date: date | |||||
| timezone: int | |||||
| def __init__(self, parent, activate_position: bool = False, position: Position = None, | |||||
| compute_date: date = date.today(), timezone: int = 0): | |||||
| super(ConfigPanel, self).__init__(parent) | |||||
| self.activate_position = activate_position | |||||
| self.position = position | |||||
| self.compute_date = compute_date | |||||
| self.timezone = timezone | |||||
| self._position_checkbox = wx.CheckBox(self, label=_('Position:')) | |||||
| self._position_change_btn = wx.Button(self) | |||||
| self.update_position_btn_label() | |||||
| self._position_change_btn.SetToolTip(wx.ToolTip(_('Change the position'))) | |||||
| self._position_checkbox.SetValue(activate_position) | |||||
| self._position_change_btn.Enable(activate_position) | |||||
| self.Bind(wx.EVT_CHECKBOX, self.on_position_checkbox, self._position_checkbox) | |||||
| self.Bind(wx.EVT_BUTTON, self.on_position_button, self._position_change_btn) | |||||
| date_lbl = wx.StaticText(self, label=_('Date:')) | |||||
| self._date_picker = wx.adv.DatePickerCtrl(self, dt=wx.DateTime(compute_date), | |||||
| style=wx.adv.DP_DEFAULT) | |||||
| self.Bind(wx.adv.EVT_DATE_CHANGED, self.date_changed, self._date_picker) | |||||
| timezone_lbl = wx.StaticText(self, label=_('Timezone:')) | |||||
| self._timezone_spin = wx.SpinCtrl(self, min=-23, max=23, value=str(timezone)) | |||||
| self.Bind(wx.EVT_SPINCTRL, self.timezone_changed, self._timezone_spin) | |||||
| sizer = wx.FlexGridSizer(2, 5, 5) | |||||
| sizer.AddGrowableCol(1, 1) | |||||
| self._compute_button = wx.Button(self, label=_('&Compute')) | |||||
| self._export_button = wx.Button(self, label=_('E&xport PDF…')) | |||||
| self.Bind(wx.EVT_BUTTON, self.compute_button_clicked, self._compute_button) | |||||
| self.Bind(wx.EVT_BUTTON, self.export_button_clicked, self._export_button) | |||||
| sizer.AddMany([(self._position_checkbox, 0, wx.EXPAND), | |||||
| (self._position_change_btn, 0, wx.EXPAND), | |||||
| (date_lbl, 0, wx.EXPAND), | |||||
| (self._date_picker, 0, wx.EXPAND), | |||||
| (timezone_lbl, 0, wx.EXPAND), | |||||
| (self._timezone_spin, 0, wx.EXPAND)]) | |||||
| main_sizer = wx.FlexGridSizer(3, 5, 5) | |||||
| main_sizer.AddGrowableCol(0, 2) | |||||
| main_sizer.AddMany([(sizer, 0, wx.EXPAND), | |||||
| (self._compute_button, 0, wx.EXPAND), | |||||
| (self._export_button, 0, wx.EXPAND)]) | |||||
| self.SetSizer(main_sizer) | |||||
| def update_position_btn_label(self): | |||||
| self._position_change_btn.Label = str(self.position) if self.position is not None else _('Unknown') | |||||
| def enable_buttons(self, enable: bool = True): | |||||
| self._compute_button.Enable(enable) | |||||
| self._export_button.Enable(enable) | |||||
| def disable_buttons(self): | |||||
| self.enable_buttons(False) | |||||
| def on_position_checkbox(self, event): | |||||
| self.activate_position = event.IsChecked() | |||||
| self._position_change_btn.Enable(self.activate_position) | |||||
| if self.activate_position and self.position is None: | |||||
| self.on_position_button() | |||||
| def on_position_button(self, _=None): | |||||
| pos_win = PositionWindow(self, self.position) | |||||
| pos_win.ShowModal() | |||||
| self.position = pos_win.position | |||||
| if self.position is not None: | |||||
| self.update_position_btn_label() | |||||
| else: | |||||
| self._position_checkbox.SetValue(False) | |||||
| self._position_change_btn.Disable() | |||||
| def date_changed(self, _): | |||||
| self.compute_date = date.fromisoformat(self._date_picker.GetValue().FormatISODate()) | |||||
| def timezone_changed(self, _): | |||||
| self.timezone = self._timezone_spin.GetValue() | |||||
| def compute_button_clicked(self, _): | |||||
| wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(EVT_COMPUTE_BUTTON.typeId, self.GetId())) | |||||
| def export_button_clicked(self, _): | |||||
| wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(EVT_EXPORT_BUTTON.typeId, self.GetId())) | |||||
| class MoonPhasePanel(wx.Panel): | |||||
| def __init__(self, parent): | |||||
| super(MoonPhasePanel, self).__init__(parent) | |||||
| self.moon_bitmap = wx.StaticBitmap(self, size=wx.Size(100, 100)) | |||||
| self.moon_txt_l1 = wx.StaticText(self) | |||||
| self.moon_txt_l2 = wx.StaticText(self) | |||||
| self.moon_txt_l1.SetFont(wx.Font(wx.DEFAULT, wx.DEFAULT, wx.NORMAL, wx.BOLD)) | |||||
| label_sizer = wx.GridSizer(3, 1, 0, 0) | |||||
| label_sizer.AddMany([(wx.StaticText(self), 0, wx.ALIGN_LEFT), | |||||
| (self.moon_txt_l1, 0, wx.ALIGN_LEFT | wx.EXPAND), | |||||
| (self.moon_txt_l2, 0, wx.ALIGN_LEFT | wx.EXPAND)]) | |||||
| main_sizer = wx.FlexGridSizer(4, 0, 10) | |||||
| main_sizer.AddGrowableCol(0) | |||||
| main_sizer.AddGrowableCol(2) | |||||
| main_sizer.AddMany([(wx.StaticText(self), 0, wx.ALIGN_LEFT), | |||||
| (self.moon_bitmap, 0, wx.ALIGN_RIGHT), | |||||
| (label_sizer, 0, wx.ALIGN_LEFT)]) | |||||
| self.SetSizer(main_sizer) | |||||
| def set_moon_phase(self, moon_phase: MoonPhase): | |||||
| moon_img = wx.Image(os.path.join(os.path.abspath(os.path.dirname(__file__)), | |||||
| '..', 'assets', 'moonphases', 'png', | |||||
| '.'.join([moon_phase.identifier.lower().replace('_', '-'), | |||||
| 'png']))) | |||||
| moon_img = moon_img.Scale(100, 100) | |||||
| bitmap = moon_img.ConvertToBitmap() | |||||
| self.moon_bitmap.SetBitmap(bitmap) | |||||
| self.moon_txt_l1.SetLabel(moon_phase.get_phase()) | |||||
| self.moon_txt_l2.SetLabel(_('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format( | |||||
| next_moon_phase=moon_phase.get_next_phase_name(), | |||||
| next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT), | |||||
| next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT) | |||||
| )) | |||||
| self.Layout() | |||||
| self.Fit() | |||||
| class ResultPanel(wx.Panel): | |||||
| ephemerides: [AsterEphemerides] | |||||
| events: [Event] | |||||
| moon_phase: Union[None, MoonPhase] | |||||
| def __init__(self, parent): | |||||
| super(ResultPanel, self).__init__(parent) | |||||
| self.moon_phase = None | |||||
| self.ephemerides = None | |||||
| self.events = None | |||||
| self._moon_phase_panel = MoonPhasePanel(self) | |||||
| self._grid_ephemerides = wx.grid.Grid(self) | |||||
| self._grid_ephemerides.SetDefaultCellAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE) | |||||
| self._grid_ephemerides.CreateGrid(numRows=0, numCols=3) | |||||
| self._grid_ephemerides.EnableEditing(False) | |||||
| self._grid_ephemerides.SetColLabelValue(0, _('Rise time')) | |||||
| self._grid_ephemerides.SetColLabelValue(1, _('Culmination time')) | |||||
| self._grid_ephemerides.SetColLabelValue(2, _('Set time')) | |||||
| self._grid_ephemerides.EnableEditing(False) | |||||
| self._list_events = wx.richtext.RichTextCtrl(self, size=wx.Size(0, 300), | |||||
| style=wx.richtext.RE_READONLY | wx.richtext.RE_MULTILINE) | |||||
| main_sizer = wx.FlexGridSizer(2, 1, 5, 5) | |||||
| main_sizer.AddGrowableCol(0) | |||||
| main_sizer.AddGrowableRow(1, 2) | |||||
| sizer = wx.FlexGridSizer(2, 5, 5) | |||||
| sizer.AddGrowableCol(0, 0) | |||||
| sizer.AddGrowableCol(1, 1) | |||||
| sizer.AddMany([(self._grid_ephemerides, 0, wx.EXPAND | wx.ALL), | |||||
| (self._list_events, 0, wx.EXPAND | wx.ALL)]) | |||||
| main_sizer.AddMany([(self._moon_phase_panel, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (sizer, 0, wx.EXPAND | wx.ALL)]) | |||||
| self.SetSizer(main_sizer) | |||||
| def render(self): | |||||
| if self.moon_phase is not None: | |||||
| self._moon_phase_panel.set_moon_phase(self.moon_phase) | |||||
| if self._grid_ephemerides.NumberRows > 0: | |||||
| self._grid_ephemerides.DeleteRows(numRows=self._grid_ephemerides.NumberRows) | |||||
| if self.ephemerides is not None and len(self.ephemerides) > 0: | |||||
| for ephemeris in self.ephemerides: | |||||
| rise_time = ephemeris.rise_time.strftime(TIME_FORMAT) if ephemeris.rise_time is not None else '' | |||||
| culmination_time = ephemeris.culmination_time.strftime(TIME_FORMAT) if ephemeris.culmination_time is not None else '' | |||||
| set_time = ephemeris.set_time.strftime(TIME_FORMAT) if ephemeris.set_time is not None else '' | |||||
| self._grid_ephemerides.AppendRows() | |||||
| row = self._grid_ephemerides.NumberRows - 1 | |||||
| self._grid_ephemerides.SetRowLabelValue(row, ephemeris.object.name) | |||||
| self._grid_ephemerides.SetCellValue(row, 0, rise_time) | |||||
| self._grid_ephemerides.SetCellValue(row, 1, culmination_time) | |||||
| self._grid_ephemerides.SetCellValue(row, 2, set_time) | |||||
| self._grid_ephemerides.Show(len(self.ephemerides) > 0) | |||||
| self._grid_ephemerides.AutoSize() | |||||
| if self.events is not None and len(self.events) > 0: | |||||
| content = '' | |||||
| for event in self.events: | |||||
| content += '\n' if content != '' else '' | |||||
| content += _('- {event_time}: {event_description}').format( | |||||
| event_time=event.start_time.strftime(TIME_FORMAT), | |||||
| event_description=event.get_description() | |||||
| ) | |||||
| self._list_events.SetValue(content) | |||||
| else: | |||||
| self._list_events.SetValue(_('No events for this day.')) | |||||
| self.Layout() | |||||
| @@ -0,0 +1,64 @@ | |||||
| import wx | |||||
| from ..data import Position | |||||
| from ..i18n import _ | |||||
| class PositionWindow(wx.Dialog): | |||||
| position: Position | |||||
| def __init__(self, parent, position: Position = None): | |||||
| super(PositionWindow, self).__init__(parent, title=_('Set position'), style=wx.DEFAULT_DIALOG_STYLE ^ wx.CLOSE_BOX) | |||||
| self.position = position | |||||
| latitude_lbl = wx.StaticText(self, label=_('Latitude:')) | |||||
| self._latitude_input = wx.SpinCtrlDouble(self, initial=position.latitude if position is not None else 0, | |||||
| min=-90, max=90) | |||||
| self._latitude_input.SetDigits(4) | |||||
| longitude_lbl = wx.StaticText(self, label=_('Longitude:')) | |||||
| self._longitude_input = wx.SpinCtrlDouble(self, initial=position.longitude if position is not None else 0, | |||||
| min=-180, max=180) | |||||
| self._longitude_input.SetDigits(4) | |||||
| ok_button = wx.Button(self, label=_('OK')) | |||||
| cancel_button = wx.Button(self, label=_('Cancel')) | |||||
| self.Bind(wx.EVT_BUTTON, self.on_ok, ok_button) | |||||
| self.Bind(wx.EVT_BUTTON, self.on_cancel, cancel_button) | |||||
| btn_sizer = wx.GridSizer(1, 2, 5, 5) | |||||
| btn_sizer.AddMany([(ok_button, 0, wx.EXPAND), | |||||
| (cancel_button, 0, wx.EXPAND)]) | |||||
| sizer = wx.FlexGridSizer(2, 5, 5) | |||||
| sizer.AddGrowableCol(0, 2) | |||||
| sizer.AddMany([(latitude_lbl, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (self._latitude_input, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (longitude_lbl, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (self._longitude_input, 0, wx.EXPAND | wx.ALL, 5), | |||||
| (wx.StaticText(self), 0, wx.EXPAND | wx.ALL, 5), | |||||
| (btn_sizer, 0, wx.EXPAND | wx.ALL, 5)]) | |||||
| self.SetSizer(sizer) | |||||
| self.Fit() | |||||
| self.Center() | |||||
| self.Bind(wx.EVT_CHAR_HOOK, self.on_key_down) | |||||
| def on_cancel(self, _=None): | |||||
| self.Close() | |||||
| def on_ok(self, _=None): | |||||
| self.position = Position(self._latitude_input.GetValue(), self._longitude_input.GetValue()) | |||||
| self.Close() | |||||
| def on_key_down(self, event): | |||||
| key = event.GetKeyCode() | |||||
| if key == wx.WXK_ESCAPE: | |||||
| self.on_cancel() | |||||
| elif key in [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER]: | |||||
| self.on_ok() | |||||
| else: | |||||
| event.Skip() | |||||
| @@ -34,6 +34,11 @@ from .i18n import _ | |||||
| from . import ephemerides | from . import ephemerides | ||||
| from .version import VERSION | from .version import VERSION | ||||
| try: | |||||
| from .gui import mainwindow | |||||
| except ModuleNotFoundError: | |||||
| mainwindow = None | |||||
| def main(): | def main(): | ||||
| environment = core.get_env() | environment = core.get_env() | ||||
| @@ -150,6 +155,14 @@ def clear_cache() -> bool: | |||||
| return True | return True | ||||
| def open_gui() -> bool: | |||||
| if mainwindow is None: | |||||
| print("Starting Kosmorro's GUI requires you to install wxPython.") | |||||
| return False | |||||
| return mainwindow.start() | |||||
| def get_args(output_formats: [str]): | def get_args(output_formats: [str]): | ||||
| today = date.today() | today = date.today() | ||||
| @@ -161,6 +174,8 @@ def get_args(output_formats: [str]): | |||||
| parser.add_argument('--version', '-v', dest='special_action', action='store_const', const=output_version, | parser.add_argument('--version', '-v', dest='special_action', action='store_const', const=output_version, | ||||
| default=None, help=_('Show the program version')) | default=None, help=_('Show the program version')) | ||||
| parser.add_argument('--gui', '-g', dest='special_action', action='store_const', const=open_gui, | |||||
| default=None, help=_("Open Kosmorro's GUI")) | |||||
| parser.add_argument('--clear-cache', dest='special_action', action='store_const', const=clear_cache, default=None, | parser.add_argument('--clear-cache', dest='special_action', action='store_const', const=clear_cache, default=None, | ||||
| help=_('Delete all the files Kosmorro stored in the cache.')) | help=_('Delete all the files Kosmorro stored in the cache.')) | ||||
| parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats, | parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats, | ||||
| @@ -39,6 +39,9 @@ | |||||
| `--no-graph` | `--no-graph` | ||||
| present the ephemerides in a table instead of a graph; PDF output format only | present the ephemerides in a table instead of a graph; PDF output format only | ||||
| `--gui`, `-g` | |||||
| open the graphical user interface | |||||
| ## ENVIRONMENT VARIABLES | ## ENVIRONMENT VARIABLES | ||||