194 lines
9.6 KiB
HTML
194 lines
9.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>NWS Weather Data Aggregator</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
.loader {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #3498db;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
.chart-img {
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
#output {
|
|
height: 500px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
|
|
|
<div class="container mx-auto p-4 md:p-8">
|
|
<header class="text-center mb-8">
|
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white">NWS Weather Data Aggregator</h1>
|
|
<p class="text-md text-gray-600 dark:text-gray-400 mt-2">Click the button to fetch and combine weather data for analysis.</p>
|
|
</header>
|
|
|
|
<!-- Data Fetching Section -->
|
|
<section class="mb-12 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
|
|
<div class="flex flex-col items-center">
|
|
<button id="fetchButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition-colors duration-300 text-lg">
|
|
Fetch All Data
|
|
</button>
|
|
<div id="loader" class="loader mt-6 hidden"></div>
|
|
<p id="status" class="mt-4 text-gray-600 dark:text-gray-400"></p>
|
|
</div>
|
|
|
|
<div id="outputContainer" class="mt-6 hidden">
|
|
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">Combined Data Output</h2>
|
|
<textarea id="output" class="w-full p-4 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-50 dark:bg-gray-700 text-gray-800 dark:text-gray-200" readonly></textarea>
|
|
<button id="copyButton" class="mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition-colors duration-300">
|
|
Copy to Clipboard
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Graphical Forecasts Section -->
|
|
<section>
|
|
<h2 class="text-2xl font-semibold border-b-2 border-gray-500 pb-2 mb-6 text-gray-900 dark:text-white">Graphical Forecasts (for reference)</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
|
|
<h3 class="font-bold text-xl mb-4 text-center">Surface Analysis</h3>
|
|
<img class="chart-img w-full rounded-md" src="https://www.wpc.ncep.noaa.gov/sfc/lrgnamsfc.gif" alt="Surface Analysis Chart">
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
|
|
<h3 class="font-bold text-xl mb-4 text-center">500mb Heights & Vorticity</h3>
|
|
<img class="chart-img w-full rounded-md" src="https://www.wpc.ncep.noaa.gov/upperair/nam.gif" alt="500mb Analysis Chart">
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
|
|
<h3 class="font-bold text-xl mb-4 text-center">850mb Temps, Heights & Winds</h3>
|
|
<img class="chart-img w-full rounded-md" src="https://www.wpc.ncep.noaa.gov/upperair/nam85.gif" alt="850mb Analysis Chart">
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg">
|
|
<h3 class="font-bold text-xl mb-4 text-center">250mb Jet Stream</h3>
|
|
<img class="chart-img w-full rounded-md" src="https://www.wpc.ncep.noaa.gov/upperair/namhi.gif" alt="250mb Analysis Chart">
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="text-center mt-12 py-4 border-t dark:border-gray-700">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Data provided by the National Weather Service.</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// --- DOM Element References ---
|
|
const fetchButton = document.getElementById('fetchButton');
|
|
const copyButton = document.getElementById('copyButton');
|
|
const outputContainer = document.getElementById('outputContainer');
|
|
const outputTextarea = document.getElementById('output');
|
|
const loader = document.getElementById('loader');
|
|
const statusText = document.getElementById('status');
|
|
|
|
// --- Data Source URLs ---
|
|
const sources = {
|
|
areaForecastDiscussion: `https://forecast.weather.gov/product.php?site=RLX&issuedby=RLX&product=AFD&format=txt&version=1&glossary=0`,
|
|
spcOutlook: `https://www.spc.noaa.gov/products/outlook/day1otlk.html`,
|
|
wpcShortRange: `https://www.wpc.ncep.noaa.gov/discussions/pmdspd.html`,
|
|
wpcMediumRange: `https://www.wpc.ncep.noaa.gov/discussions/pmdepd.html`
|
|
};
|
|
|
|
/**
|
|
* Fetches content from a URL and extracts text from a <pre> tag.
|
|
* @param {string} url - The URL to fetch data from.
|
|
* @param {string} key - A key to identify the data source for error messages.
|
|
* @returns {Promise<string>} A promise that resolves with the extracted text.
|
|
*/
|
|
async function fetchAndParse(url, key) {
|
|
try {
|
|
// NOTE: The CORS proxy has been removed as requested.
|
|
// If you experience fetching errors when hosting this on a static site,
|
|
// it's likely due to the browser's same-origin policy blocking the request.
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const preElement = doc.querySelector('pre');
|
|
return preElement ? preElement.innerText.trim() : `Could not find <pre> content for ${key}.`;
|
|
} catch (error) {
|
|
console.error(`Error fetching ${key}:`, error);
|
|
statusText.innerHTML += `<br><span class="text-red-500">Failed to fetch ${key}.</span>`;
|
|
return `Error fetching data for ${key}: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main function to orchestrate fetching all data sources.
|
|
*/
|
|
async function fetchAllData() {
|
|
// --- UI Updates: Start Loading ---
|
|
fetchButton.disabled = true;
|
|
fetchButton.classList.add('opacity-50', 'cursor-not-allowed');
|
|
loader.classList.remove('hidden');
|
|
statusText.textContent = 'Fetching data...';
|
|
outputContainer.classList.add('hidden');
|
|
|
|
const data = {};
|
|
|
|
// --- Fetch all sources concurrently ---
|
|
const promises = [
|
|
fetchAndParse(sources.areaForecastDiscussion, 'Area Forecast Discussion').then(text => data.areaForecastDiscussion = text),
|
|
fetchAndParse(sources.spcOutlook, 'SPC Outlook').then(text => data.spcOutlook = text),
|
|
fetchAndParse(sources.wpcShortRange, 'WPC Short Range').then(text => data.wpcShortRange = text),
|
|
fetchAndParse(sources.wpcMediumRange, 'WPC Medium Range').then(text => data.wpcMediumRange = text)
|
|
];
|
|
|
|
await Promise.all(promises);
|
|
|
|
// --- UI Updates: Display Results ---
|
|
loader.classList.add('hidden');
|
|
statusText.textContent = 'Data fetching complete!';
|
|
|
|
// Format the collected data as a JSON string and display it
|
|
outputTextarea.value = JSON.stringify(data, null, 2);
|
|
outputContainer.classList.remove('hidden');
|
|
|
|
// Re-enable the fetch button
|
|
fetchButton.disabled = false;
|
|
fetchButton.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
}
|
|
|
|
/**
|
|
* Copies the content of the textarea to the user's clipboard.
|
|
*/
|
|
function copyToClipboard() {
|
|
outputTextarea.select();
|
|
// Using document.execCommand as a fallback for broader compatibility
|
|
// in environments where navigator.clipboard might be restricted.
|
|
try {
|
|
document.execCommand('copy');
|
|
copyButton.textContent = 'Copied!';
|
|
setTimeout(() => { copyButton.textContent = 'Copy to Clipboard'; }, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy text: ', err);
|
|
copyButton.textContent = 'Copy Failed!';
|
|
setTimeout(() => { copyButton.textContent = 'Copy to Clipboard'; }, 2000);
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
fetchButton.addEventListener('click', fetchAllData);
|
|
copyButton.addEventListener('click', copyToClipboard);
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|