mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 00:39:26 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -14,6 +14,8 @@ Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), th
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Accessing the public version
|
||||
|
||||
You can access the public version's web interface at [https://spothole.app](https://spothole.app), and see [https://spothole.app/apidocs](https://spothole.app/apidocs) for the API details.
|
||||
|
||||
@@ -32,7 +32,7 @@ SIGS = [
|
||||
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||
CW_MODES = ["CW"]
|
||||
PHONE_MODES = ["PHONE", "SSB", "USB", "LSB", "AM", "FM", "DV", "DMR", "DSTAR", "C4FM", "M17"]
|
||||
DATA_MODES = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "PSK31", "BPSK31", "OLIVIA"]
|
||||
DATA_MODES = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "PSK31", "BPSK31", "OLIVIA", "MFSK", "MFSK32"]
|
||||
ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES
|
||||
MODE_TYPES = ["CW", "PHONE", "DATA"]
|
||||
|
||||
|
||||
BIN
images/screenshot3.png
Normal file
BIN
images/screenshot3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -24,6 +24,8 @@
|
||||
<h4 class="mt-4">Why does this website ask me if I want to install it?</h4>
|
||||
<p>Spothole is a Progressive Web App, which means you can install it on an Android or iOS device by opening the site in Chrome or Safari respectively, and clicking "Install" on the pop-up panel. It'll only prompt you once, so if you dismiss the prompt and change your mind, you'll find an Install / Add to Home Screen option on your browser's menu.</p>
|
||||
<p>Installing Spothole on your phone is completely optional, the website works exactly the same way as the "app" does.</p>
|
||||
<h4 class="mt-4">Why hasn't my spot/alert shown up yet?</h4>
|
||||
<p>To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every hour for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it reloads the data from Spothole, which is once a minute for spots or 30 minutes for alerts. So you could be waiting around three minutes to see a newly added spot, or 90 minutes to see a newly added alert.</p>
|
||||
<h4 class="mt-4">What licence does Spothole use?</h4>
|
||||
<p>Spothole's source code is licenced under the Public Domain. You can write a Spothole client, run your own server, modify it however you like, you can claim you wrote it and charge people £1000 for a copy, I don't really mind. (Please don't do the last one. But if you're using my code for something cool, it would be nice to hear from you!)</p>
|
||||
<h2 id="privacy" class="mt-4">Privacy</h2>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
% rebase('webpage_base.tpl')
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> This page is a work in progress. It will be refined as Spothole heads towards v1.0.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="row">
|
||||
<div class="col-auto me-auto pt-3">
|
||||
|
||||
@@ -153,104 +153,95 @@ div#map {
|
||||
/* BANDS PANEL */
|
||||
|
||||
div#bands-container {
|
||||
min-height: 64em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
/* Bands panel inner layout */
|
||||
div.bandCol {
|
||||
height: 100%;
|
||||
min-width: 8em;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow-y: clip;
|
||||
#bands-table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
div.bandColHeader {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
div.bandColMiddle {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
div.bandColMiddle ul {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.bandColMiddle ul li {
|
||||
display: table-row;
|
||||
line-height: 0.5em;
|
||||
}
|
||||
|
||||
/*noinspection CssUnusedSymbol*/
|
||||
div.bandColMiddle ul li.withSpots {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
div.bandColMiddle ul li span {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.bandColMiddle ul {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-left: 2px dotted;
|
||||
}
|
||||
|
||||
div.bandColHeader {
|
||||
#bands-table th {
|
||||
width: 20%;
|
||||
max-height: 40px;
|
||||
min-width: 12em;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.bandColMiddle {
|
||||
margin-left: 3px;
|
||||
border-left: 2px dotted var(--text);
|
||||
#bands-table td {
|
||||
width: 20%;
|
||||
min-width: 12em;
|
||||
height: 62em;
|
||||
}
|
||||
|
||||
div.bandColSpot {
|
||||
display: block;
|
||||
div.band-container {
|
||||
height: 62em;
|
||||
width: 20%;
|
||||
min-width: 12em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.band-markers {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 13;
|
||||
border-left: 2px dotted black;
|
||||
}
|
||||
|
||||
div.band-spots {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
canvas.band-lines-canvas {
|
||||
width: 5em;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
div.band-spot {
|
||||
position: absolute;
|
||||
left: 5em;
|
||||
padding: 0 0.25em;
|
||||
background-color: white;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
background: lightyellow;
|
||||
margin-right: 2em;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
span.bandColSpot {
|
||||
vertical-align: bottom;
|
||||
display: inline !important;
|
||||
div.band-spot:hover {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Don't wrap frequencies */
|
||||
span.bandColSpotFreq {
|
||||
white-space: nowrap;
|
||||
display: inline !important;
|
||||
div.band-spot span.band-spot-call {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
span.bandColSpotMode {
|
||||
padding-left: 0.5em;
|
||||
font-size: 0.8em;
|
||||
line-height: 0.4em;
|
||||
div.band-spot:hover span.band-spot-call {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.band-spot span.band-spot-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.band-spot:hover span.band-spot-info {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// A couple of constants that must match what's in CSS. We need to know them before the content actually renders, so we
|
||||
// can't just ask the elements themselves for their dimensions.
|
||||
BAND_COLUMN_HEIGHT_EM = 62;
|
||||
BAND_COLUMN_CANVAS_WIDTH_EM = 4;
|
||||
BAND_COLUMN_FONT_SIZE = 16;
|
||||
BAND_COLUMN_HEIGHT_PX = BAND_COLUMN_HEIGHT_EM * BAND_COLUMN_FONT_SIZE;
|
||||
BAND_COLUMN_CANVAS_WIDTH_PX = BAND_COLUMN_CANVAS_WIDTH_EM * BAND_COLUMN_FONT_SIZE;
|
||||
BAND_COLUMN_SPOT_DIV_HEIGHT_PX = BAND_COLUMN_FONT_SIZE * 1.6;
|
||||
|
||||
// Load spots and populate the bands display.
|
||||
function loadSpots() {
|
||||
$.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) {
|
||||
@@ -28,9 +37,9 @@ function buildQueryString() {
|
||||
// Update the bands display
|
||||
function updateBands() {
|
||||
// Stop here if nothing to display
|
||||
var bandsPanel = $("#bands-container");
|
||||
var bandsContainer = $("#bands-container");
|
||||
if (spots.length === 0) {
|
||||
bandsPanel.html("<div class='alert alert-danger' role='alert'>No spots match your filters.</div>");
|
||||
bandsContainer.html("<div class='alert alert-danger' role='alert'>No spots match your filters.</div>");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,73 +60,137 @@ function updateBands() {
|
||||
}
|
||||
});
|
||||
|
||||
// Build up HTML content for each band
|
||||
let html = "";
|
||||
const columnWidthPercent = Math.max(30, 100 / bandToSpots.size);
|
||||
let columnIndex = 0;
|
||||
// Track if any columns end up taller than expected, so we can resize the container and avoid vertical scroll.
|
||||
var maxHeightBand = 0;
|
||||
|
||||
// Build up table content for each band
|
||||
var table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>');
|
||||
bandToSpots.forEach(function (spotList, bandName) {
|
||||
// Get the colours for the band from the first spot, and prepare the header
|
||||
html += "<div class='bandCol' style='width:" + columnWidthPercent + "%'>";
|
||||
html += "<div class='bandColHeader' style='background-color:" + spotList[0].band_color + "; color:" + spotList[0].band_contrast_color + "'>" + spotList[0].band + "</div>";
|
||||
html += "<div class='bandColMiddle'>";
|
||||
table.find('thead tr').append(`<th style='background-color:${spotList[0].band_color}; color:${spotList[0].band_contrast_color}'>${spotList[0].band}</th>`);
|
||||
|
||||
// Get the band data to fetch start and end frequencies
|
||||
let band = options["bands"].filter(function (b) {
|
||||
return b.name === bandName;
|
||||
})[0];
|
||||
// Start printing the band
|
||||
|
||||
// Print the frequency band markers. This is 41 steps to divide the band evenly into 40 markers. One in every
|
||||
// four will show the actual frequency, the others will just be dashes.
|
||||
bandMarkersDiv = $('<div class="band-markers">');
|
||||
const freqStep = (band.end_freq - band.start_freq) / 40.0;
|
||||
html += "<ul>";
|
||||
html += "<li><span>-</span></li>";
|
||||
|
||||
// Do 40 steps down the band
|
||||
for (let i = 0; i <= 40; i++) {
|
||||
|
||||
// Work out if there are any spots in this step
|
||||
const freqStepStart = band.start_freq + i * freqStep;
|
||||
const freqStepEnd = freqStepStart + freqStep;
|
||||
const spotsInStep = spotList.filter(function (s) {
|
||||
// Normally we do >= start and < end, but in the special case where this is the last step and there is a spot
|
||||
// right at the end of the band, we include this too
|
||||
return s.freq >= freqStepStart && (s.freq < freqStepEnd || (s.freq === freqStepEnd && freqStepEnd === band.end_freq));
|
||||
});
|
||||
|
||||
if (spotsInStep.length > 0) {
|
||||
// If this step has spots in it, print them
|
||||
html += "<li class='withSpots'><span>";
|
||||
spotsInStep.sort((a, b) => (a.freq > b.freq) ? 1 : ((b.freq > a.freq) ? -1 : 0));
|
||||
spotsInStep.forEach(function (s) {
|
||||
html += "<div class='bandColSpot'><span class='bandColSpot'>" + s.dx_call + "<br/><span class='bandColSpotFreq'>" + (s.freq/1000000) + "</span>";
|
||||
if (s.mode != null && s.mode.length > 0 && s.mode !== "Unknown") {
|
||||
html += "<span class='bandColSpotMode'>" + s.mode + "</span>";
|
||||
}
|
||||
html += "</span></div>";
|
||||
});
|
||||
html += "</li></span>";
|
||||
|
||||
if (i % 4 === 0) {
|
||||
bandMarkersDiv.append("—" + ((band.start_freq + i * freqStep)/1000000).toFixed(3) + "<br/>");
|
||||
} else if (i % 4 === 2) {
|
||||
bandMarkersDiv.append("–<br/>");
|
||||
} else {
|
||||
// Step had no spots in it, so just print a marker. This is a frequency on multiples of 4, or a dash otherwise.
|
||||
if (i % 4 === 0) {
|
||||
html += "<li><span>—" + ((band.start_freq + i * freqStep)/1000000).toFixed(3) + "</span></li>";
|
||||
} else if (i % 4 === 2) {
|
||||
html += "<li><span>–</span></li>";
|
||||
} else {
|
||||
html += "<li><span>-</span></li>";
|
||||
}
|
||||
bandMarkersDiv.append("-<br/>");
|
||||
}
|
||||
}
|
||||
html += "<li><span>-</span></li>";
|
||||
html += "</ul>";
|
||||
|
||||
html += "</div></div>";
|
||||
columnIndex++;
|
||||
// Prepare the spots list
|
||||
var bandSpotsDiv = $("<div class='band-spots'>");
|
||||
var lastSpotPxDownBand = -999;
|
||||
// Sort by frequency so have a consistent order in which to plan where they will appear on the band div.
|
||||
spotList.sort(function(a, b) { return a.freq - b.freq; });
|
||||
// First calculate how we should be displaying the spots. There are three "modes" to try to place them in a
|
||||
// visually appealing way:
|
||||
// 1) Spaced normally, not going over the end of the band, so we populate them forwards.
|
||||
// 2) Would go over the end, but the spots don't fill the band, so we populate them backwards.
|
||||
// 3) Spots totally fill the band (or more), so we space them evenly starting at the top.
|
||||
// In each case, we don't add anything to the DOM yet, we just calculate "pxDownBandLabel" (how far the *top* of
|
||||
// the label is from the top of the div) and add that as a property to the spot for later use.
|
||||
if (spotList.length >= BAND_COLUMN_HEIGHT_PX / BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
|
||||
// Mode 3.
|
||||
// Just lay out all spots simply, starting at 0px offset and working down with each one touching.
|
||||
lastSpotPxDownBand = 0 - BAND_COLUMN_SPOT_DIV_HEIGHT_PX;
|
||||
spotList.forEach(s => {
|
||||
lastSpotPxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX;
|
||||
s["pxDownBandLabel"] = lastSpotPxDownBand;
|
||||
});
|
||||
} else {
|
||||
// Mode 1 or 2. Run through adding things to the list forwards as a test.
|
||||
spotList.forEach(s => {
|
||||
// Work out how far down the div to draw it
|
||||
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
|
||||
var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
|
||||
if (pxDownBand < lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
|
||||
pxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
|
||||
}
|
||||
s["pxDownBandLabel"] = pxDownBand;
|
||||
lastSpotPxDownBand = pxDownBand;
|
||||
});
|
||||
|
||||
// Work out if we overflowed the end.
|
||||
if (lastSpotPxDownBand <= BAND_COLUMN_HEIGHT_PX) {
|
||||
// Mode 1. Current positions are fine and there's nothing to do.
|
||||
} else {
|
||||
// Mode 2. Repeat the process but backwards, starting at the end and working upwards.
|
||||
lastSpotPxDownBand = 999999;
|
||||
spotList.reverse().forEach(s => {
|
||||
// Work out how far down the div to draw it
|
||||
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
|
||||
var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
|
||||
if (pxDownBand > lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
|
||||
pxDownBand = lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
|
||||
}
|
||||
s["pxDownBandLabel"] = pxDownBand;
|
||||
lastSpotPxDownBand = pxDownBand;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Now each spot is tagged with how far down the div it should go, add them to the DOM.
|
||||
spotList.forEach(s => {
|
||||
bandSpotsDiv.append(`<div class="band-spot" style="top: ${s['pxDownBandLabel']}px; border-top: 1px solid ${s.band_color}; border-left: 5px solid ${s.band_color}; border-bottom: 1px solid ${s.band_color}; border-right: 1px solid ${s.band_color};"><span class="band-spot-call">${s.dx_call}</span><span class="band-spot-info">${s.dx_call} ${(s.freq/1000000).toFixed(3)} ${s.mode}</span></div>`);
|
||||
});
|
||||
|
||||
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
|
||||
// spots have gone off the end of the band markers and stretched their div, we need to resize the canvas to
|
||||
// match, otherwise we have nowhere to draw their connecting lines.
|
||||
var canvasHeight = Math.max(BAND_COLUMN_HEIGHT_PX, lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX);
|
||||
maxHeightBand = Math.max(maxHeightBand, canvasHeight);
|
||||
|
||||
// Draw horizontal or diagonal lines to join up the "real" frequency with where the spot div ended up
|
||||
var bandLinesCanvas = $(`<canvas class='band-lines-canvas' width='${BAND_COLUMN_CANVAS_WIDTH_PX}px' height='${canvasHeight}px' style='height:${canvasHeight}px !important;'>`);
|
||||
spotList.forEach(s => {
|
||||
// Work out how far down the div to draw it
|
||||
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
|
||||
var pxDownBandFreq = (percentDownBand + 0.015) * BAND_COLUMN_HEIGHT_PX; // same fudge but add half to put the left end of the line in the right place
|
||||
var pxDownBandLabel = s["pxDownBandLabel"] + (BAND_COLUMN_SPOT_DIV_HEIGHT_PX / 1.75); // line should be to the vertical text-centre spot, not to the top corner
|
||||
|
||||
// Draw the line on the canvas
|
||||
var ctx = bandLinesCanvas[0].getContext('2d');
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
ctx.strokeStyle = s.band_color;
|
||||
ctx.moveTo(0, pxDownBandFreq);
|
||||
ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel);
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Assemble the table cell
|
||||
td = $("<td>");
|
||||
container = $("<div class='band-container'>");
|
||||
container.append(bandLinesCanvas);
|
||||
container.append(bandMarkersDiv);
|
||||
container.append(bandSpotsDiv);
|
||||
td.append(container);
|
||||
table.find('tbody tr').append(td);
|
||||
});
|
||||
|
||||
// Update the DOM with the band HTML
|
||||
bandsPanel.html(html);
|
||||
bandsContainer.html(table);
|
||||
|
||||
// Increase the height of the bands container so we don't have any vertical scroll bars except the browser ones
|
||||
bandsContainer.css("min-height", `${maxHeightBand + 42}px`);
|
||||
|
||||
// Desktop mouse wheel to scroll bands horizontally if used on the headers
|
||||
// noinspection JSDeprecatedSymbols
|
||||
$(".bandColHeader").on("wheel", () => bandsPanel.scrollLeft(bandsPanel.scrollLeft() + event.deltaY / 10.0));
|
||||
table.find('thead tr').on("wheel", () => {
|
||||
bandsContainer.scrollLeft(bandsContainer.scrollLeft() + event.deltaY / 10.0);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate through a temporary list of spots, merging duplicates in a way suitable for the band panel. If two or more
|
||||
|
||||
Reference in New Issue
Block a user