Compare commits
12 Commits
1.0
...
ae075f3ac7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae075f3ac7 | ||
|
|
efa9806c64 | ||
|
|
03829831c0 | ||
|
|
4f83468309 | ||
|
|
2165ebc103 | ||
|
|
cf46017917 | ||
|
|
c30e1616d3 | ||
|
|
422c917073 | ||
|
|
cad1f5cfdf | ||
|
|
78f8cd26f0 | ||
|
|
d6cc2673dd | ||
|
|
8f553a59f8 |
@@ -196,10 +196,12 @@ Finally, simply add the appropriate config to the `providers` section of `config
|
|||||||
|
|
||||||
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
||||||
|
|
||||||
The project contains a self-hosted copy of Font Awesome's free library, in the `/webasset/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
|
The project contains a self-hosted copy of Font Awesome's free library, in the `/webassets/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
|
||||||
|
|
||||||
|
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
|
||||||
|
|
||||||
The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery, Leaflet and Bootstrap. This project would not have been possible without these libraries, so many thanks to their developers.
|
The software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery, Leaflet and Bootstrap. This project would not have been possible without these libraries, so many thanks to their developers.
|
||||||
|
|
||||||
Particular thanks go to QRZCQ country-files.com for providing country lookup data for amateur radio, and to the developers of `pyhamtools` for making it easy to use this data as well as QRZ.com and Clublog lookup.
|
Particular thanks go to country-files.com for providing country lookup data for amateur radio, to K0SWE for [this JSON-formatted DXCC data](https://github.com/k0swe/dxcc-json/), and to the developers of `pyhamtools` for making it easy to use country-files.com data as well as QRZ.com and Clublog lookup.
|
||||||
|
|
||||||
The project's name was suggested by Harm, DK4HAA. Thanks!
|
The project's name was suggested by Harm, DK4HAA. Thanks!
|
||||||
|
|||||||
@@ -136,3 +136,12 @@ clublog-api-key: ""
|
|||||||
|
|
||||||
# Allow submitting spots to the Spothole API?
|
# Allow submitting spots to the Spothole API?
|
||||||
allow-spotting: true
|
allow-spotting: true
|
||||||
|
|
||||||
|
# Options for the web UI.
|
||||||
|
web-ui-options:
|
||||||
|
spot-count: [10, 25, 50, 100]
|
||||||
|
spot-count-default: 50
|
||||||
|
max-spot-age: [5, 10, 30, 60]
|
||||||
|
max-spot-age-default: 30
|
||||||
|
alert-count: [25, 50, 100, 200, 500]
|
||||||
|
alert-count-default: 100
|
||||||
@@ -17,3 +17,4 @@ MAX_ALERT_AGE = config["max-alert-age-sec"]
|
|||||||
SERVER_OWNER_CALLSIGN = config["server-owner-callsign"]
|
SERVER_OWNER_CALLSIGN = config["server-owner-callsign"]
|
||||||
WEB_SERVER_PORT = config["web-server-port"]
|
WEB_SERVER_PORT = config["web-server-port"]
|
||||||
ALLOW_SPOTTING = config["allow-spotting"]
|
ALLOW_SPOTTING = config["allow-spotting"]
|
||||||
|
WEB_UI_OPTIONS = config["web-ui-options"]
|
||||||
1412
core/constants.py
@@ -1,5 +1,7 @@
|
|||||||
import gzip
|
import gzip
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ from requests_cache import CachedSession
|
|||||||
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
||||||
from core.config import config
|
from core.config import config
|
||||||
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
|
from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, \
|
||||||
QRZCQ_CALLSIGN_LOOKUP_DATA, HTTP_HEADERS, HAMQTH_PRG
|
HTTP_HEADERS, HAMQTH_PRG
|
||||||
|
|
||||||
|
|
||||||
# Singleton class that provides lookup functionality.
|
# Singleton class that provides lookup functionality.
|
||||||
@@ -46,6 +48,8 @@ class LookupHelper:
|
|||||||
self.CALL_INFO_BASIC = None
|
self.CALL_INFO_BASIC = None
|
||||||
self.LOOKUP_LIB_BASIC = None
|
self.LOOKUP_LIB_BASIC = None
|
||||||
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None
|
self.COUNTRY_FILES_CTY_PLIST_DOWNLOAD_LOCATION = None
|
||||||
|
self.DXCC_JSON_DOWNLOAD_LOCATION = None
|
||||||
|
self.DXCC_DATA = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads
|
# Lookup helpers from pyhamtools. We use five (!) of these. The simplest is country-files.com, which downloads
|
||||||
@@ -84,6 +88,19 @@ class LookupHelper:
|
|||||||
filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION)
|
filename=self.CLUBLOG_XML_DOWNLOAD_LOCATION)
|
||||||
self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache')
|
self.CLUBLOG_CALLSIGN_DATA_CACHE = Cache('cache/clublog_callsign_lookup_cache')
|
||||||
|
|
||||||
|
# We also get a lookup of DXCC data from K0SWE to use for additional lookups of e.g. flags.
|
||||||
|
self.DXCC_JSON_DOWNLOAD_LOCATION = "cache/dxcc.json"
|
||||||
|
success = self.download_dxcc_json()
|
||||||
|
if success:
|
||||||
|
with open(self.DXCC_JSON_DOWNLOAD_LOCATION) as f:
|
||||||
|
tmp_dxcc_data = json.load(f)["dxcc"]
|
||||||
|
# Reformat as a map for faster lookup
|
||||||
|
self.DXCC_DATA = {}
|
||||||
|
for dxcc in tmp_dxcc_data:
|
||||||
|
self.DXCC_DATA[dxcc["entityCode"]] = dxcc
|
||||||
|
else:
|
||||||
|
logging.error("Could not download DXCC data, flags and similar data may be missing!")
|
||||||
|
|
||||||
# Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use
|
# Download the cty.plist file from country-files.com on first startup. The pyhamtools lib can actually download and use
|
||||||
# this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can
|
# this itself, but it's occasionally offline which causes it to throw an error. By downloading it separately, we can
|
||||||
# catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the
|
# catch errors and handle them, falling back to a previous copy of the file in the cache, and we can use the
|
||||||
@@ -103,6 +120,22 @@ class LookupHelper:
|
|||||||
logging.error("Exception when downloading Clublog cty.xml", e)
|
logging.error("Exception when downloading Clublog cty.xml", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Download the dxcc.json file on first startup.
|
||||||
|
def download_dxcc_json(self):
|
||||||
|
try:
|
||||||
|
logging.info("Downloading dxcc.json...")
|
||||||
|
response = SEMI_STATIC_URL_DATA_CACHE.get("https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
|
||||||
|
headers=HTTP_HEADERS).text
|
||||||
|
|
||||||
|
with open(self.DXCC_JSON_DOWNLOAD_LOCATION, "w") as f:
|
||||||
|
f.write(response)
|
||||||
|
f.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Exception when downloading dxcc.json", e)
|
||||||
|
return False
|
||||||
|
|
||||||
# Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the
|
# Download the cty.xml (gzipped) file from Clublog on first startup, so we can use it in preference to querying the
|
||||||
# database live if possible.
|
# database live if possible.
|
||||||
def download_clublog_ctyxml(self):
|
def download_clublog_ctyxml(self):
|
||||||
@@ -175,11 +208,11 @@ class LookupHelper:
|
|||||||
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
||||||
if clublog_data and "Name" in clublog_data:
|
if clublog_data and "Name" in clublog_data:
|
||||||
country = clublog_data["Name"]
|
country = clublog_data["Name"]
|
||||||
# Couldn't get anything from Clublog database, try QRZCQ data
|
# Couldn't get anything from Clublog database, try DXCC data
|
||||||
if not country:
|
if not country:
|
||||||
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
|
dxcc_data = self.get_dxcc_data_for_callsign(call)
|
||||||
if qrzcq_data and "country" in qrzcq_data:
|
if dxcc_data and "name" in dxcc_data:
|
||||||
country = qrzcq_data["country"]
|
country = dxcc_data["name"]
|
||||||
return country
|
return country
|
||||||
|
|
||||||
# Infer a DXCC ID from a callsign
|
# Infer a DXCC ID from a callsign
|
||||||
@@ -208,11 +241,11 @@ class LookupHelper:
|
|||||||
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
||||||
if clublog_data and "DXCC" in clublog_data:
|
if clublog_data and "DXCC" in clublog_data:
|
||||||
dxcc = clublog_data["DXCC"]
|
dxcc = clublog_data["DXCC"]
|
||||||
# Couldn't get anything from Clublog database, try QRZCQ data
|
# Couldn't get anything from Clublog database, try DXCC data
|
||||||
if not dxcc:
|
if not dxcc:
|
||||||
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
|
dxcc_data = self.get_dxcc_data_for_callsign(call)
|
||||||
if qrzcq_data and "dxcc" in qrzcq_data:
|
if dxcc_data and "entityCode" in dxcc_data:
|
||||||
dxcc = qrzcq_data["dxcc"]
|
dxcc = dxcc_data["entityCode"]
|
||||||
return dxcc
|
return dxcc
|
||||||
|
|
||||||
# Infer a continent shortcode from a callsign
|
# Infer a continent shortcode from a callsign
|
||||||
@@ -236,11 +269,12 @@ class LookupHelper:
|
|||||||
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
||||||
if clublog_data and "Continent" in clublog_data:
|
if clublog_data and "Continent" in clublog_data:
|
||||||
continent = clublog_data["Continent"]
|
continent = clublog_data["Continent"]
|
||||||
# Couldn't get anything from Clublog database, try QRZCQ data
|
# Couldn't get anything from Clublog database, try DXCC data
|
||||||
if not continent:
|
if not continent:
|
||||||
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
|
dxcc_data = self.get_dxcc_data_for_callsign(call)
|
||||||
if qrzcq_data and "continent" in qrzcq_data:
|
# Some DXCCs are in two continents, if so don't use the continent data as we can't be sure
|
||||||
continent = qrzcq_data["continent"]
|
if dxcc_data and "continent" in dxcc_data and len(dxcc_data["continent"]) == 1:
|
||||||
|
continent = dxcc_data["continent"][0]
|
||||||
return continent
|
return continent
|
||||||
|
|
||||||
# Infer a CQ zone from a callsign
|
# Infer a CQ zone from a callsign
|
||||||
@@ -269,11 +303,12 @@ class LookupHelper:
|
|||||||
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
clublog_data = self.get_clublog_api_data_for_callsign(call)
|
||||||
if clublog_data and "CQZ" in clublog_data:
|
if clublog_data and "CQZ" in clublog_data:
|
||||||
cqz = clublog_data["CQZ"]
|
cqz = clublog_data["CQZ"]
|
||||||
# Couldn't get anything from Clublog database, try QRZCQ data
|
# Couldn't get anything from Clublog database, try DXCC data
|
||||||
if not cqz:
|
if not cqz:
|
||||||
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
|
dxcc_data = self.get_dxcc_data_for_callsign(call)
|
||||||
if qrzcq_data and "cqz" in qrzcq_data:
|
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
|
||||||
cqz = qrzcq_data["cqz"]
|
if dxcc_data and "cq" in dxcc_data and len(dxcc_data["cq"]) == 1:
|
||||||
|
cqz = dxcc_data["cq"][0]
|
||||||
return cqz
|
return cqz
|
||||||
|
|
||||||
# Infer a ITU zone from a callsign
|
# Infer a ITU zone from a callsign
|
||||||
@@ -293,13 +328,18 @@ class LookupHelper:
|
|||||||
hamqth_data = self.get_hamqth_data_for_callsign(call)
|
hamqth_data = self.get_hamqth_data_for_callsign(call)
|
||||||
if hamqth_data and "itu" in hamqth_data:
|
if hamqth_data and "itu" in hamqth_data:
|
||||||
ituz = hamqth_data["itu"]
|
ituz = hamqth_data["itu"]
|
||||||
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try QRZCQ data
|
# Couldn't get anything from HamQTH database, Clublog doesn't provide this, so try DXCC data
|
||||||
if not ituz:
|
if not ituz:
|
||||||
qrzcq_data = self.get_qrzcq_data_for_callsign(call)
|
dxcc_data = self.get_dxcc_data_for_callsign(call)
|
||||||
if qrzcq_data and "ituz" in qrzcq_data:
|
# Some DXCCs are in multiple zones, if so don't use the zone data as we can't be sure
|
||||||
ituz = qrzcq_data["ituz"]
|
if dxcc_data and "itu" in dxcc_data and len(dxcc_data["itu"]) == 1:
|
||||||
|
ituz = dxcc_data["itu"]
|
||||||
return ituz
|
return ituz
|
||||||
|
|
||||||
|
# Get an emoji flag for a given DXCC entity ID
|
||||||
|
def get_flag_for_dxcc(self, dxcc):
|
||||||
|
return self.DXCC_DATA[dxcc]["flag"] if dxcc in self.DXCC_DATA else None
|
||||||
|
|
||||||
# Infer an operator name from a callsign (requires QRZ.com/HamQTH)
|
# Infer an operator name from a callsign (requires QRZ.com/HamQTH)
|
||||||
def infer_name_from_callsign_online_lookup(self, call):
|
def infer_name_from_callsign_online_lookup(self, call):
|
||||||
data = self.get_qrz_data_for_callsign(call)
|
data = self.get_qrz_data_for_callsign(call)
|
||||||
@@ -488,11 +528,10 @@ class LookupHelper:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Utility method to get QRZCQ data from our constants table, if we can find it
|
# Utility method to get generic DXCC data from our lookup table, if we can find it
|
||||||
def get_qrzcq_data_for_callsign(self, call):
|
def get_dxcc_data_for_callsign(self, call):
|
||||||
# Iterate in reverse order - see comments on the data structure itself
|
for entry in self.DXCC_DATA.values():
|
||||||
for entry in reversed(QRZCQ_CALLSIGN_LOOKUP_DATA):
|
if re.match(entry["prefixRegex"], call):
|
||||||
if call.startswith(entry["prefix"]):
|
|
||||||
return entry
|
return entry
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.constants import DXCC_FLAGS
|
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.sig_utils import get_icon_for_sig, get_sig_ref_info
|
from core.sig_utils import get_icon_for_sig, get_sig_ref_info
|
||||||
|
|
||||||
@@ -95,8 +94,8 @@ class Alert:
|
|||||||
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
|
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
|
||||||
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
|
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
|
||||||
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
|
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
|
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
|
||||||
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
|
# in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from datetime import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
||||||
|
|
||||||
from core.constants import DXCC_FLAGS
|
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.sig_utils import get_icon_for_sig, get_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
from core.sig_utils import get_icon_for_sig, get_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
@@ -174,8 +173,8 @@ class Spot:
|
|||||||
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
|
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
|
||||||
if self.dx_call and not self.dx_dxcc_id:
|
if self.dx_call and not self.dx_dxcc_id:
|
||||||
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
|
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
|
||||||
if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
|
self.dx_flag = lookup_helper.get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
# Clean up spotter call if it has an SSID or -# from RBN
|
# Clean up spotter call if it has an SSID or -# from RBN
|
||||||
if self.de_call and "-" in self.de_call:
|
if self.de_call and "-" in self.de_call:
|
||||||
@@ -207,8 +206,8 @@ class Spot:
|
|||||||
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
|
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
|
||||||
if not self.de_dxcc_id:
|
if not self.de_dxcc_id:
|
||||||
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
|
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
|
||||||
if self.de_dxcc_id and self.de_dxcc_id in DXCC_FLAGS and not self.de_flag:
|
if self.de_dxcc_id and not self.de_flag:
|
||||||
self.de_flag = DXCC_FLAGS[self.de_dxcc_id]
|
self.de_flag = lookup_helper.get_flag_for_dxcc(self.de_dxcc_id)
|
||||||
|
|
||||||
# Band from frequency
|
# Band from frequency
|
||||||
if self.freq and not self.band:
|
if self.freq and not self.band:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import bottle
|
|||||||
import pytz
|
import pytz
|
||||||
from bottle import run, request, response, template
|
from bottle import run, request, response, template
|
||||||
|
|
||||||
from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING
|
from core.config import MAX_SPOT_AGE, ALLOW_SPOTTING, WEB_UI_OPTIONS
|
||||||
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION, UNKNOWN_BAND
|
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS, SOFTWARE_VERSION, UNKNOWN_BAND
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.prometheus_metrics_handler import page_requests_counter, get_metrics, api_requests_counter
|
from core.prometheus_metrics_handler import page_requests_counter, get_metrics, api_requests_counter
|
||||||
@@ -469,7 +469,8 @@ class WebServer:
|
|||||||
map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["alert_providers"]))),
|
map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["alert_providers"]))),
|
||||||
"continents": CONTINENTS,
|
"continents": CONTINENTS,
|
||||||
"max_spot_age": MAX_SPOT_AGE,
|
"max_spot_age": MAX_SPOT_AGE,
|
||||||
"spot_allowed": ALLOW_SPOTTING}
|
"spot_allowed": ALLOW_SPOTTING,
|
||||||
|
"web-ui-options": WEB_UI_OPTIONS}
|
||||||
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
|
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
|
||||||
# one of our proviers.
|
# one of our proviers.
|
||||||
if ALLOW_SPOTTING:
|
if ALLOW_SPOTTING:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import pytz
|
|||||||
|
|
||||||
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
from core.sig_utils import get_icon_for_sig
|
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from spotproviders.http_spot_provider import HTTPSpotProvider
|
from spotproviders.http_spot_provider import HTTPSpotProvider
|
||||||
@@ -51,7 +50,7 @@ class GMA(HTTPSpotProvider):
|
|||||||
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
|
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
|
||||||
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
|
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
|
||||||
# to determine if it's a SOTA summit.
|
# to determine if it's a SOTA summit.
|
||||||
if ref_info["reftype"] not in ["POTA", "WWFF"] and (ref_info["reftype"] != "Summit" or ref_info["sota"] == ""):
|
if "reftype" in ref_info and ref_info["reftype"] not in ["POTA", "WWFF"] and (ref_info["reftype"] != "Summit" or ref_info["sota"] == ""):
|
||||||
match ref_info["reftype"]:
|
match ref_info["reftype"]:
|
||||||
case "Summit":
|
case "Summit":
|
||||||
spot.sig_refs[0].sig = "GMA"
|
spot.sig_refs[0].sig = "GMA"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from rss_parser import RSSParser
|
from rss_parser import RSSParser
|
||||||
|
|
||||||
from core.sig_utils import get_icon_for_sig
|
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
from spotproviders.http_spot_provider import HTTPSpotProvider
|
from spotproviders.http_spot_provider import HTTPSpotProvider
|
||||||
@@ -25,47 +26,50 @@ class WOTA(HTTPSpotProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in rss.channel.items:
|
for source_spot in rss.channel.items:
|
||||||
|
|
||||||
# Reject GUID missing or zero
|
try:
|
||||||
if not source_spot.guid or not source_spot.guid.content or source_spot.guid.content == "http://www.wota.org.uk/spots/0":
|
# Reject GUID missing or zero
|
||||||
continue
|
if not source_spot.guid or not source_spot.guid.content or source_spot.guid.content == "http://www.wota.org.uk/spots/0":
|
||||||
|
continue
|
||||||
|
|
||||||
# Pick apart the title
|
# Pick apart the title
|
||||||
title_split = source_spot.title.split(" on ")
|
title_split = source_spot.title.split(" on ")
|
||||||
dx_call = title_split[0]
|
dx_call = title_split[0]
|
||||||
ref = None
|
ref = None
|
||||||
ref_name = None
|
ref_name = None
|
||||||
if len(title_split) > 1:
|
if len(title_split) > 1:
|
||||||
ref_split = title_split[1].split(" - ")
|
ref_split = title_split[1].split(" - ")
|
||||||
ref = ref_split[0]
|
ref = ref_split[0]
|
||||||
if len(ref_split) > 1:
|
if len(ref_split) > 1:
|
||||||
ref_name = ref_split[1]
|
ref_name = ref_split[1]
|
||||||
|
|
||||||
# Pick apart the description
|
# Pick apart the description
|
||||||
desc_split = source_spot.description.split(". ")
|
desc_split = source_spot.description.split(". ")
|
||||||
freq_mode = desc_split[0].replace("Frequencies/modes:", "").strip()
|
freq_mode = desc_split[0].replace("Frequencies/modes:", "").strip()
|
||||||
freq_mode_split = freq_mode.split("-")
|
freq_mode_split = re.split(r'[\-\s]+', freq_mode)
|
||||||
freq_hz = float(freq_mode_split[0]) * 1000000
|
freq_hz = float(freq_mode_split[0]) * 1000000
|
||||||
mode = freq_mode_split[1]
|
mode = freq_mode_split[1].upper()
|
||||||
|
|
||||||
comment = None
|
comment = None
|
||||||
if len(desc_split) > 1:
|
if len(desc_split) > 1:
|
||||||
comment = desc_split[1].strip()
|
comment = desc_split[1].strip()
|
||||||
spotter = None
|
spotter = None
|
||||||
if len(desc_split) > 2:
|
if len(desc_split) > 2:
|
||||||
spotter = desc_split[2].replace("Spotted by ", "").replace(".", "").strip()
|
spotter = desc_split[2].replace("Spotted by ", "").replace(".", "").upper().strip()
|
||||||
|
|
||||||
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
|
||||||
|
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name,
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot.guid.content,
|
source_id=source_spot.guid.content,
|
||||||
dx_call=dx_call,
|
dx_call=dx_call,
|
||||||
de_call=spotter,
|
de_call=spotter,
|
||||||
freq=freq_hz,
|
freq=freq_hz,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
|
||||||
time=time.timestamp())
|
time=time.timestamp())
|
||||||
|
|
||||||
new_spots.append(spot)
|
new_spots.append(spot)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Exception parsing WOTA spot", e)
|
||||||
return new_spots
|
return new_spots
|
||||||
|
|||||||
@@ -101,11 +101,6 @@
|
|||||||
<h5 class="card-title">Number of Alerts</h5>
|
<h5 class="card-title">Number of Alerts</h5>
|
||||||
<p class="card-text spothole-card-text">Show up to
|
<p class="card-text spothole-card-text">Show up to
|
||||||
<select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
|
<select id="alerts-to-fetch" class="storeable-select form-select ms-2" oninput="filtersUpdated();" style="width: 5em;display: inline-block;">
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100" selected>100</option>
|
|
||||||
<option value="200">200</option>
|
|
||||||
<option value="500">500</option>
|
|
||||||
</select>
|
</select>
|
||||||
alerts
|
alerts
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -93,10 +93,6 @@
|
|||||||
<h5 class="card-title">Spot Age</h5>
|
<h5 class="card-title">Spot Age</h5>
|
||||||
<p class="card-text spothole-card-text">Last
|
<p class="card-text spothole-card-text">Last
|
||||||
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
||||||
<option value="300">5</option>
|
|
||||||
<option value="600">10</option>
|
|
||||||
<option value="1800" selected>30</option>
|
|
||||||
<option value="3600">60</option>
|
|
||||||
</select>
|
</select>
|
||||||
minutes
|
minutes
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -92,10 +92,6 @@
|
|||||||
<h5 class="card-title">Spot Age</h5>
|
<h5 class="card-title">Spot Age</h5>
|
||||||
<p class="card-text spothole-card-text">Last
|
<p class="card-text spothole-card-text">Last
|
||||||
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
<select id="max-spot-age" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
||||||
<option value="300">5</option>
|
|
||||||
<option value="600">10</option>
|
|
||||||
<option value="1800" selected>30</option>
|
|
||||||
<option value="3600">60</option>
|
|
||||||
</select>
|
</select>
|
||||||
minutes
|
minutes
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<p class="d-inline-flex gap-1">
|
<p class="d-inline-flex gap-1">
|
||||||
<span style="position: relative;">
|
<span style="position: relative;">
|
||||||
<i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 0px; top: 2px; padding: 10px; pointer-events: none;"></i>
|
<i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 0px; top: 2px; padding: 10px; pointer-events: none;"></i>
|
||||||
<input id="filter-dx-call" type="text" class="form-control" oninput="filtersUpdated();" placeholder="Search for call">
|
<input id="filter-dx-call" type="search" class="form-control" oninput="filtersUpdated();" placeholder="Callsign">
|
||||||
</span>
|
</span>
|
||||||
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i> Filters</button>
|
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i> Filters</button>
|
||||||
<button id="display-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i> Display</button>
|
<button id="display-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i> Display</button>
|
||||||
@@ -117,10 +117,6 @@
|
|||||||
<h5 class="card-title">Number of Spots</h5>
|
<h5 class="card-title">Number of Spots</h5>
|
||||||
<p class="card-text spothole-card-text">Show up to
|
<p class="card-text spothole-card-text">Show up to
|
||||||
<select id="spots-to-fetch" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
<select id="spots-to-fetch" class="storeable-select form-select ms-2 me-2 d-inline-block" oninput="filtersUpdated();" style="width: 5em; display: inline-block;">
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50" selected>50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
</select>
|
||||||
spots
|
spots
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- General
|
- General
|
||||||
summary: Get enumeration options
|
summary: Get enumeration options
|
||||||
description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about.
|
description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about. The call also returns a variety of other parameters that may be of use to a web UI, including the contents of the "web-ui-options" config section, which provides guidance for web UI implementations such as the built-in one on sensible configuration options such as the number of spots/alerts to retrieve, or the maximum age of spots to retrieve.
|
||||||
operationId: options
|
operationId: options
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -490,6 +490,39 @@ paths:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server.
|
description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server.
|
||||||
example: true
|
example: true
|
||||||
|
web-ui-options:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
spot-count:
|
||||||
|
type: array
|
||||||
|
description: An array of suggested "spot counts" that the web UI can retrieve from the API
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
example: 50
|
||||||
|
spot-count-default:
|
||||||
|
type: integer
|
||||||
|
example: 50
|
||||||
|
description: The suggested default "spot count" that the web UI should retrieve from the API
|
||||||
|
max-spot-age:
|
||||||
|
type: array
|
||||||
|
description: An array of suggested "maximum spot ages" that the web UI can retrieve from the API
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
example: 30
|
||||||
|
max-spot-age-default:
|
||||||
|
type: integer
|
||||||
|
example: 30
|
||||||
|
description: The suggested default "maximum spot age" that the web UI should retrieve from the API
|
||||||
|
alert-count:
|
||||||
|
type: array
|
||||||
|
description: An array of suggested "alert counts" that the web UI can retrieve from the API
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
example: 100
|
||||||
|
alert-count-default:
|
||||||
|
type: integer
|
||||||
|
example: 100
|
||||||
|
description: The suggested default "alert count" that the web UI should retrieve from the API
|
||||||
|
|
||||||
|
|
||||||
/lookup/call:
|
/lookup/call:
|
||||||
@@ -528,11 +561,11 @@ paths:
|
|||||||
example: Dorset
|
example: Dorset
|
||||||
country:
|
country:
|
||||||
type: string
|
type: string
|
||||||
description: Country of the operator
|
description: Country of the operator. Note that this is named "country" for commonality with other amateur radio tools, but in reality this is more of a "DXCC Name", as it includes many options which are not countries, just territories that DXCC uniquely identifies.
|
||||||
example: United Kingdom
|
example: England
|
||||||
flag:
|
flag:
|
||||||
type: string
|
type: string
|
||||||
description: Country flag of the operator
|
description: Country flag of the operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
|
||||||
example: ""
|
example: ""
|
||||||
continent:
|
continent:
|
||||||
type: string
|
type: string
|
||||||
@@ -759,11 +792,11 @@ components:
|
|||||||
example: Dorset
|
example: Dorset
|
||||||
dx_country:
|
dx_country:
|
||||||
type: string
|
type: string
|
||||||
description: Country of the DX operator
|
description: Country of the operator. Note that this is named "country" for commonality with other amateur radio tools, but in reality this is more of a "DXCC Name", as it includes many options which are not countries, just territories that DXCC uniquely identifies.
|
||||||
example: United Kingdom
|
example: England
|
||||||
dx_flag:
|
dx_flag:
|
||||||
type: string
|
type: string
|
||||||
description: Country flag of the DX operator
|
description: Country flag of the DX operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
|
||||||
example: ""
|
example: ""
|
||||||
dx_continent:
|
dx_continent:
|
||||||
type: string
|
type: string
|
||||||
@@ -826,11 +859,11 @@ components:
|
|||||||
example: M0TEST
|
example: M0TEST
|
||||||
de_country:
|
de_country:
|
||||||
type: string
|
type: string
|
||||||
description: Country of the spotter
|
description: Country of the operator. Note that this is named "country" for commonality with other amateur radio tools, but in reality this is more of a "DXCC Name", as it includes many options which are not countries, just territories that DXCC uniquely identifies.
|
||||||
example: United Kingdom
|
example: England
|
||||||
de_flag:
|
de_flag:
|
||||||
type: string
|
type: string
|
||||||
description: Country flag of the spotter
|
description: Country flag of the spotter. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
|
||||||
example: ""
|
example: ""
|
||||||
de_continent:
|
de_continent:
|
||||||
type: string
|
type: string
|
||||||
@@ -1050,11 +1083,11 @@ components:
|
|||||||
example: Ian
|
example: Ian
|
||||||
dx_country:
|
dx_country:
|
||||||
type: string
|
type: string
|
||||||
description: Country of the DX operator. This, and the subsequent fields, assume that all activators will be in the same country!
|
description: Country of the DX operator. Country of the operator. Note that this is named "country" for commonality with other amateur radio tools, but in reality this is more of a "DXCC Name", as it includes many options which are not countries, just territories that DXCC uniquely identifies. This, and the subsequent fields, assume that all activators will be in the same country!
|
||||||
example: United Kingdom
|
example: England
|
||||||
dx_flag:
|
dx_flag:
|
||||||
type: string
|
type: string
|
||||||
description: Country flag of the DX operator
|
description: Country flag of the DX operator. This is limited to the range of emoji flags. For some DXCCs there may not be an official emoji flag, e.g. Northern Ireland, so the appearance may vary depending on your browser and operating system. Some small islands may also have no flag. Many DXCCs may also share a flag, e.g. mainland Spain, Balearic Islands, etc.
|
||||||
example: ""
|
example: ""
|
||||||
dx_continent:
|
dx_continent:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ div.container {
|
|||||||
/* SPOTS/ALERTS PAGES, SETTINGS/STATUS AREAS */
|
/* SPOTS/ALERTS PAGES, SETTINGS/STATUS AREAS */
|
||||||
|
|
||||||
input#filter-dx-call {
|
input#filter-dx-call {
|
||||||
max-width: 10em;
|
max-width: 12em;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
@@ -71,11 +71,16 @@ td.nowrap, span.nowrap {
|
|||||||
|
|
||||||
span.flag-wrapper {
|
span.flag-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.7em;
|
width: 1.8em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.flag {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
span.band-bullet {
|
span.band-bullet {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -265,7 +270,7 @@ div.band-spot:hover span.band-spot-info {
|
|||||||
}
|
}
|
||||||
/* Filter/search DX Call field should be smaller on mobile */
|
/* Filter/search DX Call field should be smaller on mobile */
|
||||||
input#filter-dx-call {
|
input#filter-dx-call {
|
||||||
max-width: 6em;
|
max-width: 9em;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
webassets/img/flags/1.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
webassets/img/flags/10.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
webassets/img/flags/100.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
webassets/img/flags/101.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/102.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/103.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
webassets/img/flags/104.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
webassets/img/flags/105.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
webassets/img/flags/106.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
webassets/img/flags/107.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
webassets/img/flags/108.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
webassets/img/flags/109.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
webassets/img/flags/11.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
webassets/img/flags/110.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
webassets/img/flags/111.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webassets/img/flags/112.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
webassets/img/flags/113.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/114.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
webassets/img/flags/115.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/116.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
webassets/img/flags/117.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
webassets/img/flags/118.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
webassets/img/flags/119.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/12.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
webassets/img/flags/120.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
webassets/img/flags/122.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
webassets/img/flags/123.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
webassets/img/flags/124.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
webassets/img/flags/125.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
webassets/img/flags/126.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
webassets/img/flags/127.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/128.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/129.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
webassets/img/flags/13.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
webassets/img/flags/130.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
webassets/img/flags/131.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
webassets/img/flags/132.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
webassets/img/flags/133.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
webassets/img/flags/134.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/135.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
webassets/img/flags/136.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
webassets/img/flags/137.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
webassets/img/flags/138.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
webassets/img/flags/139.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/14.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
webassets/img/flags/140.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
webassets/img/flags/141.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
webassets/img/flags/142.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
webassets/img/flags/143.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
webassets/img/flags/144.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webassets/img/flags/145.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
webassets/img/flags/146.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
webassets/img/flags/147.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webassets/img/flags/148.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
webassets/img/flags/149.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
webassets/img/flags/15.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
webassets/img/flags/150.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webassets/img/flags/151.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/152.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
webassets/img/flags/153.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webassets/img/flags/154.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/155.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/157.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
webassets/img/flags/158.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
webassets/img/flags/159.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
webassets/img/flags/16.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
webassets/img/flags/160.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
webassets/img/flags/161.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
webassets/img/flags/162.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
webassets/img/flags/163.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
webassets/img/flags/164.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/165.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
webassets/img/flags/166.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
webassets/img/flags/167.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
webassets/img/flags/168.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
webassets/img/flags/169.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
webassets/img/flags/17.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
webassets/img/flags/170.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
webassets/img/flags/171.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webassets/img/flags/172.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
webassets/img/flags/173.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
webassets/img/flags/174.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
webassets/img/flags/175.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
webassets/img/flags/176.png
Normal file
|
After Width: | Height: | Size: 11 KiB |