This commit is contained in:
coulisse 2023-01-01 23:03:51 +01:00
parent 7bd3181575
commit 2e993ad7e3
77 changed files with 3301 additions and 4260 deletions

View File

@ -1,4 +1,4 @@
SPIDERWEB
<img align="center" src="static/images/icons/icon-72x72.png"/> SPIDERWEB
===
### Ham radio cluster web viewer for DxSpider
@ -8,11 +8,12 @@ SPIDERWEB
[![made-with-javascript](https://img.shields.io/badge/Made%20with-JavaScript-1f425f.svg)](https://www.javascript.com)
[![CodeFactor](https://www.codefactor.io/repository/github/coulisse/spiderweb/badge)](https://www.codefactor.io/repository/github/coulisse/spiderweb)
- **Release:** v.2.3.4
- **Release:** v2.4
- **Author:** Corrado Gerbaldo - IU1BOW.
- **Mail:** <corrado.gerbaldo@gmail.com>
- **Licensing:** Gpl V3.0 see ["LICENSE"](LICENSE) file.
- **Languages:** This application is written in Python/flask,Javascript and HTML
- **Languages:** This application is written in Python 3.11/flask,Javascript and HTML
___
**DXSpider** is a great DX Cluster software that has a usefull telnet interface.
I wrote this application in order to add a web user interface to DXSpider and show the spots collected.
@ -20,10 +21,12 @@ The user could see 50 spots at time and filter them by band, spotter continent a
For this application I've used:
- **Bootstrap** for stylesheet CSS
- **jQuery** In the header you can find the link to MS link
- **jQuery**
- **Apache ECharts** for managing charts
- **qrz.com** For each callsing found you can click on lens and you'll see him on qrz.com
- **flag-icon-css** [https://github.com/lipis/flag-icon-css](https://github.com/lipis/flag-icon-css) I used it for show the country flags
- **ng3k.com** [ng3k.com](http://ng3k.com/misc/adxo.html) I used to get information about "Announced Dx Operations". Thanks to Bill/NG3K !!!
- **silso** [sidc.be/silso](https://sidc.be/silso/) used to show propagation trend in "Chart & stats" secion
You can find my web site at [https://www.iu1bow.it](https://www.i1bow.it)
@ -65,8 +68,8 @@ To install **Flask**:
```console
foo@bar:~$ pip install flask
foo@bar:~$ pip install Flask-minify
foo@bar:~$ pip install staticjinja
foo@bar:~$ pip install flask_wtf
foo@bar:~$ pip install pandas
```
Then you have to install mysql libraries**:
```console
@ -75,19 +78,6 @@ foo@bar:~$ pip install --upgrade mysql-connector-python==8.0.12
```
Finally you have to install matplotlib and pandas in order to plots some graphics
```console
foo@bar:~$ pip3 install matplotlib
foo@bar:~$ pip3 install statsmodels
foo@bar:~$ pip3 install geopandas
foo@bar:~$ pip3 install shapely
or
foo@bar:~$ sudo -H pip3 install matplotlib --system
foo@bar:~$ sudo -H pip3 install statsmodels --system
foo@bar:~$ sudo -H pip3 install geopandas --system
foo@bar:~$ sudo -H pip3 install shapely --system
```
### Configuration
In the path `spiderweb/cfg/` rename `config.json.template` in `config.json`:
@ -120,21 +110,9 @@ Make your choiche:
```
In order to show the right *plots*, you have to generate them!
To do so you have to run *.sh* files inside *scripts* folders, or the better way is to **schedule** them with your **crontab**
```console
foo@bar:~$ crontab -e
```
then edit it in a manner like this:
```crontab
0 22 * * * /home/web/spiderweb/scripts/qso_world_map.sh > /dev/null 2>&1
0 23 * * * /home/web/spiderweb/scripts/qso_months.sh > /dev/null 2>&1
*/15 * * * * /home/web/spiderweb/scripts/propagation_heatmaps.sh > /dev/null 2>&1
0 1 * * * /home/web/spiderweb/scripts/qso_trend.sh > /dev/null 2>&1
*/30 * * * * /home/web/spiderweb/scripts/qso_hour_band.sh > /dev/null 2>&1
39 * * * * /home/web/spiderweb/scripts/monitor.sh> /dev/null 2>&1
__
```
### Crontab
Starting from version 2.4, since all activities are managed by the application, you don't need to schedule anythings
### Run test
Now you can run your web application with the following command:
@ -248,8 +226,9 @@ to
```
### Monitoring
you can use the scritp `scripts/monitoring.sh` in order to monitoring your system. Check instruction inside this scripts.
### Mobile
This application is designed for desktop and mobile phone. It is a [PWA](https://en.wikipedia.org/wiki/Progressive_web_app) so it could installed and used like an app on mobile.
### API
**Spot list**
@ -262,12 +241,18 @@ You cam retrive some informations about a callsign with **callsign**; For exampl
### Screenshots
----------
<img src="docs/images/01_desktop_main.jpg" width="300"/>
<img src="docs/images/02_desktop_plot.jpg" width="300"/>
**desktop**
<img src="docs/images/01_desktop_main.png" width="300"/>
<img src="docs/images/02_desktop_plot.png" width="300"/>
<p float="left">
<img src="docs/images/03_mobile_install.jpg" width="200"/>
<img src="docs/images/04_mobile_icon.jpg" width="200"/>
<img src="docs/images/05_mobile_splash.jpg" width="200"/>
<img src="docs/images/06_mobile_main.jpg" width="200"/>
**mobile**
<img src="docs/images/m01_mobile.png" width="200"/>
<img src="docs/images/m02_mobile.png" width="200"/>
<img src="docs/images/m03_mobile.png" width="200"/>
<img src="docs/images/m04_mobile.png" width="200"/>
</p>

Binary file not shown.

View File

@ -8,9 +8,6 @@
"timer":{
"interval":30000
},
"plot_refresh_timer":{
"interval":450000
},
"mycallsign":"XXXXXX",
"mail":"foo@bar.com",
"mail_token": "foobar",
@ -18,7 +15,7 @@
"telnet":"mysite:7300",
"menu": {
"menu_list": [
{"label":"Graphs & stats", "link": "/plots.html", "external": false},
{"label":"Charts & stats", "link": "/plots.html", "external": false},
{"label":"Sources", "link": "https://github.com/coulisse/spiderweb/" , "external": true},
{"label":"Cookies", "link": "/cookies.html", "external": false},
{"label":"Privacy", "link": "/privacy.html", "external": false}

View File

@ -2,31 +2,38 @@
"continents": [
{
"id": "AF",
"cq": "22,33,34,35,36,37,38,39"
"cq": "22,33,34,35,36,37,38,39",
"description": "Africa"
},
{
"id": "AN",
"cq": "12"
"cq": "12",
"description": "Antarctica"
},
{
"id": "AS",
"cq": "17,18,19,20,21,22,23,24,25,26,27,28"
"cq": "17,18,19,20,21,22,23,24,25,26,27,28",
"description": "Asia"
},
{
"id": "EU",
"cq": "14,15,16,20,33,40"
"cq": "14,15,16,20,33,40",
"description": "Europe"
},
{
"id": "NA",
"cq": "1,2,3,4,5,6,7,8"
"cq": "1,2,3,4,5,6,7,8",
"description": "North America"
},
{
"id": "OC",
"cq": "40,27,28,29,30,31,32"
"cq": "40,27,28,29,30,31,32",
"description": "Oceania"
},
{
"id": "SA",
"cq": "9,10,11,12,13"
"cq": "9,10,11,12,13",
"description": "South America"
}
]
}

View File

@ -185,8 +185,8 @@
{ "WPX": "166", "ISO": "is", "desc": "Iceland" },
{ "WPX": "167", "ISO": "gt", "desc": "Guatemala" },
{ "WPX": "168", "ISO": "cr", "desc": "Costa Rica" },
{ "WPX": "169", "ISO": "cr", "desc": "Cocos Island" },
{ "WPX": "169", "ISO": "cr", "desc": "Cocos (Keeling) Islands" },
{ "WPX": "169", "ISO": "cc", "desc": "Cocos Island" },
{ "WPX": "169", "ISO": "cc", "desc": "Cocos (Keeling) Islands" },
{ "WPX": "170", "ISO": "cm", "desc": "Cameroon" },
{ "WPX": "171", "ISO": "fr", "desc": "Corsica" },
{ "WPX": "172", "ISO": "cf", "desc": "Central African Republic" },

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
[loggers]
keys=root
[handlers]
keys=stream_handler,file_handler
[formatters]
keys=formatter
[logger_root]
level=INFO
handlers=stream_handler,file_handler
[handler_stream_handler]
class=StreamHandler
level=INFO
formatter=formatter
args=(sys.stderr,)
[handler_file_handler]
class=handlers.RotatingFileHandler
level=INFO
formatter=formatter
args = ('../log/plots.log', 'a',30000000, 5)
[formatter_formatter]
format=%(asctime)s [%(levelname)s] (%(filename)s) %(message)s
datefmt=

View File

@ -8,7 +8,7 @@ keys=stream_handler,file_handler
keys=formatter
[logger_root]
level=INFO
level=INFO
handlers=stream_handler,file_handler
[handler_stream_handler]

View File

@ -1,6 +1,18 @@
### Change log
Date: 28/09/2022
Release: v.2.3.4
Date: 01/01/2023
Release v.2.4
- migration to python 3.11
- added descriptions to continents
- fixed issue #23: Wrong flag for Cocos (Keeling) Islands
- proposal for improvements #25 (charts titles, removed link from connected nodes)
- reenginering of graph & stats
- upgraded bootstrap icons
- upgraded flag icons
- added new propagation chart https://sidc.be/silso/
- added Content Security Policy
___
Date: 28/09/2022
Release: v2.3.4
- fixed issue #22 propagation_heatmaps.sh fails with 'Passing a Normalize instance simultaneously with vmin/vmax is not supported.'
- replaced seaborn styles since are deprecated
___

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

BIN
docs/images/m01_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
docs/images/m02_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
docs/images/m03_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/images/m04_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

1
lib/.gitignore vendored
View File

@ -1 +1,2 @@
__pycache__
__init__.py

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

490
lib/plot_data_provider.py Normal file
View File

@ -0,0 +1,490 @@
#***********************************************************************************
# Module that contain classess for providing data to plotting front-end page
#***********************************************************************************
__author__ = 'IU1BOW - Corrado'
import threading
import time
from lib.qry import query_manager
import pandas as pd
import json
#-----------------------------------------------------------------------------------
# Base class (as template for other classes)
#-----------------------------------------------------------------------------------
class BaseDataProvider:
#glb_data is used for store and return informations to within get_data method
global glb_data
global glb_last_refresh
global glb_response
#refresh is used to refetch data from db and store in memory. you can define a
# time for refresh
def refresh(self):
self.logger.info("Class: %s refresh data",self.__class__.__name__)
return {}
#return data to the caller
def get_data(self):
self.glb_response = {}
self.glb_response.update({"last_refresh":self.glb_last_refresh})
return self.glb_response
#constructor: you have to pass logger, continent list and bands (for frequencies)
# this method call the first refresh
def __init__(self, logger, qm, continents, bands):
self.logger = logger
self.logger.info("Class: %s init start",self.__class__.__name__)
self.qm = qm
self.continents=continents
self.bands=bands
self.refresh()
self.logger.info("Class: %s init end",self.__class__.__name__)
return
#-----------------------------------------------------------------------------------
# Class for managing data for Continent/Band chart
#-----------------------------------------------------------------------------------
class ContinentsBandsProvider(BaseDataProvider):
def __init__(self,logger, qm, continents, bands):
# Calling constructor of base class
super().__init__(logger,qm,continents,bands)
def __load_data(self,band_frequencies, continents_cq):
self.logger.info("Start")
self.logger.info("doing query...")
#construct bands query
bands_qry_string = 'CASE '
self.logger.debug(band_frequencies)
for i in range(len(band_frequencies["bands"])):
bands_qry_string+=' WHEN freq between '+str(band_frequencies["bands"][i]["min"])+' AND '+ str(band_frequencies["bands"][i]["max"])
bands_qry_string+=' THEN "'+band_frequencies["bands"][i]["id"]+'"'
#construct continent region query
spottercq_qry_string = 'CASE '
spotcq_qry_string = 'CASE '
for i in range(len(continents_cq["continents"])):
spottercq_qry_string+=' WHEN spottercq in('+continents_cq["continents"][i]["cq"]+')'
spottercq_qry_string+=' THEN "' +continents_cq["continents"][i]["id"]+'"'
spotcq_qry_string+=' WHEN spotcq in('+continents_cq["continents"][i]["cq"]+')'
spotcq_qry_string+=' THEN "' +continents_cq["continents"][i]["id"]+'"'
#construct final query string
qry_string ="""
SELECT
"""+spottercq_qry_string+""" ELSE spottercq END,
"""+spotcq_qry_string+""" ELSE spotcq END,
"""+bands_qry_string+""" END,
count(0) number
from spot
where
rowid > (select max(rowid) max_rowid from spot) - 5000 and
time > UNIX_TIMESTAMP()-3600
group by 1, 2, 3
;
"""
self.logger.debug(qry_string)
self.qm.qry(qry_string)
data=self.qm.get_data()
self.logger.info("query done")
self.logger.debug (data)
return data
# function for search continent in the global data returned by query and making a cartesian product
# in order to prepare data for heatmap
def __normalize_continent(self,data_list,continent,continents_list, band_list):
data_filtered=[]
for i, item_data in enumerate(data_list):
if item_data[0]==continent and not (item_data[3] is None):
element=[]
element.append(item_data[1])
element.append(item_data[2])
element.append(item_data[3])
data_filtered.append(element)
cartesian_product = []
for j, item_continent in enumerate(continents_list):
for k, item_band in enumerate(band_list):
found=0
for lis, item_filtered in enumerate(data_filtered):
if item_filtered[0]==item_continent["id"] and item_filtered[1]==item_band["id"]:
#cartesian_product.append(item_filtered)
element=[]
element.append(j)
element.append(k)
element.append(item_filtered[2])
cartesian_product.append(element)
found=1
if found==0:
element=[]
element.append(j)
element.append(k)
element.append(0)
cartesian_product.append(element)
self.logger.debug("cartesian product for continent: "+continent)
self.logger.debug(cartesian_product)
return cartesian_product
def refresh(self):
super().refresh()
lcl_data={}
qry_data=self.__load_data(self.bands, self.continents)
for i, item in enumerate(self.continents["continents"]):
continent=item["id"]
data_de=self.__normalize_continent(qry_data,continent,self.continents["continents"],self.bands["bands"])
lcl_data.update({continent:data_de})
self.glb_data=lcl_data
self.glb_last_refresh=time.time()
threading.Timer(15*60,self.refresh).start() #periodic refresh: set time
return
def get_data(self,continent_filter):
super().get_data()
self.glb_response.update({"band activity": self.glb_data[continent_filter]})
return self.glb_response
#-----------------------------------------------------------------------------------
# Class for managing data for Spots per months chart
#-----------------------------------------------------------------------------------
class SpotsPerMounthProvider(BaseDataProvider):
def __init__(self,logger,qm):
# Calling constructor of base class
super().__init__(logger,qm,[],[])
def __load_data(self):
self.logger.info("Start")
self.logger.info("doing query...")
#construct final query string
qry_string="""
select month(s1.ym) as referring_month,
cast(sum(
case
when YEAR(s1.ym)=YEAR(now())
then s1.total
else 0
end
) as int) as current_year,
cast(sum(
case
when YEAR(s1.ym)=YEAR(now())-1
then s1.total
else 0
end
) as int) as one_year_ago,
cast(sum(
case
when YEAR(s1.ym)=YEAR(now())-2
then s1.total
else 0
end
) as int) as two_year_ago
from (
/* extract number of qso per year */
select
CAST(
CONCAT(
YEAR(FROM_UNIXTIME(time)),
'-',
right(concat('0',MONTH(FROM_UNIXTIME(time))),2),
'-',
'01'
)
AS DATE) as ym,
count(0) as total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 36 MONTH)
GROUP by 1
/*union used to initialize all months */
union select '1976-01-01', 0
union select '1976-02-01', 0
union select '1976-03-01', 0
union select '1976-04-01', 0
union select '1976-05-01', 0
union select '1976-06-01', 0
union select '1976-07-01', 0
union select '1976-08-01', 0
union select '1976-09-01', 0
union select '1976-10-01', 0
union select '1976-11-01', 0
union select '2019-12-01', 0
) as s1
group by referring_month
;
"""
self.logger.debug(qry_string)
self.qm.qry(qry_string)
data=self.qm.get_data()
self.logger.info("query done")
self.logger.debug (data)
return data
def refresh(self):
super().refresh()
lcl_data={}
qry_data=self.__load_data()
for i, item in enumerate(qry_data):
year_data={'year_0':item[1],'year_1':item[2],'year_2':item[3]}
lcl_data.update({item[0]:year_data})
self.logger.debug(lcl_data)
self.glb_data=lcl_data
self.glb_last_refresh=time.time()
threading.Timer(60*60*24,self.refresh).start() #periodic refresh: set time
return
def get_data(self,):
super().get_data()
self.glb_response.update({"spots_per_month": self.glb_data})
return self.glb_response
#-----------------------------------------------------------------------------------
# Class for managing data for Spots trend chart
#-----------------------------------------------------------------------------------
class SpotsTrend(BaseDataProvider):
def __init__(self,logger,qm):
# Calling constructor of base class
super().__init__(logger,qm,[],[])
def __load_data(self):
self.logger.info("Start")
self.logger.info("doing query...")
#construct final query string
qry_string="""
select
FROM_UNIXTIME(time,'%Y-%m-%d') as day,
count(0) as total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 60 MONTH)
GROUP by 1
;
"""
self.logger.debug(qry_string)
self.qm.qry_pd(qry_string)
df=self.qm.get_data()
self.logger.info("query done")
self.logger.debug (df)
if df is None ==0:
self.logger.warning("no data found")
#normalize data eliminating peaks
df['day']=pd.to_datetime(df['day'])
df=df.set_index('day')
df=df.resample('D').interpolate(method='pad', limit_direction='forward', axis=0)
df=df.rolling('30D').mean()
df['total']=df['total'].round(0)
return df
def refresh(self):
super().refresh()
qry_data=self.__load_data()
lcl_data={}
#iterate panda dataframe
for index, row in qry_data.iterrows():
lcl_data.update({str(index.date()):row['total']})
self.logger.debug(lcl_data)
self.glb_data=lcl_data
self.glb_last_refresh=time.time()
threading.Timer(60*60*24,self.refresh).start() #periodic refresh: set time
return
def get_data(self):
super().get_data()
self.glb_response.update({"spots_trend": self.glb_data})
return self.glb_response
#-----------------------------------------------------------------------------------
# Class for managing data for Hour/Band chart
#-----------------------------------------------------------------------------------
class HourBand(BaseDataProvider):
def __init__(self,logger,qm,bands):
# Calling constructor of base class
super().__init__(logger,qm,[],bands)
def __load_data(self):
self.logger.info("Start")
self.logger.info("doing query...")
self.logger.debug(self.bands)
#construct bands query
bands_qry_string = 'CASE '
for i in range(len(self.bands["bands"])):
bands_qry_string+=' WHEN freq between '+str(self.bands["bands"][i]["min"])+' AND '+ str(self.bands["bands"][i]["max"])
bands_qry_string+=' THEN "'+self.bands["bands"][i]["id"]+'"'
#construct bands query weight
bands_weight_qry_string = 'CASE '
for i in range(len(self.bands["bands"])):
bands_weight_qry_string+=' WHEN freq between '+str(self.bands["bands"][i]["min"])+' AND '+ str(self.bands["bands"][i]["max"])
bands_weight_qry_string+=' THEN "'+str(self.bands["bands"][i]["min"])+'"'
#construct final query string
qry_string ="""
select s1.band, s1.hour, s1.total from (
SELECT
cast(concat(HOUR (FROM_UNIXTIME(time))) as unsigned) as hour,
"""+bands_qry_string+""" ELSE "other" END as band,
cast("""+bands_weight_qry_string+""" ELSE 0 END as unsigned) as band_weight,
count(0) AS total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 1 MONTH)
and rowid > (select max(rowid)-500000 from spot)
group by 1, 2
) as s1
order by s1.band, s1.hour
;
"""
self.logger.debug(qry_string)
self.qm.qry(qry_string)
data=self.qm.get_data()
self.logger.info("query done")
self.logger.debug (data)
return data
def refresh(self):
super().refresh()
lcl_data={}
qry_data=self.__load_data()
for i,j,k in qry_data:
if i not in lcl_data:
lcl_data[i]={}
lcl_data[i].update({j:k})
self.logger.debug(lcl_data)
self.glb_data=lcl_data
self.glb_last_refresh=time.time()
threading.Timer(60*60*24,self.refresh).start() #periodic refresh: set time
return
def get_data(self):
super().get_data()
self.glb_response.update({"hour_band": self.glb_data})
return self.glb_response
#-----------------------------------------------------------------------------------
# Class for managing data for World DX SPOTS current activity
#-----------------------------------------------------------------------------------
class WorldDxSpotsLive(BaseDataProvider):
global glb_pfxt
def __init__(self,logger,qm,pfxt):
# Calling constructor of base class
self.glb_pfxt=pfxt
super().__init__(logger,qm,[],[])
def __load_data(self):
self.logger.info("Start")
self.logger.info("doing query...")
#construct final query string
qry_string ="""
select spotcall as dx
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 1 HOUR)
and rowid > (select max(rowid)-10000 from spot)
group by 1;
"""
self.logger.debug(qry_string)
self.qm.qry(qry_string)
data=self.qm.get_data()
row_headers=self.qm.get_headers()
self.logger.info("query done")
self.logger.debug (data)
#define country table for search info on callsigns
df = pd.DataFrame(columns=['row_id','dx','lat','lon'])
dx=[]
lat=[]
lon=[]
row_id=[]
idx=0
for result in data:
main_result=dict(zip(row_headers,result))
# find the country in prefix table
search_prefix=self.glb_pfxt.find(main_result["dx"])
if search_prefix["country"] != "unknown country" :
# merge recordset and contry prefix
dx.append(main_result["dx"])
lon.append(float(search_prefix["lat"]))
lat.append(-float(search_prefix["lon"]))
idx+=1
row_id.append(idx)
df['dx']=dx
df['lat']=lat
df['lon']=lon
df['row_id']=row_id
df_grp=df.groupby(["lat", "lon"])["row_id"].count().reset_index(name="count")
if df is None ==0:
logger.warning("no data found")
return df_grp
def refresh(self):
super().refresh()
lcl_data={}
qry_data=self.__load_data()
self.logger.debug(qry_data)
lcl_data=[]
for index, row in qry_data.iterrows():
record=dict(lat=row['lat'], lon=row['lon'], count=row['count'])
lcl_data.append(record)
self.logger.debug(lcl_data)
self.glb_data=lcl_data
self.glb_last_refresh=time.time()
threading.Timer(5*60,self.refresh).start() #periodic refresh: set time
return
def get_data(self):
super().get_data()
self.glb_response.update({"world_dx_spots_live": self.glb_data})
return self.glb_response

View File

@ -1,88 +0,0 @@
#*****************************************************************************************
# module used to save a plot and delete old plots
#*****************************************************************************************
__author__ = 'IU1BOW - Corrado'
import logging
import logging.config
from datetime import datetime
import glob
import os
import json
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
def del_with_retention(filename, number,ext):
#list all file with date time and extension
#and delete al file except the last "number" passed by param, and ordered by name
logger.info('deleting old files')
pattern=filename+'_????????-??????.'+ext
logger.debug('search pattern for delete files: '+pattern)
files=glob.glob(pattern)
length = len(files)
if length>number:
files_ordered=sorted(files)
i = 0
# Iterating using while loop
while i < length - number:
try:
os.remove(files_ordered[i])
logger.info('deleted file:'+files_ordered[i])
except Exception as e:
logger.error(e)
logger.error('not deleted file:'+files_ordered[i])
finally:
i += 1
return
def saveplt(plt,filename):
#convert relative to absolute path
#filename=os.path.abspath(os.path.expanduser(os.path.expandvars(filename)))
now = datetime.now() # current date and time
timestamp=now.strftime("%Y%m%d-%H%M%S")
outputfile=filename+'_'+timestamp
#saving svg file
try:
plt.savefig(outputfile+'.svg', dpi=100, bbox_inches='tight')
logger.info('plotted saved on: '+outputfile+'.svg')
except Exception as e01:
logger.error(e01)
logger.error('error saving file: '+outputfile+'.svg')
#saving png file
try:
plt.savefig(outputfile+'.png', dpi=100, bbox_inches='tight')
logger.info('plotted saved on: '+outputfile+'.png')
except Exception as e02:
logger.error(e02)
logger.error('error saving file: '+outputfile+'.png')
#creating idx file in order to match standard file name to filename with timestamp
try:
basepath=os.path.dirname(filename)
idxfile=os.path.join(basepath,'plots.json')
if os.path.exists(idxfile):
logger.debug(idxfile+' found')
with open(idxfile,'r') as jsonfile:
json_content = json.load(jsonfile) # this is now in memory! you can use it outside 'open'
else:
logger.debug(idxfile+ ' not found')
json_content={}
json_content[os.path.basename(filename)] = os.path.basename(outputfile)
with open(idxfile,'w') as jsonfile:
json.dump(json_content, jsonfile, indent=4) # you decide the indentation level
logging.info(idxfile+' updated')
except Exception as e03:
logger.error(e03)
logger.error('error writing idx: '+idxfile)
del_with_retention(filename,2,'svg')
del_with_retention(filename,2,'png')
return

View File

@ -1,161 +0,0 @@
#*****************************************************************************************
# plot propagation heat maps
#*****************************************************************************************
__author__ = 'IU1BOW - Corrado'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import sys
import os
from datetime import datetime
import logging
import logging.config
import matplotlib.gridspec as gridspec
from qry import query_manager
from plotuty import saveplt
from matplotlib.colors import LogNorm
import json
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
file_output = '../static/plots/'+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
#load band file
with open('../cfg/bands.json') as json_bands:
band_frequencies = json.load(json_bands)
#load continent file
with open('../cfg/continents.json') as json_continents:
continents_cq = json.load(json_continents)
logger.info("Start")
logger.info("doing query...")
#construct bands query
bands_qry_string = 'CASE '
for i in range(len(band_frequencies["bands"])):
bands_qry_string+=' WHEN freq between '+str(band_frequencies["bands"][i]["min"])+' AND '+ str(band_frequencies["bands"][i]["max"])
bands_qry_string+=' THEN "'+band_frequencies["bands"][i]["id"]+'"'
#construct continent region query
spottercq_qry_string = 'CASE '
spotcq_qry_string = 'CASE '
for i in range(len(continents_cq["continents"])):
spottercq_qry_string+=' WHEN spottercq in('+continents_cq["continents"][i]["cq"]+')'
spottercq_qry_string+=' THEN "' +continents_cq["continents"][i]["id"]+'"'
spotcq_qry_string+=' WHEN spotcq in('+continents_cq["continents"][i]["cq"]+')'
spotcq_qry_string+=' THEN "' +continents_cq["continents"][i]["id"]+'"'
#construct final query string
qry_string ="""
SELECT
"""+spottercq_qry_string+""" ELSE spottercq END,
"""+spotcq_qry_string+""" ELSE spotcq END,
"""+bands_qry_string+""" END,
count(0) number
from spot
where
rowid > (select max(rowid) max_rowid from spot) - 5000 and
time > UNIX_TIMESTAMP()-3600
group by 1, 2, 3
;
"""
logger.debug(qry_string)
qm=query_manager()
qm.qry(qry_string)
data=qm.get_data()
logger.info("query done")
logger.debug (data)
#plot
if data is None or len(data)==0:
logger.warning("no data found")
sys.exit(1)
logger.info("plotting...")
#preparing data
continents=continents_cq["continents"]
bands=band_frequencies["bands"]
continents_ar=[]
for i, item_continent in enumerate(continents):
continents_ar.append(item_continent["id"])
bands_ar=[]
for i, item_band in enumerate(bands):
bands_ar.append(item_band["id"])
# fucntion for search continent in the global data returned by query and making a cartesian product
# in order to prepare data for heatmap
def filter_de(data_list,continent,continents_list, band_list):
data_filtered=[]
for i, item_data in enumerate(data_list):
if item_data[0]==continent and not (item_data[3] is None):
element=[]
element.append(item_data[1])
element.append(item_data[2])
element.append(item_data[3])
data_filtered.append(element)
cartesian_product = []
for j, item_continent in enumerate(continents_list):
for k, item_band in enumerate(band_list):
found=0
for lis, item_filtered in enumerate(data_filtered):
if item_filtered[0]==item_continent["id"] and item_filtered[1]==item_band["id"]:
cartesian_product.append(item_filtered)
found=1
if found==0:
element=[]
element.append(item_continent["id"])
element.append(item_band["id"])
element.append(0)
cartesian_product.append(element)
logger.debug("cartesian product for continent: "+continent)
logger.debug(cartesian_product)
return cartesian_product
#main
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M")
for i, item in enumerate(continents):
continent=item["id"]
data_de=filter_de(data,continent,continents,bands)
dx, band, number=zip(*data_de)
number_ar = []
for j in range(0, len(number), len(bands)):
number_ar.append(number[j : j+len(bands)])
logger.debug("heatmap:")
logger.debug(np.array(number_ar))
LOGMIN = 0.1
im = plt.imshow(np.array(number_ar), cmap='YlOrRd', interpolation='none', norm=LogNorm(vmin=10, vmax=35))
for spine in plt.gca().spines.values():
spine.set_visible(False)
plt.tick_params(top='off', bottom='off', left='off', right='off', labelleft='on', labelbottom='on')
plt.gca().set_xticks(np.arange(len(bands)))
plt.gca().set_yticks(np.arange(len(continents)))
plt.gca().set_yticklabels(continents_ar, fontsize=8)
plt.gca().set_xticklabels(bands_ar, fontsize=8)
plt.gca().set_xticks(np.arange(.5,len(bands),1),minor=True)
plt.gca().set_yticks(np.arange(.5,len(continents),1),minor=True)
plt.tick_params(axis='x', which='minor', colors='w')
plt.tick_params(axis='y', which='minor', colors='w')
plt.grid(which='minor', color='blue', linestyle=':', linewidth=1)
plt.text(.94,.90,'from '+continent, size=10, va="center", ha="center", transform=plt.gca().transAxes, rotation=-30, color='white',bbox=dict(boxstyle="Round", alpha=0.7))
plt.annotate('created on '+dt_string, (0,0), (0, -20), xycoords='axes fraction', textcoords='offset points', va='top', size=8, style='italic')
saveplt(plt,file_output+'_'+continent)
plt.clf()
logger.info("End")

View File

@ -1,98 +0,0 @@
#*****************************************************************************************
# plot qso per hour / band
#*****************************************************************************************
__author__ = 'IU1BOW - Corrado'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import sys
import os
from datetime import datetime
import logging
import logging.config
import json
import matplotlib.gridspec as gridspec
from qry import query_manager
from plotuty import saveplt
from matplotlib.colors import LogNorm
from calendar import monthrange
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
file_output = '../static/plots/'+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
#load band file
with open('../cfg/bands.json') as json_bands:
band_frequencies = json.load(json_bands)
logger.info("Start")
logger.info("doing query...")
#construct bands query
bands_qry_string = 'CASE '
for i in range(len(band_frequencies["bands"])):
bands_qry_string+=' WHEN freq between '+str(band_frequencies["bands"][i]["min"])+' AND '+ str(band_frequencies["bands"][i]["max"])
bands_qry_string+=' THEN "'+band_frequencies["bands"][i]["id"]+'"'
#construct bands query weight
bands_weight_qry_string = 'CASE '
for i in range(len(band_frequencies["bands"])):
bands_weight_qry_string+=' WHEN freq between '+str(band_frequencies["bands"][i]["min"])+' AND '+ str(band_frequencies["bands"][i]["max"])
bands_weight_qry_string+=' THEN "'+str(band_frequencies["bands"][i]["min"])+'"'
#construct final query string
qry_string ="""
select s1.hour, s1.band, s1.total from (
SELECT
cast(concat(HOUR (FROM_UNIXTIME(time))) as unsigned) as hour,
"""+bands_qry_string+""" ELSE "other" END as band,
cast("""+bands_weight_qry_string+""" ELSE 0 END as unsigned) as band_weight,
cast(round(count(0)/20) as unsigned) AS total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 1 MONTH)
and rowid > (select max(rowid)-500000 from spot)
group by 1, 2
) as s1
order by s1.band_weight
;
"""
logger.debug(qry_string)
qm=query_manager()
qm.qry(qry_string)
data=qm.get_data()
logger.info("query done")
logger.debug (data)
#plot
if data is None or len(data)==0:
logger.warning("no data found")
sys.exit(1)
logger.info("plotting...")
#main
x, y, z = zip(*data)
fig, ax = plt.subplots()
plt.suptitle("QSO per hour in last month")
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M")
plt.annotate('created on '+dt_string, (0,0), (0, -20), xycoords='axes fraction', textcoords='offset points', va='top', size=8, style='italic')
plt.xticks(rotation=90)
plt.xlabel("Hours")
plt.ylabel("QSO")
plt.grid(False)
plt.subplots_adjust(left=0.15)
plt.scatter(x, y, z, c=x, cmap='jet', edgecolors='darkslategray', alpha=0.5, )
plt.xticks(np.arange(1, 24, 2.0),rotation='horizontal')
saveplt(plt,file_output)
logger.info("End")

View File

@ -1,146 +0,0 @@
#*****************************************************************************************
# plot qso per months
#*****************************************************************************************
__author__ = 'IU1BOW - Corrado'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import sys
import os
from datetime import datetime
import logging
import logging.config
import matplotlib.gridspec as gridspec
from qry import query_manager
from plotuty import saveplt
from calendar import monthrange
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
file_output = '../static/plots/'+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
logger.info("Start")
logger.info("doing query...")
#construct final query string
qry_string="""
select month(s1.ym) as referring_month,
sum(
case
when YEAR(s1.ym)=YEAR(now())
then s1.total
else 0
end
) as current_year,
sum(
case
when YEAR(s1.ym)=YEAR(now())-1
then s1.total
else 0
end
) as one_year_ago,
sum(
case
when YEAR(s1.ym)=YEAR(now())-2
then s1.total
else 0
end
) as two_year_ago
from (
/* extract number of qso per year */
select
CAST(
CONCAT(
YEAR(FROM_UNIXTIME(time)),
'-',
right(concat('0',MONTH(FROM_UNIXTIME(time))),2),
'-',
'01'
)
AS DATE) as ym,
count(0) as total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 36 MONTH)
GROUP by 1
/*union used to initialize all months */
union select '1976-01-01', 0
union select '1976-02-01', 0
union select '1976-03-01', 0
union select '1976-04-01', 0
union select '1976-05-01', 0
union select '1976-06-01', 0
union select '1976-07-01', 0
union select '1976-08-01', 0
union select '1976-09-01', 0
union select '1976-10-01', 0
union select '1976-11-01', 0
union select '2019-12-01', 0
) as s1
group by referring_month
;
"""
logger.debug(qry_string)
qm=query_manager()
qm.qry(qry_string)
data=qm.get_data()
logger.info("query done")
logger.debug (data)
if data is None or len(data)==0:
logger.warning("no data found")
sys.exit(1)
logger.info("plotting...")
months, current_year, one_year_ago, two_year_ago = zip(*data)
#plt.style.use('seaborn-colorblind')
#plt.style.use('fast')
#plt.style.use('seaborn-bright')
plt.style.use('tableau-colorblind10')
fig, ax = plt.subplots()
ax.get_yaxis().set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
plt.suptitle("QSO per month")
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M")
plt.annotate('created on '+dt_string, (0,0), (0, -20), xycoords='axes fraction', textcoords='offset points', va='top', size=8, style='italic')
plt.xticks(rotation=90)
plt.xlabel("Months")
plt.ylabel("QSO")
plt.grid(False)
plt.subplots_adjust(left=0.15)
width=0.30
#plot current year and estimate trend of last month
current_year_lst = list(current_year)
le=len(current_year_lst)-1
day=datetime.today().day
month=datetime.today().month
year=datetime.today().year
days_of_month=monthrange(year,month)
current_year_lst[month-1]=int(current_year[month-1]/day*days_of_month[1])
plt.bar(months[month-1], tuple(current_year_lst),width=width, align='edge', color='lightsteelblue')
plt.bar(months,current_year,width=width, align='edge',label=year)
#plot previous year
#calculate position of bar of previous year
months1=[]
for i in months:
months1.append(i-width)
plt.bar(months1,one_year_ago,width=width, align='edge',label=year-1)
#plot two years ago
#calculate position of bar of two years ago
months2=[]
for i in months1:
months2.append(i-width)
plt.bar(months2,two_year_ago,width=width, align='edge',label=year-2)
#plot legend and set ticks frequency (every one month)
plt.legend()
plt.xticks(np.arange(min(months), max(months)+1, 1.0),rotation='horizontal')
saveplt(plt,file_output)
logger.info("End")

View File

@ -1,88 +0,0 @@
#**********************************************************************************
# plot qso trend
#**********************************************************************************
__author__ = 'IU1BOW - Corrado'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import sys
import os
from datetime import datetime
import logging
import logging.config
import matplotlib.gridspec as gridspec
import matplotlib.dates as mdates
from qry import query_manager
from plotuty import saveplt
import pandas as pd
from statsmodels.tsa.api import ExponentialSmoothing
#from statsmodels.tsa.holtwinters import SimpleExpSmoothing
import warnings
from statsmodels.tools.sm_exceptions import ConvergenceWarning
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
file_output = '../static/plots/'+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
logger.info("Start")
logger.info("doing query...")
#construct final query string
qry_string="""
select
FROM_UNIXTIME(time,'%Y-%m-%d') as day,
count(0) as total
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 60 MONTH)
GROUP by 1
;
"""
logger.debug(qry_string)
qm=query_manager()
qm.qry_pd(qry_string)
df=qm.get_data()
logger.info("query done")
logger.debug (df)
if df is None ==0:
logger.warning("no data found")
sys.exit(1)
logger.info("plotting...")
warnings.simplefilter('ignore', ConvergenceWarning)
df['day']=pd.to_datetime(df['day'])
df=df.set_index('day')
df=df.resample('D').interpolate(method='pad', limit_direction='forward', axis=0)
df=df.rolling('30D').mean()
y=df['total']
plt.style.use('tableau-colorblind10')
fig, ax = plt.subplots(figsize=(14,3))
plt.suptitle("QSO trend")
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M")
plt.annotate('created on '+dt_string, (0,0), (0, -20), xycoords='axes fraction', textcoords='offset points', va='top', size=8, style='italic')
plt.xlabel("Time")
plt.ylabel("QSO")
plt.margins(0)
plt.grid(True)
plt.grid(which='major', color='grey', linestyle=':', linewidth=1)
plt.subplots_adjust(left=0.15)
ax.get_yaxis().set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
ax.plot(y,marker='',linestyle='-',color='#4089F9',linewidth=1,label='observed')
ax.fill_between(df.index, y, facecolor='#4089F9', alpha=0.4)
model = ExponentialSmoothing(y, seasonal_periods =52, trend='add', seasonal='add', damped_trend=True)
#model = SimpleExpSmoothing(y, trend='add', seasonal='add', damped_trend=True)
fit=model.fit()
fcast = fit.forecast(40)
ax.plot(fcast,marker='',linestyle='-', color='#23B55E', linewidth=1, label='predicted')
logger.debug(fcast)
ax.fill_between(fcast.index,fcast,facecolor='#23B55E',alpha=0.4)
# Minor ticks every month.
fmt_month = mdates.MonthLocator()
ax.xaxis.set_minor_locator(fmt_month)
ax.legend()
saveplt(plt,file_output)
logger.info("End")

View File

@ -1,107 +0,0 @@
#*****************************************************************************************
# plot qso on a world map
#*****************************************************************************************
#https://datascientyst.com/plot-latitude-longitude-pandas-dataframe-python/
__author__ = 'IU1BOW - Corrado'
import matplotlib.pyplot as plt
import pandas as pd
import sys
import os
from datetime import datetime
import logging
import logging.config
import geopandas as gpd
from geopandas import GeoDataFrame
import matplotlib.pyplot as plt
from qry import query_manager
from plotuty import saveplt
from cty import prefix_table
logging.config.fileConfig("../cfg/plots_log_config.ini", disable_existing_loggers=False)
logger = logging.getLogger(__name__)
file_output = '../static/plots/'+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
logger.info("Start")
logger.info("doing query...")
#construct final query string
qry_string ="""
select spotcall as dx
from spot
WHERE FROM_UNIXTIME(time) > DATE_SUB(now(), INTERVAL 1 MONTH)
and rowid > (select max(rowid)-500000 from spot)
group by 1;
"""
logger.debug(qry_string)
qm=query_manager()
qm.qry(qry_string)
data=qm.get_data()
row_headers=qm.get_headers()
logger.info("query done")
del qm
logger.debug (data)
#plot
if data is None or len(data)==0:
logger.warning("no data found")
sys.exit(1)
#define country table for search info on callsigns
pfxt=prefix_table()
df = pd.DataFrame(columns=['row_id','dx','lat','lon'])
dx=[]
lat=[]
lon=[]
row_id=[]
idx=0
#count=[]
for result in data:
main_result=dict(zip(row_headers,result))
# find the country in prefix table
search_prefix=pfxt.find(main_result["dx"])
if search_prefix["country"] != "unknown country" :
# merge recordset and contry prefix
dx.append(main_result["dx"])
lon.append(float(search_prefix["lat"]))
lat.append(-float(search_prefix["lon"]))
idx+=1
row_id.append(idx)
del pfxt
df['dx']=dx
df['lat']=lat
df['lon']=lon
df['row_id']=row_id
df_grp=df.groupby(["lat", "lon"])["row_id"].count().reset_index(name="count")
logger.info("plotting...")
#main
plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1)
#fig, ax = plt.subplots(figsize = (18,6))
fig, ax = plt.subplots(figsize = (14,7))
plt.suptitle("World QSO in last month")
dt_string = datetime.now().strftime("%d/%m/%Y %H:%M")
plt.annotate('created on '+dt_string, (0,0), (0, -20), xycoords='axes fraction', textcoords='offset points', va='top', size=8, style='italic')
plt.grid(False)
plt.margins(0.0)
#water = 'lightskyblue'
#water = 'skyblue'
water = 'lightsteelblue'
earth = 'cornsilk'
ax.set_aspect('equal')
ax.set_facecolor(water)
geometry=gpd.points_from_xy(df_grp.lat, df_grp.lon)
gdf = GeoDataFrame(df_grp, geometry=geometry)
world= gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
world.plot(ax=ax,color=earth,edgecolor='grey')
gdf.plot(column='count', ax=ax,alpha=0.2, markersize=gdf['count'])
logger.debug(gdf.head)
saveplt(plt,file_output)
logger.info("End")
#os._exit(0)

2
log/.gitignore vendored
View File

@ -0,0 +1,2 @@
*
!.gitignore

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,29 @@
attrs==22.1.0
certifi @ file:///opt/conda/conda-bld/certifi_1655968806487/work/certifi
charset-normalizer==2.1.0
charset-normalizer==2.1.1
click==8.1.3
click-plugins==1.1.1
cligj==0.7.2
contourpy==1.0.5
cycler==0.11.0
docopt-ng==0.8.1
easywatch==0.0.5
Fiona==1.8.21
Flask==2.2.1
Flask-Minify==0.39
Flask==2.2.2
Flask-Minify==0.41
Flask-WTF==1.0.1
fonttools==4.34.4
geopandas==0.11.1
htmlmin==0.1.12
idna==3.3
importlib-metadata==4.12.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
jsmin==3.0.1
kiwisolver==1.4.4
lesscpy==0.15.0
lesscpy==0.15.1
MarkupSafe==2.1.1
matplotlib==3.6.0
munch==2.5.0
mysql-connector-python==8.0.30
numpy==1.23.1
packaging==21.3
pandas==1.4.3
patsy==0.5.2
Pillow==9.2.0
mysql-connector-python==8.0.31
numpy==1.24.1
pandas==1.5.2
ply==3.11
protobuf==3.20.2
pyparsing==3.0.9
pyproj==3.3.1
protobuf==3.20.1
python-dateutil==2.8.2
pytz==2022.1
pytz==2022.7
rcssmin==1.1.1
requests==2.28.1
scipy==1.9.0
Shapely==1.8.2
six==1.16.0
staticjinja==4.1.3
statsmodels==0.13.2
urllib3==1.26.11
watchdog==2.1.9
Werkzeug==2.2.1
urllib3==1.26.13
watchdog==2.2.0
Werkzeug==2.2.2
WTForms==3.0.1
xxhash==3.0.0
zipp==3.8.1
xxhash==3.1.0

View File

@ -22,7 +22,6 @@ changelog=${path_docs}'/'CHANGELOG.md
#fi
echo 'get version from git'
#ver=`git describe --tags --abbrev=0`
if ! ver=$(git describe --tags --abbrev=0)
then
echo 'ERROR on get version from git'
@ -71,7 +70,6 @@ then
fi
echo 'generating static pages...'
#staticjinja build --srcpath=${path_static_html}/templates/ --outpath=${path_static_html}/ --log=info
if ! python ../lib/static_build.py
then
echo 'ERROR generating static pages'
@ -84,6 +82,11 @@ then
echo 'ERROR wrinting requirements'
exit 60
fi
echo 'remove some packages...'
sed -i '/certifi==/d' ../requirements.txt
sed -i '/staticjinja==/d' ../requirements.txt
#used to minify the application javascript
echo 'minify javascripts...'
shopt -s extglob
@ -98,7 +101,12 @@ do
shopt -u extglob
exit 80
fi
sleep 5
if [ ! -s "${path_static_js}/${file_no_ext}.min.js" ]; then
echo "File is empty"
shopt -u extglob
exit 81
fi
sleep 3
done
#used to minify css
echo 'minify css...'
@ -111,9 +119,14 @@ do
then
echo 'ERROR minifying css: '${i}
shopt -u extglob
exit 80
exit 90
fi
sleep 5
if [ ! -s "${path_static_css}/${file_no_ext}.min.css" ]; then
echo "File is empty"
shopt -u extglob
exit 91
fi
sleep 3
done
shopt -u extglob
echo

View File

@ -0,0 +1,61 @@
#!/bin/bash
#-------------------------------------------------------------
# script for creating a test database
#-------------------------------------------------------------
chr() {
local ascii=$(echo $1 | awk '{ printf("%c",$0); }')
echo ${ascii}
}
db_insert () {
n=10000
for (( i=1; i<=${n}; i++ ))
do
freq=$(shuf -i 100-50000 -n 1)
spotdxcc=$(shuf -i 1-500 -n 1)
spotterdxcc=$(shuf -i 1-500 -n 1)
spotitu=$(shuf -i 1-90 -n 1)
spotcq=$(shuf -i 1-40 -n 1)
spotteritu=$(shuf -i 1-90 -n 1)
spottercq=$(shuf -i 1-40 -n 1)
#for epoc use https://www.epochconverter.com/
timestamp=$(shuf -i 1609425866-1672497882 -n 1)
cs_letter_1=$(chr $(shuf -i 65-90 -n1))
cs_letter_2=$(chr $(shuf -i 65-90 -n1))
cs_number=$(shuf -i 1-2 -n 1)
callsign=${cs_letter_1}${cs_letter_2}${cs_number}DUMMY
#callsign=IU1BDX
#current timestamp
#sudo mysql -uroot dxcluster -e "INSERT INTO spot VALUES (${i},${freq},'IU1BOX',UNIX_TIMESTAMP(),'DUMMY TEST','IU1BOW',${spotdxcc},${spotterdxcc},'IU1BOW-2',${spotitu},${spotcq},${spotteritu},${spottercq},NULL,NULL,'5.198.229.129');"
#using random timestamp
sudo mysql -uroot dxcluster -e "INSERT INTO spot VALUES (${i},${freq},'${callsign}',${timestamp},'DUMMY TEST','IU1BOW',${spotdxcc},${spotterdxcc},'IU1BOW-2',${spotitu},${spotcq},${spotteritu},${spottercq},NULL,NULL,'5.198.229.129');"
#sudo mysql -uroot dxcluster -e "INSERT INTO spot VALUES (${i},${freq},'${callsign}',UNIX_TIMESTAMP(),'DUMMY TEST','IU1BOW',${spotdxcc},${spotterdxcc},'IU1BOW-2',${spotitu},${spotcq},${spotteritu},${spottercq},NULL,NULL,'5.198.229.129');"
#sleep 0.5
p=$(( ${i}*100/${n} ))
echo -ne ${p}'% \r'
done
echo -ne '\n'
}
bold=$(tput bold)
normal=$(tput sgr0)
echo "${bold}WARNING${normal}: this command will drop your dxcluster database"
echo "Run it only on test environment!!!"
while true; do
read -p "Would you procede? " yn
case $yn in
[Yy]* ) sudo mysql -uroot <dxcluster_schema_for_test.sql;db_insert;break;;
[Nn]* ) exit;;
* ) echo "Please answer yes or no.";;
esac
done

View File

@ -0,0 +1,71 @@
-------------------------------------------------------------
-- script for creating a test database
-------------------------------------------------------------
drop database IF EXISTS dxcluster;
create database dxcluster;
grant all privileges on dxcluster.* to webdb@localhost identified by 'pswd';
use dxcluster;
-- MySQL dump 10.16 Distrib 10.1.48-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: dxcluster
-- ------------------------------------------------------
-- Server version 10.1.48-MariaDB-0ubuntu0.18.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `spot`
--
DROP TABLE IF EXISTS `spot`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `spot` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`freq` double NOT NULL,
`spotcall` varchar(14) NOT NULL,
`time` int(11) NOT NULL,
`comment` varchar(255) DEFAULT NULL,
`spotter` varchar(14) NOT NULL,
`spotdxcc` smallint(6) DEFAULT NULL,
`spotterdxcc` smallint(6) DEFAULT NULL,
`origin` varchar(14) DEFAULT NULL,
`spotitu` tinyint(4) DEFAULT NULL,
`spotcq` tinyint(4) DEFAULT NULL,
`spotteritu` tinyint(4) DEFAULT NULL,
`spottercq` tinyint(4) DEFAULT NULL,
`spotstate` char(2) DEFAULT NULL,
`spotterstate` char(2) DEFAULT NULL,
`ipaddr` varchar(40) DEFAULT NULL,
PRIMARY KEY (`rowid`),
KEY `spot_ix1` (`time`),
KEY `spot_ix2` (`spotcall`),
KEY `spiderweb_spotter` (`spotter`)
) ENGINE=InnoDB AUTO_INCREMENT=9283036 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
---INSERT INTO `spot` VALUES (1,18123,'IU1BOX',1578091140,NULL,'IU1BOW',85,85,'IU1BOW-2',28,15,28,15,NULL,NULL,'5.198.229.129');
---INSERT INTO `spot` VALUES (1,18123,'IU1BOX',UNIX_TIMESTAMP(),NULL,'IU1BOW',85,85,'IU1BOW-2',28,15,28,15,NULL,NULL,'5.198.229.129');

View File

@ -1,4 +0,0 @@
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
echo Absolute path: ${DIR}
cd ${DIR}
python3 ../lib/propagation_heatmaps.py

View File

@ -1,4 +0,0 @@
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
echo Absolute path: ${DIR}
cd ${DIR}
python3 ../lib/qso_hour_band.py

View File

@ -1,4 +0,0 @@
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
echo Absolute path: ${DIR}
cd ${DIR}
python3 ../lib/qso_months.py

View File

@ -1,5 +0,0 @@
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
echo Absolute path: ${DIR}
cd ${DIR}
python3 ../lib/qso_trend.py

View File

@ -1,4 +0,0 @@
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
echo Absolute path: ${DIR}
cd ${DIR}
python3 ../lib/qso_world_map.py

View File

@ -1,4 +1,11 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css");
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.1/font/bootstrap-icons.css");
@font-face {
font-display: swap;
font-family: 'bootstrap-icons';
}
.badge-responsive {
width: 70px;
}
@ -10,7 +17,6 @@
.badge-responsive {
width: 40px;
}
}
@media screen and (max-width: 768px) {
@ -31,12 +37,10 @@
}
.img-flag {
// width: 80%;
background-color: white;
// box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
border: 1px solid #ddd;
border-radius: 2px;
padding: 3px;
background-color: white;
border: 1px solid #ddd;
border-radius: 2px;
padding: 3px;
background-size:cover !important;
max-width: auto;
max-height: auto;
@ -85,3 +89,40 @@ span.search-callsign {
#band {
margin-top: 5px;
}
#dashboard {
padding:10px;
gap:10px;
}
#telnet-thead {
position: sticky;top: 0
}
#form-band_activity {
width: 600px; height: 500px;
}
#chart-band_activity {
width:100%; height:400px
}
#chart-world_dx_spots_live {
width: 600px; height: 500px; gap: 0px;
}
#chart-hour_band {
width: 600px; height: 500px;
}
#silo-propagation-img {
width: 480px; height: 400px;
}
#chart-dx_spots_x_month {
width: 600px; height: 400px;
}
#chart-dx_spots_trend {
width: 700px; height: 400px;
}

View File

@ -1 +1 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css");.badge-responsive{width:70px}@media screen and (max-width:768px){.text-responsive{font-size:11px}.badge-responsive{width:40px}}@media screen and (max-width:768px){#collapseFilters.collapsing{position:absolute!important;z-index:20}#collapseFilters.collapse.show{display:block;position:absolute;z-index:20}.navbar-collapse{max-height:none!important}}.img-flag{// width:80%;background-color:white;// box-shadow:0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);border:1px solid #ddd;border-radius:2px;padding:3px;background-size:cover!important;max-width:auto;max-height:auto;height:19px!important;width:32px!important;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);-moz-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);-o-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5)}.ipcs{background-image:url("/static/images/background.webp");background-size:cover;background-repeat:no-repeat}.copyleft{display:inline-block;transform:rotate(180deg)}span.search-callsign{background:url(/static/images/search-callsign.svg) no-repeat top left;background-size:contain;cursor:pointer;display:inline-block;height:16px;width:20px}#input-group-callsign{margin-right:1rem;margin-bottom:.5rem}#collapseFilters{margin-top:10px;background-color:#dde2e6}#spotsTable{margin-top:10px}#band{margin-top:5px}
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.1/font/bootstrap-icons.css");@font-face{font-display:swap;font-family:'bootstrap-icons'}.badge-responsive{width:70px}@media screen and (max-width:768px){.text-responsive{font-size:11px}.badge-responsive{width:40px}}@media screen and (max-width:768px){#collapseFilters.collapsing{position:absolute!important;z-index:20}#collapseFilters.collapse.show{display:block;position:absolute;z-index:20}.navbar-collapse{max-height:none!important}}.img-flag{background-color:white;border:1px solid #ddd;border-radius:2px;padding:3px;background-size:cover!important;max-width:auto;max-height:auto;height:19px!important;width:32px!important;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);-moz-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);-o-box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5);box-shadow:0 2px 10px rgba(0,0,0,.5),0 2px 3px rgba(0,0,0,.5)}.ipcs{background-image:url("/static/images/background.webp");background-size:cover;background-repeat:no-repeat}.copyleft{display:inline-block;transform:rotate(180deg)}span.search-callsign{background:url(/static/images/search-callsign.svg) no-repeat top left;background-size:contain;cursor:pointer;display:inline-block;height:16px;width:20px}#input-group-callsign{margin-right:1rem;margin-bottom:.5rem}#collapseFilters{margin-top:10px;background-color:#dde2e6}#spotsTable{margin-top:10px}#band{margin-top:5px}#dashboard{padding:10px;gap:10px}#telnet-thead{position:sticky;top:0}#form-band_activity{width:600px;height:500px}#chart-band_activity{width:100%;height:400px}#chart-world_dx_spots_live{width:600px;height:500px;gap:0}#chart-hour_band{width:600px;height:500px}#silo-propagation-img{width:480px;height:400px}#chart-dx_spots_x_month{width:600px;height:400px}#chart-dx_spots_trend{width:700px;height:400px}

1
static/data/world.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -7,10 +7,6 @@
<meta http-equiv="Content-Security-Policy"
content="
script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline'
">
<meta charset="utf-8">
<meta name="description" content="Web Ham Radio DX Cluster and spot search">
<meta name="keywords" content="ham radio, dx cluster, dx spots, cluster sposts,web dx cluster,dx cluster search, DX spots">
@ -21,10 +17,13 @@
<link rel="apple-touch-icon" href="/static/images/icons/icon-apple.png">
<link rel="manifest" href="/static/manifest.webmanifest">
<link rel="stylesheet" href="/static/css/style.min.css">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" as="style" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous" onload="this.rel='stylesheet'">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" as="style" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous" onload="this.rel='stylesheet' ">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"></noscript>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.4/css/flag-icons.min.css" as="style" integrity="sha384-TeDUCuZ+Uyp1Vv0n275nnm//ANAlP5GFHCnSF4iiAdrYmBZMM6syYgykpq4kGTqL" crossorigin="anonymous" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.4/css/flag-icons.min.css"></noscript>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.6/css/flag-icons.min.css" as="style" integrity="sha512-uvXdJud8WaOlQFjlz9B15Yy2Au/bMAvz79F7Xa6OakCl2jvQPdHD0hb3dEqZRdSwG4/sknePXlE7GiarwA/9Wg==" crossorigin="anonymous" onload="this.rel='stylesheet'" >
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.6/css/flag-icons.min.css"></noscript>
<script src="static/js/load_css.min.js"></script>
@ -91,17 +90,17 @@
<span class="copyleft">&copy;</span> Copyleft:
<span id="copyDate"></span>
<a href="https://github.com/coulisse/spiderweb/" target="blank" rel="noopener">IU1BOW Spiderweb</a>
<span id="version">v.2.3.4</span>
<span id="version">v2.4</span>
</div>
</footer>
<script async src="static/js/clock.min.js"></script>
<script async src="static/js/copy_date.min.js"></script>
<script async src="static/js/load-sw.min.js"></script>
<script>
<script nonce="sedfGFG32xs">
</script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js" integrity="sha512-6ORWJX/LrnSjBzwefdNUyLCMTIsGoNP6NftMy2UAm1JBm6PRZCO1d7OHBStWpVFZLO+RerTvqX/Z9mBFfCJZ4A==" crossorigin="anonymous"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js" integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
<script async src="static/js/callsign_search.min.js"></script>

View File

@ -1,64 +0,0 @@
/**
* Build the html plots
*
* @param selector {string} The html identifier where put the plots
* @param data {array} List of the plots to show
*/
function buildHtmlPlots(selector,data) {
$(selector).empty();
//bands activity
var contBandsActivity$=$('<div class="container justify-content-center"/>');
contBandsActivity$.append($('<h3 class="text-center"/>').html('Band Activity'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_AF']+'.png" alt="propagation heatmap AF" srcset="/static/plots/'+data['propagation_heatmaps_AF']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_AN']+'.png" alt="propagation heatmap AN" srcset="/static/plots/'+data['propagation_heatmaps_AN']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_AS']+'.png" alt="propagation heatmap AS" srcset="/static/plots/'+data['propagation_heatmaps_AS']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_EU']+'.png" alt="propagation heatmap EU" srcset="/static/plots/'+data['propagation_heatmaps_EU']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_NA']+'.png" alt="propagation heatmap NA" srcset="/static/plots/'+data['propagation_heatmaps_NA']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_OC']+'.png" alt="propagation heatmap OC" srcset="/static/plots/'+data['propagation_heatmaps_OC']+'.svg">'));
contBandsActivity$.append($('<img class="img-fluid" src="/static/plots/'+data['propagation_heatmaps_SA']+'.png" alt="propagation heatmap SA" srcset="/static/plots/'+data['propagation_heatmaps_SA']+'.svg">'));
$(selector).append(contBandsActivity$);
//qso per months
$(selector).append($('<hr>'));
var contQSO$=$('<div class="container justify-content-center"/>');
contQSO$.append($('<img class="img-fluid" src="/static/plots/'+data['qso_months']+'.png" alt="Qso per months" srcset="/static/plots/'+data['qso_months']+'.svg">'));
$(selector).append(contQSO$);
//qso per bands and hour in last month
contQSO$.append($('<img class="img-fluid" src="/static/plots/'+data['qso_hour_band']+'.png" alt="Qso per hour/band" srcset="/static/plots/'+data['qso_hour_band']+'.svg">'));
$(selector).append(contQSO$);
//qso in the world and hour in last month
contQSO$.append($('<img class="img-fluid" src="/static/plots/'+data['qso_world_map']+'.png" alt="Qso per hour/band" srcset="/static/plots/'+data['qso_world_map']+'.svg">'));
$(selector).append(contQSO$);
//qso trend
contQSO$.append($('<img class="img-fluid" src="/static/plots/'+data['qso_trend']+'.png" alt="Qso trend" srcset="/static/plots/'+data['qso_trend']+'.svg">'));
$(selector).append(contQSO$);
}
/**
* Timer for refresh the plot page
*/
function plotsTimer() {
var request = new XMLHttpRequest()
request.open('GET','plotlist',true)
request.onload = function(){
try {
buildHtmlPlots('#plotlist',JSON.parse(this.response));
} catch (err) {
console.log(err);
console.log(err.stack);
}
}
request.send()
}
/*
* script loaded inline page in order to prepare data
* for next operations
*/
buildHtmlPlots('#plotlist',payload_json);
setInterval(plotsTimer, timer_interval_json);

View File

@ -1,5 +0,0 @@
function buildHtmlPlots(c,a){$(c).empty();var b=$('<div class="container justify-content-center"/>');b.append($('<h3 class="text-center"/>').html("Band Activity"));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_AF+'.png" alt="propagation heatmap AF" srcset="/static/plots/'+a.propagation_heatmaps_AF+'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_AN+'.png" alt="propagation heatmap AN" srcset="/static/plots/'+a.propagation_heatmaps_AN+
'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_AS+'.png" alt="propagation heatmap AS" srcset="/static/plots/'+a.propagation_heatmaps_AS+'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_EU+'.png" alt="propagation heatmap EU" srcset="/static/plots/'+a.propagation_heatmaps_EU+'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_NA+'.png" alt="propagation heatmap NA" srcset="/static/plots/'+
a.propagation_heatmaps_NA+'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_OC+'.png" alt="propagation heatmap OC" srcset="/static/plots/'+a.propagation_heatmaps_OC+'.svg">'));b.append($('<img class="img-fluid" src="/static/plots/'+a.propagation_heatmaps_SA+'.png" alt="propagation heatmap SA" srcset="/static/plots/'+a.propagation_heatmaps_SA+'.svg">'));$(c).append(b);$(c).append($("<hr>"));b=$('<div class="container justify-content-center"/>');b.append($('<img class="img-fluid" src="/static/plots/'+
a.qso_months+'.png" alt="Qso per months" srcset="/static/plots/'+a.qso_months+'.svg">'));$(c).append(b);b.append($('<img class="img-fluid" src="/static/plots/'+a.qso_hour_band+'.png" alt="Qso per hour/band" srcset="/static/plots/'+a.qso_hour_band+'.svg">'));$(c).append(b);b.append($('<img class="img-fluid" src="/static/plots/'+a.qso_world_map+'.png" alt="Qso per hour/band" srcset="/static/plots/'+a.qso_world_map+'.svg">'));$(c).append(b);b.append($('<img class="img-fluid" src="/static/plots/'+a.qso_trend+
'.png" alt="Qso trend" srcset="/static/plots/'+a.qso_trend+'.svg">'));$(c).append(b)}function plotsTimer(){var c=new XMLHttpRequest;c.open("GET","plotlist",!0);c.onload=function(){try{buildHtmlPlots("#plotlist",JSON.parse(this.response))}catch(a){console.log(a),console.log(a.stack)}};c.send()}buildHtmlPlots("#plotlist",payload_json);setInterval(plotsTimer,timer_interval_json);

View File

@ -0,0 +1,190 @@
/********************************************************************************
* javascript used to build band_activity chart
* ******************************************************************************/
class band_activity {
/**
* refresh and populate chart
*
* @param {String} my_cart The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
* @param {string} region This is the continent name (EU,AF,NA...) of the selected
* @param {Json} bands The frequency band list (160, 80, 60... UHF, SHF)
* @param {Json} continents The continent list( EU, SA, ...)
*/
refresh(my_chart, end_point, region, bands, continents){
// Asynchronous Data Loading
$.getJSON(end_point+'?continent='+region).done(function(data) {
// Fill in the data
var last_refresh=get_last_refresh(data);
var dataMap=Array.from(data["band activity"]).map(function (item) {
return [item[1], item[0], item[2] || '-'];
});
//options
my_chart.setOption({
tooltip: {
position: 'top',
formatter: function (p) {
var format = p.seriesName +' on ' + p.name +' band: '+'<strong>'+p.data[2]+'</strong>';
return format;
}
},
title: {
text:"Band activity",
subtext: last_refresh,
top: 'top',
left:'left'
},
toolbox: {
show: true,
showTitle: false,
orient: 'vertical',
right: 'right',
top : 'bottom',
feature: {
mark: { show: true },
dataView: { show: true, readOnly: true },
restore: { show: true },
saveAsImage: { show: true }
}
},
grid: {
height: '80%',
left: 25,
top: 50,
right: 60,
bottom: 0,
show: true,
backgroundColor: 'rgb(255, 255, 255)',
},
xAxis: {
type: 'category',
data: bands,
axisTick: {
show: true,
},
axisLine: {
show: false,
},
splitArea: {
show: true
}
},
yAxis: {
type: 'category',
data: continents,
axisTick: {
show: true,
},
axisLine: {
show: false,
},
splitArea: {
show: true
}
},
visualMap: {
calculable: true,
orient: 'vertical',
right: 'right',
top: 'center',
min: 0,
max: 30,
inRange : {
color: ['#ffffe6','yellow','red']
}
},
series: [
{
name: 'Spots',
type: 'heatmap',
data: dataMap,
label: {
show: false
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(100, 0, 0, 0.5)'
}
}
}
]
});
});
};
/**
* Chart creator
*
* @constructor
* @param {String} chart_id The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
* @param {Json} cont_cq The continent list( EU, SA, ...)
* @param {Json} band_freq The frequency band list (160, 80, 60... UHF, SHF)
*/
constructor(chart_id, end_point, cont_cq, band_freq) {
// Initialize the echarts instance based on the prepared dom
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
//populate continents array
var continents=[];
cont_cq.forEach(function myFunction(item, index) {
continents[index]=item['id']
});
//populate bands array
var bands=[];
band_freq.forEach(function myFunction(item, index) {
bands[index]=item['id']
});
//managing region
var selectedContinent=getCookie("user_region");
var selectedContinent_desc=getCookie("user_region_desc");
if (!selectedContinent) {
selectedContinent="EU";
selectedContinent_desc="Europe";
setCookie("user_region",selectedContinent,60);
setCookie("user_region_desc",selectedContinent_desc,60);
};
$('select').val(selectedContinent).change();
$('select').on('change', function() {
selectedContinent=this.value;
selectedContinent_desc=$(this).find("option:selected").text()
setCookie("user_region",selectedContinent,60);
setCookie("user_region_desc",selectedContinent_desc,60);
plot_ba.refresh(myChart, end_point, selectedContinent,bands,continents);
$('#txt_continent').text('\xa0 Based on DX SPOTS from stations in '+ selectedContinent_desc +' during the last 15 minutes, displayed by Continent and Band');
});
$('#txt_continent').text('\xa0 Based on DX SPOTS from stations in '+ selectedContinent_desc +' during the last 15 minutes, displayed by Continent and Band');
this.refresh(myChart, end_point, selectedContinent,bands,continents);
/* resize chart*/
var chart = echarts.init(document.querySelector("#"+chart_id), null);
window.addEventListener('resize',function(){
chart.resize();
})
}
}
//create object
let plot_ba = new band_activity ('chart-band_activity','/plot_get_heatmap_data',continents_cq,band_frequencies);
/*setInterval(function(){doRefresh('chart-band_activity','/plot_get_heatmap_data')}, 5000);
function doRefresh(chart_id,end_point){
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
plot_dst.refresh(myChart,end_point);
};
*/

10
static/js/plot_band_activity.min.js vendored Normal file
View File

@ -0,0 +1,10 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,c,b){a!=Array.prototype&&a!=Object.prototype&&(a[c]=b.value)};
$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,a];for(var c=0;c<a.length;++c){var b=a[c];if(b&&b.Math==Math)return b}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(a,c,b,d){if(c){b=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];e in b||(b[e]={});b=b[e]}a=a[a.length-1];d=b[a];c=c(d);c!=d&&null!=c&&$jscomp.defineProperty(b,a,{configurable:!0,writable:!0,value:c})}};
$jscomp.polyfill("Array.from",function(a){return a?a:function(a,b,d){b=null!=b?b:function(a){return a};var c=[],f="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];if("function"==typeof f){a=f.call(a);for(var g=0;!(f=a.next()).done;)c.push(b.call(d,f.value,g++))}else for(f=a.length,g=0;g<f;g++)c.push(b.call(d,a[g],g));return c}},"es6","es3");
$jscomp.findInternal=function(a,c,b){a instanceof String&&(a=String(a));for(var d=a.length,e=0;e<d;e++){var f=a[e];if(c.call(b,f,e,a))return{i:e,v:f}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(a,b){return $jscomp.findInternal(this,a,b).v}},"es6","es3");
var band_activity=function(a,c,b,d){var e=document.getElementById(a),f=echarts.init(e),g=[];b.forEach(function(a,b){g[b]=a.id});var l=[];d.forEach(function(a,b){l[b]=a.id});var h=getCookie("user_region"),k=getCookie("user_region_desc");h||(h="EU",k="Europe",setCookie("user_region",h,60),setCookie("user_region_desc",k,60));$("select").val(h).change();$("select").on("change",function(){h=this.value;k=$(this).find("option:selected").text();setCookie("user_region",h,60);setCookie("user_region_desc",k,
60);plot_ba.refresh(f,c,h,l,g);$("#txt_continent").text("  Based on DX SPOTS from stations in "+k+" during the last 15 minutes, displayed by Continent and Band")});$("#txt_continent").text("  Based on DX SPOTS from stations in "+k+" during the last 15 minutes, displayed by Continent and Band");this.refresh(f,c,h,l,g);var m=echarts.init(document.querySelector("#"+a),null);window.addEventListener("resize",function(){m.resize()})};
band_activity.prototype.refresh=function(a,c,b,d,e){$.getJSON(c+"?continent="+b).done(function(b){var c=get_last_refresh(b);b=Array.from(b["band activity"]).map(function(a){return[a[1],a[0],a[2]||"-"]});a.setOption({tooltip:{position:"top",formatter:function(a){return a.seriesName+" on "+a.name+" band: <strong>"+a.data[2]+"</strong>"}},title:{text:"Band activity",subtext:c,top:"top",left:"left"},toolbox:{show:!0,showTitle:!1,orient:"vertical",right:"right",top:"bottom",feature:{mark:{show:!0},dataView:{show:!0,
readOnly:!0},restore:{show:!0},saveAsImage:{show:!0}}},grid:{height:"80%",left:25,top:50,right:60,bottom:0,show:!0,backgroundColor:"rgb(255, 255, 255)"},xAxis:{type:"category",data:d,axisTick:{show:!0},axisLine:{show:!1},splitArea:{show:!0}},yAxis:{type:"category",data:e,axisTick:{show:!0},axisLine:{show:!1},splitArea:{show:!0}},visualMap:{calculable:!0,orient:"vertical",right:"right",top:"center",min:0,max:30,inRange:{color:["#ffffe6","yellow","red"]}},series:[{name:"Spots",type:"heatmap",data:b,
label:{show:!1},emphasis:{itemStyle:{shadowBlur:10,shadowColor:"rgba(100, 0, 0, 0.5)"}}}]})})};var plot_ba=new band_activity("chart-band_activity","/plot_get_heatmap_data",continents_cq,band_frequencies);

119
static/js/plot_common.js Normal file
View File

@ -0,0 +1,119 @@
/**
* Create a cookie
*
* @param {String} cname cookie name
* @param {string} cvalue cookie value
* @param {integer} exdays the number of the days for cookie expiration
*/
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/" + ";Samesite=Strict;Secure=True";
};
/**
* get a cookie
*
* @param {String} cname cookie name
* @returns {string} cookie value
*/
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
/**
* format the data refresh string for exhibit on charts
*
* @param {String} date date of the last refresh
* @returns {string} string formatted
*/
function get_last_refresh(data){
var dt_refresh = new Date(0); // The 0 there is the key, which sets the date to the epoch
var dt_refresh;
dt_refresh.setUTCSeconds(data["last_refresh"]);
const pad = (n,s=2) => (`${new Array(s).fill(0)}${n}`).slice(-s);
var hours = pad (dt_refresh.getHours());
var minutes = pad(dt_refresh.getMinutes());
var months_names = get_months_names();
var month = months_names[dt_refresh.getMonth()];
var day = dt_refresh.getDate();
var year = dt_refresh.getFullYear();
var last_refresh = "Data refresh: "+day+" of "+month+" "+year+" at "+hours+":"+minutes;
return last_refresh;
}
/**
* format the data refresh string for exhibit on charts
*
* @returns {Array} list of months as short names
*/
function get_months_names() {
var months_name=[];
for (let monthNumber = 1; monthNumber < 13; monthNumber++) {
const date = new Date();
date.setMonth(monthNumber - 1);
months_name.push(date.toLocaleString('en-US', { month: 'short' }));
};
return months_name;
}
/**
* format the data refresh string for exhibit on charts
*
* @param {Integer} num Number to be formatted
* @returns {string} Number formatted with K for thousands and M for millions
*/
function format_u_k_m(num) {
let label;
let sign=1;
if (num<0){
sign=-1;
}
let abs_num=Math.abs(num);
if (abs_num==0) {
label=abs_num
} else {
if (abs_num>0 && abs_num <1000) {
label=abs_num*sign;
} else {
if (abs_num>=1000 && abs_num < 1000000) {
label=abs_num/1000*sign+"K";
} else {
if (abs_num>=1000000) {
label=abs_num/1000000*sign+"M";
}
}
}
}
return label;
}
/*
function doRefresh(){
var chartDom = document.getElementById('chart-dx_spots_trend');
var myChart = echarts.init(chartDom);
plot_dst.refresh(myChart,'/plot_get_dx_spots_trend');
};
setInterval(function(){doRefresh()}, 5000);
*/

6
static/js/plot_common.min.js vendored Normal file
View File

@ -0,0 +1,6 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(a){return a.raw=a};$jscomp.createTemplateTagFirstArgWithRaw=function(a,b){a.raw=b;return a};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};
$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,a];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(a,b,c,d){if(b){c=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];e in c||(c[e]={});c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})}};$jscomp.polyfill("Array.prototype.fill",function(a){return a?a:function(a,c,d){var b=this.length||0;0>c&&(c=Math.max(0,b+c));if(null==d||d>b)d=b;d=Number(d);0>d&&(d=Math.max(0,b+d));for(c=Number(c||0);c<d;c++)this[c]=a;return this}},"es6","es3");
function setCookie(a,b,c){var d=new Date;d.setTime(d.getTime()+864E5*c);c="expires="+d.toUTCString();document.cookie=a+"="+b+";"+c+";path=/;Samesite=Strict;Secure=True"}function getCookie(a){a+="=";for(var b=decodeURIComponent(document.cookie).split(";"),c=0;c<b.length;c++){for(var d=b[c];" "==d.charAt(0);)d=d.substring(1);if(0==d.indexOf(a))return d.substring(a.length,d.length)}return""}
function get_last_refresh(a){var b=new Date(0);b.setUTCSeconds(a.last_refresh);var c=function(a,b){b=void 0===b?2:b;return(""+Array(b).fill(0)+a).slice(-b)};a=c(b.getHours());c=c(b.getMinutes());var d=get_months_names()[b.getMonth()],e=b.getDate();b=b.getFullYear();return"Data refresh: "+e+" of "+d+" "+b+" at "+a+":"+c}function get_months_names(){for(var a=[],b=1;13>b;b++){var c=new Date;c.setMonth(b-1);a.push(c.toLocaleString("en-US",{month:"short"}))}return a}
function format_u_k_m(a){var b,c=1;0>a&&(c=-1);a=Math.abs(a);0==a?b=a:0<a&&1E3>a?b=a*c:1E3<=a&&1E6>a?b=a/1E3*c+"K":1E6<=a&&(b=a/1E6*c+"M");return b};

View File

@ -0,0 +1,139 @@
/********************************************************************************
* javascript used to build dx spots per month chart
* ******************************************************************************/
class dx_spots_per_month {
/**
* refresh and populate chart
*
* @param {String} my_cart The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
refresh(my_chart,end_point) {
// Asynchronous Data Loading
$.getJSON(end_point).done(function(data) {
// Fill in the data
var last_refresh=get_last_refresh(data);
var year_now = new Date().getFullYear();
var year_0 = (year_now - 0).toString();
var year_1 = (year_now - 1).toString();
var year_2 = (year_now - 2).toString();
var months_name = get_months_names();
var dataMap_year_0=[];
var dataMap_year_1=[];
var dataMap_year_2=[];
for (let i = 1; i < 13; i++) {
dataMap_year_0.push(data.spots_per_month[i].year_0);
dataMap_year_1.push(data.spots_per_month[i].year_1);
dataMap_year_2.push(data.spots_per_month[i].year_2);
}
//options
my_chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
title: {
text: "DX SPOTS per month",
subtext: last_refresh,
top: 'top',
left:'left'
},
legend: {
data: [year_2, year_1, year_0],
bottom: "bottom"
},
toolbox: {
show: true,
showTitle: false,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
magicType: { show: true, type: ['line', 'bar', 'stack'] },
restore: { show: true },
saveAsImage: { show: true }
}
},
xAxis: [
{
type: 'category',
axisTick: { show: false },
data: months_name
}
],
yAxis: [
{
type: 'value',
axisLabel: {
formatter: (function (value){
return format_u_k_m(value)
})
}
}
],
series: [
{
name: year_2,
type: 'bar',
barGap: 0,
emphasis: {
focus: 'series'
},
data: dataMap_year_2
},
{
name: year_1,
type: 'bar',
emphasis: {
focus: 'series'
},
data: dataMap_year_1
},
{
name: year_0,
type: 'bar',
emphasis: {
focus: 'series'
},
data: dataMap_year_0
},
]
})
})
}
/**
* Chart creator
*
* @constructor
* @param {String} chart_id The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
constructor(chart_id,end_point) {
// Initialize the echarts instance based on the prepared dom
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
this.refresh(myChart,end_point);
//resize
var chart = echarts.init(document.querySelector("#"+chart_id), null);
window.addEventListener('resize',function(){
chart.resize();
})
};
}
//create object
let plot_dspm = new dx_spots_per_month ('chart-dx_spots_x_month','/plot_get_dx_spots_per_month');

View File

@ -0,0 +1,4 @@
var dx_spots_per_month=function(e,c){var a=document.getElementById(e);a=echarts.init(a);this.refresh(a,c);var m=echarts.init(document.querySelector("#"+e),null);window.addEventListener("resize",function(){m.resize()})};
dx_spots_per_month.prototype.refresh=function(e,c){$.getJSON(c).done(function(a){var c=get_last_refresh(a),b=(new Date).getFullYear(),f=(b-0).toString(),g=(b-1).toString();b=(b-2).toString();for(var n=get_months_names(),h=[],k=[],l=[],d=1;13>d;d++)h.push(a.spots_per_month[d].year_0),k.push(a.spots_per_month[d].year_1),l.push(a.spots_per_month[d].year_2);e.setOption({tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},title:{text:"DX SPOTS per month",subtext:c,top:"top",left:"left"},legend:{data:[b,
g,f],bottom:"bottom"},toolbox:{show:!0,showTitle:!1,orient:"vertical",left:"right",top:"center",feature:{mark:{show:!0},dataView:{show:!0,readOnly:!1},magicType:{show:!0,type:["line","bar","stack"]},restore:{show:!0},saveAsImage:{show:!0}}},xAxis:[{type:"category",axisTick:{show:!1},data:n}],yAxis:[{type:"value",axisLabel:{formatter:function(a){return format_u_k_m(a)}}}],series:[{name:b,type:"bar",barGap:0,emphasis:{focus:"series"},data:l},{name:g,type:"bar",emphasis:{focus:"series"},data:k},{name:f,
type:"bar",emphasis:{focus:"series"},data:h}]})})};var plot_dspm=new dx_spots_per_month("chart-dx_spots_x_month","/plot_get_dx_spots_per_month");

View File

@ -0,0 +1,133 @@
/********************************************************************************
* javascript used to build dx spots trend
* ******************************************************************************/
class dx_spots_trend {
/**
* refresh and populate chart
*
* @param {String} my_cart The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
refresh(my_chart,end_point) {
// Asynchronous Data Loading
$.getJSON(end_point).done(function(data) {
// Fill in the data
var last_refresh=get_last_refresh(data);
var dataMap=[];
for (const [key, value] of Object.entries(data["spots_trend"])) {
var tuple=[];
tuple.push(key);
tuple.push(value);
dataMap.push(tuple);
}
//options
my_chart.setOption({
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
}
},
title: {
text: "DX SPOTS trend",
subtext: last_refresh,
top: 'top',
left:'left'
},
toolbox: {
show: true,
showTitle: false,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
dataView: { show: true, readOnly: false },
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
magicType: { show: true, type: ['line', 'bar'] },
saveAsImage: {},
}
},
xAxis: {
type: 'time',
boundaryGap: false
},
yAxis: {
type: 'value',
boundaryGap: [0, '10%'],
axisLabel: {
formatter: (function (value){
return format_u_k_m(value)
})
}
},
dataZoom: [
{
type: 'inside',
start: 65,
end: 100
},
{
start: 0,
end: 20
},
],
series: [
{
name: 'Spots',
type: 'line',
smooth: true,
symbol: 'none',
itemStyle: {
color: '#078513'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: '#57fa75'
},
{
offset: 1,
color: '#118226'
}
])
},
data: dataMap
}
]
})
})
}
/**
* Chart creator
*
* @constructor
* @param {String} chart_id The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
constructor(chart_id,end_point) {
// Initialize the echarts instance based on the prepared dom
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
this.refresh(myChart,end_point);
//resize
var chart = echarts.init(document.querySelector("#"+chart_id), null);
window.addEventListener('resize',function(){
chart.resize();
})
};
}
//create object
let plot_dst = new dx_spots_trend ('chart-dx_spots_trend','/plot_get_dx_spots_trend');

8
static/js/plot_dx_spots_trend.min.js vendored Normal file
View File

@ -0,0 +1,8 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):$jscomp.arrayIterator(a)};$jscomp.owns=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};$jscomp.ASSUME_ES5=!1;
$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};
$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,a];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(a,b,c,d){if(b){c=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];e in c||(c[e]={});c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})}};$jscomp.polyfill("Object.entries",function(a){return a?a:function(a){var c=[],b;for(b in a)$jscomp.owns(a,b)&&c.push([b,a[b]]);return c}},"es8","es3");
var dx_spots_trend=function(a,b){var c=document.getElementById(a);c=echarts.init(c);this.refresh(c,b);var d=echarts.init(document.querySelector("#"+a),null);window.addEventListener("resize",function(){d.resize()})};
dx_spots_trend.prototype.refresh=function(a,b){$.getJSON(b).done(function(b){var c=get_last_refresh(b),e=[];b=$jscomp.makeIterator(Object.entries(b.spots_trend));for(var f=b.next();!f.done;f=b.next()){var g=$jscomp.makeIterator(f.value);f=g.next().value;g=g.next().value;var h=[];h.push(f);h.push(g);e.push(h)}a.setOption({tooltip:{trigger:"axis",position:function(a){return[a[0],"10%"]}},title:{text:"DX SPOTS trend",subtext:c,top:"top",left:"left"},toolbox:{show:!0,showTitle:!1,orient:"vertical",left:"right",
top:"center",feature:{dataView:{show:!0,readOnly:!1},dataZoom:{yAxisIndex:"none"},restore:{},magicType:{show:!0,type:["line","bar"]},saveAsImage:{}}},xAxis:{type:"time",boundaryGap:!1},yAxis:{type:"value",boundaryGap:[0,"10%"],axisLabel:{formatter:function(a){return format_u_k_m(a)}}},dataZoom:[{type:"inside",start:65,end:100},{start:0,end:20}],series:[{name:"Spots",type:"line",smooth:!0,symbol:"none",itemStyle:{color:"#078513"},areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,
color:"#57fa75"},{offset:1,color:"#118226"}])},data:e}]})})};var plot_dst=new dx_spots_trend("chart-dx_spots_trend","/plot_get_dx_spots_trend");

151
static/js/plot_hour_band.js Normal file
View File

@ -0,0 +1,151 @@
/********************************************************************************
* javascript used to build band/hour chart
* ******************************************************************************/
class hour_band {
/**
* refresh and populate chart
*
* @param {String} my_cart The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
* @param {Json} bands The frequency band list (160, 80, 60... UHF, SHF)
*/
refresh(my_chart,end_point,bands) {
// Asynchronous Data Loading
$.getJSON(end_point).done(function(data) {
// Fill in the dat
var last_refresh=get_last_refresh(data);
//set hour indicator names
var hour_indicators=[];
//for (let i = 0; i < 24; i++) {
for (let i = 23; i > -1; i--) {
var hour_name={};
var s=i.toString();
hour_name["name"]=s;
hour_indicators.push(hour_name);
}
//cycling whithin each bands and hours
var dataMap=[];
bands.forEach(band_item => {
var qso=[];
//for (let i = 0; i < 24; i++) {
for (let i = 23; i > -1; i--) {
try {
var value=data["hour_band"][band_item][i];
if (typeof value == 'undefined') {
value = 0;
}
qso.push(value);
} catch (TypeError) {
//TODO
}
}
var tot={"value":qso,"name":band_item};
dataMap.push(tot);
});
//options
my_chart.setOption({
legend: {
orient: 'horizontal',
left: 'left',
bottom: 'bottom'
},
title: {
text: "DX SPOTS per hour in last month",
subtext: last_refresh,
top: 'top',
right: 'right',
},
tooltip: {
trigger: "axis",
},
toolbox: {
show: true,
showTitle: false,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
mark: { show: true },
dataView: { show: true, readOnly: true },
restore: { show: true },
saveAsImage: { show: true }
}
},
radar: {
shape: 'circle',
//startAngle: 285, //0 on left side
startAngle: 105, //0 on top
indicator: hour_indicators,
center: ['47%', '46%'],
axisName: {
color: 'rgb(80,80,80)',
},
},
series: [
{
lineStyle: {
width: 2
},
type: 'radar',
symbol: 'none',
data: dataMap,
tooltip: {
trigger: 'item',
formatter: (params) => {
return "Band: "+params.name;
},
},
emphasis: {
lineStyle: {
width: 4
}
},
}
]
})
})
}
/**
* Chart creator
*
* @constructor
* @param {String} chart_id The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
* @param {Json} band_freq The frequency band list (160, 80, 60... UHF, SHF)
*/
constructor(chart_id,end_point,band_freq) {
// Initialize the echarts instance based on the prepared dom
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
//populate bands array
var bands=[];
band_freq.forEach(function myFunction(item, index) {
bands[index]=item['id']
});
this.refresh(myChart,end_point,bands);
//resize
var chart = echarts.init(document.querySelector("#"+chart_id), null);
window.addEventListener('resize',function(){
chart.resize();
})
};
}
//create object
let plot_hb = new hour_band('chart-hour_band','/plot_get_hour_band',band_frequencies);

4
static/js/plot_hour_band.min.js vendored Normal file
View File

@ -0,0 +1,4 @@
var hour_band=function(b,a,f){var c=document.getElementById(b);c=echarts.init(c);var d=[];f.forEach(function(c,b){d[b]=c.id});this.refresh(c,a,d);var e=echarts.init(document.querySelector("#"+b),null);window.addEventListener("resize",function(){e.resize()})};
hour_band.prototype.refresh=function(b,a,f){$.getJSON(a).done(function(c){for(var d=get_last_refresh(c),e=[],a=23;-1<a;a--){var g={};g.name=a.toString();e.push(g)}var h=[];f.forEach(function(a){for(var b=[],d=23;-1<d;d--)try{var e=c.hour_band[a][d];"undefined"==typeof e&&(e=0);b.push(e)}catch(k){}h.push({value:b,name:a})});b.setOption({legend:{orient:"horizontal",left:"left",bottom:"bottom"},title:{text:"DX SPOTS per hour in last month",subtext:d,top:"top",right:"right"},tooltip:{trigger:"axis"},
toolbox:{show:!0,showTitle:!1,orient:"vertical",left:"right",top:"center",feature:{mark:{show:!0},dataView:{show:!0,readOnly:!0},restore:{show:!0},saveAsImage:{show:!0}}},radar:{shape:"circle",startAngle:105,indicator:e,center:["47%","46%"],axisName:{color:"rgb(80,80,80)"}},series:[{lineStyle:{width:2},type:"radar",symbol:"none",data:h,tooltip:{trigger:"item",formatter:function(a){return"Band: "+a.name}},emphasis:{lineStyle:{width:4}}}]})})};
var plot_hb=new hour_band("chart-hour_band","/plot_get_hour_band",band_frequencies);

View File

@ -0,0 +1,169 @@
/********************************************************************************
* javascript used to build world dx spots live
* ******************************************************************************/
class world_dx_spots_live {
/**
* refresh and populate chart
*
* @param {String} my_cart The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
refresh(my_chart,end_point) {
// Asynchronous Data Loading
$.getJSON(end_point).done(function(data) {
fetch('world.json', {
method: 'GET',
headers: {
'Accept': 'application/json',
},
})
.then(response => response.text())
.then(geoJson => {
var last_refresh=get_last_refresh(data);
var dataMap=[];
data["world_dx_spots_live"].forEach(function myFunction(item, index) {
//lon, lat, number of qso
//dataMap.push({"value":[item["lon"],item["lat"],item["count"]]});
dataMap.push({"value":[item["lat"],item["lon"],item["count"]]});
});
my_chart.hideLoading();
echarts.registerMap('WR', geoJson);
my_chart.setOption( {
visualMap: {
show: false,
min: 0,
max: 50,
inRange: {
symbolSize: [5, 20]
}
},
geo: {
type: 'map',
map: 'WR',
roam: true,
zoom: 1.2,
aspectScale: 0.70,
layoutCenter: ['50%', '54%'],
layoutSize: '100%',
itemStyle: {
normal: {
areaColor: '#323c48',
borderColor: '#111'
},
emphasis: {
areaColor: '#2a333d'
}
},
label: {
emphasis: {
show: false
}
},
},
tooltip: {
trigger: 'item',
formatter: function(val) {
var out="lat: "+val.value[0]+
" lon: "+val.value[1]+"</BR>"+
"Spots: <STRONG>"+ val.value[2] +"</STRONG></BR>";
return out;
}
},
toolbox: {
show: true,
showTitle: false,
orient: 'vertical',
left: 'right',
top: 'center',
iconStyle: {
borderColor: '#fff',
},
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true }
}
},
legend: {
show: false
},
//backgroundColor: '#404a59',
backgroundColor: '#596475',
title: {
text: "World DX SPOTS in last hour",
subtext: last_refresh,
top: 'top',
right:'right',
textStyle: {
color: '#fff'
},
subtextStyle: {
color: '#fff'
}
},
series: [
{
type: 'scatter',
coordinateSystem: 'geo',
data:dataMap,
label: {
emphasis: {
position: 'right',
show: false
}
},
itemStyle: {
normal: {
color: '#eea638'
}
},
/*
symbolSize: function (val) {
return val[2] / 4;
},
*/
}
]
}) //end options
}
)
})
}
/**
* Chart creator
*
* @constructor
* @param {String} chart_id The HTML id where place chart
* @param {string} end_point This is backend end-point for chart datas
*/
constructor(chart_id,end_point) {
// Initialize the echarts instance based on the prepared dom
var chartDom = document.getElementById(chart_id);
var myChart = echarts.init(chartDom);
this.refresh(myChart,end_point);
//resize
var chart = echarts.init(document.querySelector("#"+chart_id), null);
window.addEventListener('resize',function(){
chart.resize();
})
}
}
//create object
let plot_wdsl = new world_dx_spots_live ('chart-world_dx_spots_live','/plot_get_world_dx_spots_live');

View File

@ -0,0 +1,4 @@
var world_dx_spots_live=function(b,c){var a=document.getElementById(b);a=echarts.init(a);this.refresh(a,c);var d=echarts.init(document.querySelector("#"+b),null);window.addEventListener("resize",function(){d.resize()})};
world_dx_spots_live.prototype.refresh=function(b,c){$.getJSON(c).done(function(a){fetch("world.json",{method:"GET",headers:{Accept:"application/json"}}).then(function(a){return a.text()}).then(function(c){var d=get_last_refresh(a),e=[];a.world_dx_spots_live.forEach(function(a,b){e.push({value:[a.lat,a.lon,a.count]})});b.hideLoading();echarts.registerMap("WR",c);b.setOption({visualMap:{show:!1,min:0,max:50,inRange:{symbolSize:[5,20]}},geo:{type:"map",map:"WR",roam:!0,zoom:1.2,aspectScale:.7,layoutCenter:["50%",
"54%"],layoutSize:"100%",itemStyle:{normal:{areaColor:"#323c48",borderColor:"#111"},emphasis:{areaColor:"#2a333d"}},label:{emphasis:{show:!1}}},tooltip:{trigger:"item",formatter:function(a){return"lat: "+a.value[0]+" lon: "+a.value[1]+"</BR>Spots: <STRONG>"+a.value[2]+"</STRONG></BR>"}},toolbox:{show:!0,showTitle:!1,orient:"vertical",left:"right",top:"center",iconStyle:{borderColor:"#fff"},feature:{mark:{show:!0},dataView:{show:!0,readOnly:!1},restore:{show:!0},saveAsImage:{show:!0}}},legend:{show:!1},
backgroundColor:"#596475",title:{text:"World DX SPOTS in last hour",subtext:d,top:"top",right:"right",textStyle:{color:"#fff"},subtextStyle:{color:"#fff"}},series:[{type:"scatter",coordinateSystem:"geo",data:e,label:{emphasis:{position:"right",show:!1}},itemStyle:{normal:{color:"#eea638"}}}]})})})};var plot_wdsl=new world_dx_spots_live("chart-world_dx_spots_live","/plot_get_world_dx_spots_live");

View File

@ -1,5 +1,5 @@
{
"name": "IU1BOW DXCluster v.2.3.4",
"name": "IU1BOW DXCluster v2.4",
"description": "DXCluser for ham radio by IU1BOW",
"short_name": "IU1BOW DX",
"theme_color": "#2196f3",

View File

@ -1,5 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -4,10 +4,6 @@
{% block title %}
{% endblock title %}
{% block head %}
<meta http-equiv="Content-Security-Policy"
content="
script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline'
">
<meta charset="utf-8">
<meta name="description" content="Web Ham Radio DX Cluster and spot search">
<meta name="keywords" content="ham radio, dx cluster, dx spots, cluster sposts,web dx cluster,dx cluster search, DX spots">
@ -18,10 +14,13 @@
<link rel="apple-touch-icon" href="/static/images/icons/icon-apple.png">
<link rel="manifest" href="/static/manifest.webmanifest">
<link rel="stylesheet" href="/static/css/style.min.css">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" as="style" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous" onload="this.rel='stylesheet'">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" as="style" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous" onload="this.rel='stylesheet' ">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"></noscript>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.4/css/flag-icons.min.css" as="style" integrity="sha384-TeDUCuZ+Uyp1Vv0n275nnm//ANAlP5GFHCnSF4iiAdrYmBZMM6syYgykpq4kGTqL" crossorigin="anonymous" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.4/css/flag-icons.min.css"></noscript>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.6/css/flag-icons.min.css" as="style" integrity="sha512-uvXdJud8WaOlQFjlz9B15Yy2Au/bMAvz79F7Xa6OakCl2jvQPdHD0hb3dEqZRdSwG4/sknePXlE7GiarwA/9Wg==" crossorigin="anonymous" onload="this.rel='stylesheet'" >
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.6.6/css/flag-icons.min.css"></noscript>
<script src="static/js/load_css.min.js"></script>
{% endblock head %}
</head>
@ -87,18 +86,18 @@
<span class="copyleft">&copy;</span> Copyleft:
<span id="copyDate"></span>
<a href="https://github.com/coulisse/spiderweb/" target="blank" rel="noopener">IU1BOW Spiderweb</a>
<span id="version">v.2.3.4</span>
<span id="version">v2.4</span>
</div>
</footer>
<script async src="static/js/clock.min.js"></script>
<script async src="static/js/copy_date.min.js"></script>
<script async src="static/js/load-sw.min.js"></script>
<script>
<script nonce="sedfGFG32xs">
{% block app_data %}
var my_callsign='{{callsign}}';
{% endblock app_data %}
</script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js" integrity="sha512-6ORWJX/LrnSjBzwefdNUyLCMTIsGoNP6NftMy2UAm1JBm6PRZCO1d7OHBStWpVFZLO+RerTvqX/Z9mBFfCJZ4A==" crossorigin="anonymous"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js" integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
{% block app_scripts %}
<script async src="static/js/callsign_search.min.js"></script>

View File

@ -26,20 +26,9 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<strong>Band</strong>
<select class="form-select overflow-hidden" id="band" size="14" multiple>
<option selected value="SHF">SHF</option>
<option selected value="UHF">UHF</option>
<option selected value="VHF">VHF</option>
<option selected value="6">6m</option>
<option selected value="10">10m</option>
<option selected value="12">12m</option>
<option selected value="15">15m</option>
<option selected value="17">17m</option>
<option selected value="20">20m</option>
<option selected value="30">30m</option>
<option selected value="40">40m</option>
<option selected value="60">60m</option>
<option selected value="80">80m</option>
<option selected value="160">&gt;160m</option>
{% for dict_item in bands['bands']|reverse %}
<option selected value="{{dict_item['id']}}">{{dict_item["id"]}}</option>
{% endfor %}
</select>
<p></p>
<div class="row">
@ -55,25 +44,18 @@
<div class="col">
<strong>De</strong>
<select class="form-select overflow-hidden" id="de_re" size="7" multiple>
<option selected value="EU">EU</option>
<option selected value="NA">NA</option>
<option selected value="AS">AS</option>
<option selected value="AF">AF</option>
<option selected value="OC">OC</option>
<option selected value="SA">SA</option>
<option selected value="AN">AN</option>
{% for dict_item in continents['continents'] %}
<option selected value="{{dict_item['id']}}">{{dict_item["id"]}}</option>
{% endfor %}
</select>
</div>
<div class="col">
<strong>Dx</strong>
<select class="form-select overflow-hidden" id="dx_re" size="7" multiple>
<option selected value="EU">EU</option>
<option selected value="NA">NA</option>
<option selected value="AS">AS</option>
<option selected value="AF">AF</option>
<option selected value="OC">OC</option>
<option selected value="SA">SA</option>
<option selected value="AN">AN</option>
{% for dict_item in continents['continents'] %}
<option selected value="{{dict_item['id']}}">{{dict_item["id"]}}</option>
{% endfor %}
</select>
</div>
</div>
@ -212,6 +194,9 @@
var payload_json={{payload|tojson|safe}};
var timer_interval_json = {{timer_interval}};
var my_adxo_events_json='{{ adxo_events|tojson|safe }}';
var continents_cq={{continents["continents"]|tojson|safe}};
var band_frequencies={{bands["bands"]|tojson|safe}};
{% endblock app_data %}
{% block app_scripts %}
{{ super() }}

View File

@ -1,11 +1,14 @@
{% extends "_base.html" %}
<head>
{% block title %}
<title>Some plots end stats from the dx clustes node</title>
<title>Some charts end stats from the dx clustes node</title>
{% endblock %}
{% block head %}
{{ super() }}
<meta http-equiv="refresh" content="300">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.1/echarts.min.js" integrity="sha512-OTbGFYPLe3jhy4bUwbB8nls0TFgz10kn0TLkmyA+l3FyivDs31zsXCjOis7YGDtE2Jsy0+fzW+3/OVoPVujPmQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% endblock %}
</head>
{% block titles %}
<h1 class="display-4 text-white">PLOTS & STATS</h1>
@ -15,47 +18,84 @@
{% endblock %}
{% block contents %}
<div class="row mx-auto">
<div class="col mr-3" id="plotlist"></div>
</div>
<div class="row mx-auto">
<div class="col mr-3">
<table class="table table-striped table-borderless table-sm text-responsive table-hover">
<caption>Connected notes</caption>
<h3 class="text-center">Connected nodes</h2>
<thead>
<tr>
<th scope="col">Callsign</th>
<th scope="col">Type</th>
<th scope="col">Started</th>
<th scope="col" class="d-none d-lg-table-cell d-xl-table-cell">Name</th>
<th scope="col">Avg RTT</th>
<th scope="col">Link</th>
</tr>
</thead>
<tbody>
{% for dict_item in who %}
<tr>
<td>{{dict_item["callsign"]}}</td>
<td>{{dict_item["type"]}}</td>
<td>{{dict_item["started"]}}</td>
<td class="d-none d-lg-table-cell d-xl-table-cell">{{dict_item["name"]}}</td>
<td>{{dict_item["average_rtt"]}}</td>
<td>{{dict_item["link"]}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="d-flex flex-wrap" id="dashboard">
<div class="shadow-lg bg-body mb-5 rounded" id="form-band_activity">
<div class="d-flex flex-column">
<form method="POST" id="form-continents" enctype="multipart/form-data" >
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="container">
Your continent is:
<select class="form-select flex-shrink" aria-label="continent" id="continentInput">
{% for dict_item in continents['continents']%}
<option value="{{dict_item['id']}}" >{{dict_item["description"]}}</option>
{% endfor %}
</select>
</div>
</form>
<div id="chart-band_activity"></div>
</div>
<small><sup id="txt_continent"></sup></small>
</div>
<div class="shadow-lg mb-5 bg-body rounded" id="chart-world_dx_spots_live"></div>
<div class="shadow-lg mb-5 bg-body rounded" id="chart-hour_band"></div>
<a class="shadow-lg mb-5 bg-body rounded" href="https://sidc.be/silso/" target="_blank" rel="noopener noreferrer">
<img src="https://sidc.be/silso/IMAGES/GRAPHICS/prediSC.png"class="img-fluid" id="silo-propagation-img" alt="propagation trend">
</a>
<div class="shadow-lg mb-5 bg-body rounded" id="chart-dx_spots_x_month"></div>
<div class="shadow-lg mb-5 bg-body rounded" id="chart-dx_spots_trend"></div>
<div class="container-fluid">
<div class="shadow-lg mb-5 bg-body rounded">
<strong>Physically connected callsigns to {{ mycallsign }}</strong>
<hr>
<table class="table table-striped table-borderless table-sm text-responsive table-hover">
<thead id="telnet-thead">
<tr>
<th scope="col">Callsign</th>
<th scope="col">Type</th>
<th scope="col">Started</th>
<th scope="col" class="d-none d-lg-table-cell d-xl-table-cell">Name</th>
<th scope="col">Avg RTT</th>
</tr>
</thead>
<tbody>
{% for dict_item in who %}
<tr>
<td>{{dict_item["callsign"]}}</td>
<td>{{dict_item["type"]}}</td>
<td>{{dict_item["started"]}}</td>
<td class="d-none d-lg-table-cell d-xl-table-cell">{{dict_item["name"]}}</td>
<td>{{dict_item["average_rtt"]}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock contents %}
{% block app_data %}
{{ super() }}
var payload_json={{payload|tojson|safe}};
var timer_interval_json = {{timer_interval}};
var continents_cq={{continents["continents"]|tojson|safe}};
var band_frequencies={{bands["bands"]|tojson|safe}};
{% endblock app_data %}
{% block app_scripts %}
{{ super() }}
<script defer src="static/js/plot.min.js"></script>
<script defer src="static/js/plot_common.min.js"></script>
<script defer src="static/js/plot_band_activity.min.js"></script>
<script defer src="static/js/plot_world_dx_spots_live.min.js"></script>
<script defer src="static/js/plot_hour_band.min.js"></script>
<script defer src="static/js/plot_dx_spots_trend.min.js"></script>
<script defer src="static/js/plot_dx_spots_per_month.min.js"></script>
{% endblock app_scripts %}

View File

@ -3,5 +3,6 @@ if [ "$1" == "-b" ]; then
./build.sh
cd ..
fi
python3 webapp.py
#python3 webapp.py
flask --app webapp.py run

105
webapp.py
View File

@ -12,7 +12,11 @@ from lib.dxtelnet import who
from lib.adxo import get_adxo_events
from lib.qry import query_manager
from lib.cty import prefix_table
from lib.plot_data_provider import ContinentsBandsProvider
from lib.plot_data_provider import SpotsPerMounthProvider
from lib.plot_data_provider import SpotsTrend
from lib.plot_data_provider import HourBand
from lib.plot_data_provider import WorldDxSpotsLive
logging.config.fileConfig("cfg/webapp_log_config.ini", disable_existing_loggers=True)
logger = logging.getLogger(__name__)
@ -207,6 +211,7 @@ def spotquery():
main_result["iso"]=search_prefix["iso"]
payload.append({**main_result})
return payload
except Exception as e:
logger.error(e)
@ -221,6 +226,14 @@ def get_adxo():
get_adxo()
#create data provider for charts
heatmap_cbp=ContinentsBandsProvider(logger,qm,continents_cq,band_frequencies)
bar_graph_spm=SpotsPerMounthProvider(logger,qm)
line_graph_st=SpotsTrend(logger,qm)
bubble_graph_hb=HourBand(logger,qm,band_frequencies)
geo_graph_wdsl=WorldDxSpotsLive(logger,qm,pfxt)
#ROUTINGS
@app.route('/spotlist', methods=['GET'])
def spotlist():
response=flask.Response(json.dumps(spotquery()))
@ -235,7 +248,7 @@ def who_is_connected():
@app.route('/index.html', methods=['GET'])
def spots():
payload=spotquery()
response=flask.Response(render_template('index.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],enable_cq_filter=enable_cq_filter,payload=payload,timer_interval=cfg['timer']['interval'],adxo_events=adxo_events))
response=flask.Response(render_template('index.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],enable_cq_filter=enable_cq_filter,payload=payload,timer_interval=cfg['timer']['interval'],adxo_events=adxo_events,continents=continents_cq,bands=band_frequencies))
return response
@app.route('/service-worker.js', methods=['GET'])
@ -244,26 +257,16 @@ def sw():
@app.route('/offline.html')
def root():
return app.send_static_file('html/offline.html')
return app.send_static_file('html/offline.html')
@app.route('/plotlist', methods=['GET'])
def plotlist():
#get url parameters
idxfile=os.path.join(app.root_path,os.path.basename(app.static_url_path),'plots','plots.json')
if os.path.exists(idxfile):
with open(idxfile,'r') as jsonfile:
json_content = json.load(jsonfile)
else:
json_content={}
response=json_content
return response
@app.route('/world.json')
def world_data():
return app.send_static_file('data/world.json')
@app.route('/plots.html')
def plots():
payload=plotlist()
whoj=who_is_connected()
response=flask.Response(render_template('plots.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],payload=payload,timer_interval=cfg['plot_refresh_timer']['interval'],who=whoj))
response=flask.Response(render_template('plots.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],who=whoj,continents=continents_cq,bands=band_frequencies))
return response
@ -285,7 +288,7 @@ def sitemap():
def callsign():
payload=spotquery()
callsign=request.args.get('c')
response=flask.Response(render_template('callsign.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],payload=payload,timer_interval=cfg['timer']['interval'],callsign=callsign,adxo_events=adxo_events))
response=flask.Response(render_template('callsign.html',mycallsign=cfg['mycallsign'],telnet=cfg['telnet'],mail=cfg['mail'],menu_list=cfg['menu']['menu_list'],payload=payload,timer_interval=cfg['timer']['interval'],callsign=callsign,adxo_events=adxo_events,continents=continents_cq,bands=band_frequencies))
return response
#API that search a callsign and return all informations about that
@ -296,7 +299,48 @@ def find_callsign():
if response is None:
response=flask.Response(status=204)
return response
@app.route('/plot_get_heatmap_data', methods=['GET'])
def get_heatmap_data():
continent=request.args.get('continent')
response=flask.Response(json.dumps(heatmap_cbp.get_data(continent)))
logger.debug(response)
if response is None:
response=flask.Response(status=204)
return response
@app.route('/plot_get_dx_spots_per_month', methods=['GET'])
def get_dx_spots_per_month():
response=flask.Response(json.dumps(bar_graph_spm.get_data()))
logger.debug(response)
if response is None:
response=flask.Response(status=204)
return response
@app.route('/plot_get_dx_spots_trend', methods=['GET'])
def get_dx_spots_trend():
response=flask.Response(json.dumps(line_graph_st.get_data()))
logger.debug(response)
if response is None:
response=flask.Response(status=204)
return response
@app.route('/plot_get_hour_band', methods=['GET'])
def get_dx_hour_band():
response=flask.Response(json.dumps(bubble_graph_hb.get_data()))
logger.debug(response)
if response is None:
response=flask.Response(status=204)
return response
@app.route('/plot_get_world_dx_spots_live', methods=['GET'])
def get_world_dx_spots_live():
response=flask.Response(json.dumps(geo_graph_wdsl.get_data()))
logger.debug(response)
if response is None:
response=flask.Response(status=204)
return response
@app.context_processor
def inject_template_scope():
injections = dict()
@ -304,7 +348,6 @@ def inject_template_scope():
value = request.cookies.get('cookie_consent')
return value == 'true'
injections.update(cookies_check=cookies_check)
return injections
@app.after_request
@ -314,10 +357,30 @@ def add_security_headers(resp):
resp.headers['X-Frame-Options']='SAMEORIGIN'
resp.headers['X-Content-Type-Options']='nosniff'
resp.headers['Referrer-Policy']='strict-origin-when-cross-origin'
resp.headers['Cache-Control']='no-cache, no-store, must-revalidate'
resp.headers['Cache-Control']='public, no-cache'
resp.headers['Pragma']='no-cache'
resp.headers['Content-Security-Policy']="\
default-src 'self';\
script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline';\
style-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline';\
object-src 'none';base-uri 'self';\
connect-src 'self' cdn.jsdelivr.net cdnjs.cloudflare.com sidc.be;\
font-src 'self' cdn.jsdelivr.net;\
frame-src 'self';\
form-action 'none';\
frame-ancestors 'none';\
img-src 'self' data: cdnjs.cloudflare.com sidc.be;\
manifest-src 'self';\
media-src 'self';\
worker-src 'self';\
"
return resp
# style-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx' 'sha512-uvXdJud8WaOlQFjlz9B15Yy2Au/bMAvz79F7Xa6OakCl2jvQPdHD0hb3dEqZRdSwG4/sknePXlE7GiarwA/9Wg==';\
# style-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline';\
#script-src 'self' cdnjs.cloudflare.com cdn.jsdelivr.net 'unsafe-inline'
if __name__ == '__main__':
app.run(host='0.0.0.0')