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 => {