feat: Implement METAR ranking and bias display towards interesting reports
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
+138
-4
@@ -67,6 +67,111 @@
|
||||
let currentMetarSections = [];
|
||||
let currentSectionIndex = 0;
|
||||
|
||||
// --- METAR Ranking ---
|
||||
|
||||
function getMetarParts(metarString) {
|
||||
const rawParts = metarString.split(/\s+/).filter(Boolean);
|
||||
const parts = [];
|
||||
for (let i = 0; i < rawParts.length; i++) {
|
||||
let part = rawParts[i];
|
||||
// Combine mixed fraction visibility, e.g., "1 1/2SM" which is split
|
||||
if (part.match(/^\d+$/) && (i + 1) < rawParts.length && rawParts[i+1].match(/^\d+\/\d+SM$/)) {
|
||||
part = `${part} ${rawParts[i+1]}`;
|
||||
i++;
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function parseVisibilityToNumber(visStr) {
|
||||
visStr = visStr.replace('SM', '');
|
||||
const parts = visStr.split(' ');
|
||||
let value = 0;
|
||||
parts.forEach(p => {
|
||||
if (p.includes('/')) {
|
||||
const frac = p.split('/');
|
||||
if (frac.length === 2 && !isNaN(parseInt(frac[0])) && !isNaN(parseInt(frac[1])) && parseInt(frac[1]) !== 0) {
|
||||
value += parseInt(frac[0]) / parseInt(frac[1]);
|
||||
}
|
||||
} else if (!isNaN(parseInt(p))) {
|
||||
value += parseInt(p);
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function calculateMetarScore(metarString, componentFrequencies, totalMetars) {
|
||||
let score = 0;
|
||||
const parts = getMetarParts(metarString);
|
||||
const uniqueParts = new Set(parts);
|
||||
|
||||
// --- Component Scoring & Significant Weather Heuristics ---
|
||||
parts.forEach(part => {
|
||||
// Wind
|
||||
if (part.endsWith('KT')) {
|
||||
if (part.includes('G')) score += 2; // Gusts
|
||||
if (part.substring(0,3) === 'VRB') score += 2;
|
||||
}
|
||||
if (/^\d{3}V\d{3}$/.test(part)) score += 2; // Variable wind range
|
||||
|
||||
// Weather Phenomena
|
||||
if (part.match(/^([+-]|VC)?(MI|PR|BC|DR|BL|SH|TS|FZ|DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS|TR)+$/)) {
|
||||
score += 2;
|
||||
if (part.startsWith('+')) score += 3; // Heavy intensity
|
||||
if (part.includes('TS')) score += 5; // Thunderstorm
|
||||
if (part.includes('FZ')) score += 5; // Freezing
|
||||
if (part.includes('GR') || part.includes('GS')) score += 5; // Hail
|
||||
if (part.includes('SQ')) score += 5; // Squalls
|
||||
if (part.includes('FC')) score += 10; // Funnel cloud
|
||||
if (part.includes('DS') || part.includes('SS')) score += 4; // Dust/Sand storm
|
||||
if (part.includes('VA')) score += 10; // Volcanic Ash
|
||||
}
|
||||
|
||||
// Visibility
|
||||
if (part.endsWith('SM')) {
|
||||
const vis = parseVisibilityToNumber(part);
|
||||
if (vis <= 0.25) score += 8; // <= 1/4sm
|
||||
else if (vis <= 0.5) score += 5; // <= 1/2sm
|
||||
else if (vis <= 1) score += 3; // <= 1sm
|
||||
}
|
||||
|
||||
// Clouds
|
||||
if (part.match(/^(BKN|OVC)/)) {
|
||||
score += 1;
|
||||
const ceilingAlt = parseInt(part.substring(3, 6));
|
||||
if (!isNaN(ceilingAlt)) {
|
||||
const ceilingFt = ceilingAlt * 100;
|
||||
if (ceilingFt < 500) score += 8;
|
||||
else if (ceilingFt < 1000) score += 5;
|
||||
}
|
||||
if (part.endsWith('CB') || part.endsWith('TCU')) score += 5; // Cumulonimbus / Towering Cumulus
|
||||
}
|
||||
});
|
||||
|
||||
// Remarks
|
||||
const rmkIndex = parts.indexOf('RMK');
|
||||
if (rmkIndex !== -1) {
|
||||
score += 3;
|
||||
const remarkParts = parts.slice(rmkIndex + 1);
|
||||
score += remarkParts.length * 0.5; // More remarks, more interesting
|
||||
if (remarkParts.includes('PRESFR') || remarkParts.includes('PRESRR')) score += 5;
|
||||
if (remarkParts.some(p => p.includes('TORNADO') || p.includes('FUNNEL'))) score += 15;
|
||||
}
|
||||
|
||||
// --- Rarity Scoring ---
|
||||
let rarityScore = 0;
|
||||
uniqueParts.forEach(part => {
|
||||
const freq = componentFrequencies[part] || 0;
|
||||
if (freq > 0) {
|
||||
rarityScore += Math.log(totalMetars / freq);
|
||||
}
|
||||
});
|
||||
score += rarityScore;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// --- Decoding Functions ---
|
||||
|
||||
function decodeAirport(code, metarString) {
|
||||
@@ -619,8 +724,10 @@
|
||||
metarDisplay.textContent = 'No METARs loaded.';
|
||||
return;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * metars.length);
|
||||
const metarString = metars[randomIndex];
|
||||
// Bias random selection towards the more interesting (higher-scored) METARs at the start of the array.
|
||||
// Math.random() * Math.random() skews distribution towards 0.
|
||||
const randomIndex = Math.floor(Math.random() * Math.random() * metars.length);
|
||||
const metarString = metars[randomIndex].metar;
|
||||
|
||||
currentMetarSections = generateMetarSections(metarString);
|
||||
currentSectionIndex = 0;
|
||||
@@ -631,15 +738,42 @@
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const lines = data.split('\n');
|
||||
const rawMetars = [];
|
||||
lines.forEach(line => {
|
||||
if (line.includes('METAR')) {
|
||||
const metarString = line.substring(line.indexOf('METAR') + 6).replace('=', '').trim();
|
||||
if (metarString) {
|
||||
metars.push(metarString);
|
||||
rawMetars.push(metarString);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`Loaded ${metars.length} METARs.`);
|
||||
|
||||
// --- Rank METARs for interest ---
|
||||
|
||||
// 1. Calculate component frequencies for rarity scoring
|
||||
const componentFrequencies = {};
|
||||
rawMetars.forEach(metarString => {
|
||||
const parts = getMetarParts(metarString);
|
||||
parts.forEach(part => {
|
||||
componentFrequencies[part] = (componentFrequencies[part] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Score and store each METAR
|
||||
rawMetars.forEach(metarString => {
|
||||
const score = calculateMetarScore(metarString, componentFrequencies, rawMetars.length);
|
||||
metars.push({ metar: metarString, score: score });
|
||||
});
|
||||
|
||||
// 3. Sort by score descending
|
||||
metars.sort((a, b) => b.score - a.score);
|
||||
|
||||
console.log(`Loaded and ranked ${metars.length} METARs.`);
|
||||
console.log(`Top 5 most interesting METARs:`);
|
||||
for (let i = 0; i < Math.min(5, metars.length); i++) {
|
||||
console.log(` - Score: ${metars[i].score.toFixed(2)}, METAR: ${metars[i].metar}`);
|
||||
}
|
||||
|
||||
displayNewMetar(); // Display initial METAR
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
Reference in New Issue
Block a user