2015-10-12 00:23:37 +00:00
#!/usr/bin/env python3
2013-06-30 01:42:03 +00:00
2018-02-24 11:23:49 +00:00
# Copyright (C) 2013-2018 Christian Thomas Jacobs.
2013-06-30 01:42:03 +00:00
# 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/>.
2017-02-21 16:47:35 +00:00
from gi . repository import GObject
2013-06-30 01:42:03 +00:00
import logging
2017-02-10 18:50:34 +00:00
from os . path import expanduser
2018-02-24 11:23:49 +00:00
from datetime import datetime
2017-02-10 18:50:34 +00:00
try :
import configparser
except ImportError :
import ConfigParser as configparser
2013-09-14 20:10:52 +00:00
try :
2016-01-27 16:23:09 +00:00
import numpy
logging . info ( " Using version %s of numpy. " % ( numpy . __version__ ) )
import matplotlib
logging . info ( " Using version %s of matplotlib. " % ( matplotlib . __version__ ) )
2018-02-23 22:24:57 +00:00
import cartopy
logging . info ( " Using version %s of cartopy. " % ( cartopy . __version__ ) )
2016-01-27 16:23:09 +00:00
from matplotlib . backends . backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
have_necessary_modules = True
2015-04-14 21:09:53 +00:00
except ImportError as e :
2016-01-27 16:23:09 +00:00
logging . warning ( e )
2018-02-24 11:23:49 +00:00
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. " )
2016-01-27 16:23:09 +00:00
have_necessary_modules = False
2018-01-18 20:52:44 +00:00
try :
import geocoder
have_geocoder = True
except ImportError :
logging . warning ( " Could not import the geocoder module! " )
have_geocoder = False
class Point :
""" A point on the grey line map. """
def __init__ ( self , name , latitude , longitude , style = " yo " ) :
""" Set up the point ' s attributes.
: arg str name : The name that identifies the point .
: arg float latitude : The latitude of the point on the map .
: arg float longitude : The longitude of the point on the map .
: arg str style : The style of the point when plotted . By default it is a filled yellow circle .
"""
self . name = name
self . latitude = latitude
self . longitude = longitude
self . style = style
return
2016-01-27 16:23:09 +00:00
2013-06-30 01:42:03 +00:00
2018-02-24 11:23:49 +00:00
class WorldMap :
2013-06-30 01:42:03 +00:00
2018-02-24 11:23:49 +00:00
""" A tool for visualising the world map. """
2016-01-27 16:23:09 +00:00
2017-04-01 17:10:24 +00:00
def __init__ ( self , application ) :
2018-02-24 11:23:49 +00:00
""" Set up the drawing canvas and the timer which will re-plot the world map every 30 minutes.
2016-01-27 16:23:09 +00:00
2017-04-01 17:10:24 +00:00
: arg application : The PyQSO application containing the main Gtk window , etc .
2016-01-27 16:23:09 +00:00
"""
2018-02-24 11:23:49 +00:00
logging . debug ( " Setting up the world map... " )
2017-02-21 16:47:35 +00:00
2017-04-01 17:10:24 +00:00
self . application = application
self . builder = self . application . builder
2018-01-18 20:52:44 +00:00
self . points = [ ]
2016-01-27 16:23:09 +00:00
2018-01-18 20:52:44 +00:00
if ( have_necessary_modules ) :
self . fig = matplotlib . figure . Figure ( )
self . canvas = FigureCanvas ( self . fig ) # For embedding in the Gtk application
2018-02-24 11:23:49 +00:00
self . builder . get_object ( " worldmap " ) . pack_start ( self . canvas , True , True , 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).
2018-01-18 20:52:44 +00:00
2018-02-24 11:42:32 +00:00
# Add the QTH coordinates for plotting, if available.
2017-02-10 18:50:34 +00:00
config = configparser . ConfigParser ( )
have_config = ( config . read ( expanduser ( ' ~/.config/pyqso/preferences.ini ' ) ) != [ ] )
( section , option ) = ( " general " , " show_qth " )
if ( have_config and config . has_option ( section , option ) ) :
2017-07-09 10:42:42 +00:00
if ( config . getboolean ( section , option ) ) :
2017-02-10 18:50:34 +00:00
try :
2018-01-18 20:52:44 +00:00
qth_name = config . get ( " general " , " qth_name " )
qth_latitude = float ( config . get ( " general " , " qth_latitude " ) )
qth_longitude = float ( config . get ( " general " , " qth_longitude " ) )
self . add_point ( qth_name , qth_latitude , qth_longitude , " ro " )
2017-05-07 15:38:36 +00:00
except ValueError :
2018-02-24 11:23:49 +00:00
logging . warning ( " Unable to get the QTH name, latitude and/or longitude. The QTH will not be pinpointed on the world map. Check preferences? " )
2016-01-27 16:23:09 +00:00
2018-02-24 11:23:49 +00:00
self . builder . get_object ( " worldmap " ) . show_all ( )
2016-01-27 16:23:09 +00:00
2018-02-24 11:23:49 +00:00
logging . debug ( " World map ready! " )
2016-01-27 16:23:09 +00:00
return
2018-01-18 20:52:44 +00:00
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 ) :
2018-02-24 11:23:49 +00:00
""" Pinpoint the location of a QSO on the world map based on the COUNTRY field.
2018-01-18 20:52:44 +00:00
: arg r : The QSO record containing the location to pinpoint .
"""
if ( have_geocoder ) :
country = r [ " COUNTRY " ]
callsign = r [ " CALL " ]
# Get the latitude-longitude coordinates of the country.
if ( country ) :
try :
g = geocoder . google ( country )
latitude , longitude = g . latlng
logging . debug ( " QTH coordinates found: ( %s , %s ) " , str ( latitude ) , str ( longitude ) )
self . add_point ( callsign , latitude , longitude )
except ValueError :
logging . exception ( " Unable to lookup QTH coordinates. " )
except Exception :
logging . exception ( " Unable to lookup QTH coordinates. Check connection to the internets? Lookup limit reached? " )
return
2016-01-27 16:23:09 +00:00
def draw ( self ) :
""" Draw the world map and the grey line on top of it.
2018-02-24 11:23:49 +00:00
: 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 ) .
2016-01-27 16:23:09 +00:00
: rtype : bool
"""
if ( have_necessary_modules ) :
2017-02-21 18:16:02 +00:00
toolbox = self . builder . get_object ( " toolbox " )
tools = self . builder . get_object ( " tools " )
if ( tools . get_current_page ( ) != 1 or not toolbox . get_visible ( ) ) :
2018-02-24 11:23:49 +00:00
# Don't re-draw if the world map is not visible.
2016-01-27 16:23:09 +00:00
return True # We need to return True in case this is method was called by a timer event.
else :
2018-02-23 22:24:57 +00:00
# Set up the world map.
2016-01-27 16:23:09 +00:00
self . fig . clf ( )
2018-02-23 22:24:57 +00:00
ax = self . fig . add_subplot ( 111 , projection = cartopy . crs . PlateCarree ( ) )
ax . set_extent ( [ - 180 , 180 , - 90 , 90 ] )
2018-02-24 11:42:32 +00:00
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
2018-02-23 22:24:57 +00:00
ax . add_feature ( cartopy . feature . LAND )
ax . add_feature ( cartopy . feature . OCEAN )
ax . add_feature ( cartopy . feature . COASTLINE )
ax . add_feature ( cartopy . feature . BORDERS , alpha = 0.25 )
2018-02-24 11:23:49 +00:00
# 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).
dt = datetime . utcnow ( )
axial_tilt = 23.5
2018-02-24 11:30:48 +00:00
reference_solstice = datetime ( 2016 , 6 , 21 , 22 , 22 )
2018-02-24 11:23:49 +00:00
days_per_year = 365.2425
seconds_per_day = 86400.0
2018-02-24 11:30:48 +00:00
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
2018-02-24 11:23:49 +00:00
2018-02-24 11:30:48 +00:00
pole_longitude = longitude
if latitude > 0 :
pole_latitude = - 90 + latitude
central_rotated_longitude = 180
2018-02-24 11:23:49 +00:00
else :
2018-02-24 11:30:48 +00:00
pole_latitude = 90 + latitude
central_rotated_longitude = 0
2018-02-24 11:23:49 +00:00
2018-02-24 11:30:48 +00:00
rotated_pole = cartopy . crs . RotatedPole ( pole_latitude = pole_latitude , pole_longitude = pole_longitude , central_rotated_longitude = central_rotated_longitude )
2018-02-24 11:23:49 +00:00
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 = ' grey ' , alpha = 0.75 )
2017-02-10 18:50:34 +00:00
2018-01-18 20:52:44 +00:00
# Plot points on the map.
if ( self . points ) :
2018-02-23 22:24:57 +00:00
logging . debug ( " Plotting QTHs on the map... " )
2018-01-18 20:52:44 +00:00
for p in self . points :
2018-02-23 22:24:57 +00:00
ax . plot ( p . longitude , p . latitude , p . style , transform = cartopy . crs . PlateCarree ( ) )
2018-02-24 11:30:48 +00:00
ax . text ( p . longitude + 0.02 * p . longitude , p . latitude + 0.02 * p . latitude , p . name , color = " black " , size = " small " , weight = " bold " )
2017-02-10 18:50:34 +00:00
2016-01-27 16:23:09 +00:00
return True
else :
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.