diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54c71a2..a2cffe0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,13 @@
## [UNRELEASED]
### Added
-- Added support for the SAT_NAME, SAT_MODE, PROP_MODE, and GRIDSQUARE ADIF fields for the purposes of satellite QSO logging.
+- 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.
+- A separate World Map tab in the Preferences dialog.
+- A navigation bar for the World Map tool.
+- The option of showing Maidenhead grid squares on the World Map, and the option of shading in worked grid squares.
+- Basic copy/paste functionality for individual records.
+- 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.
diff --git a/pyqso/preferences_dialog.py b/pyqso/preferences_dialog.py
index bfc88da..47dcb22 100644
--- a/pyqso/preferences_dialog.py
+++ b/pyqso/preferences_dialog.py
@@ -67,6 +67,7 @@ class PreferencesDialog:
self.records = RecordsPage(self.dialog, self.builder)
self.import_export = ImportExportPage(self.dialog, self.builder)
self.hamlib = HamlibPage(self.dialog, self.builder)
+ self.world_map = WorldMapPage(self.dialog, self.builder)
self.dialog.show_all()
@@ -106,6 +107,11 @@ class PreferencesDialog:
for key in list(self.hamlib.data.keys()):
config.set("hamlib", key.lower(), str(self.hamlib.data[key]))
+ # World Map
+ config.add_section("world_map")
+ for key in list(self.world_map.data.keys()):
+ config.set("world_map", key.lower(), str(self.world_map.data[key]))
+
# Write the preferences to file.
with open(os.path.expanduser(PREFERENCES_FILE), 'w') as f:
config.write(f)
@@ -177,44 +183,6 @@ class GeneralPage:
else:
self.sources["KEEP_OPEN"].set_active(False)
- # Pin-point QTH on grey line map.
- self.sources["SHOW_QTH"] = self.builder.get_object("general_show_qth_checkbutton")
- (section, option) = ("general", "show_qth")
- if(have_config and config.has_option(section, option)):
- self.sources["SHOW_QTH"].set_active(config.getboolean(section, option))
- else:
- self.sources["SHOW_QTH"].set_active(False)
-
- self.sources["QTH_NAME"] = self.builder.get_object("general_qth_name_entry")
- button = self.builder.get_object("general_qth_lookup")
- button.connect("clicked", self.lookup_callback) # Uses geocoding to find the latitude-longitude coordinates.
-
- self.sources["QTH_LATITUDE"] = self.builder.get_object("general_qth_coordinates_latitude_entry")
- self.sources["QTH_LONGITUDE"] = self.builder.get_object("general_qth_coordinates_longitude_entry")
-
- (section, option) = ("general", "show_qth")
- # Disable the text entry boxes if the SHOW_QTH checkbox is not checked.
- if(have_config and config.has_option(section, option)):
- self.sources["QTH_NAME"].set_sensitive(self.sources["SHOW_QTH"].get_active())
- self.sources["QTH_LATITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active())
- self.sources["QTH_LONGITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active())
- button.set_sensitive(self.sources["SHOW_QTH"].get_active())
- else:
- self.sources["QTH_NAME"].set_sensitive(False)
- self.sources["QTH_LATITUDE"].set_sensitive(False)
- self.sources["QTH_LONGITUDE"].set_sensitive(False)
- button.set_sensitive(False)
- (section, option) = ("general", "qth_name")
- if(have_config and config.has_option(section, option)):
- self.sources["QTH_NAME"].set_text(config.get(section, option))
- (section, option) = ("general", "qth_latitude")
- if(have_config and config.has_option(section, option)):
- self.sources["QTH_LATITUDE"].set_text(config.get(section, option))
- (section, option) = ("general", "qth_longitude")
- if(have_config and config.has_option(section, option)):
- self.sources["QTH_LONGITUDE"].set_text(config.get(section, option))
- self.sources["SHOW_QTH"].connect("toggled", self.on_show_qth_toggled)
-
return
@property
@@ -226,10 +194,6 @@ class GeneralPage:
data["DEFAULT_LOGBOOK"] = self.sources["DEFAULT_LOGBOOK"].get_active()
data["DEFAULT_LOGBOOK_PATH"] = os.path.expanduser(self.sources["DEFAULT_LOGBOOK_PATH"].get_text())
data["KEEP_OPEN"] = self.sources["KEEP_OPEN"].get_active()
- data["SHOW_QTH"] = self.sources["SHOW_QTH"].get_active()
- data["QTH_NAME"] = self.sources["QTH_NAME"].get_text()
- data["QTH_LATITUDE"] = self.sources["QTH_LATITUDE"].get_text()
- data["QTH_LONGITUDE"] = self.sources["QTH_LONGITUDE"].get_text()
return data
def on_default_logbook_toggled(self, widget, data=None):
@@ -257,40 +221,6 @@ class GeneralPage:
dialog.destroy()
return
- def on_show_qth_toggled(self, widget, data=None):
- if(widget.get_active()):
- self.sources["QTH_NAME"].set_sensitive(True)
- self.sources["QTH_LATITUDE"].set_sensitive(True)
- self.sources["QTH_LONGITUDE"].set_sensitive(True)
- self.builder.get_object("general_qth_lookup").set_sensitive(True)
- else:
- self.sources["QTH_NAME"].set_sensitive(False)
- self.sources["QTH_LATITUDE"].set_sensitive(False)
- self.sources["QTH_LONGITUDE"].set_sensitive(False)
- self.builder.get_object("general_qth_lookup").set_sensitive(False)
- return
-
- def lookup_callback(self, widget=None):
- """ Perform geocoding of the QTH location to obtain latitude-longitude coordinates. """
- if(not have_geocoder):
- error(parent=self.parent, message="Geocoder module could not be imported. Geocoding aborted.")
- return
- logging.debug("Geocoding QTH location...")
- name = self.sources["QTH_NAME"].get_text()
- try:
- g = geocoder.google(name)
- latitude, longitude = g.latlng
- self.sources["QTH_LATITUDE"].set_text(str(latitude))
- self.sources["QTH_LONGITUDE"].set_text(str(longitude))
- logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude))
- except ValueError as e:
- error(parent=self.parent, message="Unable to lookup QTH coordinates. Is the QTH name correct?")
- logging.exception(e)
- except Exception as e:
- error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
- logging.exception(e)
- return
-
class ViewPage:
@@ -552,3 +482,122 @@ class HamlibPage:
data["RIG_PATHNAME"] = self.sources["RIG_PATHNAME"].get_text()
data["RIG_MODEL"] = self.sources["RIG_MODEL"].get_active_text()
return data
+
+
+class WorldMapPage:
+
+ """ The section of the preferences dialog containing World Map preferences. """
+
+ def __init__(self, parent, builder):
+ """ Set up the World Map page of the Preferences dialog. """
+
+ self.parent = parent
+ self.builder = builder
+ self.sources = {}
+
+ # Remember that the have_config conditional in the PyQSO class may be out-of-date the next time the user opens up the preferences dialog
+ # because a configuration file may have been created after launching the application. Let's check to see if one exists again...
+ config = configparser.ConfigParser()
+ have_config = (config.read(PREFERENCES_FILE) != [])
+
+ # Option to pin-point QTH on grey line map.
+ self.sources["SHOW_QTH"] = self.builder.get_object("world_map_show_qth_checkbutton")
+ (section, option) = ("world_map", "show_qth")
+ if(have_config and config.has_option(section, option)):
+ self.sources["SHOW_QTH"].set_active(config.getboolean(section, option))
+ else:
+ self.sources["SHOW_QTH"].set_active(False)
+
+ self.sources["QTH_NAME"] = self.builder.get_object("world_map_qth_name_entry")
+ button = self.builder.get_object("world_map_qth_lookup")
+ button.connect("clicked", self.lookup_callback) # Uses geocoding to find the latitude-longitude coordinates.
+
+ self.sources["QTH_LATITUDE"] = self.builder.get_object("world_map_qth_coordinates_latitude_entry")
+ self.sources["QTH_LONGITUDE"] = self.builder.get_object("world_map_qth_coordinates_longitude_entry")
+
+ (section, option) = ("world_map", "show_qth")
+ # Disable the text entry boxes if the SHOW_QTH checkbox is not checked.
+ if(have_config and config.has_option(section, option)):
+ self.sources["QTH_NAME"].set_sensitive(self.sources["SHOW_QTH"].get_active())
+ self.sources["QTH_LATITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active())
+ self.sources["QTH_LONGITUDE"].set_sensitive(self.sources["SHOW_QTH"].get_active())
+ button.set_sensitive(self.sources["SHOW_QTH"].get_active())
+ else:
+ self.sources["QTH_NAME"].set_sensitive(False)
+ self.sources["QTH_LATITUDE"].set_sensitive(False)
+ self.sources["QTH_LONGITUDE"].set_sensitive(False)
+ button.set_sensitive(False)
+ (section, option) = ("world_map", "qth_name")
+ if(have_config and config.has_option(section, option)):
+ self.sources["QTH_NAME"].set_text(config.get(section, option))
+ (section, option) = ("world_map", "qth_latitude")
+ if(have_config and config.has_option(section, option)):
+ self.sources["QTH_LATITUDE"].set_text(config.get(section, option))
+ (section, option) = ("world_map", "qth_longitude")
+ if(have_config and config.has_option(section, option)):
+ self.sources["QTH_LONGITUDE"].set_text(config.get(section, option))
+ self.sources["SHOW_QTH"].connect("toggled", self.on_show_qth_toggled)
+
+ # Option to show Maidenhead grid squares.
+ self.sources["SHOW_GRID_SQUARES"] = self.builder.get_object("world_map_show_grid_squares_checkbutton")
+ (section, option) = ("world_map", "show_grid_squares")
+ if(have_config and config.has_option(section, option)):
+ self.sources["SHOW_GRID_SQUARES"].set_active(config.getboolean(section, option))
+ else:
+ self.sources["SHOW_GRID_SQUARES"].set_active(False)
+
+ # Option to shade in worked Maidenhead grid squares.
+ self.sources["SHADE_WORKED_GRID_SQUARES"] = self.builder.get_object("world_map_shade_worked_grid_squares_checkbutton")
+ (section, option) = ("world_map", "shade_worked_grid_squares")
+ if(have_config and config.has_option(section, option)):
+ self.sources["SHADE_WORKED_GRID_SQUARES"].set_active(config.getboolean(section, option))
+ else:
+ self.sources["SHADE_WORKED_GRID_SQUARES"].set_active(False)
+
+ return
+
+ @property
+ def data(self):
+ """ User preferences regarding World Map settings. """
+ data = {}
+ data["SHOW_QTH"] = self.sources["SHOW_QTH"].get_active()
+ data["QTH_NAME"] = self.sources["QTH_NAME"].get_text()
+ data["QTH_LATITUDE"] = self.sources["QTH_LATITUDE"].get_text()
+ data["QTH_LONGITUDE"] = self.sources["QTH_LONGITUDE"].get_text()
+ data["SHOW_GRID_SQUARES"] = self.sources["SHOW_GRID_SQUARES"].get_active()
+ data["SHADE_WORKED_GRID_SQUARES"] = self.sources["SHADE_WORKED_GRID_SQUARES"].get_active()
+ return data
+
+ def on_show_qth_toggled(self, widget, data=None):
+ if(widget.get_active()):
+ self.sources["QTH_NAME"].set_sensitive(True)
+ self.sources["QTH_LATITUDE"].set_sensitive(True)
+ self.sources["QTH_LONGITUDE"].set_sensitive(True)
+ self.builder.get_object("world_map_qth_lookup").set_sensitive(True)
+ else:
+ self.sources["QTH_NAME"].set_sensitive(False)
+ self.sources["QTH_LATITUDE"].set_sensitive(False)
+ self.sources["QTH_LONGITUDE"].set_sensitive(False)
+ self.builder.get_object("world_map_qth_lookup").set_sensitive(False)
+ return
+
+ def lookup_callback(self, widget=None):
+ """ Perform geocoding of the QTH location to obtain latitude-longitude coordinates. """
+ if(not have_geocoder):
+ error(parent=self.parent, message="Geocoder module could not be imported. Geocoding aborted.")
+ return
+ logging.debug("Geocoding QTH location...")
+ name = self.sources["QTH_NAME"].get_text()
+ try:
+ g = geocoder.google(name)
+ latitude, longitude = g.latlng
+ self.sources["QTH_LATITUDE"].set_text(str(latitude))
+ self.sources["QTH_LONGITUDE"].set_text(str(longitude))
+ logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude))
+ except ValueError as e:
+ error(parent=self.parent, message="Unable to lookup QTH coordinates. Is the QTH name correct?")
+ logging.exception(e)
+ except Exception as e:
+ error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
+ logging.exception(e)
+ return
diff --git a/pyqso/res/pyqso.glade b/pyqso/res/pyqso.glade
index ae5fe50..9a9817d 100644
--- a/pyqso/res/pyqso.glade
+++ b/pyqso/res/pyqso.glade
@@ -792,7 +792,7 @@
-
-
+
True
False
World Map
@@ -1441,183 +1441,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.1
-
-
- True
- False
- 0
-
-
- True
- False
- 2
- 2
-
-
- True
- False
- vertical
- 2
-
-
- Pin-point QTH on world map
- True
- True
- False
- 0
- True
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 2
-
-
- True
- False
- Name
- 10
- 0
-
-
- False
- True
- 2
- 0
-
-
-
-
- True
- True
- This might be the name of the city or road in which your radio station is located.
-
-
- True
- True
- 1
-
-
-
-
- True
- True
- True
- Lookup the latitude-longitude coordinates for the QTH based on the QTH's name
- True
-
-
- True
- False
- gtk-info
- 1
-
-
-
-
- False
- True
- 2
- 2
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 2
-
-
- True
- False
- Latitude
- 10
- 0
-
-
- False
- True
- 2
- 0
-
-
-
-
- True
- True
-
-
- True
- True
- 1
-
-
-
-
- True
- False
- Longitude
- 10
- 0
-
-
- False
- True
- 2
- 2
-
-
-
-
- True
- True
-
-
- True
- True
- 2
- 3
-
-
-
-
- False
- True
- 2
-
-
-
-
-
-
-
-
- True
- False
- QTH
-
-
-
-
- True
- True
- 2
-
-
@@ -2898,6 +2721,294 @@ Base64-encoded plain text in the configuration file.
False
+
+
+ True
+ False
+ vertical
+ 2
+
+
+ True
+ False
+ 0
+
+
+ True
+ False
+ 2
+ 2
+
+
+ True
+ False
+ vertical
+ 2
+
+
+ Pin-point QTH on world map
+ True
+ True
+ False
+ 0
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 2
+
+
+ True
+ False
+ Name
+ 10
+ 0
+
+
+ False
+ True
+ 2
+ 0
+
+
+
+
+ True
+ True
+ This might be the name of the city or road in which your radio station is located.
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ True
+ True
+ Lookup the latitude-longitude coordinates for the QTH based on the QTH's name
+ True
+
+
+ True
+ False
+ gtk-info
+ 1
+
+
+
+
+ False
+ True
+ 2
+ 2
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ 2
+
+
+ True
+ False
+ Latitude
+ 10
+ 0
+
+
+ False
+ True
+ 2
+ 0
+
+
+
+
+ True
+ True
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+ Longitude
+ 10
+ 0
+
+
+ False
+ True
+ 2
+ 2
+
+
+
+
+ True
+ True
+
+
+ True
+ True
+ 2
+ 3
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+
+
+ True
+ False
+ QTH
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+ 0
+
+
+ True
+ False
+ 2
+ 2
+
+
+ True
+ False
+ vertical
+ 2
+
+
+ True
+ False
+
+
+ Show grid squares
+ True
+ True
+ False
+ Allows multiple QSOs to be entered in quick succession. Especially useful for contest stations or special event stations where pileups may be expected.
+ 0
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+
+
+ Shade worked grid squares
+ True
+ True
+ False
+ Allows multiple QSOs to be entered in quick succession. Especially useful for contest stations or special event stations where pileups may be expected.
+ 0
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+ True
+ False
+ Maidenhead Grid Squares
+
+
+
+
+ True
+ True
+ 2
+
+
+
+
+ 5
+
+
+
+
+ True
+ False
+ World Map
+
+
+ 5
+ False
+
+
True
diff --git a/pyqso/world_map.py b/pyqso/world_map.py
index bd2d9f2..36908bc 100644
--- a/pyqso/world_map.py
+++ b/pyqso/world_map.py
@@ -19,6 +19,8 @@
from gi.repository import GObject
import logging
+import sqlite3 as sqlite
+import re
from os.path import expanduser
from datetime import datetime
try:
@@ -33,6 +35,7 @@ try:
import cartopy
logging.info("Using version %s of cartopy." % (cartopy.__version__))
from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
+ from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3
have_necessary_modules = True
except ImportError as e:
logging.warning(e)
@@ -46,6 +49,12 @@ except ImportError:
have_geocoder = False
+class NavigationToolbar(NavigationToolbar2GTK3):
+ """ Navigation tools for the World Map. """
+ # Only include a subset of the tools.
+ toolitems = [t for t in NavigationToolbar2GTK3.toolitems if t[0] in ("Home", "Zoom", "Save")]
+
+
class Point:
""" A point on the grey line map. """
def __init__(self, name, latitude, longitude, style="yo"):
@@ -64,6 +73,66 @@ class Point:
return
+class Maidenhead:
+
+ """ The Maidenhead Locator System. """
+
+ def __init__(self):
+ self.upper = "ABCDEFGHIJKLMNOPQR"
+ self.lower = "abcdefghijklmnopqrstuvwx"
+ return
+
+ def ll2gs(self, latitude, longitude):
+ """ Convert latitude-longitude coordinates to a Maidenhead grid square locator.
+ This is based on the code by Walter Underwood, K6WRU (https://ham.stackexchange.com/questions/221/how-can-one-convert-from-lat-long-to-grid-square).
+
+ :arg float latitude: The latitude.
+ :arg float longitude: The longitude.
+ :rtype: str
+ :returns: The Maidenhead grid square locator.
+ """
+
+ adjusted_latitude = latitude + 90
+ adjusted_longitude = longitude + 180
+ field_latitude = self.upper[int(adjusted_latitude/10)]
+ field_longitude = self.upper[int(adjusted_longitude/20)]
+ square_latitude = int(adjusted_latitude % 10)
+ square_longitude = int((adjusted_longitude/2) % 10)
+
+ return ("%s"*4) % (field_longitude, field_latitude, square_longitude, square_latitude)
+
+ def gs2ll(self, grid_square):
+ """ Convert a Maidenhead grid square locator to latitude-longitude coordinates.
+ This is based on the gridSquareToLatLon function in HamGridSquare.js by Paul Brewer, KI6CQ (https://gist.github.com/DrPaulBrewer/4279e9d234a1bd6dd3c0), released under the MIT license.
+
+ :arg str grid_square: The Maidenhead grid square locator.
+ :rtype: tuple
+ :returns: The latitude-longitude coordinates in a tuple.
+ """
+
+ m = re.match(r"^[A-X][A-X][0-9][0-9]$", grid_square)
+ if(m):
+ gs = m.group(0)
+ latitude = self.latitude4(gs)+0.5
+ longitude = self.longitude4(gs)+1.0
+ else:
+ m = re.match(r"^[A-X][A-X][0-9][0-9][a-x][a-x]$", grid_square)
+ if(m):
+ gs = m.group(0)
+ latitude = self.latitude4(gs) + (1.0/60.0)*2.5*(ord(gs[5])-ord("a")+0.5)
+ longitude = self.longitude4(gs) + (1.0/60.0)*5*(ord(gs[4])-ord("a")+0.5)
+ else:
+ raise ValueError("Unable to parse grid square string.")
+
+ return (latitude, longitude)
+
+ def latitude4(self, g):
+ return 10*(ord(g[1]) - ord("A")) + int(g[3])-90
+
+ def longitude4(self, g):
+ return 20*(ord(g[0]) - ord("A")) + 2*int(g[2])-180
+
+
class WorldMap:
""" A tool for visualising the world map. """
@@ -82,24 +151,37 @@ class WorldMap:
if(have_necessary_modules):
self.fig = matplotlib.figure.Figure()
self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application
- self.builder.get_object("worldmap").pack_start(self.canvas, True, True, 0)
+ self.builder.get_object("world_map").pack_start(self.canvas, True, True, 0)
+ toolbar = NavigationToolbar(self.canvas, self.application.window)
+ self.builder.get_object("world_map").pack_start(toolbar, False, False, 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).
# 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")
+ (section, option) = ("world_map", "show_qth")
if(have_config and config.has_option(section, option)):
if(config.getboolean(section, option)):
try:
- qth_name = config.get("general", "qth_name")
- qth_latitude = float(config.get("general", "qth_latitude"))
- qth_longitude = float(config.get("general", "qth_longitude"))
+ qth_name = config.get("world_map", "qth_name")
+ qth_latitude = float(config.get("world_map", "qth_latitude"))
+ qth_longitude = float(config.get("world_map", "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 world map. Check preferences?")
- self.builder.get_object("worldmap").show_all()
+ # Maidenhead grid squares.
+ self.maidenhead = Maidenhead()
+ self.show_grid_squares = False
+ self.shade_worked_grid_squares = False
+ (section, option) = ("world_map", "show_grid_squares")
+ if(have_config and config.has_option(section, option)):
+ self.show_grid_squares = config.getboolean(section, option)
+ (section, option) = ("world_map", "shade_worked_grid_squares")
+ if(have_config and config.has_option(section, option)):
+ self.shade_worked_grid_squares = config.getboolean(section, option)
+
+ self.builder.get_object("world_map").show_all()
logging.debug("World map ready!")
@@ -142,6 +224,30 @@ class WorldMap:
return
+ def get_worked_grid_squares(self, logbook):
+ """ Updated the array of worked grid squares.
+
+ :arg logbook: The logbook containing logs which in turn contain QSOs.
+ :returns: A two-dimensional array of boolean values showing which grid squares have been worked.
+ :rtype: numpy.array
+ """
+
+ worked_grid_squares = numpy.zeros((len(self.maidenhead.upper), len(self.maidenhead.upper)), dtype=bool)
+
+ for log in logbook.logs:
+ try:
+ records = log.records
+ for r in records:
+ if(r["GRIDSQUARE"]):
+ grid_square = r["GRIDSQUARE"][0:2].upper() # Only consider the field value (e.g. IO).
+ worked_grid_squares[self.maidenhead.upper.index(grid_square[1]), self.maidenhead.upper.index(grid_square[0])] = True
+
+ except sqlite.Error as e:
+ logging.error("Could not update the array of worked grid squares for log '%s' because of a database error." % log.name)
+ logging.exception(e)
+
+ return worked_grid_squares
+
def draw(self):
""" Draw the world map and the grey line on top of it.
@@ -169,7 +275,7 @@ class WorldMap:
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.OCEAN)
ax.add_feature(cartopy.feature.COASTLINE)
ax.add_feature(cartopy.feature.BORDERS, alpha=0.4)
@@ -212,6 +318,24 @@ class WorldMap:
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")
+ # Draw Maidenhead grid squares and shade in the worked squares.
+ x = numpy.linspace(-180, 180, len(list(self.maidenhead.upper))+1)
+ y = numpy.linspace(-90, 90, len(list(self.maidenhead.upper))+1)
+ if(self.show_grid_squares):
+ if(self.shade_worked_grid_squares):
+ worked_grid_squares = self.get_worked_grid_squares(self.application.logbook)
+ masked = numpy.ma.masked_array(worked_grid_squares, worked_grid_squares == 0)
+ else:
+ z = numpy.zeros((len(self.maidenhead.upper), len(self.maidenhead.upper)), dtype=bool)
+ masked = numpy.ma.masked_array(z, z == 0)
+ ax.pcolormesh(x, y, masked, transform=cartopy.crs.PlateCarree(), cmap='Purples', vmin=0, vmax=1, edgecolors="k", linewidth=2, alpha=0.4)
+
+ # Grid square labels.
+ for i in range(len(self.maidenhead.upper)):
+ for j in range(len(self.maidenhead.upper)):
+ text = self.maidenhead.upper[i]+self.maidenhead.upper[j]
+ ax.text((x[i]+x[i+1])/2.0, (y[j]+y[j+1])/2.0, text, ha="center", va="center", size=8, color="w")
+
return True
else:
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.