mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-12-15 16:43:38 +00:00
Compare commits
5 Commits
0c5b5f2062
...
3964134db9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3964134db9 | ||
|
|
04435e770a | ||
|
|
a4645171e4 | ||
|
|
65d546ef7e | ||
|
|
193838b9d3 |
@@ -10,7 +10,7 @@ The API is deliberately well-defined with an OpenAPI specification and auto-gene
|
||||
|
||||
Spothole itself is also open source, Public Domain licenced code that anyone can take and modify.
|
||||
|
||||
Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, the UK Packet Repeater Network, and NG3K.
|
||||
Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, the UK Packet Repeater Network, and NG3K.
|
||||
|
||||

|
||||
|
||||
@@ -198,6 +198,8 @@ As well as being my work, I have also gratefully received feature patches from S
|
||||
|
||||
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 software uses a number of Python libraries as listed in `requirements.txt`, and a number of JavaScript libraries such as jQuery and moment.js. 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.
|
||||
|
||||
The project's name was suggested by Harm, DK4HAA. Thanks!
|
||||
|
||||
48
alertproviders/bota.py
Normal file
48
alertproviders/bota.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from bs4 import BeautifulSoup
|
||||
from alertproviders.http_alert_provider import HTTPAlertProvider
|
||||
from core.sig_utils import get_icon_for_sig
|
||||
from data.alert import Alert
|
||||
from data.sig_ref import SIGRef
|
||||
|
||||
|
||||
# Alert provider for Beaches on the Air
|
||||
class BOTA(HTTPAlertProvider):
|
||||
POLL_INTERVAL_SEC = 3600
|
||||
ALERTS_URL = "https://www.beachesontheair.com/"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
def http_response_to_alerts(self, http_response):
|
||||
new_alerts = []
|
||||
# Find the table of upcoming alerts
|
||||
bs = BeautifulSoup(http_response.content.decode(), features="lxml")
|
||||
tbody = bs.body.find('div', attrs={'class': 'view-activations-public'}).find('table', attrs={'class': 'views-table'}).find('tbody')
|
||||
for row in tbody.find_all('tr'):
|
||||
cells = row.find_all('td')
|
||||
first_cell_text = str(cells[0].find('a').contents[0]).strip()
|
||||
ref_name = first_cell_text.split(" by ")[0]
|
||||
dx_call = str(cells[1].find('a').contents[0]).strip().upper()
|
||||
|
||||
# Get the date, dealing with the fact we get no year so have to figure out if it's last year or next year
|
||||
date_text = str(cells[2].find('span').contents[0]).strip()
|
||||
date_time = datetime.strptime(date_text,"%d %b - %H:%M UTC").replace(tzinfo=pytz.UTC)
|
||||
date_time = date_time.replace(year=datetime.now(pytz.UTC).year)
|
||||
# If this was more than a day ago, activation is actually next year
|
||||
if date_time < datetime.now(pytz.UTC) - timedelta(days=1):
|
||||
date_time = date_time.replace(year=datetime.now(pytz.UTC).year + 1)
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(source=self.name,
|
||||
dx_calls=[dx_call],
|
||||
sig="BOTA",
|
||||
sig_refs=[SIGRef(id=ref_name, name=ref_name, url="https://www.beachesontheair.com/beaches/" + ref_name.lower().replace(" ", "-"))],
|
||||
icon=get_icon_for_sig("BOTA"),
|
||||
start_time=date_time.timestamp(),
|
||||
is_dxpedition=False)
|
||||
|
||||
new_alerts.append(alert)
|
||||
return new_alerts
|
||||
@@ -104,6 +104,10 @@ alert-providers:
|
||||
class: "WOTA"
|
||||
name: "WOTA"
|
||||
enabled: true
|
||||
-
|
||||
class: "BOTA"
|
||||
name: "BOTA"
|
||||
enabled: true
|
||||
-
|
||||
class: "NG3K"
|
||||
name: "NG3K"
|
||||
|
||||
@@ -27,7 +27,8 @@ SIGS = [
|
||||
SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania", ref_regex=r""),
|
||||
SIG(name="WAB", description="Worked All Britain", icon="table-cells-large", ref_regex=r"[A-Z]{1,2}[0-9]{2}"),
|
||||
SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}"),
|
||||
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}")
|
||||
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
||||
SIG(name="BOTA", description="Beaches on the Air", icon="water")
|
||||
]
|
||||
|
||||
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||
@@ -216,7 +217,7 @@ DXCC_FLAGS = {
|
||||
146: "\U0001F1F1\U0001F1F9", # LITHUANIA
|
||||
147: "", # LORD HOWE ISLAND
|
||||
148: "\U0001F1FB\U0001F1EA", # VENEZUELA
|
||||
149: "", # AZORES
|
||||
149: "\U0001F1F5\U0001F1F9", # AZORES
|
||||
150: "\U0001F1E6\U0001F1FA", # AUSTRALIA
|
||||
151: "", # MALYJ VYSOTSKIJ ISLAND
|
||||
152: "\U0001F1F2\U0001F1F4", # MACAO
|
||||
|
||||
@@ -11,4 +11,4 @@ class SIG:
|
||||
# and Field Spotter. Does not include the "fa-" prefix.
|
||||
icon: str
|
||||
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||
ref_regex: str
|
||||
ref_regex: str = None
|
||||
@@ -11,4 +11,5 @@ psutil~=7.1.0
|
||||
requests-sse~=0.5.2
|
||||
rss-parser~=2.1.1
|
||||
pyproj~=3.7.2
|
||||
prometheus_client~=0.23.1
|
||||
prometheus_client~=0.23.1
|
||||
beautifulsoup4~=4.14.2
|
||||
@@ -246,6 +246,9 @@ class WebServer:
|
||||
case "comment_includes":
|
||||
comment_includes = query.get(k).strip()
|
||||
spots = [s for s in spots if s.comment and comment_includes.upper() in s.comment.upper()]
|
||||
case "dx_call_includes":
|
||||
dx_call_includes = query.get(k).strip()
|
||||
spots = [s for s in spots if s.dx_call and dx_call_includes.upper() in s.dx_call.upper()]
|
||||
case "allow_qrt":
|
||||
# If false, spots that are flagged as QRT are not returned.
|
||||
prevent_qrt = query.get(k).upper() == "FALSE"
|
||||
@@ -320,6 +323,9 @@ class WebServer:
|
||||
case "dx_continent":
|
||||
dxconts = query.get(k).split(",")
|
||||
alerts = [a for a in alerts if a.dx_continent and a.dx_continent in dxconts]
|
||||
case "dx_call_includes":
|
||||
dx_call_includes = query.get(k).strip()
|
||||
spots = [a for a in alerts if a.dx_call and dx_call_includes.upper() in a.dx_call.upper()]
|
||||
# If we have a "limit" parameter, we apply that last, regardless of where it appeared in the list of keys.
|
||||
if "limit" in query.keys():
|
||||
alerts = alerts[:int(query.get("limit"))]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
|
||||
import aprslib
|
||||
@@ -58,5 +58,5 @@ class APRSIS(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from APRS-IS.")
|
||||
@@ -1,17 +1,16 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import pytz
|
||||
import telnetlib3
|
||||
|
||||
from core.constants import SIGS
|
||||
from core.sig_utils import ANY_SIG_REGEX, ANY_XOTA_SIG_REF_REGEX, get_icon_for_sig, get_ref_regex_for_sig
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from core.sig_utils import ANY_SIG_REGEX, get_icon_for_sig, get_ref_regex_for_sig
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from spotproviders.spot_provider import SpotProvider
|
||||
|
||||
|
||||
@@ -97,7 +96,7 @@ class DXCluster(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from DX Cluster " + self.hostname + ".")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import pytz
|
||||
import telnetlib3
|
||||
|
||||
from data.spot import Spot
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from data.spot import Spot
|
||||
from spotproviders.spot_provider import SpotProvider
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class RBN(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from RBN on port " + str(self.port) + ".")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -74,14 +74,15 @@ class WOTA(HTTPSpotProvider):
|
||||
time=time.timestamp())
|
||||
|
||||
# WOTA name/grid/lat/lon lookup
|
||||
wota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
|
||||
for feature in wota_data["features"]:
|
||||
if feature["properties"]["wotaId"] == spot.sig_refs[0]:
|
||||
spot.sig_refs[0].name = feature["properties"]["title"]
|
||||
spot.dx_latitude = feature["geometry"]["coordinates"][1]
|
||||
spot.dx_longitude = feature["geometry"]["coordinates"][0]
|
||||
spot.dx_grid = feature["properties"]["qthLocator"]
|
||||
break
|
||||
if ref:
|
||||
wota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
|
||||
for feature in wota_data["features"]:
|
||||
if feature["properties"]["wotaId"] == ref:
|
||||
spot.sig_refs[0].name = feature["properties"]["title"]
|
||||
spot.dx_latitude = feature["geometry"]["coordinates"][1]
|
||||
spot.dx_longitude = feature["geometry"]["coordinates"][0]
|
||||
spot.dx_grid = feature["properties"]["qthLocator"]
|
||||
break
|
||||
|
||||
new_spots.append(spot)
|
||||
return new_spots
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
<p>In amateur radio terminology, the "DX" contact is the "interesting" one that is using the frequency shown. They might be on a remote island or just in a local park, but either way it's interesting enough that someone has "spotted" them. The callsign listed under "DE" is the person who spotted the "DX" operator. "Modes" are the type of communication they are using. You might see "CW" which is Morse Code, or voice "modes" like SSB or FM, or more exotic "data" modes which are used for computer-to-computer communication.</p>
|
||||
<h4 class="mt-4">What data sources are supported?</h4>
|
||||
<p>Spothole can retrieve spots from: Telnet-based DX clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, and the UK Packet Repeater Network.</p>
|
||||
<p>Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, and WOTA.</p>
|
||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, IOTA, MOTS, ARLHS, ILLW, SIOTA, WCA, ZLOTA, KRMNPA, WOTA, WAB & WAI.</p>
|
||||
<p>Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, WOTA and BOTA.</p>
|
||||
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
|
||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, IOTA, MOTS, ARLHS, ILLW, SIOTA, WCA, ZLOTA, KRMNPA, WOTA, BOTA, WAB & WAI.</p>
|
||||
<h4 class="mt-4">How is this better than DXheat, DXsummit, POTA's own website, etc?</h4>
|
||||
<p>It's probably not? But it's nice to have choice.</p>
|
||||
<p>I think it's got two key advantages over those sites:</p>
|
||||
@@ -36,6 +37,9 @@
|
||||
<p>Settings you select from Spothole's menus are sent to the server, in order to provide the data with the requested filters. They are also stored in your browser's local storage, so that your preferences are remembered between sessions.</p>
|
||||
<p>There are no trackers, no ads, and no cookies.</p>
|
||||
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
|
||||
<h2 class="mt-4">Thanks</h2>
|
||||
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, and other online tools on which Spothole's data is based.</p>
|
||||
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set.</p>
|
||||
</div>
|
||||
|
||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
@@ -14,6 +14,10 @@
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<p class="d-inline-flex gap-1">
|
||||
<div style="position: relative; display: inline-block; top: 2px;">
|
||||
<i class="fa-solid fa-magnifying-glass" style="position: absolute; left: 0px; padding: 10px; pointer-events: none;"></i>
|
||||
<input id="filter-dx-call" type="text" class="form-control hideonmobile me-3" oninput="filtersUpdated();" placeholder="Search for call" style="max-width: 10em; padding-left: 2em;">
|
||||
</div>
|
||||
<button id="add-spot-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleAddSpotPanel();"><i class="fa-solid fa-comment"></i> Add Spot</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>
|
||||
|
||||
@@ -62,6 +62,7 @@ paths:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
@@ -87,6 +88,7 @@ paths:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
- name: needs_sig
|
||||
@@ -205,6 +207,12 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- name: dx_call_includes
|
||||
in: query
|
||||
description: "Limit the alerts to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches."
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: comment_includes
|
||||
in: query
|
||||
description: "Return only spots where the comment includes the provided string (case-insensitive)."
|
||||
@@ -284,6 +292,7 @@ paths:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
@@ -309,11 +318,12 @@ paths:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
- name: dx_continent
|
||||
in: query
|
||||
description: "Limit the alerts to only ones where the DX (the operator being spotted) is on the given continent(s). To select more than one continent, supply a comma-separated list."
|
||||
description: "Limit the alerts to only ones where the DX operator is on the given continent(s). To select more than one continent, supply a comma-separated list."
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -325,6 +335,12 @@ paths:
|
||||
- AF
|
||||
- OC
|
||||
- AN
|
||||
- name: dx_call_includes
|
||||
in: query
|
||||
description: "Limit the alerts to only ones where the DX callsign includes the supplied string (case-insensitive). Generally a complete callsign, but you can supply a shorter string for partial matches."
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
@@ -768,6 +784,7 @@ components:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
example: POTA
|
||||
@@ -921,6 +938,7 @@ components:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
example: POTA
|
||||
@@ -950,6 +968,7 @@ components:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
|
||||
@@ -59,14 +59,6 @@ button#add-spot-button {
|
||||
|
||||
/* SPOTS/ALERTS PAGES, MAIN TABLE */
|
||||
|
||||
/* Custom version of Bootstrap table colouring to colour 2 in every 4 rows, because of our second row per spot that
|
||||
appears on mobile */
|
||||
.table-striped-custom > tbody > tr:nth-of-type(4n+3) > *,
|
||||
.table-striped-custom > tbody > tr:nth-of-type(4n+4) > * {
|
||||
--bs-table-color-type: var(--bs-table-striped-color);
|
||||
--bs-table-bg-type: var(--bs-table-striped-bg);
|
||||
}
|
||||
|
||||
td.nowrap, span.nowrap {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ function updateTable() {
|
||||
var showRef = $("#tableShowRef")[0].checked;
|
||||
|
||||
// Populate table with headers
|
||||
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
|
||||
let table = $('<table class="table table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
|
||||
if (showStartTime) {
|
||||
table.find('thead tr').append(`<th>${useLocalTime ? "Start (Local)" : "Start UTC"}</th>`);
|
||||
}
|
||||
@@ -107,10 +107,18 @@ function updateTable() {
|
||||
|
||||
// Add a row to tbody for each alert in the provided list
|
||||
function addAlertRowsToTable(tbody, alerts) {
|
||||
var count = 0;
|
||||
alerts.forEach(a => {
|
||||
// Create row
|
||||
let $tr = $('<tr>');
|
||||
|
||||
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
|
||||
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
|
||||
// which cause the table-striped colouring to go awry.
|
||||
if (count % 2 == 1) {
|
||||
$tr.addClass("table-active");
|
||||
}
|
||||
|
||||
// Use local time instead of UTC?
|
||||
var useLocalTime = $("#timeZone")[0].value == "local";
|
||||
|
||||
@@ -205,9 +213,17 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
}
|
||||
|
||||
// Format sig_refs
|
||||
var sig_refs = ""
|
||||
if (a["sig_refs"]) {
|
||||
sig_refs = a["sig_refs"].map(a => `<span class='nowrap'>${a}</span>`).join(", ");
|
||||
var sig_refs = "";
|
||||
if (a["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < a["sig_refs"].length; i++) {
|
||||
if (a["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${a["sig_refs"][i]["url"]}' title='${a["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${a["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${a["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
|
||||
// Populate the row
|
||||
@@ -236,6 +252,9 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
|
||||
// Second row for mobile view only, containing source, ref, freqs/modes & comment
|
||||
$tr2 = $("<tr class='hidenotonmobile'>");
|
||||
if (count % 2 == 1) {
|
||||
$tr2.addClass("table-active");
|
||||
}
|
||||
$td2 = $("<td colspan='100'>");
|
||||
if (showSource) {
|
||||
$td2.append(`<span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> `);
|
||||
@@ -251,6 +270,8 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
}
|
||||
$tr2.append($td2);
|
||||
tbody.append($tr2);
|
||||
|
||||
count++;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -106,11 +106,17 @@ function getTooltipText(s) {
|
||||
|
||||
// Format sig_refs
|
||||
var sig_refs = "";
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
if (s["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
if (s["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
|
||||
// DX
|
||||
ttt = `<span class='nowrap'><span class='icon-wrapper'>${dx_flag}</span> <a href='https://www.qrz.com/db/${dx_call}' target='_blank' class="dx-link">${dx_call}</a></span><br/>`;
|
||||
|
||||
@@ -20,6 +20,9 @@ function buildQueryString() {
|
||||
}
|
||||
});
|
||||
str = str + "limit=" + $("#spots-to-fetch option:selected").val();
|
||||
if ($("#filter-dx-call").val() != "") {
|
||||
str = str + "&dx_call_includes=" + encodeURIComponent($("#filter-dx-call").val());
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
@@ -43,7 +46,7 @@ function updateTable() {
|
||||
var showDE = $("#tableShowDE")[0].checked;
|
||||
|
||||
// Populate table with headers
|
||||
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
|
||||
let table = $('<table class="table table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
|
||||
if (showTime) {
|
||||
table.find('thead tr').append(`<th>${useLocalTime ? "Local" : "UTC"}</th>`);
|
||||
}
|
||||
@@ -76,10 +79,18 @@ function updateTable() {
|
||||
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
spots.forEach(s => {
|
||||
// Create row
|
||||
let $tr = $('<tr>');
|
||||
|
||||
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
|
||||
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
|
||||
// which cause the table-striped colouring to go awry.
|
||||
if (count % 2 == 1) {
|
||||
$tr.addClass("table-active");
|
||||
}
|
||||
|
||||
// Show faded out if QRT
|
||||
if (s["qrt"] == true) {
|
||||
$tr.addClass("table-faded");
|
||||
@@ -161,11 +172,17 @@ function updateTable() {
|
||||
|
||||
// Format sig_refs
|
||||
var sig_refs = "";
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
if (s["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
if (s["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
|
||||
// Format DE flag
|
||||
var de_flag = "<i class='fa-solid fa-circle-question'></i>";
|
||||
@@ -224,6 +241,9 @@ function updateTable() {
|
||||
|
||||
// Second row for mobile view only, containing type, ref & comment
|
||||
$tr2 = $("<tr class='hidenotonmobile'>");
|
||||
if (count % 2 == 1) {
|
||||
$tr2.addClass("table-active");
|
||||
}
|
||||
if (s["qrt"] == true) {
|
||||
$tr2.addClass("table-faded");
|
||||
}
|
||||
@@ -242,6 +262,8 @@ function updateTable() {
|
||||
}
|
||||
$tr2.append($td2);
|
||||
table.find('tbody').append($tr2);
|
||||
|
||||
count++;
|
||||
});
|
||||
|
||||
// Update DOM
|
||||
|
||||
Reference in New Issue
Block a user