diff --git a/.travis.yml b/.travis.yml index 437752e..37c566e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ sudo: required dist: trusty -group: deprecated-2017Q2 language: python @@ -12,7 +11,7 @@ virtualenv: before_install: - sudo apt-get update -qq - - sudo apt-get install -yq xvfb gir1.2-gtk-3.0 python3-gi-cairo python-mpltoolkits.basemap python3-numpy python3-matplotlib python3-sphinx python-libhamlib2 python3-flake8 python3-pip + - sudo apt-get install -yq xvfb python3 python3-pip gir1.2-gtk-3.0 python3-gi-cairo python3-flake8 python3-numpy python3-matplotlib python3-sphinx python-libhamlib2 - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b10744..54c71a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [UNRELEASED] +### Added +- Added support for the SAT_NAME, SAT_MODE, PROP_MODE, and GRIDSQUARE ADIF fields for the purposes of satellite QSO logging. +- Pinpointing of callsigns on the world map by looking up the latitude-longitude coordinates based on the value in the COUNTRY field. A new right-click popup menu has been created for this purpose. +- Added basic copy/paste functionality for individual records. +- Added a requirements.txt file for the purpose of installing dependencies. + +### Changed +- Renamed the GreyLine class to WorldMap, since it now does more than just grey line plotting. +- Improved the section on dependencies in the README. + +### Fixed +- Updated the list of supported ADIF fields. + ## [1.0.0] - 2017-08-02 ### Added - Pin-pointing of QTH on grey line map. @@ -95,6 +109,7 @@ - QSO filtering and sorting. - Duplicate record removal. +[UNRELEASED]: https://github.com/ctjacobs/pyqso/compare/v1.0.0...HEAD [1.0.0]: https://github.com/ctjacobs/pyqso/compare/v0.3...v1.0.0 [0.3]: https://github.com/ctjacobs/pyqso/compare/v0.2...v0.3 [0.2]: https://github.com/ctjacobs/pyqso/compare/v0.1...v0.2 diff --git a/README.md b/README.md index f861dcd..8b54eb5 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,19 @@ As the name suggests, PyQSO is written primarily in the [Python](https://www.pyt * gir1.2-gtk-3.0 * python3-gi-cairo -Several extra packages are necessary to enable the full functionality of PyQSO, such as the grey line tool. Many of these (specified in the `requirements.txt` file) can be readily installed system-wide using the Python package manager by issuing the following command in the terminal: +Several extra packages are necessary to enable the full functionality of PyQSO. Many of these (specified in the `requirements.txt` file) can be readily installed system-wide using the Python package manager by issuing the following command in the terminal: sudo pip3 install -U -r requirements.txt but the complete list is given below: * python3-matplotlib (version 1.3.0 or later) -* python3-mpltoolkits.basemap * python3-numpy * libxcb-render0-dev * python3-cairocffi +* libproj-dev (version 4.9.0 or later) +* libgeos-dev (version 3.3.3 or later) +* [cartopy](http://scitools.org.uk/cartopy/) (for drawing the world map and grey line) * [geocoder](https://pypi.python.org/pypi/geocoder) (for QTH lookups) * python3-sphinx (for building the documentation) * python3-hamlib (for Hamlib support) diff --git a/docs/source/conf.py b/docs/source/conf.py index 08d9da7..1fe185e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,16 +51,16 @@ master_doc = 'index' # General information about the project. project = u'PyQSO' -copyright = u'2015-2017, Christian Thomas Jacobs' +copyright = u'2015-2018, Christian Thomas Jacobs' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.0.0' +version = '1.1.0-dev' # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = '1.1.0-dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index b6deef7..76fbc5b 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -24,7 +24,7 @@ include: - Progress tracker for the `DXCC `_ award. -- Grey line plotter. +- World map with grey line. - Filter QSOs based on callsign (e.g. only display contacts with callsigns beginning with "M6"). @@ -65,5 +65,5 @@ If you have any comments or questions about PyQSO, please send them via email to Structure of this documentation ------------------------------- -The structure of this documentation is as follows. The section on `Getting Started `_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management `_ section explains how to create a log in the logbook, as well as the basic operations that users can perform with existing logs, such as printing, importing/exporting logs, and sorting. The `Record Management `_ section deals with the bottom layer of the three-tier model - the creation, deletion, and modification of QSO records in a log. The `Toolbox `_ section introduces the PyQSO toolbox which contains three tools that are useful to amateur radio operators: a DX cluster, a grey line plotter, and an awards progress tracker. Finally, the `Preferences `_ section explains how users can set up Hamlib support and show/hide various fields in a log, along with several other user preferences that can be set via the Preferences dialog window. A `keyboard shortcuts list `_ is also available for reference. +The structure of this documentation is as follows. The section on `Getting Started `_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management `_ section explains how to create a log in the logbook, as well as the basic operations that users can perform with existing logs, such as printing, importing/exporting logs, and sorting. The `Record Management `_ section deals with the bottom layer of the three-tier model - the creation, deletion, and modification of QSO records in a log. The `Toolbox `_ section introduces the PyQSO toolbox which contains three tools that are useful to amateur radio operators: a DX cluster, a world map, and an awards progress tracker. Finally, the `Preferences `_ section explains how users can set up Hamlib support and show/hide various fields in a log, along with several other user preferences that can be set via the Preferences dialog window. A `keyboard shortcuts list `_ is also available for reference. diff --git a/docs/source/preferences.rst b/docs/source/preferences.rst index 880ebaa..a7f203f 100644 --- a/docs/source/preferences.rst +++ b/docs/source/preferences.rst @@ -17,7 +17,7 @@ Under the ``General`` tab, the user can choose to: - Keep the ``Add Record`` dialog window open after a new QSO is added, in preparation for the next QSO. -- Pin-point the user's QTH on the grey line map by specifying the latitude-longitude coordinates (or looking them up based on the QTH's name, e.g. city name). +- Pin-point the user's QTH on the world map by specifying the latitude-longitude coordinates (or looking them up based on the QTH's name, e.g. city name). .. _figure:summary: .. figure:: images/summary.png diff --git a/docs/source/toolbox.rst b/docs/source/toolbox.rst index c44f218..145fedc 100644 --- a/docs/source/toolbox.rst +++ b/docs/source/toolbox.rst @@ -31,20 +31,18 @@ adjacent ``Send Command`` button (or pressing the Enter key). The DX cluster frame. -Grey line +World map --------- -The grey line tool (see figure:grey_line_) can be used to -check which parts of the world are in darkness. The position of the grey -line is automatically updated every 30 minutes. +The world map tool (see figure:world_map_) can be used to plot the QTH of your station and stations that you have contacted. It also features a grey line to check which parts of the world are in darkness. The position of the grey line is automatically updated every 30 minutes. The user's QTH can be pin-pointed on the map by specifying the QTH's location (e.g. city name) and latitude-longitude coordinates in the preferences. If the `geocoder `_ library is installed then these coordinates can be filled in for you by clicking the lookup button after entering the QTH's name, otherwise the coordinates will have to be entered manually. - .. _figure:grey_line: - .. figure:: images/grey_line.png + .. _figure:world_map: + .. figure:: images/world_map.png :align: center - The grey line tool with the user's QTH (e.g. Southampton) pin-pointed on the map. + The world map tool with the user's QTH (e.g. Southampton) pin-pointed. Awards ------ diff --git a/pyqso/logbook.py b/pyqso/logbook.py index 271ceb2..00ff396 100644 --- a/pyqso/logbook.py +++ b/pyqso/logbook.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2012-2017 Christian Thomas Jacobs. +# Copyright (C) 2012-2018 Christian Thomas Jacobs. # This file is part of PyQSO. @@ -1053,7 +1053,7 @@ class Logbook: return def pinpoint_callback(self, widget=None, path=None): - """ A callback function used to pinpoint the callsign on the grey line map. """ + """ A callback function used to pinpoint the callsign on the world map. """ try: log_index = self.get_log_index() @@ -1065,7 +1065,7 @@ class Logbook: logging.error(e) return - self.application.toolbox.grey_line.pinpoint(r) + self.application.toolbox.world_map.pinpoint(r) return diff --git a/pyqso/popup.py b/pyqso/popup.py index 14e6aea..d44e81b 100644 --- a/pyqso/popup.py +++ b/pyqso/popup.py @@ -33,7 +33,7 @@ class Popup: # Collect Gtk menu items and connect signals. self.items = {} - # Plot selected QSO on the grey line map. + # Plot selected QSO on the world map. self.items["PINPOINT"] = self.builder.get_object("mitem_pinpoint") self.items["PINPOINT"].connect("activate", self.application.logbook.pinpoint_callback) diff --git a/pyqso/res/pyqso.glade b/pyqso/res/pyqso.glade index 5e1da6b..ae5fe50 100644 --- a/pyqso/res/pyqso.glade +++ b/pyqso/res/pyqso.glade @@ -792,7 +792,7 @@ - + True False vertical @@ -806,10 +806,10 @@ - + True False - Grey Line + World Map 1 @@ -908,8 +908,8 @@ dialog pyqso PyQSO - 1.0.0 - Copyright (C) 2012-2017 Christian Thomas Jacobs + 1.1.0-dev + Copyright (C) 2012-2018 Christian Thomas Jacobs A contact logging tool for amateur radio operators. http://christianjacobs.uk/pyqso This program is free software: you can redistribute it and/or modify @@ -1460,7 +1460,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.2 - Pin-point QTH on grey line map + Pin-point QTH on world map True True False diff --git a/pyqso/toolbox.py b/pyqso/toolbox.py index dc0fc9b..154a276 100644 --- a/pyqso/toolbox.py +++ b/pyqso/toolbox.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2013-2017 Christian Thomas Jacobs. +# Copyright (C) 2013-2018 Christian Thomas Jacobs. # This file is part of PyQSO. @@ -18,7 +18,7 @@ # along with PyQSO. If not, see . from pyqso.dx_cluster import DXCluster -from pyqso.grey_line import GreyLine +from pyqso.world_map import WorldMap from pyqso.awards import Awards @@ -38,7 +38,7 @@ class Toolbox: self.tools = self.builder.get_object("tools") self.dx_cluster = DXCluster(self.application) - self.grey_line = GreyLine(self.application) + self.world_map = WorldMap(self.application) self.awards = Awards(self.application) self.tools.connect_after("switch-page", self.on_switch_page) @@ -52,7 +52,7 @@ class Toolbox: return def on_switch_page(self, widget, label, new_page): - """ Re-draw the Grey Line if the user switches to the grey line tab. """ - if(widget.get_tab_label(label).get_text() == "Grey Line"): - self.grey_line.draw() + """ Re-draw the WorldMap if the user switches to the World Map tab. """ + if(widget.get_tab_label(label).get_text() == "World Map"): + self.world_map.draw() return diff --git a/pyqso/grey_line.py b/pyqso/world_map.py similarity index 61% rename from pyqso/grey_line.py rename to pyqso/world_map.py index c08bfa3..bd2d9f2 100644 --- a/pyqso/grey_line.py +++ b/pyqso/world_map.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2013-2017 Christian Thomas Jacobs. +# Copyright (C) 2013-2018 Christian Thomas Jacobs. # This file is part of PyQSO. @@ -19,8 +19,8 @@ from gi.repository import GObject import logging -from datetime import datetime from os.path import expanduser +from datetime import datetime try: import configparser except ImportError: @@ -30,13 +30,13 @@ try: logging.info("Using version %s of numpy." % (numpy.__version__)) import matplotlib logging.info("Using version %s of matplotlib." % (matplotlib.__version__)) - import mpl_toolkits.basemap - logging.info("Using version %s of mpl_toolkits.basemap." % (mpl_toolkits.basemap.__version__)) + import cartopy + logging.info("Using version %s of cartopy." % (cartopy.__version__)) from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas have_necessary_modules = True except ImportError as e: logging.warning(e) - logging.warning("Could not import a non-standard Python module needed by the GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.") + logging.warning("Could not import a non-standard Python module needed by the WorldMap class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.") have_necessary_modules = False try: import geocoder @@ -64,16 +64,16 @@ class Point: return -class GreyLine: +class WorldMap: - """ A tool for visualising the grey line. """ + """ A tool for visualising the world map. """ def __init__(self, application): - """ Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes. + """ Set up the drawing canvas and the timer which will re-plot the world map every 30 minutes. :arg application: The PyQSO application containing the main Gtk window, etc. """ - logging.debug("Setting up the grey line...") + logging.debug("Setting up the world map...") self.application = application self.builder = self.application.builder @@ -82,10 +82,10 @@ class GreyLine: if(have_necessary_modules): self.fig = matplotlib.figure.Figure() self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application - self.builder.get_object("greyline").pack_start(self.canvas, True, True, 0) - self.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible). + self.builder.get_object("worldmap").pack_start(self.canvas, True, True, 0) + self.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the world map automatically after 30 minutes (if the world map tool is visible). - # Plot the QTH coordinates, if available. + # Add the QTH coordinates for plotting, if available. config = configparser.ConfigParser() have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != []) (section, option) = ("general", "show_qth") @@ -97,11 +97,11 @@ class GreyLine: qth_longitude = float(config.get("general", "qth_longitude")) self.add_point(qth_name, qth_latitude, qth_longitude, "ro") except ValueError: - logging.warning("Unable to get the QTH name, latitude and/or longitude. The QTH will not be pinpointed on the grey line map. Check preferences?") + logging.warning("Unable to get the QTH name, latitude and/or longitude. The QTH will not be pinpointed on the world map. Check preferences?") - self.builder.get_object("greyline").show_all() + self.builder.get_object("worldmap").show_all() - logging.debug("Grey line ready!") + logging.debug("World map ready!") return @@ -119,7 +119,7 @@ class GreyLine: return def pinpoint(self, r): - """ Pinpoint the location of a QSO on the grey line map based on the COUNTRY field. + """ Pinpoint the location of a QSO on the world map based on the COUNTRY field. :arg r: The QSO record containing the location to pinpoint. """ @@ -145,7 +145,7 @@ class GreyLine: def draw(self): """ Draw the world map and the grey line on top of it. - :returns: Always returns True to satisfy the GObject timer, unless the necessary GreyLine dependencies are not satisfied (in which case, the method returns False so as to not re-draw the canvas). + :returns: Always returns True to satisfy the GObject timer, unless the necessary WorldMap dependencies are not satisfied (in which case, the method returns False so as to not re-draw the canvas). :rtype: bool """ @@ -153,32 +153,64 @@ class GreyLine: toolbox = self.builder.get_object("toolbox") tools = self.builder.get_object("tools") if(tools.get_current_page() != 1 or not toolbox.get_visible()): - # Don't re-draw if the grey line is not visible. + # Don't re-draw if the world map is not visible. return True # We need to return True in case this is method was called by a timer event. else: - logging.debug("Drawing the grey line...") - # Re-draw the grey line + # Set up the world map. + logging.debug("Drawing the world map...") self.fig.clf() - sub = self.fig.add_subplot(111) + ax = self.fig.add_subplot(111, projection=cartopy.crs.PlateCarree()) + ax.set_extent([-180, 180, -90, 90]) + ax.set_aspect("auto") - # Draw the map of the world. This is based on the example from: - # http://matplotlib.org/basemap/users/examples.html - m = mpl_toolkits.basemap.Basemap(projection="mill", lon_0=0, ax=sub, resolution="c", fix_aspect=False) - m.drawcountries(linewidth=0.4) - m.drawcoastlines(linewidth=0.4) - m.drawparallels(numpy.arange(-90, 90, 30), labels=[1, 0, 0, 0]) - m.drawmeridians(numpy.arange(m.lonmin, m.lonmax+30, 60), labels=[0, 0, 0, 1]) - m.drawmapboundary(fill_color="skyblue") - m.fillcontinents(color="green", lake_color="skyblue") - m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF. - logging.debug("Grey line drawn.") + gl = ax.gridlines(draw_labels=True) + gl.xlabels_top = False + gl.ylabels_right = False + gl.xformatter = cartopy.mpl.gridliner.LONGITUDE_FORMATTER + gl.yformatter = cartopy.mpl.gridliner.LATITUDE_FORMATTER + ax.add_feature(cartopy.feature.LAND, facecolor="green") + ax.add_feature(cartopy.feature.OCEAN, color="skyblue") + ax.add_feature(cartopy.feature.COASTLINE) + ax.add_feature(cartopy.feature.BORDERS, alpha=0.4) + + # Draw the grey line. This is based on the code from the Cartopy Aurora Forecast example (http://scitools.org.uk/cartopy/docs/latest/gallery/aurora_forecast.html) and used under the Open Government Licence (http://scitools.org.uk/cartopy/docs/v0.15/copyright.html). + logging.debug("Drawing the grey line...") + dt = datetime.utcnow() + axial_tilt = 23.5 + reference_solstice = datetime(2016, 6, 21, 22, 22) + days_per_year = 365.2425 + seconds_per_day = 86400.0 + + days_since_reference = (dt - reference_solstice).total_seconds()/seconds_per_day + latitude = axial_tilt*numpy.cos(2*numpy.pi*days_since_reference/days_per_year) + seconds_since_midnight = (dt - datetime(dt.year, dt.month, dt.day)).seconds + longitude = -(seconds_since_midnight/seconds_per_day - 0.5)*360 + + pole_longitude = longitude + if latitude > 0: + pole_latitude = -90 + latitude + central_rotated_longitude = 180 + else: + pole_latitude = 90 + latitude + central_rotated_longitude = 0 + + rotated_pole = cartopy.crs.RotatedPole(pole_latitude=pole_latitude, pole_longitude=pole_longitude, central_rotated_longitude=central_rotated_longitude) + + x = numpy.empty(360) + y = numpy.empty(360) + x[:180] = -90 + y[:180] = numpy.arange(-90, 90.) + x[180:] = 90 + y[180:] = numpy.arange(90, -90., -1) + + ax.fill(x, y, transform=rotated_pole, color="black", alpha=0.5) # Plot points on the map. if(self.points): + logging.debug("Plotting QTHs on the map...") for p in self.points: - x, y = m(p.longitude, p.latitude) - m.plot(x, y, p.style) - sub.text(x+0.01*x, y+0.01*y, p.name, color="white", size="small", weight="bold") + ax.plot(p.longitude, p.latitude, p.style, transform=cartopy.crs.PlateCarree()) + ax.text(p.longitude+0.02*p.longitude, p.latitude+0.02*p.latitude, p.name, color="white", size="small", weight="bold") return True else: diff --git a/requirements.txt b/requirements.txt index e0a74fd..44036ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ numpy matplotlib>=1.3.0 -basemap +cartopy>=0.16.0 cairocffi sphinx geocoder diff --git a/setup.py b/setup.py index c373e55..97d5abc 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2013-2017 Christian Thomas Jacobs. +# Copyright (C) 2013-2018 Christian Thomas Jacobs. # This file is part of PyQSO. @@ -20,7 +20,7 @@ from setuptools import setup setup(name="PyQSO", - version="1.0.0", + version="1.1.0-dev", description="A contact logging tool for amateur radio operators.", author="Christian Thomas Jacobs", author_email="christian@christianjacobs.uk",