2.4
67
README.md
@ -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>
|
||||
|
||||
|
BIN
__pycache__/webapp.cpython-311.pyc
Normal 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}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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" },
|
||||
|
@ -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=
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
___
|
||||
|
Before Width: | Height: | Size: 120 KiB |
BIN
docs/images/01_desktop_main.png
Normal file
After Width: | Height: | Size: 564 KiB |
Before Width: | Height: | Size: 146 KiB |
BIN
docs/images/02_desktop_plot.png
Normal file
After Width: | Height: | Size: 681 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 464 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 126 KiB |
BIN
docs/images/m01_mobile.png
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
docs/images/m02_mobile.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
docs/images/m03_mobile.png
Normal file
After Width: | Height: | Size: 123 KiB |
BIN
docs/images/m04_mobile.png
Normal file
After Width: | Height: | Size: 68 KiB |
1
lib/.gitignore
vendored
@ -1 +1,2 @@
|
||||
__pycache__
|
||||
__init__.py
|
490
lib/plot_data_provider.py
Normal 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
|
@ -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
|
@ -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")
|
@ -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")
|
@ -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")
|
@ -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")
|
@ -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
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
2122
log/plots.log
1727
log/webapp.log
@ -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
|
||||
|
@ -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
|
||||
|
61
scripts/dxcluster_schema_for_test.sh
Executable 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
|
||||
|
||||
|
71
scripts/dxcluster_schema_for_test.sql
Normal 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');
|
||||
|
@ -1,4 +0,0 @@
|
||||
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
|
||||
echo Absolute path: ${DIR}
|
||||
cd ${DIR}
|
||||
python3 ../lib/propagation_heatmaps.py
|
@ -1,4 +0,0 @@
|
||||
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
|
||||
echo Absolute path: ${DIR}
|
||||
cd ${DIR}
|
||||
python3 ../lib/qso_hour_band.py
|
@ -1,4 +0,0 @@
|
||||
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
|
||||
echo Absolute path: ${DIR}
|
||||
cd ${DIR}
|
||||
python3 ../lib/qso_months.py
|
@ -1,5 +0,0 @@
|
||||
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
|
||||
echo Absolute path: ${DIR}
|
||||
cd ${DIR}
|
||||
python3 ../lib/qso_trend.py
|
||||
|
@ -1,4 +0,0 @@
|
||||
DIR=$(realpath -s $0|sed 's|\(.*\)/.*|\1|')
|
||||
echo Absolute path: ${DIR}
|
||||
cd ${DIR}
|
||||
python3 ../lib/qso_world_map.py
|
@ -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;
|
||||
}
|
||||
|
2
static/css/style.min.css
vendored
@ -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
@ -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">©</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>
|
||||
|
@ -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);
|
5
static/js/plot.min.js
vendored
@ -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);
|
190
static/js/plot_band_activity.js
Normal 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
@ -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
@ -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
@ -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};
|
139
static/js/plot_dx_spots_per_month.js
Normal 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');
|
4
static/js/plot_dx_spots_per_month.min.js
vendored
Normal 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");
|
133
static/js/plot_dx_spots_trend.js
Normal 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
@ -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
@ -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
@ -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);
|
169
static/js/plot_world_dx_spots_live.js
Normal 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');
|
4
static/js/plot_world_dx_spots_live.min.js
vendored
Normal 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");
|
@ -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",
|
||||
|
5
static/plots/.gitignore
vendored
@ -1,5 +0,0 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
|
@ -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">©</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>
|
||||
|
@ -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">>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() }}
|
||||
|
@ -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 %}
|
||||
|
3
test.sh
@ -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
@ -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')
|
||||
|
||||
|