Migrate to a world map / grey line that uses Cartopy (#64)

Migrating grey line functionality over to a Cartopy-based implementation to address issue #62.

Also renamed GreyLine to WorldMap, and bumped the version to v1.1.0-dev.
pull/65/head
Christian T. Jacobs 2018-02-24 13:24:39 +00:00 zatwierdzone przez GitHub
rodzic f283df065b
commit f222d5cc30
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 117 dodań i 71 usunięć

Wyświetl plik

@ -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"

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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.

Wyświetl plik

@ -24,7 +24,7 @@ include:
- Progress tracker for the `DXCC <http://www.arrl.org/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 <getting_started.html>`_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management <log_management.html>`_ 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 <record_management.html>`_ section deals with the bottom layer of the three-tier model - the creation, deletion, and modification of QSO records in a log. The `Toolbox <toolbox.html>`_ 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 <preferences.html>`_ 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 <shortcuts.html>`_ is also available for reference.
The structure of this documentation is as follows. The section on `Getting Started <getting_started.html>`_ provides information on the PyQSO installation process through to creating a new logbook (or opening an existing one). The `Log Management <log_management.html>`_ 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 <record_management.html>`_ section deals with the bottom layer of the three-tier model - the creation, deletion, and modification of QSO records in a log. The `Toolbox <toolbox.html>`_ 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 <preferences.html>`_ 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 <shortcuts.html>`_ is also available for reference.

Wyświetl plik

@ -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

Wyświetl plik

@ -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 <https://pypi.python.org/pypi/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
------

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -792,7 +792,7 @@
</packing>
</child>
<child>
<object class="GtkBox" id="greyline">
<object class="GtkBox" id="worldmap">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@ -806,10 +806,10 @@
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="greyline_label">
<object class="GtkLabel" id="worldmap_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Grey Line</property>
<property name="label" translatable="yes">World Map</property>
</object>
<packing>
<property name="position">1</property>
@ -908,8 +908,8 @@
<property name="type_hint">dialog</property>
<property name="transient_for">pyqso</property>
<property name="program_name">PyQSO</property>
<property name="version">1.0.0</property>
<property name="copyright" translatable="yes">Copyright (C) 2012-2017 Christian Thomas Jacobs</property>
<property name="version">1.1.0-dev</property>
<property name="copyright" translatable="yes">Copyright (C) 2012-2018 Christian Thomas Jacobs</property>
<property name="comments" translatable="yes">A contact logging tool for amateur radio operators.</property>
<property name="website">http://christianjacobs.uk/pyqso</property>
<property name="license" translatable="yes">This program is free software: you can redistribute it and/or modify
@ -1460,7 +1460,7 @@ along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.</pro
<property name="spacing">2</property>
<child>
<object class="GtkCheckButton" id="general_show_qth_checkbutton">
<property name="label" translatable="yes">Pin-point QTH on grey line map</property>
<property name="label" translatable="yes">Pin-point QTH on world map</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>

Wyświetl plik

@ -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 <http://www.gnu.org/licenses/>.
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

Wyświetl plik

@ -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:

Wyświetl plik

@ -1,6 +1,6 @@
numpy
matplotlib>=1.3.0
basemap
cartopy>=0.16.0
cairocffi
sphinx
geocoder

Wyświetl plik

@ -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",