Porównaj commity
44 Commity
Autor | SHA1 | Data |
---|---|---|
Christian T. Jacobs | df629e7a64 | |
WB5VQX | 5efe5849ff | |
Christian Jacobs | 71624162bb | |
Christian Jacobs | f9837e14ba | |
Christian Jacobs | 99b5746f52 | |
Christian Jacobs | c6845c082e | |
Christian Jacobs | 095743e3ba | |
Christian Jacobs | ff8c6956fc | |
Christian Jacobs | 42d4966d9f | |
Christian Jacobs | 9cae123db1 | |
Christian Jacobs | 591ed62869 | |
Christian Jacobs | 108f0d00b5 | |
Christian Jacobs | 833da88ae2 | |
Christian Jacobs | d41d20779b | |
Christian Jacobs | f597122721 | |
Christian Jacobs | 3f29d763e4 | |
Christian Jacobs | 5988771a02 | |
Christian Jacobs | 9609b9c3d4 | |
Christian Jacobs | fd41340109 | |
Christian Jacobs | c0212f72d7 | |
Christian Jacobs | 05dee365b3 | |
Christian Jacobs | f1236f70e6 | |
Christian Jacobs | ed95a86710 | |
Christian Jacobs | 7dd02c35c0 | |
Christian Jacobs | d23dd54473 | |
Christian Jacobs | 1fcdb4f6ee | |
Christian Jacobs | 785d3b320f | |
Christian Jacobs | 6834673e28 | |
Christian Jacobs | b47ffa1366 | |
Christian Jacobs | d945d40064 | |
Christian T. Jacobs | ae8f45450a | |
Tomasz Nycz | b20e238ad9 | |
Christian Jacobs | 154673c189 | |
Christian T. Jacobs | 5198547c6c | |
Christian T. Jacobs | f222d5cc30 | |
Christian Jacobs | f283df065b | |
Christian Jacobs | 26e871bcab | |
Christian Jacobs | ee6018b04a | |
Christian Jacobs | 89f1ca46b4 | |
Christian Jacobs | 1ef87caf5e | |
Christian Jacobs | 946f03d95f | |
Christian T. Jacobs | 7a162b2a2d | |
Christian T. Jacobs | d06e326cbd | |
Christian T. Jacobs | 4550f841f7 |
|
@ -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"
|
||||
|
||||
|
|
18
CHANGELOG.md
|
@ -1,5 +1,22 @@
|
|||
# Change Log
|
||||
|
||||
## [1.1.0] - 2018-04-02
|
||||
### 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 GRIDSQUARE field (or COUNTRY field if the GRIDSQUARE is not specified). A new right-click popup menu has been created for this purpose.
|
||||
- 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.
|
||||
- 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 +112,7 @@
|
|||
- QSO filtering and sorting.
|
||||
- Duplicate record removal.
|
||||
|
||||
[1.1.0]: https://github.com/ctjacobs/pyqso/compare/v1.0.0...v1.1.0
|
||||
[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
|
||||
|
|
86
README.md
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2013-2017 Christian Thomas Jacobs.
|
||||
Copyright (C) 2013-2018 Christian Thomas Jacobs.
|
||||
|
||||
This file is part of PyQSO.
|
||||
|
||||
|
@ -15,61 +15,36 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
PyQSO
|
||||
=====
|
||||
# PyQSO
|
||||
|
||||
PyQSO is a contact logging tool for amateur radio operators.
|
||||
|
||||
[![Build Status](https://travis-ci.org/ctjacobs/pyqso.svg)](https://travis-ci.org/ctjacobs/pyqso)
|
||||
[![Documentation Status](https://readthedocs.org/projects/pyqso/badge/?version=latest)](https://readthedocs.org/projects/pyqso/?badge=latest)
|
||||
|
||||
Installation and running
|
||||
------------------------
|
||||
## Dependencies
|
||||
|
||||
Assuming that the current working directory is PyQSO's base directory (the directory that the `Makefile` is in), PyQSO can be run without installation by issuing the following command in the terminal:
|
||||
As the name suggests, PyQSO is written primarily in the [Python](https://www.python.org/) programming language (version 3.x). The graphical user interface has been developed using the [GTK+ library](https://www.gtk.org/) through the [PyGObject bindings](https://pygobject.readthedocs.io). Therefore, in order to run PyQSO, the Python interpreter must be present on your system along with support for GTK+. On many Linux-based systems this can be accomplished by installing the following Debian packages:
|
||||
|
||||
python3 bin/pyqso
|
||||
|
||||
If the `pip3` package manager is available on your system then PyQSO can be installed system-wide using:
|
||||
|
||||
sudo make install
|
||||
|
||||
Once installed, the following command will run PyQSO:
|
||||
|
||||
pyqso
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The PyQSO documentation is stored in the `docs` directory. It can be built with the following command:
|
||||
|
||||
make docs
|
||||
|
||||
which will produce an HTML version of the documentation in `docs/build/html` that can be opened in a web browser.
|
||||
|
||||
Alternatively, a ready-built version of the PyQSO documentation can be found on [Read the Docs](http://pyqso.readthedocs.io/).
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
PyQSO depends on the following Debian packages:
|
||||
|
||||
* gir1.2-gtk-3.0
|
||||
* python3
|
||||
* python3-gi-cairo (for log printing purposes)
|
||||
* gir1.2-gtk-3.0
|
||||
* python3-gi-cairo
|
||||
|
||||
The following extra packages are necessary to fully enable the grey line tool and the plotting of logbook statistics:
|
||||
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
|
||||
* [geocoder](https://pypi.python.org/pypi/geocoder) (installable with `pip3` and used for QTH lookups)
|
||||
* [cartopy](http://scitools.org.uk/cartopy/), for drawing the world map. This package in turn depends on python3-scipy, python3-cairocffi, cython, libproj-dev (version 4.9.0 or later), and libgeos-dev (version 3.3.3 or later).
|
||||
* [geocoder](https://pypi.python.org/pypi/geocoder), for QTH lookups.
|
||||
* python3-sphinx, for building the documentation.
|
||||
* python3-hamlib, for Hamlib support.
|
||||
|
||||
The following extra package is necessary to build the documentation:
|
||||
|
||||
* python3-sphinx
|
||||
### Hamlib support
|
||||
|
||||
There currently does not exist a Python 3-compatible Debian package for [Hamlib](http://www.hamlib.org). This library must be built manually to enable Hamlib support. As per the instructions on the [Hamlib mailing list](https://sourceforge.net/p/hamlib/mailman/message/35692744/), run the following commands in the Hamlib root directory (you may need to run `sudo apt-get install build-essential autoconf automake libtool` beforehand):
|
||||
|
||||
|
@ -83,7 +58,32 @@ You will also need to append the Hamlib `bindings` and `bindings/.libs` director
|
|||
|
||||
export PYTHONPATH=$PYTHONPATH:/path/to/hamlib/bindings:/path/to/hamlib/bindings/.libs
|
||||
|
||||
Contact
|
||||
-------
|
||||
## Installing and running
|
||||
|
||||
Assuming that the current working directory is PyQSO's base directory (the directory that the `Makefile` is in), PyQSO can be run without installation by issuing the following command in the terminal:
|
||||
|
||||
python3 bin/pyqso
|
||||
|
||||
If the Python package manager `pip3` is available on your system then PyQSO can be installed system-wide using:
|
||||
|
||||
sudo make install
|
||||
|
||||
Once installed, the following command will run PyQSO:
|
||||
|
||||
pyqso
|
||||
|
||||
## Documentation
|
||||
|
||||
Guidance on how to use PyQSO is available on [Read the Docs](http://pyqso.readthedocs.io/) and in the screencast below.
|
||||
|
||||
[![PyQSO: A Logging Tool for Amateur Radio Operators](https://img.youtube.com/vi/sVdZl9KnDsk/0.jpg)](https://www.youtube.com/watch?v=sVdZl9KnDsk)
|
||||
|
||||
The documentation can also be built locally with the following command:
|
||||
|
||||
make docs
|
||||
|
||||
which will produce an HTML version of the documentation in `docs/build/html` that can be opened in a web browser.
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any comments or questions about PyQSO please send them via email to Christian Jacobs, M0UOS, at <christian@christianjacobs.uk>.
|
||||
|
|
14
bin/pyqso
|
@ -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.
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
from gi import require_version
|
||||
require_version('Gtk', '3.0')
|
||||
require_version('PangoCairo', '1.0')
|
||||
from gi.repository import Gtk, GdkPixbuf
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf
|
||||
import argparse
|
||||
try:
|
||||
import configparser
|
||||
|
@ -34,7 +34,7 @@ import pkg_resources
|
|||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.info("PyQSO version 1.0.0")
|
||||
logging.info("PyQSO version 1.1.0")
|
||||
|
||||
# This will help Python find the PyQSO modules that need to be imported below.
|
||||
pyqso_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), os.pardir)
|
||||
|
@ -44,6 +44,7 @@ sys.path.insert(0, pyqso_path)
|
|||
from pyqso.adif import *
|
||||
from pyqso.logbook import *
|
||||
from pyqso.menu import *
|
||||
from pyqso.popup import *
|
||||
from pyqso.toolbar import *
|
||||
from pyqso.toolbox import *
|
||||
from pyqso.preferences_dialog import *
|
||||
|
@ -93,10 +94,15 @@ class PyQSO:
|
|||
self.logbook = Logbook(self)
|
||||
self.toolbox = Toolbox(self)
|
||||
|
||||
# Set up menu and tool bars. These classes depend on the Logbook and Toolbox class.
|
||||
# Set up the menu and toolbar. These classes depend on the Logbook and Toolbox class.
|
||||
self.menu = Menu(self)
|
||||
self.popup = Popup(self)
|
||||
self.toolbar = Toolbar(self)
|
||||
|
||||
# Clipboard for copy/paste operations.
|
||||
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
|
||||
# Show the main window.
|
||||
self.window.show_all()
|
||||
|
||||
if(have_config):
|
||||
|
|
|
@ -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'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0.0'
|
||||
release = '1.1.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
Getting started
|
||||
===============
|
||||
|
||||
Demonstration
|
||||
-------------
|
||||
|
||||
The screencast below demonstrates how to install, configure and use PyQSO (focussing on version 1.0.0 only). Detailed instructions are also available in the sections that follow.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; height: auto;">
|
||||
<iframe src="https://www.youtube.com/embed/sVdZl9KnDsk" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
System requirements
|
||||
-------------------
|
||||
|
||||
|
@ -8,11 +20,11 @@ It is recommended that users run PyQSO on the Linux operating system,
|
|||
since all development and testing of PyQSO takes place there.
|
||||
|
||||
As the name suggests, PyQSO is written primarily in the `Python <https://www.python.org/>`_
|
||||
programming language (version 3.x). The graphical user interface has been built using
|
||||
programming language (version 3.x). The graphical user interface has been developed using
|
||||
the `GTK+ library <https://www.gtk.org/>`_ through the `PyGObject bindings <https://pygobject.readthedocs.io>`_. PyQSO also uses an
|
||||
`SQLite <https://www.sqlite.org/>`_ embedded database to manage all the contacts an amateur radio
|
||||
operator makes. Users must therefore make sure that the Python
|
||||
interpreter and any additional software dependencies are satisfied
|
||||
interpreter is installed and that any additional software dependencies are satisfied
|
||||
before PyQSO can be run successfully. The list of software packages that
|
||||
PyQSO depends on is provided in the ``README.md`` file.
|
||||
|
||||
|
@ -82,7 +94,7 @@ Once the logbook has been opened, its name will appear in the status bar. All lo
|
|||
.. figure:: images/logbook.png
|
||||
:align: center
|
||||
|
||||
The PyQSO main window, showing the records in a log called ``SO50`` (for contacts via the `amateur radio satellite <https://www.amsat.org/>`_ SO-50), and the DX cluster tool in the toolbox below it.
|
||||
The PyQSO main window, showing the records in a log called ``SO50`` (for contacts via the `amateur radio satellite <https://www.amsat.org/>`_ SO-50), and the World Map tool in the toolbox below it.
|
||||
|
||||
Closing a logbook
|
||||
-----------------
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 61 KiB Po Szerokość: | Wysokość: | Rozmiar: 58 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 45 KiB Po Szerokość: | Wysokość: | Rozmiar: 54 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 62 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 97 KiB Po Szerokość: | Wysokość: | Rozmiar: 202 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 50 KiB Po Szerokość: | Wysokość: | Rozmiar: 51 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 154 KiB |
|
@ -24,7 +24,7 @@ include:
|
|||
|
||||
- Progress tracker for the `DXCC <http://www.arrl.org/dxcc/>`_ award.
|
||||
|
||||
- Grey line plotter.
|
||||
- World map with grey line and Maidenhead grid squares.
|
||||
|
||||
- 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.
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ name must not be a number.
|
|||
**Note:** When logs are stored in the database file, field/column names from
|
||||
the ADIF standard are used. However, please note that only the following
|
||||
subset of all the ADIF fields is considered: CALL, QSO\_DATE, TIME\_ON,
|
||||
FREQ, BAND, MODE, SUBMODE, TX\_PWR, RST\_SENT, RST\_RCVD, QSL\_SENT, QSL\_RCVD,
|
||||
NOTES, NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, IOTA. Visit the `ADIF website <http://adif.org/>`_ for more information about these fields.
|
||||
FREQ, BAND, MODE, SUBMODE, PROP\_MODE, TX\_PWR, RST\_SENT, RST\_RCVD, QSL\_SENT, QSL\_RCVD,
|
||||
NOTES, NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, IOTA, GRIDSQUARE, SAT\_NAME, SAT\_MODE. Visit the `ADIF website <http://adif.org/>`_ for more information about these fields.
|
||||
|
||||
Renaming a log
|
||||
--------------
|
||||
|
|
|
@ -17,8 +17,6 @@ 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).
|
||||
|
||||
.. _figure:summary:
|
||||
.. figure:: images/summary.png
|
||||
:align: center
|
||||
|
@ -60,9 +58,9 @@ PyQSO currently supports the ``NOTES`` field in the ADIF specification, but not
|
|||
Hamlib support
|
||||
--------------
|
||||
|
||||
PyQSO features rudimentary support for the `Hamlib <http://hamlib.sourceforge.net/>`_ library. The name and
|
||||
path of the radio device connected to the user's computer can be
|
||||
specified in the ``Hamlib`` tab of the preferences dialog. Upon adding a
|
||||
new record to the log, PyQSO will use Hamlib to retrieve the current
|
||||
frequency and mode that the radio device is set to and automatically fill in the
|
||||
Frequency and Mode fields.
|
||||
PyQSO features rudimentary support for the `Hamlib <http://hamlib.sourceforge.net/>`_ library. The name and path of the radio device connected to the user's computer can be specified in the ``Hamlib`` tab of the preferences dialog. Upon adding a new record to the log, PyQSO will use Hamlib to retrieve the current frequency and mode that the radio device is set to and automatically fill in the Frequency and Mode fields.
|
||||
|
||||
World Map
|
||||
---------
|
||||
|
||||
The user can pinpoint their 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) in the ``World Map`` tab. Maidenhead grid squares can also be rendered, with worked grid squares shaded, which is particularly useful for satellite operating.
|
||||
|
|
|
@ -49,6 +49,17 @@ An existing record can be edited by:
|
|||
|
||||
This will bring up the same dialog window as before.
|
||||
|
||||
Copying/pasting a record
|
||||
------------------------
|
||||
|
||||
An existing record can be copied and pasted by:
|
||||
|
||||
- Selecting it and right-clicking to bring up the popup menu.
|
||||
|
||||
- Selecting ``Copy``.
|
||||
|
||||
- Right-clicking again and selecting ``Paste``. This will duplicate the record, with the duplicate becoming the latest record in the selected log.
|
||||
|
||||
Deleting a record
|
||||
-----------------
|
||||
|
||||
|
|
|
@ -31,20 +31,20 @@ 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.
|
||||
The user's QTH can be pinpointed 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 need to be entered manually.
|
||||
|
||||
.. _figure:grey_line:
|
||||
.. figure:: images/grey_line.png
|
||||
The location of a worked station may also be plotted by right-clicking on the relevant QSO in the main window and selecting ``Pinpoint`` from the popup menu.
|
||||
|
||||
.. _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) pinpointed in red, and several other worked stations pinpointed in yellow. Worked grid squares are shaded purple.
|
||||
|
||||
Awards
|
||||
------
|
||||
|
|
|
@ -35,6 +35,7 @@ AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S",
|
|||
"BAND": "E",
|
||||
"MODE": "E",
|
||||
"SUBMODE": "E",
|
||||
"PROP_MODE": "E",
|
||||
"TX_PWR": "N",
|
||||
"RST_SENT": "S",
|
||||
"RST_RCVD": "S",
|
||||
|
@ -48,12 +49,15 @@ AVAILABLE_FIELD_NAMES_TYPES = {"CALL": "S",
|
|||
"DXCC": "N",
|
||||
"CQZ": "N",
|
||||
"ITUZ": "N",
|
||||
"IOTA": "C"}
|
||||
"IOTA": "C",
|
||||
"GRIDSQUARE": "S",
|
||||
"SAT_NAME": "S",
|
||||
"SAT_MODE": "S"}
|
||||
# Note: The logbook uses the ADIF field names for the database column names.
|
||||
# This list is used to display the columns in a logical order.
|
||||
AVAILABLE_FIELD_NAMES_ORDERED = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "SUBMODE", "TX_PWR",
|
||||
AVAILABLE_FIELD_NAMES_ORDERED = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "SUBMODE", "PROP_MODE", "TX_PWR",
|
||||
"RST_SENT", "RST_RCVD", "QSL_SENT", "QSL_RCVD", "NOTES", "NAME",
|
||||
"ADDRESS", "STATE", "COUNTRY", "DXCC", "CQZ", "ITUZ", "IOTA"]
|
||||
"ADDRESS", "STATE", "COUNTRY", "DXCC", "CQZ", "ITUZ", "IOTA", "GRIDSQUARE", "SAT_NAME", "SAT_MODE"]
|
||||
# Define the more user-friendly versions of the field names.
|
||||
AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL": "Callsign",
|
||||
"QSO_DATE": "Date",
|
||||
|
@ -62,6 +66,7 @@ AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL": "Callsign",
|
|||
"BAND": "Band",
|
||||
"MODE": "Mode",
|
||||
"SUBMODE": "Submode",
|
||||
"PROP_MODE": "Propagation Mode",
|
||||
"TX_PWR": "TX Power (W)",
|
||||
"RST_SENT": "RST Sent",
|
||||
"RST_RCVD": "RST Received",
|
||||
|
@ -75,7 +80,10 @@ AVAILABLE_FIELD_NAMES_FRIENDLY = {"CALL": "Callsign",
|
|||
"DXCC": "DXCC",
|
||||
"CQZ": "CQ Zone",
|
||||
"ITUZ": "ITU Zone",
|
||||
"IOTA": "IOTA Designator"}
|
||||
"IOTA": "IOTA Designator",
|
||||
"GRIDSQUARE": "Grid Square",
|
||||
"SAT_NAME": "Satellite Name",
|
||||
"SAT_MODE": "Satellite Mode"}
|
||||
|
||||
# A: AwardList
|
||||
# B: Boolean
|
||||
|
@ -104,6 +112,7 @@ MODES = {"": ("",),
|
|||
"FAX": ("",),
|
||||
"FM": ("",),
|
||||
"FSK441": ("",),
|
||||
"FT8": ("",),
|
||||
"HELL": ("", "FMHELL", "FSKHELL", "HELL80", "HFSK", "PSKHELL"),
|
||||
"ISCAT": ("", "ISCAT-A", "ISCAT-B"),
|
||||
"JT4": ("", "JT4A", "JT4B", "JT4C", "JT4D", "JT4E", "JT4F", "JT4G"),
|
||||
|
@ -186,6 +195,8 @@ BANDS = ["", "2190m", "630m", "560m", "160m", "80m", "60m", "40m", "30m", "20m",
|
|||
# The lower and upper frequency bounds (in MHz) for each band in BANDS.
|
||||
BANDS_RANGES = [(None, None), (0.136, 0.137), (0.472, 0.479), (0.501, 0.504), (1.8, 2.0), (3.5, 4.0), (5.102, 5.4065), (7.0, 7.3), (10.0, 10.15), (14.0, 14.35), (18.068, 18.168), (21.0, 21.45), (24.890, 24.99), (28.0, 29.7), (50.0, 54.0), (70.0, 71.0), (144.0, 148.0), (222.0, 225.0), (420.0, 450.0), (902.0, 928.0), (1240.0, 1300.0), (2300.0, 2450.0), (3300.0, 3500.0), (5650.0, 5925.0), (10000.0, 10500.0), (24000.0, 24250.0), (47000.0, 47200.0), (75500.0, 81000.0), (119980.0, 120020.0), (142000.0, 149000.0), (241000.0, 250000.0)]
|
||||
|
||||
PROPAGATION_MODES = ["", "AS", "AUE", "AUR", "BS", "ECH", "EME", "ES", "F2", "FAI", "INTERNET", "ION", "IRL", "MS", "RPT", "RS", "SAT", "TEP", "TR"]
|
||||
|
||||
ADIF_VERSION = "3.0.4"
|
||||
|
||||
|
||||
|
@ -337,7 +348,7 @@ class ADIF:
|
|||
|
||||
<adif_ver:%d>%s
|
||||
<programid:5>PyQSO
|
||||
<programversion:5>1.0.0
|
||||
<programversion:5>1.1.0
|
||||
<eoh>\n""" % (dt, len(records), len(str(ADIF_VERSION)), ADIF_VERSION))
|
||||
|
||||
# Then write each record to the file.
|
||||
|
|
|
@ -49,7 +49,7 @@ class Cabrillo:
|
|||
|
||||
# Header
|
||||
f.write("""START-OF-LOG: %s\n""" % (CABRILLO_VERSION))
|
||||
f.write("""CREATED-BY: PyQSO v1.0.0\n""")
|
||||
f.write("""CREATED-BY: PyQSO v1.1.0\n""")
|
||||
f.write("""CALLSIGN: %s\n""" % (mycall))
|
||||
f.write("""CONTEST: %s\n""" % (contest))
|
||||
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2013-2017 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/>.
|
||||
|
||||
from gi.repository import GObject
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from os.path import expanduser
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
try:
|
||||
import numpy
|
||||
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__))
|
||||
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.")
|
||||
have_necessary_modules = False
|
||||
|
||||
|
||||
class GreyLine:
|
||||
|
||||
""" A tool for visualising the grey line. """
|
||||
|
||||
def __init__(self, application):
|
||||
""" Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes.
|
||||
|
||||
:arg application: The PyQSO application containing the main Gtk window, etc.
|
||||
"""
|
||||
logging.debug("Setting up the grey line...")
|
||||
|
||||
self.application = application
|
||||
self.builder = self.application.builder
|
||||
|
||||
# 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):
|
||||
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("greyline").show_all()
|
||||
|
||||
logging.debug("Grey line ready!")
|
||||
|
||||
return
|
||||
|
||||
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).
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
if(have_necessary_modules):
|
||||
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.
|
||||
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
|
||||
self.fig.clf()
|
||||
sub = self.fig.add_subplot(111)
|
||||
|
||||
# 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.")
|
||||
|
||||
# Pin-point QTH on the map.
|
||||
if(self.show_qth):
|
||||
qth_x, qth_y = m(self.qth_longitude, self.qth_latitude)
|
||||
m.plot(qth_x, qth_y, "ro")
|
||||
sub.text(qth_x+0.015*qth_x, qth_y+0.015*qth_y, self.qth_name, color="white", size="medium", weight="bold")
|
||||
|
||||
return True
|
||||
else:
|
||||
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
|
144
pyqso/logbook.py
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2012-2017 Christian Thomas Jacobs.
|
||||
# Copyright (C) 2012-2018 Christian Thomas Jacobs.
|
||||
|
||||
# This file is part of PyQSO.
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
import sqlite3 as sqlite
|
||||
import json
|
||||
from os.path import expanduser
|
||||
try:
|
||||
import configparser
|
||||
|
@ -247,6 +248,14 @@ class Logbook:
|
|||
self.application.menu.set_record_items_sensitive(True)
|
||||
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):
|
||||
""" Create a new log in the logbook. """
|
||||
|
||||
|
@ -336,6 +345,7 @@ class Logbook:
|
|||
self.sorter.pop(log_index)
|
||||
self.filter.pop(log_index)
|
||||
# And finally remove the tab in the Logbook.
|
||||
self.notebook.set_current_page(page_index - 1)
|
||||
self.notebook.remove_page(page_index)
|
||||
|
||||
self.summary.update()
|
||||
|
@ -382,8 +392,10 @@ class Logbook:
|
|||
self.treeview.append(Gtk.TreeView(model=self.sorter[index]))
|
||||
self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
||||
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[index].set_mode(Gtk.SelectionMode.SINGLE)
|
||||
|
||||
# Allow the Log to be scrolled up/down.
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
|
@ -643,12 +655,14 @@ class Logbook:
|
|||
|
||||
def export_log_adif(self, widget=None):
|
||||
""" 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.
|
||||
if(page_index == 0): # If we are on the Summary page...
|
||||
logging.debug("No log currently selected!")
|
||||
# Get the index of the selected tab in the logbook.
|
||||
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:
|
||||
error(parent=self.application.window, message=e)
|
||||
return
|
||||
|
||||
log_index = self.get_log_index()
|
||||
log = self.logs[log_index]
|
||||
|
||||
dialog = Gtk.FileChooserDialog("Export Log as ADIF",
|
||||
|
@ -703,12 +717,14 @@ class Logbook:
|
|||
|
||||
def export_log_cabrillo(self, widget=None):
|
||||
""" 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.
|
||||
if(page_index == 0): # If we are on the Summary page...
|
||||
logging.debug("No log currently selected!")
|
||||
# Get the index of the selected tab in the logbook.
|
||||
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:
|
||||
error(parent=self.application.window, message=e)
|
||||
return
|
||||
|
||||
log_index = self.get_log_index()
|
||||
log = self.logs[log_index]
|
||||
|
||||
dialog = Gtk.FileChooserDialog("Export Log as Cabrillo",
|
||||
|
@ -775,11 +791,14 @@ class Logbook:
|
|||
""" 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. """
|
||||
|
||||
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
|
||||
if(page_index == 0): # If we are on the Summary page...
|
||||
logging.debug("No log currently selected!")
|
||||
# Get the index of the selected tab in the logbook.
|
||||
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:
|
||||
error(parent=self.application.window, message=e)
|
||||
return
|
||||
log_index = self.get_log_index()
|
||||
log = self.logs[log_index]
|
||||
|
||||
# Retrieve the records.
|
||||
|
@ -798,7 +817,7 @@ class Logbook:
|
|||
|
||||
def add_record_callback(self, widget):
|
||||
""" 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:
|
||||
log_index = self.get_log_index()
|
||||
if(log_index is None):
|
||||
|
@ -1034,6 +1053,66 @@ class Logbook:
|
|||
|
||||
return
|
||||
|
||||
def pinpoint_callback(self, widget=None, path=None):
|
||||
""" A callback function used to pinpoint the callsign on the world 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.world_map.pinpoint(r)
|
||||
|
||||
return
|
||||
|
||||
def copy_callback(self, widget=None, path=None):
|
||||
""" A callback function used to copy selected logs. """
|
||||
|
||||
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
|
||||
|
||||
d = {}
|
||||
for key in r.keys():
|
||||
d[key.upper()] = r[key]
|
||||
j = json.dumps(d)
|
||||
|
||||
self.application.clipboard.set_text(j, len(j))
|
||||
|
||||
return
|
||||
|
||||
def clipboard_text_received(self, clipboard, text, log):
|
||||
r = json.loads(text)
|
||||
log.add_record(r)
|
||||
return
|
||||
|
||||
def paste_callback(self, widget=None, path=None):
|
||||
""" A callback function used to paste selected logs. """
|
||||
|
||||
try:
|
||||
log_index = self.get_log_index()
|
||||
if(log_index is None):
|
||||
raise ValueError("Could not determine the log index.")
|
||||
l = self.logs[log_index]
|
||||
except ValueError as e:
|
||||
logging.error(e)
|
||||
return
|
||||
|
||||
self.application.clipboard.request_text(self.clipboard_text_received, l)
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def log_count(self):
|
||||
""" Return the total number of logs in the logbook.
|
||||
|
@ -1074,7 +1153,7 @@ class Logbook:
|
|||
""" 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.
|
||||
: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
|
||||
"""
|
||||
if(name is None):
|
||||
|
@ -1095,6 +1174,37 @@ class Logbook:
|
|||
break
|
||||
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):
|
||||
""" Retrieve all the logs in the logbook file, and create Log objects that represent them.
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
#!/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 world map.
|
||||
self.items["PINPOINT"] = self.builder.get_object("mitem_pinpoint")
|
||||
self.items["PINPOINT"].connect("activate", self.application.logbook.pinpoint_callback)
|
||||
|
||||
self.items["COPY"] = self.builder.get_object("mitem_copy")
|
||||
self.items["COPY"].connect("activate", self.application.logbook.copy_callback)
|
||||
|
||||
self.items["PASTE"] = self.builder.get_object("mitem_paste")
|
||||
self.items["PASTE"].connect("activate", self.application.logbook.paste_callback)
|
||||
|
||||
return
|
|
@ -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?")
|
||||
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 pinpoint 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
|
||||
|
|
|
@ -115,6 +115,12 @@ class RecordDialog:
|
|||
self.sources["SUBMODE"].append_text("")
|
||||
self.sources["SUBMODE"].set_active(0) # Set an empty string initially. As soon as the user selects a particular MODE, the available SUBMODES will appear.
|
||||
|
||||
# PROP_MODE
|
||||
self.sources["PROP_MODE"] = self.builder.get_object("qso_propagation_mode_combo")
|
||||
for propagation_mode in PROPAGATION_MODES:
|
||||
self.sources["PROP_MODE"].append_text(propagation_mode)
|
||||
self.sources["PROP_MODE"].set_active(0) # Set an empty string as the default option.
|
||||
|
||||
# POWER
|
||||
self.sources["TX_PWR"] = self.builder.get_object("qso_power_entry")
|
||||
|
||||
|
@ -167,6 +173,17 @@ class RecordDialog:
|
|||
# IOTA
|
||||
self.sources["IOTA"] = self.builder.get_object("station_iota_entry")
|
||||
|
||||
# GRIDSQUARE
|
||||
self.sources["GRIDSQUARE"] = self.builder.get_object("station_gridsquare_entry")
|
||||
|
||||
# SATELLITE INFORMATION
|
||||
|
||||
# SAT_NAME
|
||||
self.sources["SAT_NAME"] = self.builder.get_object("satellite_name_entry")
|
||||
|
||||
# SAT_MODE
|
||||
self.sources["SAT_MODE"] = self.builder.get_object("satellite_mode_entry")
|
||||
|
||||
# Populate various fields, if possible.
|
||||
if(index is not None):
|
||||
# The record already exists, so display its current data in the input boxes.
|
||||
|
@ -191,6 +208,8 @@ class RecordDialog:
|
|||
elif(field_names[i] == "SUBMODE"):
|
||||
# Skip, because this has been (or will be) handled when populating the MODE field.
|
||||
continue
|
||||
elif(field_names[i] == "PROP_MODE"):
|
||||
self.sources[field_names[i]].set_active(PROPAGATION_MODES.index(data))
|
||||
elif(field_names[i] == "QSL_SENT"):
|
||||
self.sources[field_names[i]].set_active(qsl_sent_options.index(data))
|
||||
elif(field_names[i] == "QSL_RCVD"):
|
||||
|
@ -269,6 +288,8 @@ class RecordDialog:
|
|||
return self.sources["MODE"].get_active_text()
|
||||
elif(field_name == "SUBMODE"):
|
||||
return self.sources["SUBMODE"].get_active_text()
|
||||
elif(field_name == "PROP_MODE"):
|
||||
return self.sources["PROP_MODE"].get_active_text()
|
||||
elif(field_name == "BAND" or field_name == "QSL_SENT" or field_name == "QSL_RCVD"):
|
||||
return self.sources[field_name].get_active_text()
|
||||
elif(field_name == "NOTES"):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,361 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2013-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/>.
|
||||
|
||||
from gi.repository import GObject
|
||||
import logging
|
||||
import sqlite3 as sqlite
|
||||
import re
|
||||
from os.path import expanduser
|
||||
from datetime import datetime
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
try:
|
||||
import numpy
|
||||
logging.info("Using version %s of numpy." % (numpy.__version__))
|
||||
import matplotlib
|
||||
logging.info("Using version %s of matplotlib." % (matplotlib.__version__))
|
||||
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)
|
||||
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
|
||||
have_geocoder = True
|
||||
except ImportError:
|
||||
logging.warning("Could not import the geocoder module!")
|
||||
have_geocoder = False
|
||||
|
||||
if(have_necessary_modules):
|
||||
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"):
|
||||
""" 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 Maidenhead:
|
||||
|
||||
""" The Maidenhead Locator System. """
|
||||
|
||||
def __init__(self):
|
||||
self.upper = "ABCDEFGHIJKLMNOPQR"
|
||||
self.lower = "abcdefghijklmnopqrstuvwx"
|
||||
return
|
||||
|
||||
def ll2gs(self, latitude, longitude, subsquare=False):
|
||||
""" 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.
|
||||
:arg bool subsquare: Option to include the subsquare (thereby obtaining a 6-character Maidenhead locator).
|
||||
: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)
|
||||
|
||||
if(subsquare):
|
||||
adjusted_latitude_remainder = (adjusted_latitude - int(adjusted_latitude)) * 60
|
||||
adjusted_longitude_remainder = ((adjusted_longitude) - int(adjusted_longitude/2)*2) * 60
|
||||
subsquare_latitude = self.lower[int(adjusted_latitude_remainder/2.5)]
|
||||
subsquare_longitude = self.lower[int(adjusted_longitude_remainder/5)]
|
||||
return ("%s"*6) % (field_longitude, field_latitude, square_longitude, square_latitude, subsquare_longitude, subsquare_latitude)
|
||||
else:
|
||||
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. """
|
||||
|
||||
def __init__(self, application):
|
||||
""" 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 world map...")
|
||||
|
||||
self.application = application
|
||||
self.builder = self.application.builder
|
||||
self.points = []
|
||||
|
||||
if(have_necessary_modules):
|
||||
self.fig = matplotlib.figure.Figure()
|
||||
self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application
|
||||
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) = ("world_map", "show_qth")
|
||||
if(have_config and config.has_option(section, option)):
|
||||
if(config.getboolean(section, option)):
|
||||
try:
|
||||
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?")
|
||||
|
||||
# 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!")
|
||||
|
||||
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 world map.
|
||||
|
||||
:arg r: The QSO record containing the location to pinpoint.
|
||||
"""
|
||||
|
||||
if(have_geocoder):
|
||||
callsign = r["CALL"]
|
||||
gridsquare = r["GRIDSQUARE"]
|
||||
country = r["COUNTRY"]
|
||||
|
||||
# Get the latitude-longitude coordinates. Use any GRIDSQUARE information first since this is likely to be more accurate than the COUNTRY field.
|
||||
if(gridsquare):
|
||||
try:
|
||||
latitude, longitude = self.maidenhead.gs2ll(gridsquare)
|
||||
logging.debug("QTH coordinates found: (%s, %s)", str(latitude), str(longitude))
|
||||
self.add_point(callsign, latitude, longitude)
|
||||
return
|
||||
except ValueError:
|
||||
logging.exception("Unable to lookup QTH coordinates.")
|
||||
|
||||
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)
|
||||
return
|
||||
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 get_worked_grid_squares(self, logbook):
|
||||
""" Get 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.
|
||||
|
||||
: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
|
||||
"""
|
||||
|
||||
if(have_necessary_modules):
|
||||
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 world map is not visible.
|
||||
return True # We need to return True in case this is method was called by a timer event.
|
||||
else:
|
||||
# Set up the world map.
|
||||
logging.debug("Drawing the world map...")
|
||||
self.fig.clf()
|
||||
ax = self.fig.add_subplot(111, projection=cartopy.crs.PlateCarree())
|
||||
ax.set_extent([-180, 180, -90, 90])
|
||||
ax.set_aspect("auto")
|
||||
|
||||
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="olivedrab")
|
||||
ax.add_feature(cartopy.feature.OCEAN, facecolor="cornflowerblue")
|
||||
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:
|
||||
ax.plot(p.longitude, p.latitude, p.style, transform=cartopy.crs.PlateCarree())
|
||||
projected_x, projected_y = ax.projection.transform_point(p.longitude, p.latitude, src_crs=cartopy.crs.PlateCarree())
|
||||
ax.annotate(p.name, xy=(projected_x, projected_y), xytext=(0, 2.5), textcoords="offset points", 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="Reds", vmin=0, vmax=1, edgecolors="k", linewidth=1.5, 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="small", color="w", family="monospace", alpha=0.4)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
|
|
@ -0,0 +1,6 @@
|
|||
numpy
|
||||
matplotlib>=1.3.0
|
||||
cairocffi
|
||||
cartopy>=0.16.0
|
||||
sphinx
|
||||
geocoder
|
4
setup.py
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2013-2017 Christian Thomas Jacobs.
|
||||
# Copyright (C) 2013-2018 Christian Thomas Jacobs.
|
||||
|
||||
# This file is part of PyQSO.
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(name="PyQSO",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
description="A contact logging tool for amateur radio operators.",
|
||||
author="Christian Thomas Jacobs",
|
||||
author_email="christian@christianjacobs.uk",
|
||||
|
|
|
@ -108,7 +108,7 @@ class TestADIF(unittest.TestCase):
|
|||
assert("""
|
||||
<adif_ver:5>3.0.4
|
||||
<programid:5>PyQSO
|
||||
<programversion:5>1.0.0
|
||||
<programversion:5>1.1.0
|
||||
<eoh>
|
||||
<call:7>TEST123
|
||||
<qso_date:8>20120402
|
||||
|
@ -150,7 +150,7 @@ class TestADIF(unittest.TestCase):
|
|||
assert("""
|
||||
<adif_ver:5>3.0.4
|
||||
<programid:5>PyQSO
|
||||
<programversion:5>1.0.0
|
||||
<programversion:5>1.1.0
|
||||
<eoh>
|
||||
<call:7>TEST123
|
||||
<qso_date:8>20120402
|
||||
|
|
|
@ -35,7 +35,7 @@ class TestCabrillo(unittest.TestCase):
|
|||
records = [{'TIME_ON': '1955', 'BAND': '40m', 'CALL': 'TEST', 'FREQ': "145.550", 'MODE': 'FM', 'QSO_DATE': '20130322', 'RST_SENT': '59 001', 'RST_RCVD': '59 002'}, {'TIME_ON': '0820', 'BAND': '20m', 'CALL': 'TEST2ABC', 'FREQ': "144.330", 'MODE': 'SSB', 'QSO_DATE': '20150227', 'RST_SENT': '55 020', 'RST_RCVD': '57 003'}, {'TIME_ON': '0832', 'BAND': '2m', 'CALL': 'HELLO', 'FREQ': "145.550", 'MODE': 'FM', 'QSO_DATE': '20150227', 'RST_SENT': '59 001', 'RST_RCVD': '59 002'}]
|
||||
|
||||
expected = """START-OF-LOG: 3.0
|
||||
CREATED-BY: PyQSO v1.0.0
|
||||
CREATED-BY: PyQSO v1.1.0
|
||||
CALLSIGN: MYCALL
|
||||
CONTEST: MYCONTEST
|
||||
QSO: 145550.0 FM 2013-03-22 1955 MYCALL 59 001 TEST 59 002 0
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
#!/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/>.
|
||||
|
||||
import unittest
|
||||
try:
|
||||
import unittest.mock as mock
|
||||
except ImportError:
|
||||
import mock
|
||||
from pyqso.world_map import Maidenhead, WorldMap
|
||||
|
||||
|
||||
class TestMaidenhead(unittest.TestCase):
|
||||
|
||||
""" The unit tests for the Maidenhead class. """
|
||||
|
||||
def setUp(self):
|
||||
""" Set up the Maidenhead object needed for the unit tests. """
|
||||
self.maidenhead = Maidenhead()
|
||||
|
||||
def test_ll2gs(self):
|
||||
""" Check that a latitude-longitude coordinate can correctly be converted to a Maidenhead grid square. """
|
||||
latitude = 51.0593
|
||||
longitude = -1.4262
|
||||
assert self.maidenhead.ll2gs(latitude, longitude, subsquare=False) == "IO91"
|
||||
assert self.maidenhead.ll2gs(latitude, longitude, subsquare=True) == "IO91gb"
|
||||
|
||||
def test_gs2ll(self):
|
||||
""" Check that a Maidenhead grid square can correctly be converted to a latitude-longitude coordinate. """
|
||||
gs4 = "JN05"
|
||||
assert self.maidenhead.gs2ll(gs4) == (45.5, 1.0)
|
||||
gs6 = "JN05aa"
|
||||
assert self.maidenhead.gs2ll(gs6) == (45.020833333333336, 0.041666666666666664)
|
||||
gs6 = "IO91gb"
|
||||
assert self.maidenhead.gs2ll(gs6) == (51.0625, -1.4583333333333335)
|
||||
|
||||
|
||||
class TestWorldMap(unittest.TestCase):
|
||||
|
||||
""" The unit tests for the WorldMap class. """
|
||||
|
||||
def setUp(self):
|
||||
""" Set up the WorldMap object needed for the unit tests. """
|
||||
PyQSO = mock.MagicMock()
|
||||
self.world_map = WorldMap(application=PyQSO())
|
||||
|
||||
def test_get_worked_grid_squares(self):
|
||||
""" Check that the worked grid squares are determined correctly. """
|
||||
Logbook = mock.MagicMock()
|
||||
Log = mock.MagicMock()
|
||||
logbook = Logbook()
|
||||
l = Log()
|
||||
l.records = [{"CALL": "TEST123", "COUNTRY": "England", "GRIDSQUARE": "IO91gb"}, {"CALL": "TEST456", "COUNTRY": "England", "GRIDSQUARE": "IO90hv"}, {"CALL": "TEST789", "COUNTRY": "England", "GRIDSQUARE": None}]
|
||||
logbook.logs = [l]
|
||||
worked_grid_squares = self.world_map.get_worked_grid_squares(logbook=logbook)
|
||||
assert worked_grid_squares[14, 8] # IO square.
|
||||
|
||||
if(__name__ == '__main__'):
|
||||
unittest.main()
|