mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-07-29 16:29:19 +02:00
offline optimization, new signal chart, ui optimization
This commit is contained in:
+39
-14
@@ -84,16 +84,20 @@ function handleConnect(clientIp, currentUsers, ws, callback) {
|
||||
}
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
console.error("Error fetching location data:", err);
|
||||
consoleCmd.logError("Error fetching location data:", err.code);
|
||||
callback("User allowed");
|
||||
});
|
||||
}
|
||||
|
||||
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
|
||||
const connectionTime = new Date().toLocaleString([], options);
|
||||
let bannedASCache = { data: null, timestamp: 0 };
|
||||
|
||||
https.get("https://fmdx.org/banned_as.json", (banResponse) => {
|
||||
function fetchBannedAS(callback) {
|
||||
const now = Date.now();
|
||||
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) {
|
||||
return callback(null, bannedASCache.data);
|
||||
}
|
||||
|
||||
const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => {
|
||||
let banData = "";
|
||||
|
||||
banResponse.on("data", (chunk) => {
|
||||
@@ -103,6 +107,36 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
banResponse.on("end", () => {
|
||||
try {
|
||||
const bannedAS = JSON.parse(banData).banned_as || [];
|
||||
bannedASCache = { data: bannedAS, timestamp: now };
|
||||
callback(null, bannedAS);
|
||||
} catch (error) {
|
||||
console.error("Error parsing banned AS list:", error);
|
||||
callback(null, []); // Default to allowing user
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set timeout for the request (5 seconds)
|
||||
req.setTimeout(5000, () => {
|
||||
console.error("Error: Request timed out while fetching banned AS list.");
|
||||
req.abort();
|
||||
callback(null, []); // Default to allowing user
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
console.error("Error fetching banned AS list:", err);
|
||||
callback(null, []); // Default to allowing user
|
||||
});
|
||||
}
|
||||
|
||||
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
|
||||
const connectionTime = new Date().toLocaleString([], options);
|
||||
|
||||
fetchBannedAS((error, bannedAS) => {
|
||||
if (error) {
|
||||
console.error("Error fetching banned AS list:", error);
|
||||
}
|
||||
|
||||
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
|
||||
return callback("User banned");
|
||||
@@ -124,19 +158,10 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
|
||||
`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
|
||||
);
|
||||
|
||||
callback("User allowed");
|
||||
} catch (error) {
|
||||
console.error("Error parsing banned AS list:", error);
|
||||
callback("User allowed");
|
||||
}
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
console.error("Error fetching banned AS list:", err);
|
||||
callback("User allowed");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function formatUptime(uptimeInSeconds) {
|
||||
const secondsInMinute = 60;
|
||||
const secondsInHour = secondsInMinute * 60;
|
||||
|
||||
+1
-3
@@ -469,9 +469,7 @@ wss.on('connection', (ws, request) => {
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers--;
|
||||
}
|
||||
dataHandler.showOnlineUsers(currentUsers);
|
||||
|
||||
+26
-22
@@ -13,8 +13,14 @@ let usStatesGeoJson = null; // To cache the GeoJSON data for US states
|
||||
// Load the US states GeoJSON data
|
||||
async function loadUsStatesGeoJson() {
|
||||
if (!usStatesGeoJson) {
|
||||
try {
|
||||
const response = await fetch(usStatesGeoJsonUrl);
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
usStatesGeoJson = await response.json();
|
||||
} catch (error) {
|
||||
console.error("Failed to load US States GeoJSON:", error);
|
||||
usStatesGeoJson = null; // Ensure it's null so it can retry later
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +85,17 @@ async function fetchTx(freq, piCode, rdsPs) {
|
||||
|
||||
const url = "https://maps.fmdx.org/api/?freq=" + freq;
|
||||
|
||||
return fetch(url, {
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(async (data) => {
|
||||
try {
|
||||
const response = await fetch(url, { redirect: 'manual' });
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
cachedData[freq] = data;
|
||||
await loadUsStatesGeoJson();
|
||||
return processData(data, piCode, rdsPs);
|
||||
})
|
||||
.catch(error => {
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
});
|
||||
return null; // Return null to indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
async function processData(data, piCode, rdsPs) {
|
||||
@@ -182,29 +187,28 @@ function checkEs() {
|
||||
let esSwitch = false;
|
||||
|
||||
if (now - esSwitchCache.lastCheck < esFetchInterval) {
|
||||
esSwitch = esSwitchCache.esSwitch;
|
||||
} else if (serverConfig.identification.lat > 20) {
|
||||
return esSwitchCache.esSwitch;
|
||||
}
|
||||
|
||||
if (serverConfig.identification.lat > 20) {
|
||||
esSwitchCache.lastCheck = now;
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (serverConfig.identification.lon < -32) {
|
||||
if (data.north_america.max_frequency != "No data") {
|
||||
esSwitch = true;
|
||||
if ((serverConfig.identification.lon < -32 && data.north_america.max_frequency !== "No data") ||
|
||||
(serverConfig.identification.lon >= -32 && data.europe.max_frequency !== "No data")) {
|
||||
esSwitchCache.esSwitch = true;
|
||||
}
|
||||
} else {
|
||||
if (data.europe.max_frequency != "No data") {
|
||||
esSwitch = true;
|
||||
}
|
||||
}
|
||||
esSwitchCache.esSwitch = esSwitch;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching data:", error);
|
||||
console.error("Error fetching Es data:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return esSwitch;
|
||||
return esSwitchCache.esSwitch;
|
||||
}
|
||||
|
||||
function haversine(lat1, lon1, lat2, lon2) {
|
||||
|
||||
+15
-2
@@ -22,6 +22,15 @@ h1#tuner-name {
|
||||
transition: 0.3s ease color;
|
||||
}
|
||||
|
||||
#tuner-name i {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#tuner-name i.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
h1#tuner-name:hover {
|
||||
color: var(--color-main-bright);
|
||||
}
|
||||
@@ -75,9 +84,13 @@ label {
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
width: 100%;
|
||||
height: 175px;
|
||||
width: calc(100% - 20px);
|
||||
height: 140px;
|
||||
overflow: hidden;
|
||||
border-radius: 15px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#data-ant {
|
||||
|
||||
Vendored
+9
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
-25
@@ -4,8 +4,12 @@
|
||||
<title><%= tunerName %> - FM-DX Webserver</title>
|
||||
<link href="css/entry.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/flags.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link href="css/libs/fontawesome.css" type="text/css" rel="stylesheet">
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<script src="js/libs/chart.umd.min.js"></script>
|
||||
<script src="js/libs/luxon.min.js"></script>
|
||||
<script src="js/libs/chartjs-adapter-luxon.umd.min.js"></script>
|
||||
<script src="js/libs/chartjs-plugin-streaming.min.js"></script>
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -39,7 +43,7 @@
|
||||
<div class="wrapper-outer dashboard-panel" style="padding-top: 20px; z-index: 10; position: relative;">
|
||||
<div class="panel-100-real m-0 flex-container bg-phone flex-phone-column" style="min-height: 64px; max-width: 1160px; margin-top: 10px;align-items: center; justify-content: space-between; padding-left: 20px;padding-right: 10px;">
|
||||
<h1 id="tuner-name" class="text-left flex-container flex-phone flex-center" style="padding-bottom: 3px; padding-right: 5px;height: 64px;">
|
||||
<span class="text-200-px" style="max-width: 450px;"><%= tunerName %></span> <i class="fa-solid fa-chevron-down p-left-10" style="font-size: 15px;"></i>
|
||||
<span class="text-200-px" style="max-width: 450px;padding-right: 10px;"><%= tunerName %></span> <i class="fa-solid fa-chevron-down" style="font-size: 15px;"></i>
|
||||
</h1>
|
||||
<% if(!publicTuner || tunerLock) { %>
|
||||
<div class="tuner-status p-10 color-3">
|
||||
@@ -109,26 +113,15 @@
|
||||
<div style="width: 1px;background: var(--color-2);" class="m-10 hide-phone"></div>
|
||||
|
||||
<div>
|
||||
<% const presets = [1, 2, 3, 4]; %>
|
||||
|
||||
<div style="height: 64px;" class="flex-center flex-phone">
|
||||
<button class="no-bg color-4 hover-brighten" id="preset1" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||
<% presets.forEach(preset => { %>
|
||||
<button class="no-bg color-4 hover-brighten" id="preset<%= preset %>" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||
<span style="font-size: 10px; color: var(--color-text);" id="preset1-text"></span>
|
||||
</button>
|
||||
|
||||
<button class="no-bg color-4 hover-brighten" id="preset2" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||
<span style="font-size: 10px; color: var(--color-text);" id="preset2-text"></span>
|
||||
</button>
|
||||
|
||||
<button class="no-bg color-4 hover-brighten" id="preset3" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||
<span style="font-size: 10px; color: var(--color-text);" id="preset3-text"></span>
|
||||
</button>
|
||||
|
||||
<button class="no-bg color-4 hover-brighten" id="preset4" style="padding: 6px; width: 64px; min-width: 64px;">
|
||||
<i class="fa-solid fa-wave-square fa-lg top-10"></i><br>
|
||||
<span style="font-size: 10px; color: var(--color-text);" id="preset4-text"></span>
|
||||
<span style="font-size: 10px; color: var(--color-text);" id="preset<%= preset %>-text"></span>
|
||||
</button>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flex-phone" style="align-items: center; height: 64px;">
|
||||
@@ -440,11 +433,6 @@
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'RDS PS Underscores', id: 'ps-underscores'}) %>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Imperial units', id: 'imperial-units'}) %>
|
||||
|
||||
<div class="form-group bottom-20 hide-desktop" style="float: none;">
|
||||
<label for="users-online"><i class="fa-solid fa-user"></i> Users online</label>
|
||||
<span class="users-online" name="users-online">0</span>
|
||||
</div>
|
||||
|
||||
<% if (isAdminAuthenticated) { %>
|
||||
<p class="color-3">You are logged in as an adminstrator.</p>
|
||||
<div class="admin-quick-dashboard">
|
||||
|
||||
@@ -46,3 +46,11 @@ function tuneTo(freq) {
|
||||
function resetRDS() {
|
||||
socket.send("T0");
|
||||
}
|
||||
|
||||
function getCurrentFreq() {
|
||||
currentFreq = $('#data-frequency').text();
|
||||
currentFreq = parseFloat(currentFreq).toFixed(3);
|
||||
currentFreq = parseFloat(currentFreq);
|
||||
|
||||
return currentFreq;
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ $(document).ready(function() {
|
||||
chatNicknameSave.click(function() {
|
||||
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`;
|
||||
localStorage.setItem('nickname', currentNickname);
|
||||
savedNickname = currentNickname; // Update the savedNickname variable
|
||||
savedNickname = currentNickname;
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
chatNicknameInput.blur();
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
var currentDate = new Date('Feb 16, 2025 15:00:00');
|
||||
var currentDate = new Date('Feb 23, 2025 15:30:00');
|
||||
var day = currentDate.getDate();
|
||||
var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1
|
||||
var year = currentDate.getFullYear();
|
||||
var formattedDate = day + '/' + month + '/' + year;
|
||||
var currentVersion = 'v1.3.5 [' + formattedDate + ']';
|
||||
var currentVersion = 'v1.3.6 [' + formattedDate + ']';
|
||||
|
||||
getInitialSettings();
|
||||
removeUrlParameters();
|
||||
|
||||
Vendored
+20
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* chartjs-adapter-luxon v1.3.1
|
||||
* https://www.chartjs.org
|
||||
* (c) 2023 chartjs-adapter-luxon Contributors
|
||||
* Released under the MIT license
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));
|
||||
+7
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+147
-157
@@ -3,8 +3,8 @@
|
||||
|
||||
|
||||
var parsedData, signalChart, previousFreq;
|
||||
var signalData = [];
|
||||
var data = [];
|
||||
var signalData = [];
|
||||
let updateCounter = 0;
|
||||
let messageCounter = 0; // Count for WebSocket data length returning 0
|
||||
let messageData = 800; // Initial value anything above 0
|
||||
@@ -32,7 +32,7 @@ const usa_programmes = [
|
||||
const rdsMode = localStorage.getItem('rdsMode');
|
||||
|
||||
$(document).ready(function () {
|
||||
var canvas = $('#signal-canvas')[0];
|
||||
const signalToggle = $("#signal-units-toggle");
|
||||
|
||||
var $panel = $('.admin-quick-dashboard');
|
||||
var panelWidth = $panel.outerWidth();
|
||||
@@ -48,11 +48,6 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
|
||||
canvas.width = canvas.parentElement.clientWidth;
|
||||
canvas.height = canvas.parentElement.clientHeight;
|
||||
|
||||
// Start updating the canvas
|
||||
initCanvas();
|
||||
fillPresets();
|
||||
|
||||
signalToggle.on("change", function () {
|
||||
@@ -205,7 +200,6 @@ $(document).ready(function () {
|
||||
$(freqContainer).on("click", function () {
|
||||
textInput.focus();
|
||||
});
|
||||
initTooltips();
|
||||
|
||||
//FMLIST logging
|
||||
$('.popup-content').on('click', function(event) {
|
||||
@@ -274,6 +268,8 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
|
||||
initCanvas();
|
||||
initTooltips();
|
||||
});
|
||||
|
||||
function getServerTime() {
|
||||
@@ -294,29 +290,21 @@ function getServerTime() {
|
||||
|
||||
const serverOptions = {
|
||||
...options,
|
||||
timeZone: 'Etc/UTC' // Add timeZone only for server time
|
||||
timeZone: 'Etc/UTC'
|
||||
};
|
||||
|
||||
const formattedServerTime = new Date(serverTimeUtc).toLocaleString(navigator.language ? navigator.language : 'en-US', serverOptions);
|
||||
|
||||
$("#server-time").text(formattedServerTime);
|
||||
|
||||
// Get and format user's local time directly without specifying timeZone:
|
||||
const localTime = new Date();
|
||||
const formattedLocalTime = new Date(localTime).toLocaleString(navigator.language ? navigator.language : 'en-US', options);
|
||||
|
||||
// Display client time:
|
||||
$("#client-time").text(formattedLocalTime);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.error("Error fetching server time:", errorThrown);
|
||||
// Handle error gracefully (e.g., display a fallback message)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendPingRequest() {
|
||||
const timeoutDuration = 15000; // Ping response can become buggy if it exceeds 20 seconds
|
||||
const timeoutDuration = 5000;
|
||||
const startTime = new Date().getTime();
|
||||
|
||||
const fetchWithTimeout = (url, options, timeout = timeoutDuration) => {
|
||||
@@ -407,7 +395,6 @@ function sendPingRequest() {
|
||||
}
|
||||
}
|
||||
|
||||
// Automatic UI resume on WebSocket reconnect
|
||||
function handleWebSocketMessage(event) {
|
||||
if (event.data == 'KICK') {
|
||||
console.log('Kick initiated.')
|
||||
@@ -429,151 +416,161 @@ function handleWebSocketMessage(event) {
|
||||
// Attach the message handler
|
||||
socket.onmessage = handleWebSocketMessage;
|
||||
|
||||
function initCanvas(parsedData) {
|
||||
signalToggle = $("#signal-units-toggle");
|
||||
const signalBuffer = [];
|
||||
|
||||
// Check if signalChart is already initialized
|
||||
if (!signalChart) {
|
||||
const canvas = $('#signal-canvas')[0];
|
||||
const context = canvas.getContext('2d');
|
||||
const maxDataPoints = 300;
|
||||
const pointWidth = (canvas.width - 80) / maxDataPoints;
|
||||
function initCanvas() {
|
||||
const ctx = document.getElementById("signal-canvas").getContext("2d");
|
||||
|
||||
window.signalChart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
datasets: [{
|
||||
label: "Signal Strength",
|
||||
borderColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-4").trim(),
|
||||
borderWidth: 2,
|
||||
fill: {
|
||||
target: 'start'
|
||||
},
|
||||
backgroundColor: () => getComputedStyle(document.documentElement).getPropertyValue("--color-1-transparent").trim(),
|
||||
tension: 0.6,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
left: -10,
|
||||
right: -10,
|
||||
bottom: -10
|
||||
},
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
elements: {
|
||||
point: { radius: 0 },
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "realtime",
|
||||
ticks: { display: false },
|
||||
border: { display: false },
|
||||
grid: { display: false, borderWidth: 0, borderColor: "transparent" },
|
||||
realtime: {
|
||||
duration: 30000,
|
||||
refresh: 75,
|
||||
delay: 150,
|
||||
onRefresh: (chart) => {
|
||||
if (!chart?.data?.datasets || parsedData?.sig === undefined) return;
|
||||
|
||||
signalChart = {
|
||||
canvas,
|
||||
context,
|
||||
parsedData,
|
||||
maxDataPoints,
|
||||
pointWidth,
|
||||
color2: null,
|
||||
color3: null,
|
||||
color4: null,
|
||||
signalUnit: localStorage.getItem('signalUnit'),
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
switch(signalChart.signalUnit) {
|
||||
case 'dbuv': signalChart.offset = 11.25; break;
|
||||
case 'dbm': signalChart.offset = 120; break;
|
||||
default: signalChart.offset = 0;
|
||||
signalBuffer.push(parsedData.sig);
|
||||
if (signalBuffer.length > 8) {
|
||||
signalBuffer.shift();
|
||||
}
|
||||
// Initialize colors and signal unit
|
||||
updateChartSettings(signalChart);
|
||||
const avgSignal = signalBuffer.reduce((sum, val) => sum + val, 0) / signalBuffer.length;
|
||||
|
||||
// Periodically check for color and signal unit updates
|
||||
setInterval(() => {
|
||||
updateChartSettings(signalChart);
|
||||
}, 1000); // Check every 1 second
|
||||
}
|
||||
|
||||
updateCanvas(parsedData, signalChart);
|
||||
}
|
||||
|
||||
function updateChartSettings(signalChart) {
|
||||
// Update colors
|
||||
const newColor2 = getComputedStyle(document.documentElement).getPropertyValue('--color-2').trim();
|
||||
const newColor3 = getComputedStyle(document.documentElement).getPropertyValue('--color-3').trim();
|
||||
const newColor4 = getComputedStyle(document.documentElement).getPropertyValue('--color-4').trim();
|
||||
if (newColor2 !== signalChart.color2 || newColor4 !== signalChart.color4) {
|
||||
signalChart.color2 = newColor2;
|
||||
signalChart.color3 = newColor3;
|
||||
signalChart.color4 = newColor4;
|
||||
}
|
||||
|
||||
// Update signal unit
|
||||
const newSignalUnit = localStorage.getItem('signalUnit');
|
||||
if (newSignalUnit !== signalChart.signalUnit) {
|
||||
signalChart.signalUnit = newSignalUnit;
|
||||
// Adjust the offset based on the new signal unit
|
||||
switch(newSignalUnit) {
|
||||
case 'dbuv': signalChart.offset = 11.25; break;
|
||||
case 'dbm': signalChart.offset = 120; break;
|
||||
default: signalChart.offset = 0;
|
||||
chart.data.datasets[0].data.push({
|
||||
x: Date.now(),
|
||||
y: avgSignal
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCanvas(parsedData, signalChart) {
|
||||
const { context, canvas, maxDataPoints, pointWidth, color2, color3, color4, offset } = signalChart;
|
||||
|
||||
if (data.length > maxDataPoints) {
|
||||
data = data.slice(data.length - maxDataPoints);
|
||||
}
|
||||
|
||||
const actualLowestValue = Math.min(...data);
|
||||
const actualHighestValue = Math.max(...data);
|
||||
const zoomMinValue = actualLowestValue - ((actualHighestValue - actualLowestValue) / 2);
|
||||
const zoomMaxValue = actualHighestValue + ((actualHighestValue - actualLowestValue) / 2);
|
||||
const zoomAvgValue = (zoomMaxValue - zoomMinValue) / 2 + zoomMinValue;
|
||||
|
||||
// Clear the canvas
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.beginPath();
|
||||
|
||||
const startingIndex = Math.max(0, data.length - maxDataPoints);
|
||||
|
||||
for (let i = startingIndex; i < data.length; i++) {
|
||||
const x = canvas.width - (data.length - i) * pointWidth - 40;
|
||||
const y = canvas.height - (data[i] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
|
||||
|
||||
if (i === startingIndex) {
|
||||
context.moveTo(x, y);
|
||||
} else {
|
||||
const prevX = canvas.width - (data.length - i + 1) * pointWidth - 40;
|
||||
const prevY = canvas.height - (data[i - 1] - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
|
||||
const interpolatedX = (x + prevX) / 2;
|
||||
const interpolatedY = (y + prevY) / 2;
|
||||
|
||||
context.quadraticCurveTo(prevX, prevY, interpolatedX, interpolatedY);
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grace: 0.25,
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
display: false // Hide default labels
|
||||
},
|
||||
grid: {
|
||||
display: false, // Hide default grid lines
|
||||
},
|
||||
},
|
||||
y2: {
|
||||
position: 'right', // Position on the right side
|
||||
beginAtZero: false,
|
||||
grace: 0.25,
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
maxTicksLimit: 3,
|
||||
display: false // Hide default labels for the right axis
|
||||
},
|
||||
grid: {
|
||||
display: false, // No grid for right axis
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false }
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
id: 'customYAxisLabels',
|
||||
afterDraw: (chart) => {
|
||||
const { ctx, scales, chartArea } = chart;
|
||||
const yAxis = scales.y;
|
||||
const y2Axis = scales.y2;
|
||||
|
||||
context.strokeStyle = color4;
|
||||
context.lineWidth = 2;
|
||||
context.stroke();
|
||||
const gridLineColor = getComputedStyle(document.documentElement).getPropertyValue("--color-2-transparent").trim(); // Grid color using CSS variable
|
||||
const textColor = getComputedStyle(document.documentElement).getPropertyValue("--color-3-transparent").trim(); // Use the same color for text labels
|
||||
|
||||
// Draw horizontal lines for lowest, highest, and average values
|
||||
context.strokeStyle = color3;
|
||||
context.lineWidth = 1;
|
||||
ctx.save();
|
||||
ctx.font = "12px Titillium Web";
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textAlign = "center";
|
||||
|
||||
// Draw the lowest value line
|
||||
const lowestY = canvas.height - (zoomMinValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
|
||||
context.beginPath();
|
||||
context.moveTo(40, lowestY - 18);
|
||||
context.lineTo(canvas.width - 40, lowestY - 18);
|
||||
context.stroke();
|
||||
const leftX = yAxis.left + 20;
|
||||
const rightX = y2Axis.right - 20;
|
||||
|
||||
// Draw the highest value line
|
||||
const highestY = canvas.height - (zoomMaxValue - zoomMinValue) * (canvas.height / (zoomMaxValue - zoomMinValue));
|
||||
context.beginPath();
|
||||
context.moveTo(40, highestY + 10);
|
||||
context.lineTo(canvas.width - 40, highestY + 10);
|
||||
context.stroke();
|
||||
const offset = 10;
|
||||
|
||||
const avgY = canvas.height / 2;
|
||||
context.beginPath();
|
||||
context.moveTo(40, avgY - 7);
|
||||
context.lineTo(canvas.width - 40, avgY - 7);
|
||||
context.stroke();
|
||||
yAxis.ticks.forEach((tick, index) => {
|
||||
const y = yAxis.getPixelForValue(tick.value);
|
||||
var adjustedY = Math.max(yAxis.top + 13, Math.min(y, yAxis.bottom - 6));
|
||||
const isMiddleTick = index === Math.floor(yAxis.ticks.length / 2);
|
||||
|
||||
// Label the lines with their values
|
||||
context.fillStyle = color4;
|
||||
context.font = '12px Titillium Web';
|
||||
let adjustedTickValue;
|
||||
switch(localStorage.getItem("signalUnit")) {
|
||||
case "dbuv": adjustedTickValue = tick.value - 11.25; break;
|
||||
case "dbm": adjustedTickValue = tick.value - -120; break;
|
||||
default: adjustedTickValue = tick.value; break;
|
||||
}
|
||||
|
||||
context.textAlign = 'right';
|
||||
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, 35, lowestY - 14);
|
||||
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, 35, highestY + 14);
|
||||
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, 35, avgY - 3);
|
||||
if (isMiddleTick) { adjustedY += 3; }
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(adjustedTickValue.toFixed(1), leftX + 25, adjustedY);
|
||||
|
||||
context.textAlign = 'left';
|
||||
context.fillText(`${(zoomMinValue - offset).toFixed(1)}`, canvas.width - 35, lowestY - 14);
|
||||
context.fillText(`${(zoomMaxValue - offset).toFixed(1)}`, canvas.width - 35, highestY + 14);
|
||||
context.fillText(`${(zoomAvgValue - offset).toFixed(1)}`, canvas.width - 35, avgY - 3);
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(adjustedTickValue.toFixed(1), rightX - 25, adjustedY); // Right side
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => updateCanvas(parsedData, signalChart));
|
||||
}, 1000 / 15);
|
||||
const gridLineWidth = 0.5; // Make the lines thinner to avoid overlapping text
|
||||
const adjustedGridTop = chartArea.top + offset;
|
||||
const adjustedGridBottom = chartArea.bottom - offset;
|
||||
const middleY = chartArea.top + chartArea.height / 2;
|
||||
const padding = 45; // 30px inward on both sides
|
||||
|
||||
// Helper function to draw a horizontal line
|
||||
function drawGridLine(y) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(chartArea.left + padding, y);
|
||||
ctx.lineTo(chartArea.right - padding, y);
|
||||
ctx.strokeStyle = gridLineColor;
|
||||
ctx.lineWidth = gridLineWidth;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw the three horizontal grid lines
|
||||
drawGridLine(adjustedGridTop);
|
||||
drawGridLine(adjustedGridBottom);
|
||||
drawGridLine(middleY);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
let reconnectTimer = null;
|
||||
@@ -641,14 +638,6 @@ function processString(string, errors) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function getCurrentFreq() {
|
||||
currentFreq = $('#data-frequency').text();
|
||||
currentFreq = parseFloat(currentFreq).toFixed(3);
|
||||
currentFreq = parseFloat(currentFreq);
|
||||
|
||||
return currentFreq;
|
||||
}
|
||||
|
||||
function checkKey(e) {
|
||||
e = e || window.event;
|
||||
|
||||
@@ -899,14 +888,12 @@ function throttle(fn, wait) {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Utility function to update element's text if changed
|
||||
function updateTextIfChanged($element, newText) {
|
||||
if ($element.text() !== newText) {
|
||||
$element.text(newText);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to update element's HTML content if changed
|
||||
function updateHtmlIfChanged($element, newHtml) {
|
||||
if ($element.html() !== newHtml) {
|
||||
$element.html(newHtml);
|
||||
@@ -1005,7 +992,7 @@ const updateDataElements = throttle(function(parsedData) {
|
||||
$dataRt1.attr('aria-label', parsedData.rt1);
|
||||
$('#users-online-container').attr("aria-label", "Online users: " + parsedData.users);
|
||||
}
|
||||
}, 100); // Update at most once every 100 milliseconds
|
||||
}, 75); // Update at most once every 100 milliseconds
|
||||
|
||||
let isEventListenerAdded = false;
|
||||
|
||||
@@ -1115,8 +1102,11 @@ function showTunerDescription() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#tuner-name i").toggleClass("rotated");
|
||||
|
||||
if ($(window).width() < 768) {
|
||||
$('.dashboard-panel-plugin-list').slideToggle(300);
|
||||
$('#users-online-container').slideToggle(300);
|
||||
$('.chatbutton').slideToggle(300);
|
||||
$('#settings').slideToggle(300);
|
||||
}
|
||||
|
||||
+22
-8
@@ -1,8 +1,22 @@
|
||||
$.getScript('./js/api.js');
|
||||
$.getScript('./js/main.js');
|
||||
$.getScript('./js/dropdown.js');
|
||||
$.getScript('./js/modal.js');
|
||||
$.getScript('./js/settings.js');
|
||||
$.getScript('./js/chat.js');
|
||||
$.getScript('./js/toast.js');
|
||||
$.getScript('./js/plugins.js');
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadScriptsInOrder() {
|
||||
await loadScript('./js/api.js');
|
||||
await loadScript('./js/main.js');
|
||||
await loadScript('./js/dropdown.js');
|
||||
await loadScript('./js/modal.js');
|
||||
await loadScript('./js/settings.js');
|
||||
await loadScript('./js/chat.js');
|
||||
await loadScript('./js/toast.js');
|
||||
await loadScript('./js/plugins.js');
|
||||
}
|
||||
|
||||
loadScriptsInOrder();
|
||||
|
||||
Reference in New Issue
Block a user