kopia lustrzana https://github.com/ctjacobs/pyqso
Callsign map (#61)
Pinpoint selected callsigns on the grey line 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.pull/65/head
rodzic
d06e326cbd
commit
7a162b2a2d
|
@ -44,6 +44,7 @@ sys.path.insert(0, pyqso_path)
|
||||||
from pyqso.adif import *
|
from pyqso.adif import *
|
||||||
from pyqso.logbook import *
|
from pyqso.logbook import *
|
||||||
from pyqso.menu import *
|
from pyqso.menu import *
|
||||||
|
from pyqso.popup import *
|
||||||
from pyqso.toolbar import *
|
from pyqso.toolbar import *
|
||||||
from pyqso.toolbox import *
|
from pyqso.toolbox import *
|
||||||
from pyqso.preferences_dialog import *
|
from pyqso.preferences_dialog import *
|
||||||
|
@ -93,8 +94,9 @@ class PyQSO:
|
||||||
self.logbook = Logbook(self)
|
self.logbook = Logbook(self)
|
||||||
self.toolbox = Toolbox(self)
|
self.toolbox = Toolbox(self)
|
||||||
|
|
||||||
# Set up menu and tool bars. These classes depend on the Logbook and Toolbox class.
|
# Set up menu and toolbar. These classes depend on the Logbook and Toolbox class.
|
||||||
self.menu = Menu(self)
|
self.menu = Menu(self)
|
||||||
|
self.popup = Popup(self)
|
||||||
self.toolbar = Toolbar(self)
|
self.toolbar = Toolbar(self)
|
||||||
|
|
||||||
self.window.show_all()
|
self.window.show_all()
|
||||||
|
|
|
@ -38,6 +38,30 @@ except ImportError as e:
|
||||||
logging.warning(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 GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.")
|
||||||
have_necessary_modules = False
|
have_necessary_modules = False
|
||||||
|
try:
|
||||||
|
import geocoder
|
||||||
|
have_geocoder = True
|
||||||
|
except ImportError:
|
||||||
|
logging.warning("Could not import the geocoder module!")
|
||||||
|
have_geocoder = False
|
||||||
|
|
||||||
|
|
||||||
|
class Point:
|
||||||
|
""" A point on the grey line map. """
|
||||||
|
def __init__(self, name, latitude, longitude, style="yo"):
|
||||||
|
""" Set up the point's attributes.
|
||||||
|
|
||||||
|
:arg str name: The name that identifies the point.
|
||||||
|
:arg float latitude: The latitude of the point on the map.
|
||||||
|
:arg float longitude: The longitude of the point on the map.
|
||||||
|
:arg str style: The style of the point when plotted. By default it is a filled yellow circle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.latitude = latitude
|
||||||
|
self.longitude = longitude
|
||||||
|
self.style = style
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class GreyLine:
|
class GreyLine:
|
||||||
|
@ -53,22 +77,7 @@ class GreyLine:
|
||||||
|
|
||||||
self.application = application
|
self.application = application
|
||||||
self.builder = self.application.builder
|
self.builder = self.application.builder
|
||||||
|
self.points = []
|
||||||
# Get the QTH coordinates, if available.
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
|
|
||||||
(section, option) = ("general", "show_qth")
|
|
||||||
self.show_qth = False
|
|
||||||
if(have_config and config.has_option(section, option)):
|
|
||||||
if(config.getboolean(section, option)):
|
|
||||||
self.show_qth = True
|
|
||||||
try:
|
|
||||||
self.qth_name = config.get("general", "qth_name")
|
|
||||||
self.qth_latitude = float(config.get("general", "qth_latitude"))
|
|
||||||
self.qth_longitude = float(config.get("general", "qth_longitude"))
|
|
||||||
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?")
|
|
||||||
self.show_qth = False
|
|
||||||
|
|
||||||
if(have_necessary_modules):
|
if(have_necessary_modules):
|
||||||
self.fig = matplotlib.figure.Figure()
|
self.fig = matplotlib.figure.Figure()
|
||||||
|
@ -76,12 +85,63 @@ class GreyLine:
|
||||||
self.builder.get_object("greyline").pack_start(self.canvas, True, True, 0)
|
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.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible).
|
||||||
|
|
||||||
|
# Plot the QTH coordinates, if available.
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != [])
|
||||||
|
(section, option) = ("general", "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"))
|
||||||
|
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?")
|
||||||
|
|
||||||
self.builder.get_object("greyline").show_all()
|
self.builder.get_object("greyline").show_all()
|
||||||
|
|
||||||
logging.debug("Grey line ready!")
|
logging.debug("Grey line ready!")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def add_point(self, name, latitude, longitude, style="yo"):
|
||||||
|
""" Add a point and re-draw the map.
|
||||||
|
|
||||||
|
:arg str name: The name that identifies the point.
|
||||||
|
:arg float latitude: The latitude of the point on the map.
|
||||||
|
:arg float longitude: The longitude of the point on the map.
|
||||||
|
:arg str style: The style of the point when plotted. By default it is a filled yellow circle.
|
||||||
|
"""
|
||||||
|
p = Point(name, latitude, longitude, style)
|
||||||
|
self.points.append(p)
|
||||||
|
self.draw()
|
||||||
|
return
|
||||||
|
|
||||||
|
def pinpoint(self, r):
|
||||||
|
""" Pinpoint the location of a QSO on the grey line map based on the COUNTRY field.
|
||||||
|
|
||||||
|
:arg r: The QSO record containing the location to pinpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if(have_geocoder):
|
||||||
|
country = r["COUNTRY"]
|
||||||
|
callsign = r["CALL"]
|
||||||
|
|
||||||
|
# Get the latitude-longitude coordinates of the country.
|
||||||
|
if(country):
|
||||||
|
try:
|
||||||
|
g = geocoder.google(country)
|
||||||
|
latitude, longitude = g.latlng
|
||||||
|
logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude))
|
||||||
|
self.add_point(callsign, latitude, longitude)
|
||||||
|
except ValueError:
|
||||||
|
logging.exception("Unable to lookup QTH coordinates.")
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
@ -113,11 +173,12 @@ class GreyLine:
|
||||||
m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
|
m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
|
||||||
logging.debug("Grey line drawn.")
|
logging.debug("Grey line drawn.")
|
||||||
|
|
||||||
# Pin-point QTH on the map.
|
# Plot points on the map.
|
||||||
if(self.show_qth):
|
if(self.points):
|
||||||
qth_x, qth_y = m(self.qth_longitude, self.qth_latitude)
|
for p in self.points:
|
||||||
m.plot(qth_x, qth_y, "ro")
|
x, y = m(p.longitude, p.latitude)
|
||||||
sub.text(qth_x+0.015*qth_x, qth_y+0.015*qth_y, self.qth_name, color="white", size="medium", weight="bold")
|
m.plot(x, y, p.style)
|
||||||
|
sub.text(x+0.01*x, y+0.01*y, p.name, color="white", size="small", weight="bold")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -247,6 +247,14 @@ class Logbook:
|
||||||
self.application.menu.set_record_items_sensitive(True)
|
self.application.menu.set_record_items_sensitive(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def on_button_release_event(self, treeview, event):
|
||||||
|
""" Show a popup menu when the user right-clicks a record in the logbook. """
|
||||||
|
|
||||||
|
if(event.button == 3):
|
||||||
|
self.application.popup.menu.popup(None, None, None, None, event.button, event.time)
|
||||||
|
self.application.popup.menu.show_all()
|
||||||
|
return True
|
||||||
|
|
||||||
def new_log(self, widget=None):
|
def new_log(self, widget=None):
|
||||||
""" Create a new log in the logbook. """
|
""" Create a new log in the logbook. """
|
||||||
|
|
||||||
|
@ -382,8 +390,10 @@ class Logbook:
|
||||||
self.treeview.append(Gtk.TreeView(model=self.sorter[index]))
|
self.treeview.append(Gtk.TreeView(model=self.sorter[index]))
|
||||||
self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
||||||
self.treeview[index].connect("row-activated", self.edit_record_callback)
|
self.treeview[index].connect("row-activated", self.edit_record_callback)
|
||||||
|
self.treeview[index].connect("button-release-event", self.on_button_release_event)
|
||||||
self.treeselection.append(self.treeview[index].get_selection())
|
self.treeselection.append(self.treeview[index].get_selection())
|
||||||
self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
|
self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
|
||||||
|
|
||||||
# Allow the Log to be scrolled up/down.
|
# Allow the Log to be scrolled up/down.
|
||||||
sw = Gtk.ScrolledWindow()
|
sw = Gtk.ScrolledWindow()
|
||||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||||
|
@ -643,12 +653,14 @@ class Logbook:
|
||||||
|
|
||||||
def export_log_adif(self, widget=None):
|
def export_log_adif(self, widget=None):
|
||||||
""" Export the log (that is currently selected) to an ADIF file. """
|
""" Export the log (that is currently selected) to an ADIF file. """
|
||||||
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
|
# Get the index of the selected tab in the logbook.
|
||||||
if(page_index == 0): # If we are on the Summary page...
|
try:
|
||||||
logging.debug("No log currently selected!")
|
log_index = self.get_log_index()
|
||||||
|
if(log_index is None):
|
||||||
|
raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
|
||||||
|
except ValueError as e:
|
||||||
|
error(parent=self.application.window, message=e)
|
||||||
return
|
return
|
||||||
|
|
||||||
log_index = self.get_log_index()
|
|
||||||
log = self.logs[log_index]
|
log = self.logs[log_index]
|
||||||
|
|
||||||
dialog = Gtk.FileChooserDialog("Export Log as ADIF",
|
dialog = Gtk.FileChooserDialog("Export Log as ADIF",
|
||||||
|
@ -703,12 +715,14 @@ class Logbook:
|
||||||
|
|
||||||
def export_log_cabrillo(self, widget=None):
|
def export_log_cabrillo(self, widget=None):
|
||||||
""" Export the log (that is currently selected) to a Cabrillo file. """
|
""" Export the log (that is currently selected) to a Cabrillo file. """
|
||||||
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
|
# Get the index of the selected tab in the logbook.
|
||||||
if(page_index == 0): # If we are on the Summary page...
|
try:
|
||||||
logging.debug("No log currently selected!")
|
log_index = self.get_log_index()
|
||||||
|
if(log_index is None):
|
||||||
|
raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
|
||||||
|
except ValueError as e:
|
||||||
|
error(parent=self.application.window, message=e)
|
||||||
return
|
return
|
||||||
|
|
||||||
log_index = self.get_log_index()
|
|
||||||
log = self.logs[log_index]
|
log = self.logs[log_index]
|
||||||
|
|
||||||
dialog = Gtk.FileChooserDialog("Export Log as Cabrillo",
|
dialog = Gtk.FileChooserDialog("Export Log as Cabrillo",
|
||||||
|
@ -775,11 +789,14 @@ class Logbook:
|
||||||
""" Print all the records in the log (that is currently selected).
|
""" Print all the records in the log (that is currently selected).
|
||||||
Note that only a few important fields are printed because of the restricted width of the page. """
|
Note that only a few important fields are printed because of the restricted width of the page. """
|
||||||
|
|
||||||
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
|
# Get the index of the selected tab in the logbook.
|
||||||
if(page_index == 0): # If we are on the Summary page...
|
try:
|
||||||
logging.debug("No log currently selected!")
|
log_index = self.get_log_index()
|
||||||
|
if(log_index is None):
|
||||||
|
raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
|
||||||
|
except ValueError as e:
|
||||||
|
error(parent=self.application.window, message=e)
|
||||||
return
|
return
|
||||||
log_index = self.get_log_index()
|
|
||||||
log = self.logs[log_index]
|
log = self.logs[log_index]
|
||||||
|
|
||||||
# Retrieve the records.
|
# Retrieve the records.
|
||||||
|
@ -798,7 +815,7 @@ class Logbook:
|
||||||
|
|
||||||
def add_record_callback(self, widget):
|
def add_record_callback(self, widget):
|
||||||
""" A callback function used to add a particular record/QSO. """
|
""" A callback function used to add a particular record/QSO. """
|
||||||
# Get the log index.
|
# Get the index of the selected tab in the logbook.
|
||||||
try:
|
try:
|
||||||
log_index = self.get_log_index()
|
log_index = self.get_log_index()
|
||||||
if(log_index is None):
|
if(log_index is None):
|
||||||
|
@ -1034,6 +1051,23 @@ class Logbook:
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def pinpoint_callback(self, widget=None, path=None):
|
||||||
|
""" A callback function used to pinpoint the callsign on the grey line map. """
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_index = self.get_log_index()
|
||||||
|
row_index = self.get_record_index()
|
||||||
|
if(log_index is None or row_index is None):
|
||||||
|
raise ValueError("Could not determine the log and/or record index.")
|
||||||
|
r = self.logs[log_index].get_record_by_index(row_index)
|
||||||
|
except ValueError as e:
|
||||||
|
logging.error(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.application.toolbox.grey_line.pinpoint(r)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_count(self):
|
def log_count(self):
|
||||||
""" Return the total number of logs in the logbook.
|
""" Return the total number of logs in the logbook.
|
||||||
|
@ -1074,7 +1108,7 @@ class Logbook:
|
||||||
""" Given the name of a log, return its index in the list of Log objects.
|
""" Given the name of a log, return its index in the list of Log objects.
|
||||||
|
|
||||||
:arg str name: The name of the log. If None, use the name of the currently-selected log.
|
:arg str name: The name of the log. If None, use the name of the currently-selected log.
|
||||||
:returns: The index of the named log in the list of Log objects. Returns None is the log cannot be found.
|
:returns: The index of the named log in the list of Log objects. Returns None if the log cannot be found.
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
if(name is None):
|
if(name is None):
|
||||||
|
@ -1095,6 +1129,37 @@ class Logbook:
|
||||||
break
|
break
|
||||||
return log_index
|
return log_index
|
||||||
|
|
||||||
|
def get_record_index(self):
|
||||||
|
""" Return the index of the currently selected record.
|
||||||
|
|
||||||
|
:returns: The index of the currently selected record in the currently selected log. Returns None if the record or log cannot be found.
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the index of the selected log.
|
||||||
|
try:
|
||||||
|
log_index = self.get_log_index()
|
||||||
|
if(log_index is None):
|
||||||
|
raise ValueError("The log index could not be determined. Perhaps the Summary page is selected?")
|
||||||
|
except ValueError as e:
|
||||||
|
logging.error(e)
|
||||||
|
return None
|
||||||
|
log = self.logs[log_index]
|
||||||
|
|
||||||
|
# Get the selected row in the log.
|
||||||
|
(sort_model, path) = self.treeselection[log_index].get_selected_rows()
|
||||||
|
try:
|
||||||
|
sort_iter = sort_model.get_iter(path[0])
|
||||||
|
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
|
||||||
|
# ...and the ListStore model (i.e. the log) is a child of the filter model.
|
||||||
|
child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
|
||||||
|
row_index = log.get_value(child_iter, 0)
|
||||||
|
except IndexError:
|
||||||
|
logging.error("Could not find the selected row's index!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return row_index
|
||||||
|
|
||||||
def get_logs(self):
|
def get_logs(self):
|
||||||
""" Retrieve all the logs in the logbook file, and create Log objects that represent them.
|
""" Retrieve all the logs in the logbook file, and create Log objects that represent them.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (C) 2018 Christian Thomas Jacobs.
|
||||||
|
|
||||||
|
# This file is part of PyQSO.
|
||||||
|
|
||||||
|
# PyQSO is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PyQSO is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
class Popup:
|
||||||
|
|
||||||
|
""" The popup menu that appears when a QSO record is right-clicked. """
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
""" Set up popup menu items. """
|
||||||
|
|
||||||
|
self.application = application
|
||||||
|
self.builder = self.application.builder
|
||||||
|
|
||||||
|
self.menu = self.builder.get_object("popup")
|
||||||
|
|
||||||
|
# Collect Gtk menu items and connect signals.
|
||||||
|
self.items = {}
|
||||||
|
|
||||||
|
# Plot selected QSO on the grey line map.
|
||||||
|
self.items["PINPOINT"] = self.builder.get_object("mitem_pinpoint")
|
||||||
|
self.items["PINPOINT"].connect("activate", self.application.logbook.pinpoint_callback)
|
||||||
|
|
||||||
|
return
|
|
@ -287,7 +287,7 @@ class GeneralPage:
|
||||||
error(parent=self.parent, message="Unable to lookup QTH coordinates. Is the QTH name correct?")
|
error(parent=self.parent, message="Unable to lookup QTH coordinates. Is the QTH name correct?")
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets?")
|
error(parent=self.parent, message="Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached?")
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -4547,6 +4547,18 @@ Base64-encoded plain text in the configuration file.</property>
|
||||||
<action-widget response="-5">telnet_connection_ok_button</action-widget>
|
<action-widget response="-5">telnet_connection_ok_button</action-widget>
|
||||||
</action-widgets>
|
</action-widgets>
|
||||||
</object>
|
</object>
|
||||||
|
<object class="GtkMenu" id="popup">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkMenuItem" id="mitem_pinpoint">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="label" translatable="yes">Pinpoint</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
<object class="GtkBox" id="summary_page">
|
<object class="GtkBox" id="summary_page">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
|
Ładowanie…
Reference in New Issue