Added the option of showing Maidenhead grid squares on the World Map, and the option of shading in worked grid squares. Addresses issue #59. Note that this introduces a new class called Maidenhead, which is capable of converting between latitude-longitude coordinates and grid squares. However, this functionality isn't currently used.
pull/66/head
Christian T. Jacobs 2018-03-10 15:49:44 +00:00 zatwierdzone przez GitHub
rodzic f222d5cc30
commit 5198547c6c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 552 dodań i 265 usunięć

Wyświetl plik

@ -2,10 +2,13 @@
## [UNRELEASED] ## [UNRELEASED]
### Added ### 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. - 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. - A separate World Map tab in the Preferences dialog.
- Added a requirements.txt file for the purpose of installing dependencies. - 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 ### Changed
- Renamed the GreyLine class to WorldMap, since it now does more than just grey line plotting. - Renamed the GreyLine class to WorldMap, since it now does more than just grey line plotting.

Wyświetl plik

@ -67,6 +67,7 @@ class PreferencesDialog:
self.records = RecordsPage(self.dialog, self.builder) self.records = RecordsPage(self.dialog, self.builder)
self.import_export = ImportExportPage(self.dialog, self.builder) self.import_export = ImportExportPage(self.dialog, self.builder)
self.hamlib = HamlibPage(self.dialog, self.builder) self.hamlib = HamlibPage(self.dialog, self.builder)
self.world_map = WorldMapPage(self.dialog, self.builder)
self.dialog.show_all() self.dialog.show_all()
@ -106,6 +107,11 @@ class PreferencesDialog:
for key in list(self.hamlib.data.keys()): for key in list(self.hamlib.data.keys()):
config.set("hamlib", key.lower(), str(self.hamlib.data[key])) 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. # Write the preferences to file.
with open(os.path.expanduser(PREFERENCES_FILE), 'w') as f: with open(os.path.expanduser(PREFERENCES_FILE), 'w') as f:
config.write(f) config.write(f)
@ -177,44 +183,6 @@ class GeneralPage:
else: else:
self.sources["KEEP_OPEN"].set_active(False) 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 return
@property @property
@ -226,10 +194,6 @@ class GeneralPage:
data["DEFAULT_LOGBOOK"] = self.sources["DEFAULT_LOGBOOK"].get_active() data["DEFAULT_LOGBOOK"] = self.sources["DEFAULT_LOGBOOK"].get_active()
data["DEFAULT_LOGBOOK_PATH"] = os.path.expanduser(self.sources["DEFAULT_LOGBOOK_PATH"].get_text()) data["DEFAULT_LOGBOOK_PATH"] = os.path.expanduser(self.sources["DEFAULT_LOGBOOK_PATH"].get_text())
data["KEEP_OPEN"] = self.sources["KEEP_OPEN"].get_active() 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 return data
def on_default_logbook_toggled(self, widget, data=None): def on_default_logbook_toggled(self, widget, data=None):
@ -257,40 +221,6 @@ class GeneralPage:
dialog.destroy() dialog.destroy()
return 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: class ViewPage:
@ -552,3 +482,122 @@ class HamlibPage:
data["RIG_PATHNAME"] = self.sources["RIG_PATHNAME"].get_text() data["RIG_PATHNAME"] = self.sources["RIG_PATHNAME"].get_text()
data["RIG_MODEL"] = self.sources["RIG_MODEL"].get_active_text() data["RIG_MODEL"] = self.sources["RIG_MODEL"].get_active_text()
return data 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

Wyświetl plik

@ -792,7 +792,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="worldmap"> <object class="GtkBox" id="world_map">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@ -806,7 +806,7 @@
</packing> </packing>
</child> </child>
<child type="tab"> <child type="tab">
<object class="GtkLabel" id="worldmap_label"> <object class="GtkLabel" id="world_map_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">World Map</property> <property name="label" translatable="yes">World Map</property>
@ -1441,183 +1441,6 @@ along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.</pro
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkFrame" id="preferences_general_qth">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkAlignment" id="preferences_general_qth_alignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">2</property>
<property name="right_padding">2</property>
<child>
<object class="GtkBox" id="preferences_general_qth_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkCheckButton" id="general_show_qth_checkbutton">
<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>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="general_qth_name_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="general_qth_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Name</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="general_qth_name_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">This might be the name of the city or road in which your radio station is located.</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="general_qth_lookup">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Lookup the latitude-longitude coordinates for the QTH based on the QTH's name</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="image25">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-info</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="general_qth_coordinates_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="general_qth_coordinates_latitude_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Latitude</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="general_qth_coordinates_latitude_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="general_qth_coordinates_longitude_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Longitude</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="general_qth_coordinates_longitude_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="preferences_general_qth_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">QTH</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
</child> </child>
<child type="tab"> <child type="tab">
@ -2898,6 +2721,294 @@ Base64-encoded plain text in the configuration file.</property>
<property name="tab_fill">False</property> <property name="tab_fill">False</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkBox" id="preferences_world_map_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkFrame" id="preferences_world_map_qth">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkAlignment" id="preferences_world_map_qth_alignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">2</property>
<property name="right_padding">2</property>
<child>
<object class="GtkBox" id="preferences_world_map_qth_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkCheckButton" id="world_map_show_qth_checkbutton">
<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>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="world_map_qth_name_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="world_map_qth_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Name</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="world_map_qth_name_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">This might be the name of the city or road in which your radio station is located.</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="world_map_qth_lookup">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Lookup the latitude-longitude coordinates for the QTH based on the QTH's name</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="image33">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-info</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="world_map_qth_coordinates_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="world_map_qth_coordinates_latitude_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Latitude</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="world_map_qth_coordinates_latitude_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="world_map_qth_coordinates_longitude_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Longitude</property>
<property name="width_chars">10</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="world_map_qth_coordinates_longitude_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">2</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="preferences_world_map_qth_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">QTH</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="preferences_world_map_grid_squares">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkAlignment" id="preferences_world_map_grid_squares_alignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">2</property>
<property name="right_padding">2</property>
<child>
<object class="GtkBox" id="preferences_world_map_grid_squares_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="world_map_show_grid_squares_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCheckButton" id="world_map_show_grid_squares_checkbutton">
<property name="label" translatable="yes">Show grid squares</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Allows multiple QSOs to be entered in quick succession. Especially useful for contest stations or special event stations where pileups may be expected.</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="world_map_shade_worked_grid_squares_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCheckButton" id="world_map_shade_worked_grid_squares_checkbutton">
<property name="label" translatable="yes">Shade worked grid squares</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Allows multiple QSOs to be entered in quick succession. Especially useful for contest stations or special event stations where pileups may be expected.</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="preferences_world_map_grid_squares_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Maidenhead Grid Squares</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">5</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="preferences_world_map_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">World Map</property>
</object>
<packing>
<property name="position">5</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

Wyświetl plik

@ -19,6 +19,8 @@
from gi.repository import GObject from gi.repository import GObject
import logging import logging
import sqlite3 as sqlite
import re
from os.path import expanduser from os.path import expanduser
from datetime import datetime from datetime import datetime
try: try:
@ -33,6 +35,7 @@ try:
import cartopy import cartopy
logging.info("Using version %s of cartopy." % (cartopy.__version__)) logging.info("Using version %s of cartopy." % (cartopy.__version__))
from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3
have_necessary_modules = True have_necessary_modules = True
except ImportError as e: except ImportError as e:
logging.warning(e) logging.warning(e)
@ -46,6 +49,12 @@ except ImportError:
have_geocoder = False 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: class Point:
""" A point on the grey line map. """ """ A point on the grey line map. """
def __init__(self, name, latitude, longitude, style="yo"): def __init__(self, name, latitude, longitude, style="yo"):
@ -64,6 +73,66 @@ class Point:
return 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: class WorldMap:
""" A tool for visualising the world map. """ """ A tool for visualising the world map. """
@ -82,24 +151,37 @@ class WorldMap:
if(have_necessary_modules): if(have_necessary_modules):
self.fig = matplotlib.figure.Figure() self.fig = matplotlib.figure.Figure()
self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application 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). 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. # Add the QTH coordinates for plotting, if available.
config = configparser.ConfigParser() config = configparser.ConfigParser()
have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != []) 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(have_config and config.has_option(section, option)):
if(config.getboolean(section, option)): if(config.getboolean(section, option)):
try: try:
qth_name = config.get("general", "qth_name") qth_name = config.get("world_map", "qth_name")
qth_latitude = float(config.get("general", "qth_latitude")) qth_latitude = float(config.get("world_map", "qth_latitude"))
qth_longitude = float(config.get("general", "qth_longitude")) qth_longitude = float(config.get("world_map", "qth_longitude"))
self.add_point(qth_name, qth_latitude, qth_longitude, "ro") self.add_point(qth_name, qth_latitude, qth_longitude, "ro")
except ValueError: 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?") 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!") logging.debug("World map ready!")
@ -142,6 +224,30 @@ class WorldMap:
return 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): def draw(self):
""" Draw the world map and the grey line on top of it. """ 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.xformatter = cartopy.mpl.gridliner.LONGITUDE_FORMATTER
gl.yformatter = cartopy.mpl.gridliner.LATITUDE_FORMATTER gl.yformatter = cartopy.mpl.gridliner.LATITUDE_FORMATTER
ax.add_feature(cartopy.feature.LAND, facecolor="green") 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.COASTLINE)
ax.add_feature(cartopy.feature.BORDERS, alpha=0.4) 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.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") 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 return True
else: else:
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported. return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.