| @@ -4,3 +4,4 @@ source = | |||
| kosmorrolib | |||
| omit = | |||
| test/* | |||
| kosmorrolib/gui/* | |||
| @@ -1,6 +1,8 @@ | |||
| __pycache__ | |||
| /build | |||
| /build/* | |||
| !/build/distrib | |||
| /dist | |||
| kosmorro.egg-info | |||
| .coverage | |||
| coverage.xml | |||
| @@ -28,7 +28,7 @@ function canRun() { | |||
| return 0 | |||
| } | |||
| # Asserts that command $1 has finished with sucess | |||
| # Asserts that command $1 has finished with success | |||
| # $1: the command to run | |||
| function assertSuccess() { | |||
| if ! canRun "$2"; then | |||
| @@ -48,7 +48,7 @@ function assertSuccess() { | |||
| echo -n '.' | |||
| } | |||
| # Asserts that command $1 has finished with sucess | |||
| # Asserts that command $1 has finished with failure | |||
| # $1: the command to run | |||
| function assertFailure() { | |||
| if ! canRun "$2"; then | |||
| @@ -6,6 +6,7 @@ test: | |||
| unset KOSMORRO_TIMEZONE; \ | |||
| LANG=C pipenv run python3 -m coverage run -m unittest test | |||
| .PHONY: build | |||
| build: i18n | |||
| python3 setup.py sdist bdist_wheel | |||
| @@ -54,3 +55,30 @@ finish-release: env | |||
| @echo | |||
| @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." | |||
| 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": { | |||
| "hashes": [ | |||
| "sha256:3726d35c0f93a28631a003880e2aa6cc93c401d62bc6919c5cb497217ba30c55", | |||
| "sha256:afe359cd5b350e1b3895372bda32af8f0260638c7c4a31a5c0f15aa6a96f40d9" | |||
| "sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6", | |||
| "sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1" | |||
| ], | |||
| "index": "pypi", | |||
| "version": "==2.1.1" | |||
| "version": "==2.1.2" | |||
| }, | |||
| "docopt": { | |||
| "hashes": [ | |||
| @@ -292,11 +292,11 @@ | |||
| }, | |||
| "isort": { | |||
| "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": { | |||
| "hashes": [ | |||
| @@ -334,11 +334,11 @@ | |||
| }, | |||
| "pylint": { | |||
| "hashes": [ | |||
| "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", | |||
| "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" | |||
| "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", | |||
| "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" | |||
| ], | |||
| "index": "pypi", | |||
| "version": "==2.5.3" | |||
| "version": "==2.6.0" | |||
| }, | |||
| "pylintfileheader": { | |||
| "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. | |||
| 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 | |||
| value: 0 | |||
| level: 1 | |||
| header-max-length: | |||
| header-max-length: | |||
| applicable: always | |||
| value: 120 | |||
| level: 2 | |||
| @@ -14,4 +14,5 @@ rules: | |||
| value: lower-case | |||
| scope-empty: | |||
| 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: | |||
| return date.fromisoformat(date_arg) | |||
| 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): | |||
| def get_offset(date_arg: str, signifier: str): | |||
| if re.search(r'([0-9]+)' + signifier, date_arg): | |||
| @@ -272,7 +272,7 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||
| 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.longitude = longitude | |||
| self.aster = aster | |||
| @@ -287,3 +287,6 @@ class Position: | |||
| longitude_degrees=self.longitude) | |||
| 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, | |||
| show_graph=self.show_graph) | |||
| 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" | |||
| " installed.\nPlease look at the documentation at http://kosmorro.space " | |||
| "for more information.")) | |||
| "for more information.")) from error | |||
| @staticmethod | |||
| 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) | |||
| 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) | |||
| @@ -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) | |||
| end = datetime.date(end.year, end.month, end.day - 1) | |||
| raise OutOfRangeDateError(start, end) | |||
| raise OutOfRangeDateError(start, end) from error | |||
| 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) | |||
| 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): | |||
| def __init__(self, msg: str): | |||
| super(UnavailableFeatureError, self).__init__() | |||
| super().__init__() | |||
| self.msg = msg | |||
| class OutOfRangeDateError(RuntimeError): | |||
| def __init__(self, min_date: date, max_date: date): | |||
| super(OutOfRangeDateError, self).__init__() | |||
| super().__init__() | |||
| self.min_date = min_date | |||
| self.max_date = max_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 .version import VERSION | |||
| try: | |||
| from .gui import mainwindow | |||
| except ModuleNotFoundError: | |||
| mainwindow = None | |||
| def main(): | |||
| environment = core.get_env() | |||
| @@ -150,6 +155,14 @@ def clear_cache() -> bool: | |||
| 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]): | |||
| 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, | |||
| 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, | |||
| help=_('Delete all the files Kosmorro stored in the cache.')) | |||
| parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats, | |||
| @@ -39,6 +39,9 @@ | |||
| `--no-graph` | |||
| present the ephemerides in a table instead of a graph; PDF output format only | |||
| `--gui`, `-g` | |||
| open the graphical user interface | |||
| ## ENVIRONMENT VARIABLES | |||