Porównaj commity

...

44 Commity

Autor SHA1 Wiadomość Data
Christian T. Jacobs df629e7a64
Merge pull request #70 from WB5VQX/master
Corrected log deletion bug.
2019-02-22 20:59:28 +00:00
WB5VQX 5efe5849ff Corrected log deletion bug. 2019-02-16 20:58:59 -06:00
Christian Jacobs 71624162bb Updated year range. 2018-04-02 18:52:23 +01:00
Christian Jacobs f9837e14ba In turn. 2018-04-02 17:54:28 +01:00
Christian Jacobs 99b5746f52 Bumping version number to 1.1.0. 2018-04-02 17:38:32 +01:00
Christian Jacobs c6845c082e Updated dependency list. 2018-04-02 17:30:49 +01:00
Christian Jacobs 095743e3ba Preparing for v1.1.0 release. 2018-04-02 16:11:52 +01:00
Christian Jacobs ff8c6956fc Added the link to the screencast. 2018-04-02 16:11:40 +01:00
Christian Jacobs 42d4966d9f Updated screenshot. 2018-04-01 20:41:00 +01:00
Christian Jacobs 9cae123db1 Updating screenshots. 2018-04-01 20:31:04 +01:00
Christian Jacobs 591ed62869 More experimenting with colour schemes. 2018-04-01 18:41:19 +01:00
Christian Jacobs 108f0d00b5 Updated screenshots. 2018-03-31 22:44:30 +01:00
Christian Jacobs 833da88ae2 Remove hyphen in pin-point. 2018-03-31 22:36:02 +01:00
Christian Jacobs d41d20779b Added documentation on copy/pasting QSOs and the world map tool. 2018-03-31 22:32:56 +01:00
Christian Jacobs f597122721 Use annotations to label pinpoints. 2018-03-31 22:32:46 +01:00
Christian Jacobs 3f29d763e4 Renamed grey_line.png to world_map.png. 2018-03-31 21:54:13 +01:00
Christian Jacobs 5988771a02 More experimenting with colors. 2018-03-31 21:49:15 +01:00
Christian Jacobs 9609b9c3d4 Experimenting with the colors in the World Map. 2018-03-31 16:01:36 +01:00
Christian Jacobs fd41340109 Updated the screenshot of the Edit Record dialog. 2018-03-31 14:57:19 +01:00
Christian Jacobs c0212f72d7 Updating the World Map documentation. 2018-03-28 22:49:44 +01:00
Christian Jacobs 05dee365b3 Bump version number to v1.1.0. 2018-03-28 22:33:28 +01:00
Christian Jacobs f1236f70e6 Added a note about how the station locations are plotted based on GRIDSQUARE (or COUNTRY) field. 2018-03-28 21:21:24 +01:00
Christian Jacobs ed95a86710 Use the Maidenhead object that is already defined. 2018-03-28 21:17:06 +01:00
Christian Jacobs 7dd02c35c0 Only define the NavigationToolbar derived class if the necessary modules are installed. 2018-03-28 21:12:14 +01:00
Christian Jacobs d23dd54473 Import only those classes that are used in the unit tests. 2018-03-28 20:51:46 +01:00
Christian Jacobs 1fcdb4f6ee Added WorldMap unit tests. 2018-03-28 19:14:20 +01:00
Christian Jacobs 785d3b320f Added Maidenhead unit tests. 2018-03-28 18:32:04 +01:00
Christian Jacobs 6834673e28 Add option to return a 6-character Maidenhead locator. 2018-03-28 18:31:43 +01:00
Christian Jacobs b47ffa1366 Try the country field if the grid square field fails to provide the correct information. 2018-03-27 21:38:48 +01:00
Christian Jacobs d945d40064 Make use of GRIDSQUARE value when plotting a station's location on the world map. 2018-03-27 19:49:18 +01:00
Christian T. Jacobs ae8f45450a
Merge pull request #66 from merkato/patch-1
WSJT-X FT8 mode in adif.py
2018-03-22 20:42:20 +00:00
Tomasz Nycz b20e238ad9
WSJT-X FT8 mode in adif.py 2018-03-22 20:56:23 +01:00
Christian Jacobs 154673c189 Small docstring update. 2018-03-10 15:54:23 +00:00
Christian T. Jacobs 5198547c6c
Maidenhead (#65)
Added the option of showing Maidenhead grid squares on the World Map, and the option of shading in worked grid squares. Addresses issue #59. Note that this introduces a new class called Maidenhead, which is capable of converting between latitude-longitude coordinates and grid squares. However, this functionality isn't currently used.
2018-03-10 15:49:44 +00:00
Christian T. Jacobs f222d5cc30
Migrate to a world map / grey line that uses Cartopy (#64)
Migrating grey line functionality over to a Cartopy-based implementation to address issue #62.

Also renamed GreyLine to WorldMap, and bumped the version to v1.1.0-dev.
2018-02-24 13:24:39 +00:00
Christian Jacobs f283df065b Merge branch 'master' of https://github.com/ctjacobs/pyqso into copypaste 2018-02-23 17:04:09 +00:00
Christian Jacobs 26e871bcab Small improvement to the wording. 2018-02-13 21:05:36 +00:00
Christian Jacobs ee6018b04a Added a requirements.txt file for the purpose of installing dependencies. Addresses issue #63. 2018-02-13 21:03:25 +00:00
Christian Jacobs 89f1ca46b4 Small improvements to the Getting Started section. 2018-02-13 21:02:22 +00:00
Christian Jacobs 1ef87caf5e Adding more explanation about PyQSO's dependencies. 2018-02-13 21:01:19 +00:00
Christian Jacobs 946f03d95f Added basic copy/paste functionality for individual records. 2018-02-04 23:15:57 +00:00
Christian T. Jacobs 7a162b2a2d
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.
2018-01-18 20:52:44 +00:00
Christian T. Jacobs d06e326cbd Update list of supported ADIF fields. 2017-09-04 15:53:44 +01:00
Christian T. Jacobs 4550f841f7 Add support for satellite QSOs. Addresses issue #55. 2017-09-04 15:48:50 +01:00
33 zmienionych plików z 1547 dodań i 529 usunięć

Wyświetl plik

@ -1,6 +1,5 @@
sudo: required
dist: trusty
group: deprecated-2017Q2
language: python
@ -12,7 +11,7 @@ virtualenv:
before_install:
- sudo apt-get update -qq
- sudo apt-get install -yq xvfb gir1.2-gtk-3.0 python3-gi-cairo python-mpltoolkits.basemap python3-numpy python3-matplotlib python3-sphinx python-libhamlib2 python3-flake8 python3-pip
- sudo apt-get install -yq xvfb python3 python3-pip gir1.2-gtk-3.0 python3-gi-cairo python3-flake8 python3-numpy python3-matplotlib python3-sphinx python-libhamlib2
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"

Wyświetl plik

@ -1,5 +1,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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2012-2017 Christian Thomas Jacobs.
# Copyright (C) 2012-2018 Christian Thomas Jacobs.
# This file is part of PyQSO.
@ -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):

Wyświetl plik

@ -51,16 +51,16 @@ master_doc = 'index'
# General information about the project.
project = u'PyQSO'
copyright = u'2015-2017, Christian Thomas Jacobs'
copyright = u'2015-2018, Christian Thomas Jacobs'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0.0'
version = '1.1.0'
# 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.

Wyświetl plik

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

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 61 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 58 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 45 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 54 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 62 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 97 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 202 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 50 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 51 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 154 KiB

Wyświetl plik

@ -24,7 +24,7 @@ include:
- Progress tracker for the `DXCC <http://www.arrl.org/dxcc/>`_ award.
- Grey line plotter.
- World map with grey line 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.

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2012-2017 Christian Thomas Jacobs.
# Copyright (C) 2012-2018 Christian Thomas Jacobs.
# This file is part of PyQSO.
@ -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.

46
pyqso/popup.py 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Christian Thomas Jacobs.
# Copyright (C) 2013-2018 Christian Thomas Jacobs.
# This file is part of PyQSO.
@ -18,7 +18,7 @@
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
from pyqso.dx_cluster import DXCluster
from pyqso.grey_line import GreyLine
from pyqso.world_map import WorldMap
from pyqso.awards import Awards
@ -38,7 +38,7 @@ class Toolbox:
self.tools = self.builder.get_object("tools")
self.dx_cluster = DXCluster(self.application)
self.grey_line = GreyLine(self.application)
self.world_map = WorldMap(self.application)
self.awards = Awards(self.application)
self.tools.connect_after("switch-page", self.on_switch_page)
@ -52,7 +52,7 @@ class Toolbox:
return
def on_switch_page(self, widget, label, new_page):
""" Re-draw the Grey Line if the user switches to the grey line tab. """
if(widget.get_tab_label(label).get_text() == "Grey Line"):
self.grey_line.draw()
""" Re-draw the WorldMap if the user switches to the World Map tab. """
if(widget.get_tab_label(label).get_text() == "World Map"):
self.world_map.draw()
return

361
pyqso/world_map.py 100644
Wyświetl plik

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

6
requirements.txt 100644
Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2013-2017 Christian Thomas Jacobs.
# Copyright (C) 2013-2018 Christian Thomas Jacobs.
# This file is part of PyQSO.
@ -20,7 +20,7 @@
from setuptools import setup
setup(name="PyQSO",
version="1.0.0",
version="1.1.0",
description="A contact logging tool for amateur radio operators.",
author="Christian Thomas Jacobs",
author_email="christian@christianjacobs.uk",

Plik binarny nie jest wyświetlany.

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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