mirror of https://github.com/DJ2LS/FreeDATA.git
Prettified Code!
parent
73204849f2
commit
092c4622ec
|
|
@ -7,11 +7,12 @@ in C/C++, and the frontend is build with VueJS, so you will need Python (from
|
|||
3.10 to 3.13) to work on the backend, and node+npm to work on the frontend.
|
||||
|
||||
System dependencies required:
|
||||
- python3 (with virtual environment support)
|
||||
- portaudio
|
||||
- hamlib (optional, there is a vendored version bundled with the freedata
|
||||
server, or you can build it from source)
|
||||
- nodejs + npm
|
||||
|
||||
- python3 (with virtual environment support)
|
||||
- portaudio
|
||||
- hamlib (optional, there is a vendored version bundled with the freedata
|
||||
server, or you can build it from source)
|
||||
- nodejs + npm
|
||||
|
||||
Example for debian trixie: `sudo apt update && sudo apt install python3
|
||||
python3-venv nodejs npm portaudio19-dev`
|
||||
|
|
@ -37,6 +38,7 @@ To fetch the `npm` dependencies go inside `./freedata_gui` and run `npm
|
|||
install`. See `./freedata_gui/README.md` for additional informations.
|
||||
|
||||
## Linting and Formatting (python-only)
|
||||
|
||||
The python sources must be formatted and checked using `ruff`. There is a CI
|
||||
workflow that block all PRs that do not pass the linter/formatter checks.
|
||||
|
||||
|
|
@ -54,5 +56,6 @@ usefull in the CI. In the future it will no longer be necessary
|
|||
fix automatically, you must fix them manually.
|
||||
|
||||
## Testing backend
|
||||
|
||||
To run the backend tests run inside the virtual environment `python -m unittest
|
||||
discover tests`
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import {
|
|||
validateCallsignWithoutSSID,
|
||||
} from "./freedata";
|
||||
import { processFreedataMessages } from "./messagesHandler";
|
||||
import { processFreedataDomains, processFreedataBroadcastsPerDomain } from "./broadcastsHandler";
|
||||
import {
|
||||
processFreedataDomains,
|
||||
processFreedataBroadcastsPerDomain,
|
||||
} from "./broadcastsHandler";
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
|
@ -327,13 +330,12 @@ export async function setStationInfo(callsign, info) {
|
|||
return await apiPost(`/freedata/station/${callsign}`, info);
|
||||
}
|
||||
|
||||
|
||||
export async function getFreedataBroadcasts() {
|
||||
broadcastStore.loading = true;
|
||||
try {
|
||||
const res = await apiGet("/freedata/broadcasts");
|
||||
if (res) {
|
||||
processFreedataBroadcasts(res)
|
||||
processFreedataBroadcasts(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching broadcasts:", error);
|
||||
|
|
@ -347,7 +349,7 @@ export async function getFreedataBroadcastsPerDomain(domain) {
|
|||
try {
|
||||
const res = await apiGet(`/freedata/broadcasts/${domain}/`);
|
||||
if (res) {
|
||||
processFreedataBroadcastsPerDomain(res)
|
||||
processFreedataBroadcastsPerDomain(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching broadcasts:", error);
|
||||
|
|
@ -356,13 +358,12 @@ export async function getFreedataBroadcastsPerDomain(domain) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getFreedataDomains() {
|
||||
broadcastStore.loading = true;
|
||||
try {
|
||||
const res = await apiGet("/freedata/broadcasts/domains");
|
||||
if (res) {
|
||||
processFreedataDomains(res)
|
||||
processFreedataDomains(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching broadcasts:", error);
|
||||
|
|
@ -389,12 +390,8 @@ export async function deleteFreedataBroadcastDomain(id) {
|
|||
return await apiDelete(`/freedata/broadcasts/${id}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function postFreedataBroadcastADIF(id) {
|
||||
return await apiPost(`/freedata/broadcasts/${id}/adif`, {
|
||||
action: "retransmit",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ const MAX_BLOCKS = 10;
|
|||
|
||||
export function addDataToAudio(data) {
|
||||
const int16 = new Int16Array(data);
|
||||
const copy = new Int16Array(int16); // Kopie für Sicherheit
|
||||
/*
|
||||
const copy = new Int16Array(int16); // Kopie für Sicherheit
|
||||
/*
|
||||
const stream = audio.rxStream;
|
||||
|
||||
if (stream.length >= MAX_BLOCKS) {
|
||||
|
|
@ -19,7 +19,5 @@ export function addDataToAudio(data) {
|
|||
|
||||
stream.push(copy);
|
||||
*/
|
||||
audio.addBlock(copy);
|
||||
|
||||
|
||||
audio.addBlock(copy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { setActivePinia } from 'pinia';
|
||||
import pinia from '../store/index';
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { useBroadcastStore } from '../store/broadcastStore.js';
|
||||
import { useBroadcastStore } from "../store/broadcastStore.js";
|
||||
import {
|
||||
deleteFreedataBroadcastDomain,
|
||||
deleteFreedataBroadcastMessage,
|
||||
postFreedataBroadcastADIF,
|
||||
retransmitFreedataBroadcast,
|
||||
sendFreedataBroadcastMessage
|
||||
sendFreedataBroadcastMessage,
|
||||
} from "@/js/api";
|
||||
const broadcast = useBroadcastStore(pinia);
|
||||
|
||||
export async function processFreedataBroadcastsPerDomain(data) {
|
||||
console.log(data)
|
||||
console.log(data);
|
||||
broadcast.setBroadcastsForDomain(data);
|
||||
}
|
||||
|
||||
|
|
@ -25,12 +25,10 @@ export async function processFreedataDomains(data) {
|
|||
// sum of unread_count
|
||||
broadcast.totalUnreadMessages = Object.values(data).reduce(
|
||||
(sum, domain) => sum + (domain.unread_count || 0),
|
||||
0
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function newBroadcastMessage(params) {
|
||||
sendFreedataBroadcastMessage(params);
|
||||
broadcast.triggerScrollToBottom();
|
||||
|
|
@ -42,7 +40,7 @@ export function repeatBroadcastTransmission(id) {
|
|||
|
||||
export function deleteBroadcastDomainFromDB(domain) {
|
||||
deleteFreedataBroadcastDomain(domain);
|
||||
broadcast.selectedDomain = '';
|
||||
broadcast.selectedDomain = "";
|
||||
}
|
||||
|
||||
export function sendBroadcastADIFviaUDP(domain) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
getModemState,
|
||||
getRadioStatus,
|
||||
getSysInfo,
|
||||
getFreedataDomains,
|
||||
getFreedataBroadcastsPerDomain
|
||||
getFreedataDomains,
|
||||
getFreedataBroadcastsPerDomain,
|
||||
} from "./api";
|
||||
import { processFreedataMessages } from "./messagesHandler";
|
||||
import { processRadioStatus } from "./radioHandler";
|
||||
|
|
@ -45,7 +45,7 @@ export async function loadAllData() {
|
|||
getOverallHealth();
|
||||
getFreedataMessages();
|
||||
getFreedataDomains();
|
||||
getFreedataBroadcastsPerDomain(broadcast.selectedDomain)
|
||||
getFreedataBroadcastsPerDomain(broadcast.selectedDomain);
|
||||
processFreedataMessages();
|
||||
processRadioStatus();
|
||||
}
|
||||
|
|
@ -111,8 +111,8 @@ export function eventDispatcher(data) {
|
|||
switch (data["message-db"]) {
|
||||
case "changed":
|
||||
console.log("fetching new messages...");
|
||||
getFreedataDomains();
|
||||
getFreedataBroadcastsPerDomain(broadcast.selectedDomain)
|
||||
getFreedataDomains();
|
||||
getFreedataBroadcastsPerDomain(broadcast.selectedDomain);
|
||||
|
||||
var messages = getFreedataMessages();
|
||||
processFreedataMessages(messages);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function connect(endpoint, dispatcher) {
|
|||
`${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`,
|
||||
);
|
||||
|
||||
if (endpoint.includes("audio")){
|
||||
if (endpoint.includes("audio")) {
|
||||
socket.binaryType = "arraybuffer";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
|
||||
export function parseMetar(raw) {
|
||||
const out = {
|
||||
iata: null,
|
||||
day: null, hh: null, mm: null,
|
||||
wind: { dir_deg: null, spd_kt: null, gust_kt: null, var_from: null, var_to: null },
|
||||
day: null,
|
||||
hh: null,
|
||||
mm: null,
|
||||
wind: {
|
||||
dir_deg: null,
|
||||
spd_kt: null,
|
||||
gust_kt: null,
|
||||
var_from: null,
|
||||
var_to: null,
|
||||
},
|
||||
visibility: { code: null, meters: null },
|
||||
temp_c: null,
|
||||
dew_c: null,
|
||||
|
|
@ -11,66 +18,72 @@ export function parseMetar(raw) {
|
|||
auto: false,
|
||||
cor: false,
|
||||
clouds: [],
|
||||
raw: raw?.trim() || ''
|
||||
raw: raw?.trim() || "",
|
||||
};
|
||||
if (!raw) return out;
|
||||
|
||||
const tokens = raw.replace('=', '').trim().split(/\s+/);
|
||||
const tokens = raw.replace("=", "").trim().split(/\s+/);
|
||||
|
||||
out.auto = tokens.includes('AUTO');
|
||||
out.cor = tokens.includes('COR');
|
||||
out.auto = tokens.includes("AUTO");
|
||||
out.cor = tokens.includes("COR");
|
||||
|
||||
for (const t of tokens) {
|
||||
if (/^[A-Z]{3}$/.test(t)) { out.iata = t; break; }
|
||||
if (/^[A-Z]{4}$/.test(t)) { out.iata = t; break; } // IACA override
|
||||
if (/^[A-Z]{3}$/.test(t)) {
|
||||
out.iata = t;
|
||||
break;
|
||||
}
|
||||
if (/^[A-Z]{4}$/.test(t)) {
|
||||
out.iata = t;
|
||||
break;
|
||||
} // IACA override
|
||||
}
|
||||
|
||||
|
||||
|
||||
const tTok = tokens.find(t => /^\d{6}Z$/.test(t));
|
||||
const tTok = tokens.find((t) => /^\d{6}Z$/.test(t));
|
||||
if (tTok) {
|
||||
out.day = tTok.slice(0,2);
|
||||
out.hh = tTok.slice(2,4);
|
||||
out.mm = tTok.slice(4,6);
|
||||
out.day = tTok.slice(0, 2);
|
||||
out.hh = tTok.slice(2, 4);
|
||||
out.mm = tTok.slice(4, 6);
|
||||
}
|
||||
|
||||
const wTok = tokens.find(t => /^(?:\d{3}|VRB)\d{2,3}(G\d{2,3})?KT$/.test(t));
|
||||
const wTok = tokens.find((t) =>
|
||||
/^(?:\d{3}|VRB)\d{2,3}(G\d{2,3})?KT$/.test(t),
|
||||
);
|
||||
if (wTok) {
|
||||
const m = wTok.match(/^(\d{3}|VRB)(\d{2,3})(?:G(\d{2,3}))?KT$/);
|
||||
if (m) {
|
||||
out.wind.dir_deg = m[1] === 'VRB' ? null : parseInt(m[1], 10);
|
||||
out.wind.spd_kt = parseInt(m[2], 10);
|
||||
out.wind.dir_deg = m[1] === "VRB" ? null : parseInt(m[1], 10);
|
||||
out.wind.spd_kt = parseInt(m[2], 10);
|
||||
out.wind.gust_kt = m[3] ? parseInt(m[3], 10) : null;
|
||||
}
|
||||
}
|
||||
const vTok = tokens.find(t => /^\d{3}V\d{3}$/.test(t));
|
||||
const vTok = tokens.find((t) => /^\d{3}V\d{3}$/.test(t));
|
||||
if (vTok) {
|
||||
const m = vTok.match(/^(\d{3})V(\d{3})$/);
|
||||
out.wind.var_from = parseInt(m[1], 10);
|
||||
out.wind.var_to = parseInt(m[2], 10);
|
||||
out.wind.var_to = parseInt(m[2], 10);
|
||||
}
|
||||
|
||||
if (tokens.includes('CAVOK')) {
|
||||
out.visibility.code = 'CAVOK';
|
||||
if (tokens.includes("CAVOK")) {
|
||||
out.visibility.code = "CAVOK";
|
||||
out.visibility.meters = 10000;
|
||||
} else {
|
||||
const vis = tokens.find(t => /^\d{4}$/.test(t));
|
||||
const vis = tokens.find((t) => /^\d{4}$/.test(t));
|
||||
if (vis) {
|
||||
out.visibility.code = vis;
|
||||
out.visibility.meters = parseInt(vis, 10);
|
||||
}
|
||||
}
|
||||
|
||||
const tdTok = tokens.find(t => /^(M?\d{1,2})\/(M?\d{1,2})$/.test(t));
|
||||
const tdTok = tokens.find((t) => /^(M?\d{1,2})\/(M?\d{1,2})$/.test(t));
|
||||
if (tdTok) {
|
||||
const m = tdTok.match(/^(M?\d{1,2})\/(M?\d{1,2})$/);
|
||||
out.temp_c = _signed(m[1]);
|
||||
out.dew_c = _signed(m[2]);
|
||||
out.dew_c = _signed(m[2]);
|
||||
}
|
||||
|
||||
const qTok = tokens.find(t => /^(Q\d{4}|A\d{4})$/.test(t));
|
||||
const qTok = tokens.find((t) => /^(Q\d{4}|A\d{4})$/.test(t));
|
||||
if (qTok) {
|
||||
if (qTok.startsWith('Q')) out.qnh_hpa = parseInt(qTok.slice(1), 10);
|
||||
if (qTok.startsWith("Q")) out.qnh_hpa = parseInt(qTok.slice(1), 10);
|
||||
else {
|
||||
const inHg = parseInt(qTok.slice(1), 10) / 100;
|
||||
out.qnh_hpa = Math.round(inHg * 33.8639);
|
||||
|
|
@ -83,7 +96,7 @@ export function parseMetar(raw) {
|
|||
out.clouds.push({
|
||||
cover: m[1],
|
||||
base_ft: parseInt(m[2], 10) * 100,
|
||||
type: m[3] || null
|
||||
type: m[3] || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -91,93 +104,117 @@ export function parseMetar(raw) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function _signed(s) { return s?.startsWith('M') ? -parseInt(s.slice(1),10) : parseInt(s,10); }
|
||||
function _signed(s) {
|
||||
return s?.startsWith("M") ? -parseInt(s.slice(1), 10) : parseInt(s, 10);
|
||||
}
|
||||
|
||||
// ---------------- Flight Category ----------------
|
||||
|
||||
export function determineFlightCategory(parsed) {
|
||||
const cavok = parsed.visibility.code === 'CAVOK';
|
||||
const cavok = parsed.visibility.code === "CAVOK";
|
||||
let ceiling = null;
|
||||
for (const c of parsed.clouds || []) {
|
||||
if (c.cover === 'BKN' || c.cover === 'OVC') {
|
||||
if (c.cover === "BKN" || c.cover === "OVC") {
|
||||
if (ceiling === null || c.base_ft < ceiling) ceiling = c.base_ft;
|
||||
}
|
||||
}
|
||||
const v = parsed.visibility.meters ?? 0;
|
||||
if (cavok) return 'VFR';
|
||||
if (v >= 5000 && (ceiling === null || ceiling > 3000)) return 'VFR';
|
||||
if ((v >= 3000 && v < 5000) || (ceiling !== null && ceiling >= 1000 && ceiling <= 3000)) return 'MVFR';
|
||||
if ((v >= 1500 && v < 3000) || (ceiling !== null && ceiling >= 500 && ceiling < 1000)) return 'IFR';
|
||||
return 'LIFR';
|
||||
if (cavok) return "VFR";
|
||||
if (v >= 5000 && (ceiling === null || ceiling > 3000)) return "VFR";
|
||||
if (
|
||||
(v >= 3000 && v < 5000) ||
|
||||
(ceiling !== null && ceiling >= 1000 && ceiling <= 3000)
|
||||
)
|
||||
return "MVFR";
|
||||
if (
|
||||
(v >= 1500 && v < 3000) ||
|
||||
(ceiling !== null && ceiling >= 500 && ceiling < 1000)
|
||||
)
|
||||
return "IFR";
|
||||
return "LIFR";
|
||||
}
|
||||
|
||||
export function categoryClass(cat) {
|
||||
switch (cat) {
|
||||
case 'VFR': return 'text-bg-success';
|
||||
case 'MVFR': return 'text-bg-primary';
|
||||
case 'IFR': return 'text-bg-danger';
|
||||
case 'LIFR': return 'text-bg-magenta';
|
||||
default: return 'text-bg-secondary';
|
||||
case "VFR":
|
||||
return "text-bg-success";
|
||||
case "MVFR":
|
||||
return "text-bg-primary";
|
||||
case "IFR":
|
||||
return "text-bg-danger";
|
||||
case "LIFR":
|
||||
return "text-bg-magenta";
|
||||
default:
|
||||
return "text-bg-secondary";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Formatters (labels shown in Vue) ----------------
|
||||
|
||||
export function windToString(w) {
|
||||
if (!w) return '—';
|
||||
const base = (w.dir_deg == null)
|
||||
? `VRB/${w.spd_kt ?? 0}kt`
|
||||
: `${w.dir_deg}°/${w.spd_kt ?? 0}kt`;
|
||||
const gust = w.gust_kt ? ` G${w.gust_kt}` : '';
|
||||
const varrng = (w.var_from != null && w.var_to != null) ? ` V${w.var_from}-${w.var_to}` : '';
|
||||
if (!w) return "—";
|
||||
const base =
|
||||
w.dir_deg == null
|
||||
? `VRB/${w.spd_kt ?? 0}kt`
|
||||
: `${w.dir_deg}°/${w.spd_kt ?? 0}kt`;
|
||||
const gust = w.gust_kt ? ` G${w.gust_kt}` : "";
|
||||
const varrng =
|
||||
w.var_from != null && w.var_to != null ? ` V${w.var_from}-${w.var_to}` : "";
|
||||
return base + gust + varrng;
|
||||
}
|
||||
|
||||
export function visibilityToString(v) {
|
||||
return v?.code || '—';
|
||||
return v?.code || "—";
|
||||
}
|
||||
|
||||
export function cloudsToString(clouds) {
|
||||
if (!clouds || clouds.length === 0) return '—';
|
||||
return clouds.map(c => `${c.cover}${String(Math.round(c.base_ft/100)).padStart(3,'0')}${c.type || ''}`).join(' ');
|
||||
if (!clouds || clouds.length === 0) return "—";
|
||||
return clouds
|
||||
.map(
|
||||
(c) =>
|
||||
`${c.cover}${String(Math.round(c.base_ft / 100)).padStart(3, "0")}${c.type || ""}`,
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function tempDewToString(t, d) {
|
||||
const tStr = (typeof t === 'number') ? `${t}` : '—';
|
||||
const dStr = (typeof d === 'number') ? `${d}` : '—';
|
||||
const tStr = typeof t === "number" ? `${t}` : "—";
|
||||
const dStr = typeof d === "number" ? `${d}` : "—";
|
||||
return `${tStr}/${dStr}°C`;
|
||||
}
|
||||
|
||||
export function qnhToString(q) {
|
||||
return (typeof q === 'number') ? `${q} hPa` : '—';
|
||||
return typeof q === "number" ? `${q} hPa` : "—";
|
||||
}
|
||||
|
||||
export function timeToString(day, hh, mm) {
|
||||
return (day && hh && mm) ? `${day}.${hh}:${mm}Z` : '—';
|
||||
return day && hh && mm ? `${day}.${hh}:${mm}Z` : "—";
|
||||
}
|
||||
|
||||
// ---------------- SVG Compass + Windbarb (WMO-like) ----------------
|
||||
|
||||
export function buildCompassSVG(wind, opts = {}) {
|
||||
const size = opts.size ?? 160;
|
||||
const cx = size/2, cy = size/2;
|
||||
const cx = size / 2,
|
||||
cy = size / 2;
|
||||
const dirDeg = wind?.dir_deg;
|
||||
const spd = wind?.spd_kt ?? 0;
|
||||
const gust = wind?.gust_kt;
|
||||
|
||||
const showDir = (dirDeg == null) ? null : (dirDeg + 180) % 360;
|
||||
const transform = (showDir == null) ? '' : `transform="rotate(${showDir} ${cx} ${cy})"`;
|
||||
const showDir = dirDeg == null ? null : (dirDeg + 180) % 360;
|
||||
const transform =
|
||||
showDir == null ? "" : `transform="rotate(${showDir} ${cx} ${cy})"`;
|
||||
|
||||
const svgGrid = `
|
||||
<circle cx="${cx}" cy="${cy}" r="${size*0.4375}" fill="none" stroke="#ced4da"/>
|
||||
<circle cx="${cx}" cy="${cy}" r="${size*0.3125}" fill="none" stroke="#e9ecef"/>
|
||||
<circle cx="${cx}" cy="${cy}" r="${size*0.1875}" fill="none" stroke="#f1f3f5"/>
|
||||
<line x1="${size*0.0625}" y1="${cy}" x2="${size*0.9375}" y2="${cy}" stroke="#dee2e6"/>
|
||||
<line x1="${cx}" y1="${size*0.0625}" x2="${cx}" y2="${size*0.9375}" stroke="#dee2e6"/>
|
||||
<text x="${cx}" y="${size*0.0375}" text-anchor="middle" font-size="10" fill="#6c757d">N</text>
|
||||
<text x="${size*0.9625}" y="${cy+4}" text-anchor="end" font-size="10" fill="#6c757d">E</text>
|
||||
<text x="${cx}" y="${size*0.975}" text-anchor="middle" font-size="10" fill="#6c757d">S</text>
|
||||
<text x="${size*0.0375}" y="${cy+4}" text-anchor="start" font-size="10" fill="#6c757d">W</text>
|
||||
<circle cx="${cx}" cy="${cy}" r="${size * 0.4375}" fill="none" stroke="#ced4da"/>
|
||||
<circle cx="${cx}" cy="${cy}" r="${size * 0.3125}" fill="none" stroke="#e9ecef"/>
|
||||
<circle cx="${cx}" cy="${cy}" r="${size * 0.1875}" fill="none" stroke="#f1f3f5"/>
|
||||
<line x1="${size * 0.0625}" y1="${cy}" x2="${size * 0.9375}" y2="${cy}" stroke="#dee2e6"/>
|
||||
<line x1="${cx}" y1="${size * 0.0625}" x2="${cx}" y2="${size * 0.9375}" stroke="#dee2e6"/>
|
||||
<text x="${cx}" y="${size * 0.0375}" text-anchor="middle" font-size="10" fill="#6c757d">N</text>
|
||||
<text x="${size * 0.9625}" y="${cy + 4}" text-anchor="end" font-size="10" fill="#6c757d">E</text>
|
||||
<text x="${cx}" y="${size * 0.975}" text-anchor="middle" font-size="10" fill="#6c757d">S</text>
|
||||
<text x="${size * 0.0375}" y="${cy + 4}" text-anchor="start" font-size="10" fill="#6c757d">W</text>
|
||||
`;
|
||||
|
||||
let windGroup = `<circle cx="${cx}" cy="${cy}" r="10" stroke="#0d6efd" stroke-width="3" fill="none"/>`;
|
||||
|
|
@ -186,9 +223,10 @@ export function buildCompassSVG(wind, opts = {}) {
|
|||
windGroup = `<g ${transform}>${_wmoBarb(cx, cy, spd, size)}</g>`;
|
||||
}
|
||||
|
||||
const gustRing = (gust && gust > spd)
|
||||
? `<circle cx="${cx}" cy="${cy}" r="${size*0.48}" fill="none" stroke="#0d6efd" stroke-dasharray="4 4" />`
|
||||
: '';
|
||||
const gustRing =
|
||||
gust && gust > spd
|
||||
? `<circle cx="${cx}" cy="${cy}" r="${size * 0.48}" fill="none" stroke="#0d6efd" stroke-dasharray="4 4" />`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="display:block;max-width:${size}px">
|
||||
|
|
@ -199,32 +237,42 @@ export function buildCompassSVG(wind, opts = {}) {
|
|||
}
|
||||
|
||||
function _wmoBarb(cx, cy, spdKt, size) {
|
||||
const stroke = '#0d6efd';
|
||||
const stroke = "#0d6efd";
|
||||
const lw = 3;
|
||||
const staffLen = size * 0.45;
|
||||
const step = Math.max(6, size * 0.05);
|
||||
const barbLong = Math.max(12, size * 0.10);
|
||||
const barbHalf = barbLong * 0.6;
|
||||
const barbLong = Math.max(12, size * 0.1);
|
||||
const barbHalf = barbLong * 0.6;
|
||||
|
||||
let s = Math.round((spdKt || 0) / 5) * 5;
|
||||
const tri = Math.floor(s / 50); s -= tri * 50;
|
||||
const ten = Math.floor(s / 10); s -= ten * 10;
|
||||
const tri = Math.floor(s / 50);
|
||||
s -= tri * 50;
|
||||
const ten = Math.floor(s / 10);
|
||||
s -= ten * 10;
|
||||
const five = Math.floor(s / 5);
|
||||
|
||||
const elems = [];
|
||||
elems.push(`<line x1="${cx}" y1="${cy}" x2="${cx}" y2="${cy - staffLen}" stroke="${stroke}" stroke-width="${lw}" />`);
|
||||
elems.push(
|
||||
`<line x1="${cx}" y1="${cy}" x2="${cx}" y2="${cy - staffLen}" stroke="${stroke}" stroke-width="${lw}" />`,
|
||||
);
|
||||
|
||||
let y = cy - staffLen + step*1.2;
|
||||
let y = cy - staffLen + step * 1.2;
|
||||
for (let i = 0; i < tri; i++) {
|
||||
elems.push(`<polygon points="${cx},${y} ${cx},${y - step} ${cx + barbLong},${y - step/2}" fill="${stroke}" />`);
|
||||
elems.push(
|
||||
`<polygon points="${cx},${y} ${cx},${y - step} ${cx + barbLong},${y - step / 2}" fill="${stroke}" />`,
|
||||
);
|
||||
y += step;
|
||||
}
|
||||
for (let i = 0; i < ten; i++) {
|
||||
elems.push(`<line x1="${cx}" y1="${y}" x2="${cx + barbLong}" y2="${y - barbLong*0.35}" stroke="${stroke}" stroke-width="${lw}" />`);
|
||||
elems.push(
|
||||
`<line x1="${cx}" y1="${y}" x2="${cx + barbLong}" y2="${y - barbLong * 0.35}" stroke="${stroke}" stroke-width="${lw}" />`,
|
||||
);
|
||||
y += step;
|
||||
}
|
||||
if (five) {
|
||||
elems.push(`<line x1="${cx}" y1="${y}" x2="${cx + barbHalf}" y2="${y - barbHalf*0.35}" stroke="${stroke}" stroke-width="${lw}" />`);
|
||||
elems.push(
|
||||
`<line x1="${cx}" y1="${y}" x2="${cx + barbHalf}" y2="${y - barbHalf * 0.35}" stroke="${stroke}" stroke-width="${lw}" />`,
|
||||
);
|
||||
}
|
||||
return elems.join('\n');
|
||||
return elems.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,11 +53,6 @@ export const useAudioStore = defineStore("audioStore", () => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const loadAudioDevices = async () => {
|
||||
try {
|
||||
const devices = await getAudioDevices();
|
||||
|
|
@ -83,7 +78,6 @@ export const useAudioStore = defineStore("audioStore", () => {
|
|||
resetBuffer,
|
||||
get bufferedBlockCount() {
|
||||
return readyBlocks;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
|
||||
export const useBroadcastStore = defineStore("broadcastStore", () => {
|
||||
|
||||
// Indicator if we are loading data
|
||||
var loading = ref(false);
|
||||
|
||||
|
|
@ -29,21 +27,20 @@ export const useBroadcastStore = defineStore("broadcastStore", () => {
|
|||
const newPriority = ref("");
|
||||
|
||||
// selectedMessage
|
||||
const selectedMessage = ref()
|
||||
const selectedMessage = ref();
|
||||
|
||||
// unread message counter
|
||||
var totalUnreadMessages = ref(0);
|
||||
|
||||
|
||||
function triggerScrollToBottom() {
|
||||
scrollTrigger.value++;
|
||||
}
|
||||
|
||||
function setDomains(data){
|
||||
function setDomains(data) {
|
||||
domains.value = data;
|
||||
}
|
||||
|
||||
function setBroadcastsForDomain(data){
|
||||
function setBroadcastsForDomain(data) {
|
||||
domainBroadcasts.value = data;
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +58,6 @@ export const useBroadcastStore = defineStore("broadcastStore", () => {
|
|||
newPriority,
|
||||
newMessageType,
|
||||
selectedMessage,
|
||||
totalUnreadMessages
|
||||
totalUnreadMessages,
|
||||
};
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const defaultConfig = {
|
|||
EXP: {
|
||||
enable_ring_buffer: false,
|
||||
enable_vhf: false,
|
||||
enable_groupchat: false
|
||||
enable_groupchat: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue