diff --git a/metar.html b/metar.html
index 1ab4070..e35bba7 100644
--- a/metar.html
+++ b/metar.html
@@ -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 => {