chore: release v0.23.2
This commit is contained in:
@@ -24,7 +24,7 @@ const en: Dict = {
|
||||
'upd.restartNote': 'OpsLog will restart on the new version.',
|
||||
'upd.retry': 'Retry', 'upd.browser': 'Open page', 'upd.checking': 'Checking for updates…', 'upd.upToDate': "You're up to date",
|
||||
'rate.title': 'QSO rate (QSOs/hour) — projected from the last 10 / 60 minutes',
|
||||
'lotw.userTip': 'LoTW user — last upload {date} ({days} days ago)',
|
||||
'lotw.feedStale': "ARRL's user list itself was last built on {date}, so this figure may be older than the station", 'lotw.userTip': 'LoTW user — last upload {date} ({days} days ago)',
|
||||
'menu.file': 'File', 'menu.edit': 'Edit', 'menu.view': 'View', 'menu.tools': 'Tools',
|
||||
'file.import': 'Import ADIF…', 'file.export': 'Export ADIF…', 'file.exporting': 'Exporting…',
|
||||
'file.exportCabrillo': 'Export Cabrillo…', 'file.deleteAll': 'Delete all QSOs…', 'file.exit': 'Exit',
|
||||
@@ -40,7 +40,7 @@ const en: Dict = {
|
||||
'cwd.tipOff': 'CW decoder · click to enable (decodes RX audio in CW mode)',
|
||||
'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode',
|
||||
'alert.tuneHint': 'Click to tune the rig to this spot (freq + mode) and fill the call', 'alert.dismiss': 'Dismiss', 'alert.pending': '{n} recent spot alert(s) — click to view', 'alert.noneShort': 'No recent alerts', 'alert.recent': 'Recent alerts', 'alert.clear': 'Clear',
|
||||
'menu.help': 'Help', 'prof.switchTitle': 'Switch station profile', 'prof.manage': 'Manage profiles…', 'help.about': 'About OpsLog', 'help.donate': '♥ Support OpsLog (donate)', 'tools.duplicates': 'Find duplicates…',
|
||||
'menu.help': 'Help', 'prof.switchTitle': 'Switch station profile', 'prof.manage': 'Manage profiles…', 'logview.title': "Diagnostic log", 'logview.empty': "(log is empty)", 'logview.searchPh': "Filter lines — e.g. cluster, acom, cat", 'logview.matches': "{n} of {total} lines", 'logview.autoScroll': "Follow the end", 'logview.updated': "refreshed {time}", 'logview.copy': "Copy", 'logview.toEnd': "Jump to end", 'help.about': 'About OpsLog', 'help.donate': '♥ Support OpsLog (donate)', 'tools.duplicates': 'Find duplicates…',
|
||||
'help.sendLog': 'Send log to F4BPO', 'help.sendLogBusy': 'Sending log…',
|
||||
'help.sendLogOk': 'Log sent to F4BPO — thanks, 73!', 'help.sendLogFail': 'Could not send log: {err}',
|
||||
// Duplicates modal
|
||||
@@ -445,7 +445,7 @@ const fr: Dict = {
|
||||
'live.offlineTip': 'Hors ligne — aucun QSO loggé depuis 5 minutes',
|
||||
'live.stationsTitle': 'Stations on air', 'live.stationsEmpty': 'Aucune station ne reporte pour le moment.', 'live.stationsHide': 'Masquer',
|
||||
'rate.title': 'Rythme QSO (QSO/heure) — projeté sur les 10 / 60 dernières minutes',
|
||||
'lotw.userTip': 'Utilisateur LoTW — dernier upload {date} (il y a {days} j)',
|
||||
'lotw.feedStale': "la liste ARRL elle-même date du {date}, ce chiffre peut donc être plus vieux que la station", 'lotw.userTip': 'Utilisateur LoTW — dernier upload {date} (il y a {days} j)',
|
||||
'menu.file': 'Fichier', 'menu.edit': 'Édition', 'menu.view': 'Affichage', 'menu.tools': 'Outils',
|
||||
'file.import': 'Importer ADIF…', 'file.export': 'Exporter ADIF…', 'file.exporting': 'Export…',
|
||||
'file.exportCabrillo': 'Exporter Cabrillo…', 'file.deleteAll': 'Supprimer tous les QSO…', 'file.exit': 'Quitter',
|
||||
@@ -461,7 +461,7 @@ const fr: Dict = {
|
||||
'cwd.tipOff': 'Décodeur CW · clic pour activer (décode l’audio RX en mode CW)',
|
||||
'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest',
|
||||
'alert.tuneHint': 'Cliquer pour accorder la radio sur ce spot (fréq + mode) et remplir l\'indicatif', 'alert.dismiss': 'Fermer', 'alert.pending': '{n} alerte(s) de spot récente(s) — cliquer pour voir', 'alert.noneShort': 'Aucune alerte récente', 'alert.recent': 'Alertes récentes', 'alert.clear': 'Effacer',
|
||||
'menu.help': 'Aide', 'prof.switchTitle': 'Changer de profil de station', 'prof.manage': 'Gérer les profils…', 'help.about': 'À propos d\'OpsLog', 'help.donate': '♥ Soutenir OpsLog (don)', 'tools.duplicates': 'Trouver les doublons…',
|
||||
'menu.help': 'Aide', 'prof.switchTitle': 'Changer de profil de station', 'prof.manage': 'Gérer les profils…', 'logview.title': "Journal de diagnostic", 'logview.empty': "(journal vide)", 'logview.searchPh': "Filtrer les lignes — ex. cluster, acom, cat", 'logview.matches': "{n} lignes sur {total}", 'logview.autoScroll': "Suivre la fin", 'logview.updated': "actualisé {time}", 'logview.copy': "Copier", 'logview.toEnd': "Aller à la fin", 'help.about': 'À propos d\'OpsLog', 'help.donate': '♥ Soutenir OpsLog (don)', 'tools.duplicates': 'Trouver les doublons…',
|
||||
'help.sendLog': 'Envoyer le journal à F4BPO', 'help.sendLogBusy': 'Envoi du journal…',
|
||||
'help.sendLogOk': 'Journal envoyé à F4BPO — merci, 73 !', 'help.sendLogFail': 'Échec de l\'envoi du journal : {err}',
|
||||
'dup.title': 'Trouver les doublons', 'dup.scanning': 'Analyse du journal…', 'dup.none': 'Aucun doublon trouvé. 🎉',
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Sunrise / sunset for a station, in UTC.
|
||||
//
|
||||
// Built on greyline.ts's sunPosition() rather than on a second implementation of
|
||||
// the solar equations: the map's terminator and the entry strip's sunrise must
|
||||
// never disagree, and there is a whole comment block over there about a sign
|
||||
// error that produced six months of plausible-looking wrong answers. One source
|
||||
// of astronomy, one place to be wrong.
|
||||
//
|
||||
// Everything is UTC. Amateur radio runs on UTC, the log is in UTC, and a local
|
||||
// time would raise the question "local to whom — me or the DX?".
|
||||
import { sunPosition } from './greyline';
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
// Standard altitude of the Sun's centre at rise/set: half a solar diameter below
|
||||
// the horizon plus atmospheric refraction.
|
||||
const H0 = -0.833;
|
||||
|
||||
// solarAltitude returns the Sun's elevation in degrees at a place and instant.
|
||||
function solarAltitude(date: Date, latDeg: number, lonDeg: number): number {
|
||||
const { dec, gha } = sunPosition(date);
|
||||
const ha = (gha + lonDeg) * DEG; // local hour angle
|
||||
const d = dec * DEG;
|
||||
const lat = latDeg * DEG;
|
||||
return Math.asin(Math.sin(lat) * Math.sin(d) + Math.cos(lat) * Math.cos(d) * Math.cos(ha)) / DEG;
|
||||
}
|
||||
|
||||
export type SunTimes = {
|
||||
rise: string; // "HH:MM" UTC, "" when the Sun does not rise that day
|
||||
set: string;
|
||||
polarDay: boolean; // Sun stays up all day
|
||||
polarNight: boolean; // Sun never comes up
|
||||
};
|
||||
|
||||
// sunTimes scans the UTC day a minute at a time and reports where the Sun
|
||||
// crosses the rise/set altitude.
|
||||
//
|
||||
// A scan, not the closed-form hour-angle formula, because the closed form needs
|
||||
// special-casing for the polar cases and for days where the declination shifts
|
||||
// enough to matter, and it silently returns NaN rather than saying "no sunrise
|
||||
// here today". 1440 evaluations is nothing, and the failure mode is honest: no
|
||||
// crossing found means no sunrise, which is a real answer above the Arctic
|
||||
// circle. The crossing minute is then refined by linear interpolation, so the
|
||||
// result is accurate to well under a minute despite the coarse scan.
|
||||
export function sunTimes(date: Date, latDeg: number, lonDeg: number): SunTimes {
|
||||
const out: SunTimes = { rise: '', set: '', polarDay: false, polarNight: false };
|
||||
if (!isFinite(latDeg) || !isFinite(lonDeg)) return out;
|
||||
|
||||
const start = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||
const minute = 60000;
|
||||
let prev = solarAltitude(new Date(start), latDeg, lonDeg);
|
||||
const anyUp = prev > H0;
|
||||
let anyDown = prev <= H0;
|
||||
let sawUp = anyUp;
|
||||
|
||||
for (let i = 1; i <= 1440; i++) {
|
||||
const alt = solarAltitude(new Date(start + i * minute), latDeg, lonDeg);
|
||||
if (alt > H0) sawUp = true; else anyDown = true;
|
||||
if (prev <= H0 && alt > H0 && !out.rise) {
|
||||
out.rise = fmt(start + (i - 1 + frac(prev, alt)) * minute);
|
||||
} else if (prev > H0 && alt <= H0 && !out.set) {
|
||||
out.set = fmt(start + (i - 1 + frac(prev, alt)) * minute);
|
||||
}
|
||||
prev = alt;
|
||||
}
|
||||
if (!out.rise && !out.set) {
|
||||
out.polarDay = sawUp && !anyDown;
|
||||
out.polarNight = !sawUp;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// frac is where between two samples the altitude crosses H0.
|
||||
function frac(a: number, b: number): number {
|
||||
const d = b - a;
|
||||
return d === 0 ? 0 : (H0 - a) / d;
|
||||
}
|
||||
|
||||
function fmt(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
const p = (n: number) => String(n).padStart(2, '0');
|
||||
return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
|
||||
}
|
||||
Reference in New Issue
Block a user