985 lines
30 KiB
HTML
985 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>RLX Ver Helper</title>
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
|
<script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js" integrity="sha256-6XMVI0zB8cRzfZjqKcD01PBsAy3FlDASrlC8SxCpInY=" crossorigin="anonymous"></script>
|
|
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.1/themes/smoothness/jquery-ui.css">
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css">
|
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
|
|
<script src="https://cdn.rawgit.com/hayeswise/Leaflet.PointInPolygon/v1.0.0/wise-leaflet-pip.js"></script>
|
|
<script src="https://unpkg.com/@mapbox/leaflet-pip@latest/leaflet-pip.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<style type="text/css">
|
|
body, html {
|
|
height: 100%;
|
|
width: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#container {
|
|
display: flex;
|
|
height: 100%;
|
|
width: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
#pickwwa {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
width: 300px;
|
|
background: white;
|
|
padding: 10px;
|
|
z-index: 9998;
|
|
}
|
|
|
|
p {
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
line-height: 1.5; /* Optional: controls line spacing */
|
|
}
|
|
|
|
#richTextArea {
|
|
margin-left: 500px; /* Space for pickwwa + padding */
|
|
margin-right: 5px; /* Space for map + padding */
|
|
margin-top: 10px;
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: #f9f9f9;
|
|
border: 1px solid #ddd;
|
|
overflow-y: auto;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
#selectable-list {
|
|
list-style-type: none;
|
|
padding: 0;
|
|
z-index: 9999;
|
|
}
|
|
|
|
#selectable-list li {
|
|
padding: 10px;
|
|
margin: 5px 0;
|
|
background-color: #f0f0f0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#selectable-list li.selected {
|
|
background-color: #007bff;
|
|
color: white;
|
|
}
|
|
|
|
.list-label {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.my-label {
|
|
font-size: 6px;
|
|
border: 0px solid #ccc;
|
|
border-radius: 1px;
|
|
padding: 1px 2px;
|
|
}
|
|
#hydrograph {
|
|
display: none;
|
|
}
|
|
</style>
|
|
|
|
<div id="container">
|
|
<div id="pickwwa">
|
|
<label for="yeartowork">Choose a year:</label>
|
|
<select name="yeartowork" id="yeartowork" onchange="getwwas()">
|
|
<option value="2025">2025</option>
|
|
<option value="2024">2024</option>
|
|
<option value="2023">2023</option>
|
|
<option value="2022">2022</option>
|
|
</select>
|
|
<ul id="selectable-list"></ul>
|
|
<label for="lsrbuffer">Buffer LSR hrs on back end of product</label>
|
|
<input id="lsrbuffer" name="lsrbuffer" type="input" value="24">
|
|
<input id="LSR" type="button" value="Load LSRs" onclick="loadlsr();" />
|
|
</div>
|
|
|
|
<div id="mapid"></div>
|
|
|
|
<div id="richTextArea">
|
|
<canvas id="hydrograph" width="800" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
|
|
async function getNWSGaugesByBoundingBox(xmin, ymin, xmax, ymax) {
|
|
// Construct the API URL with provided bounding box values
|
|
const url = `https://api.water.noaa.gov/nwps/v1/gauges?bbox.xmin=${xmin}&bbox.ymin=${ymin}&bbox.xmax=${xmax}&bbox.ymax=${ymax}&srid=EPSG_4326&catfim=false`;
|
|
|
|
try {
|
|
// Fetch the data
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Check if the response is OK
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Parse the JSON response
|
|
const data = await response.json();
|
|
|
|
// Return the gauge data (assuming the response contains a 'data' array)
|
|
return data || [];
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching NWS gauge data:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function getRLXgauges() {
|
|
// Example bounding box values from your URL
|
|
const xmin = -83.254395; // West longitude
|
|
const ymin = 36.932330; // South latitude
|
|
const xmax = -78.508301; // East longitude
|
|
const ymax = 40.145289; // North latitude
|
|
|
|
const gauges = await getNWSGaugesByBoundingBox(xmin, ymin, xmax, ymax);
|
|
return gauges
|
|
|
|
}
|
|
|
|
|
|
|
|
var wwalsrtypes = []
|
|
|
|
|
|
|
|
function isNumber(value) {
|
|
// Check if the value is of type 'number' and is finite
|
|
return typeof value === 'number' && isFinite(value);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var mymap = L.map('mapid', {zoomDelta: 0.25, zoomSnap: 0}).setView([38.508, -82.652480], 8.0);
|
|
|
|
|
|
|
|
|
|
var countiesjson = 'https://wx.stoat.org/main.php?service=ver'
|
|
|
|
|
|
|
|
|
|
const sbwprods = ["TO","SV","EW","SQ","FA","FL","FF"]
|
|
|
|
|
|
const phenomenonCodes = {
|
|
"AF": "Ashfall (land)",
|
|
"AS": "Air Stagnation",
|
|
"BH": "Beach Hazard",
|
|
"BW": "Brisk Wind",
|
|
"BZ": "Blizzard",
|
|
"CF": "Coastal Flood",
|
|
"CW": "Cold Weather",
|
|
"DF": "Debris Flow",
|
|
"DS": "Dust Storm",
|
|
"DU": "Blowing Dust",
|
|
"EC": "Extreme Cold",
|
|
"EH": "Excessive Heat",
|
|
"XH": "Extreme Heat",
|
|
"EW": "Extreme Wind",
|
|
"FA": "Flood",
|
|
"FF": "Flash Flood",
|
|
"FG": "Dense Fog (land)",
|
|
"FL": "Flood (Forecast Points)",
|
|
"FR": "Frost",
|
|
"FW": "Fire Weather",
|
|
"FZ": "Freeze",
|
|
"GL": "Gale",
|
|
"HF": "Hurricane Force Wind",
|
|
"HT": "Heat",
|
|
"HU": "Hurricane",
|
|
"HW": "High Wind",
|
|
"HY": "Hydrologic",
|
|
"HZ": "Hard Freeze",
|
|
"IS": "Ice Storm",
|
|
"LE": "Lake Effect Snow",
|
|
"LO": "Low Water",
|
|
"LS": "Lakeshore Flood",
|
|
"LW": "Lake Wind",
|
|
"MA": "Marine",
|
|
"MF": "Dense Fog (marine)",
|
|
"MH": "Ashfall (marine)",
|
|
"MS": "Dense Smoke (marine)",
|
|
"RP": "Rip Current Risk",
|
|
"SC": "Small Craft",
|
|
"SE": "Hazardous Seas",
|
|
"SM": "Dense Smoke (land)",
|
|
"SR": "Storm",
|
|
"SS": "Storm Surge",
|
|
"SQ": "Snow Squall",
|
|
"SU": "High Surf",
|
|
"SV": "Severe Thunderstorm",
|
|
"TO": "Tornado",
|
|
"TR": "Tropical Storm",
|
|
"TS": "Tsunami",
|
|
"TY": "Typhoon",
|
|
"UP": "Heavy Freezing Spray",
|
|
"WC": "Wind Chill",
|
|
"WI": "Wind",
|
|
"WS": "Winter Storm",
|
|
"WW": "Winter Weather",
|
|
"ZF": "Freezing Fog",
|
|
"ZR": "Freezing Rain",
|
|
"ZY": "Freezing Spray"
|
|
};
|
|
|
|
function getPhenSig(code,sig) {
|
|
// Convert the code to uppercase to ensure case-insensitive lookup
|
|
code = code.toUpperCase();
|
|
|
|
// Check if the code exists in the phenomenonCodes object
|
|
if (code in phenomenonCodes) {
|
|
var phenomenon = phenomenonCodes[code];
|
|
}
|
|
if (sig in sigCodes) {
|
|
var significance = sigCodes[sig];
|
|
}
|
|
|
|
return phenomenon + " " + significance
|
|
|
|
}
|
|
|
|
|
|
sigCodes = {
|
|
"W": "Warning",
|
|
"A": "Watch",
|
|
"Y": "Advisory",
|
|
"S": "Statement",
|
|
}
|
|
|
|
function getUsgsIdFromNwsGauge(gaugeId, startDate, endDate) {
|
|
const nwsUrl = "https://api.water.noaa.gov/nwps/v1/gauges/" + gaugeId;
|
|
|
|
return fetch(nwsUrl, {
|
|
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }
|
|
})
|
|
.then(nwsResponse => {
|
|
if (!nwsResponse.ok) {
|
|
throw new Error("NWS API error! Status: " + nwsResponse.status);
|
|
}
|
|
return nwsResponse.json();
|
|
})
|
|
.then(nwsData => {
|
|
let usgsId = nwsData.usgsId || null;
|
|
if (!usgsId && nwsData.dataAttribution && Array.isArray(nwsData.dataAttribution)) {
|
|
const usgsEntry = nwsData.dataAttribution.find(
|
|
entry => entry.abbrev === "USGS" && entry.text.match(/^\d{8}$/)
|
|
);
|
|
usgsId = usgsEntry ? usgsEntry.text : null;
|
|
}
|
|
console.log("USGS ID for " + gaugeId + ": " + (usgsId || "Not found"));
|
|
gaugeName = nwsData ? nwsData.name : null;
|
|
if (!usgsId) {
|
|
console.log("No USGS ID found, skipping requests.");
|
|
return null;
|
|
}
|
|
|
|
// Fetch flood stages
|
|
const floodStageUrl = "https://waterdata.usgs.gov/flood-stage/" + usgsId;
|
|
console.log("Fetching flood stages from: " + floodStageUrl);
|
|
|
|
return Promise.all([
|
|
fetch(floodStageUrl, {
|
|
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }
|
|
}).then(response => {
|
|
if (!response.ok) {
|
|
throw new Error("Flood stage API error! Status: " + response.status);
|
|
}
|
|
return response.json();
|
|
}),
|
|
fetch("https://nwis.waterservices.usgs.gov/nwis/iv/?" +
|
|
"site=" + usgsId + "&" +
|
|
"startDT=" + startDate + "&" +
|
|
"endDT=" + endDate + "&" +
|
|
"parameterCd=00065&" +
|
|
"format=json", {
|
|
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }
|
|
}).then(response => {
|
|
if (!response.ok) {
|
|
throw new Error("USGS API error! Status: " + response.status);
|
|
}
|
|
return response.json();
|
|
})
|
|
])
|
|
.then(([floodStageData, usgsData]) => {
|
|
// Extract flood stages
|
|
const floodStages = floodStageData.stages || {};
|
|
console.log("Flood stages:", floodStages);
|
|
|
|
// Extract gauge height data
|
|
if (usgsData.value && usgsData.value.timeSeries && usgsData.value.timeSeries.length > 0) {
|
|
const timeSeries = usgsData.value.timeSeries[0];
|
|
const values = timeSeries.values[0].value;
|
|
const results = values.map(value => ({
|
|
timestamp: value.dateTime,
|
|
gaugeHeight: parseFloat(value.value)
|
|
}));
|
|
console.log("Data retrieved:", results);
|
|
|
|
generateHydrograph(results, floodStages,gaugeId,gaugeName);
|
|
return results;
|
|
} else {
|
|
console.log("No data found for the specified range.");
|
|
return [];
|
|
}
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error("Error processing " + gaugeId + ": ", error);
|
|
return null;
|
|
});
|
|
}
|
|
|
|
function generateHydrograph(data, floodStages,gaugeId,gaugeName) {
|
|
const ctx = document.getElementById("hydrograph").getContext("2d");
|
|
|
|
const labels = data.map(d => new Date(d.timestamp).toLocaleString());
|
|
const gaugeHeights = data.map(d => d.gaugeHeight);
|
|
|
|
// Build datasets for flood stages
|
|
const floodDatasets = [];
|
|
if (floodStages.action !== null) {
|
|
floodDatasets.push({
|
|
label: "Action Stage (" + floodStages.action + " ft)",
|
|
data: Array(labels.length).fill(floodStages.action),
|
|
borderColor: "yellow",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
fill: false,
|
|
borderDash: [5, 5]
|
|
});
|
|
}
|
|
if (floodStages.minor !== null) {
|
|
floodDatasets.push({
|
|
label: "Minor Flood (" + floodStages.minor + " ft)",
|
|
data: Array(labels.length).fill(floodStages.minor),
|
|
borderColor: "orange",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
fill: false,
|
|
borderDash: [5, 5]
|
|
});
|
|
}
|
|
if (floodStages.moderate !== null) {
|
|
floodDatasets.push({
|
|
label: "Moderate Flood (" + floodStages.moderate + " ft)",
|
|
data: Array(labels.length).fill(floodStages.moderate),
|
|
borderColor: "red",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
fill: false,
|
|
borderDash: [5, 5]
|
|
});
|
|
}
|
|
if (floodStages.major !== null) {
|
|
floodDatasets.push({
|
|
label: "Major Flood (" + floodStages.major + " ft)",
|
|
data: Array(labels.length).fill(floodStages.major),
|
|
borderColor: "purple",
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
fill: false,
|
|
borderDash: [5, 5]
|
|
});
|
|
}
|
|
|
|
new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: "Gauge Height (ft)",
|
|
data: gaugeHeights,
|
|
borderColor: "blue",
|
|
fill: false,
|
|
tension: 0.1
|
|
},
|
|
...floodDatasets // Spread flood stage datasets
|
|
]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
scales: {
|
|
x: {
|
|
title: { display: true, text: "Time" }
|
|
},
|
|
y: {
|
|
title: { display: true, text: "Gauge Height (ft)" },
|
|
suggestedMin: Math.min(...gaugeHeights, floodStages.action || Infinity, floodStages.minor || Infinity, floodStages.moderate || Infinity, floodStages.major || Infinity) - 2,
|
|
suggestedMax: Math.max(...gaugeHeights, floodStages.action || -Infinity, floodStages.minor || -Infinity, floodStages.moderate || -Infinity, floodStages.major || -Infinity) + 2
|
|
}
|
|
},
|
|
plugins: {
|
|
title: { display: true, text: "Hydrograph for " + gaugeId + " (" + gaugeName + ") with Flood Stages" },
|
|
legend: { position: "top" }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayHydrograph(gaugeName) {
|
|
const canvas = document.getElementById("hydrograph");
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
// Hide the canvas (assuming it's already hidden via CSS, or add this line)
|
|
canvas.style.display = "none";
|
|
|
|
// Create/update the image
|
|
let img = document.getElementById("hydrograph-image");
|
|
if (!img) {
|
|
img = document.createElement("img");
|
|
img.id = "hydrograph-image";
|
|
const container = document.getElementById("hydrograph-container");
|
|
container.appendChild(img);
|
|
|
|
// Optional: Add a download button
|
|
const downloadBtn = document.createElement("a");
|
|
downloadBtn.textContent = "Download Hydrograph";
|
|
downloadBtn.id = "download-link";
|
|
container.appendChild(downloadBtn);
|
|
}
|
|
|
|
// Set the image source
|
|
const imageData = canvas.toDataURL("image/png");
|
|
img.src = imageData;
|
|
|
|
// Update download link with custom filename
|
|
const downloadLink = document.getElementById("download-link");
|
|
if (downloadLink) {
|
|
downloadLink.href = imageData;
|
|
downloadLink.download = `${gaugeName}.png`; // Sets filename to GAUGENAME.png
|
|
}
|
|
|
|
// Clear the canvas for reuse
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
|
|
var Esri_WorldStreetMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
|
|
attribution: 'Tiles © Esri'
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function getwwas() {
|
|
|
|
var e = document.getElementById("yeartowork");
|
|
var value = e.options[e.selectedIndex].value;
|
|
|
|
year = value
|
|
url = `https://mesonet.agron.iastate.edu/json/vtec_events.py?wfo=RLX&year=${year}`
|
|
$.getJSON(url, function(data) {
|
|
events = []
|
|
for (i in data.events) {
|
|
if (data.events[i].significance != "A") {
|
|
events.push(data.events[i])
|
|
}
|
|
}
|
|
createEventSelector(events);
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createEventSelector(events) {
|
|
if ($("#eventSelector").length > 0) {
|
|
|
|
$("#eventSelector").remove();
|
|
}
|
|
|
|
const select = document.createElement('select');
|
|
select.id = 'eventSelector';
|
|
|
|
select.multiple = true; // This allows multiple selections
|
|
|
|
// Add size attribute to show more than one option at a time
|
|
select.size = Math.min(events.length, 25); // Display up to 10 options or all if fewer
|
|
|
|
// Populate the select with options from the events array
|
|
events.forEach(event => {
|
|
const option = document.createElement('option');
|
|
|
|
var whatitis = getPhenSig(event.phenomena,event.significance) + " " + event.eventid
|
|
//option.value = event.eventid;
|
|
option.value = whatitis;
|
|
option.text = `${whatitis} ${event.issue.slice(0, -4) + 'Z'}-${event.init_expire.slice(0, -4) + 'Z'}`;
|
|
select.add(option);
|
|
});
|
|
|
|
// Find the 'pickwwa' div to append the select to
|
|
const pickwwaDiv = document.getElementById('pickwwa');
|
|
if (pickwwaDiv) {
|
|
pickwwaDiv.appendChild(select);
|
|
} else {
|
|
console.error('Div with ID "pickwwa" not found.');
|
|
}
|
|
|
|
// Event listener for handling multiple selections
|
|
select.addEventListener('change', function() {
|
|
fullarray = []
|
|
|
|
var selectedOptions = Array.from(this.selectedOptions);
|
|
var selectedEvents = selectedOptions.map(option =>
|
|
events.find(e => getPhenSig(e.phenomena,e.significance) + " " + e.eventid == option.value)
|
|
);
|
|
|
|
const sortedEvents = selectedEvents.sort((a, b) => {
|
|
const significanceA = a.significance;
|
|
const significanceB = b.significance;
|
|
|
|
// Assign sorting weights: "W" has higher priority (lower value) than "Y"
|
|
if (significanceA === "Y" && significanceB !== "Y") return -1; // a comes first
|
|
if (significanceB === "Y" && significanceA !== "Y") return 1; // b comes first
|
|
// If both are the same or neither is "W", keep original order
|
|
return 0;
|
|
});
|
|
|
|
|
|
|
|
plotselectedproducts(sortedEvents)
|
|
});
|
|
|
|
return select;
|
|
}
|
|
|
|
|
|
function getEarliestAndLatest(fullarray) {
|
|
if (!Array.isArray(fullarray) || fullarray.length === 0) {
|
|
throw new Error("Input must be a non-empty array");
|
|
}
|
|
|
|
let earliestIssue = null;
|
|
let latestExpire = null;
|
|
|
|
for (const event of fullarray) {
|
|
const issueDate = new Date(event.issue);
|
|
const expireDate = new Date(event.init_expire);
|
|
|
|
if (isNaN(issueDate) || isNaN(expireDate)) {
|
|
console.warn(`Skipping event with invalid dates: ${JSON.stringify(event)}`);
|
|
continue;
|
|
}
|
|
|
|
if (!earliestIssue || issueDate < new Date(earliestIssue)) {
|
|
earliestIssue = event.issue;
|
|
}
|
|
if (!latestExpire || expireDate > new Date(latestExpire)) {
|
|
latestExpire = event.init_expire;
|
|
}
|
|
}
|
|
|
|
if (!earliestIssue || !latestExpire) {
|
|
throw new Error("No valid dates found in the array");
|
|
}
|
|
|
|
return { earliestIssue, latestExpire };
|
|
}
|
|
|
|
function plotselectedproducts(events) {
|
|
|
|
lsrs = grabLSRS(events)
|
|
let fullarray = [];
|
|
let promises = [];
|
|
|
|
// Convert events to array if it's an object and loop with for...of
|
|
const eventArray = Array.isArray(events) ? events : Object.values(events);
|
|
|
|
for (let event of eventArray) {
|
|
let year = yearfromissue(event.issue);
|
|
let url;
|
|
|
|
if (sbwprods.includes(event.phenomena)) {
|
|
url = `https://mesonet.agron.iastate.edu/geojson/vtec_event.py?wfo=RLX&phenomena=${event.phenomena}&significance=${event.significance}&etn=${event.eventid}&year=${year}&sbw=1`;
|
|
} else {
|
|
url = `https://mesonet.agron.iastate.edu/geojson/vtec_event.py?wfo=RLX&phenomena=${event.phenomena}&significance=${event.significance}&etn=${event.eventid}&year=${year}`;
|
|
}
|
|
|
|
// Create a promise for each request and push the result to fullarray
|
|
const promise = $.getJSON(url).then(function(data) {
|
|
const combined = {event,data}
|
|
fullarray.push(combined);
|
|
return data; // Optional: return data if needed later
|
|
});
|
|
|
|
promises.push(promise);
|
|
}
|
|
|
|
// Wait for all promises to resolve before calling generateText
|
|
Promise.all(promises)
|
|
.then(() => {
|
|
|
|
generateText(fullarray, events);
|
|
})
|
|
.catch(error => {
|
|
console.error('One or more requests failed:', error);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function grabLSRS(fullarray) {
|
|
console.log(fullarray)
|
|
lsrtimes = getEarliestAndLatest(fullarray)
|
|
lsrstart = lsrtimes.earliestIssue
|
|
lsrend = lsrtimes.latestExpire
|
|
|
|
//lsrend = convertTimestamp(lsrendobj.toISOString())
|
|
// lsrstart = convertTimestamp(lsrstartobj.toISOString())
|
|
var url = `https://mesonet.agron.iastate.edu/geojson/lsr.geojson?sts=${lsrstart}&ets=${lsrend}&wfos=RLX`
|
|
$.getJSON(url, function(data) {
|
|
data = deduplicateGeoJSON(data);
|
|
return data
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
function generateText(events,lsrs) {
|
|
let htmlContent = '';
|
|
events.forEach((event, index) => {
|
|
htmlContent += printEventSummary(event)
|
|
});
|
|
document.getElementById('richTextArea').innerHTML = htmlContent;
|
|
}
|
|
|
|
|
|
|
|
|
|
const markersLayer = L.layerGroup().addTo(mymap);
|
|
|
|
|
|
|
|
|
|
//Pull all lsrs, check appropriate types, add under product
|
|
|
|
|
|
|
|
|
|
function getRGBByWarningType(warningType) {
|
|
const warning = warnings.find(w => w.warningType === warningType);
|
|
return warning ? warning.colorName : null;
|
|
}
|
|
|
|
|
|
function getLSRbyWarnType(warningType) {
|
|
const warning = warnings.find(w => w.warningType === warningType);
|
|
return warning ? warning.code : null;
|
|
|
|
}
|
|
|
|
function wwacolor(feature) {
|
|
code = feature.properties.phenomena
|
|
sig = feature.properties.significance
|
|
product = getPhenSig(code,sig)
|
|
producthex = getRGBByWarningType(product)
|
|
lsrcodes = getLSRbyWarnType(product)
|
|
wwalsrtypes = lsrcodes
|
|
if (producthex != null) {
|
|
return producthex
|
|
|
|
|
|
}
|
|
else {
|
|
return 'purple'
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function yearfromissue(issue) {
|
|
var date = new Date(issue)
|
|
return date.getFullYear()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function deduplicateGeoJSON(geojson) {
|
|
const toTimestamp = (dateStr) => new Date(dateStr).getTime();
|
|
let featuresArray = geojson.features;
|
|
featuresArray.sort((a, b) => {
|
|
return toTimestamp(b.properties.utc_valid) - toTimestamp(a.properties.utc_valid);
|
|
});
|
|
const seenCoords = new Set();
|
|
const uniqueFeatures = featuresArray.filter(feature => {
|
|
const coords = JSON.stringify(feature.geometry.coordinates);
|
|
if (!seenCoords.has(coords)) {
|
|
seenCoords.add(coords);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return {
|
|
type: "FeatureCollection",
|
|
features: uniqueFeatures
|
|
};
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isMarkerInsidePolygon(latlng, polygon) {
|
|
leafletPip.bassackwards = true
|
|
return leafletPip.pointInLayer(latlng, polygon, true); // If results has length, the point is inside
|
|
}
|
|
|
|
function convertZuluToLocalInput(zuluTimestamp) {
|
|
// Check if the input is valid
|
|
if (!zuluTimestamp || !zuluTimestamp.endsWith('Z')) {
|
|
return null; // Return null for invalid input
|
|
}
|
|
|
|
try {
|
|
// Create a Date object from the Zulu timestamp
|
|
const date = new Date(zuluTimestamp);
|
|
|
|
// Check if the date is valid
|
|
if (isNaN(date.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
// Convert to ISO string and remove the 'Z' and milliseconds
|
|
// Format will be YYYY-MM-DDTHH:MM:SS
|
|
const localFormat = date.toISOString().slice(0, 19);
|
|
|
|
return localFormat;
|
|
} catch (error) {
|
|
console.error('Error converting timestamp:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function convertTimestamp(timestamp) {
|
|
// Remove the 'Z' from the end and split the date and time
|
|
const dateTimeParts = timestamp.split('T'); // Split into date and time
|
|
const date = dateTimeParts[0]; // YYYY-MM-DD
|
|
const time = dateTimeParts[1].slice(0, 5); // HH:mm (from HH:mm:ss)
|
|
const [year, month, day] = date.split('-'); // [YYYY, MM, DD]
|
|
const formatted = `${year}${month}${day}${time.replace(":", "")}`; // YYYYMMDDHHmm
|
|
|
|
return formatted;
|
|
}
|
|
|
|
|
|
function addHoursToDate(date, hours) {
|
|
let newDate = new Date(date);
|
|
newDate.setHours(newDate.getHours() + hours);
|
|
return newDate;
|
|
}
|
|
|
|
function loadlsr() {
|
|
padding = parseInt(document.getElementById('lsrbuffer').value)
|
|
var lsrendbuffer = addHoursToDate(lsrendobj,padding)
|
|
lsrend = convertTimestamp(lsrendbuffer.toISOString())
|
|
lsrstart = convertTimestamp(lsrstartobj.toISOString())
|
|
lsrendbuffer = lsrendobj
|
|
markersLayer.clearLayers()
|
|
geoJSONwwa1.setStyle({fillColor: 'transparent'})
|
|
|
|
templsr = []
|
|
var newlsr = []
|
|
var templsrnotin = []
|
|
var verarray = []
|
|
var markers = []
|
|
var url = `https://mesonet.agron.iastate.edu/geojson/lsr.geojson?sts=${lsrstart}&ets=${lsrend}&wfos=RLX`
|
|
$.getJSON(url, function(data) {
|
|
data = deduplicateGeoJSON(data);
|
|
values = wwalsrtypes
|
|
//if we have lsr types we want, only show those
|
|
if (values) {
|
|
for(var i in data.features) {
|
|
if (values.includes(data.features[i].properties.type)) {
|
|
var returnval = isMarkerInsidePolygon(data.features[i].geometry.coordinates.reverse(),geoJSONwwa1);
|
|
if (returnval.length > 0) {
|
|
//data.features[i].properties.state_zone = (returnval[0].feature.id).replace(/Z/g, '')
|
|
data.features[i].properties.state_zone = returnval[0].feature.properties.state_zone
|
|
templsr.push(data.features[i])
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
//if we don't have lsr types we want, show everything
|
|
if (!values) {
|
|
for(var i in data.features) {
|
|
|
|
var returnval = isMarkerInsidePolygon(data.features[i].geometry.coordinates.reverse(),geoJSONwwa1);
|
|
if (returnval.length > 0) {
|
|
data.features[i].properties.state_zone = returnval[0].feature.properties.state_zone
|
|
templsr.push(data.features[i])
|
|
|
|
}
|
|
}
|
|
}
|
|
for (k in templsr) {
|
|
var marker = styleLSR(templsr[k].geometry.coordinates,templsr[k].properties.magnitude,templsr[k].properties.state_zone,templsr[k].properties)
|
|
|
|
markersLayer.addLayer(marker)
|
|
var flag = newflag(templsr[k].properties.type,templsr[k].properties.state_zone,templsr[k].properties.magf)
|
|
var sbwflag = 'test'
|
|
//var flag = checkagainstcriteria(templsr[k].properties.state_zone,templsr[k].properties.magf)
|
|
//create a second array or combine for polygon based products
|
|
if (flag != null) {
|
|
verarray.push([templsr[k].properties.state_zone,flag])
|
|
}
|
|
}
|
|
console.log(verarray)
|
|
zones = processZones(verarray)
|
|
console.log(verarray,zones)
|
|
geoJSONwwa1.eachLayer(function(layer) {
|
|
zone = layer.feature.properties.state + layer.feature.properties.zone
|
|
for (var l in zones) {
|
|
//console.log(zones[l],zone)
|
|
if (zone == zones[l][0]) {
|
|
if (zones[l][1].warning >= 2) {
|
|
layer.setStyle({fillColor: 'gold'});
|
|
|
|
}
|
|
else if (zones[l][1].advisory >= 2) {
|
|
layer.setStyle({fillColor: 'green'});
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
});
|
|
geoJSONwwas.eachLayer(function(layer) {
|
|
if (sbwprods.includes(layer.feature.properties.phenomena)) {
|
|
console.log(layer)
|
|
}
|
|
});
|
|
|
|
//add lsr autoupdate for products that are less than 24hrs from expiration
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function printEventSummary(obj) {
|
|
const { event, data } = obj;
|
|
const hasFeatures = data.features && data.features.length > 0;
|
|
const featureIds = hasFeatures ? data.features.map(feature => feature.id || 'N/A').join(', ') : 'N/A';
|
|
const generateRadmapUrl = (uri) => {
|
|
const vtec = uri.split('/').pop(); // Extracts "2025-O-NEW-KRLX-CW-Y-0013" from "/vtec/event/2025-O-NEW-KRLX-CW-Y-0013"
|
|
return `https://mesonet.agron.iastate.edu/GIS/radmap.php?layers[]=nexrad&layers[]=sbw&layers[]=sbwh&layers[]=uscounties&vtec=${vtec}`;
|
|
};
|
|
const radmapUrl = generateRadmapUrl(event.uri);
|
|
const toEST = (utcDate) => {
|
|
const date = new Date(utcDate);
|
|
date.setUTCHours(date.getUTCHours() - 5);
|
|
return date.toLocaleString('en-US', {
|
|
timeZone: 'UTC',
|
|
year: 'numeric',
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
// Function to format UTC date in 24-hour format without seconds
|
|
const toUTC = (utcDate) => {
|
|
return new Date(utcDate).toLocaleString('en-US', {
|
|
timeZone: 'UTC',
|
|
hour12: false,
|
|
year: 'numeric',
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
decoded = getPhenSig(event.phenomena, event.significance)
|
|
let html = `<div class="event-summary">
|
|
<p><strong>Event:</strong> ${event.phenomena}.${event.significance} - ${decoded}</p>
|
|
<p><strong>Issued:</strong> ${toUTC(event.issue)} (UTC) ${toEST(event.issue)} (EST)</p>
|
|
<p><strong>Initial Expiration:</strong> ${toUTC(event.init_expire)} (UTC) ${toEST(event.init_expire)} (EST)</p>
|
|
<p><strong>Expiration:</strong> ${toUTC(event.expire)} (UTC) ${toEST(event.expire)} (EST)</p>
|
|
<p><strong>Locations:</strong> ${event.locations ? event.locations : 'Not specified'}</p>
|
|
<p><strong>Radar Map:</strong> <img src="${radmapUrl}" alt="Radar map for ${event.phenomena}.${event.significance}"></p>
|
|
<p><strong>Timing Graph:</strong><a href="https://wx.stoat.org/metar.html?start=${convertZuluToLocalInput(event.issue)}&end=${convertZuluToLocalInput(event.init_expire)}" target="_blank">Link</a></p>
|
|
<p><strong>Power Outages For Duration:</strong><a href="https://wx.stoat.org/outage.html?start=${convertZuluToLocalInput(event.issue)}&end=${convertZuluToLocalInput(event.init_expire)}" target="_blank">Link</a></p>
|
|
|
|
`;
|
|
|
|
|
|
//https://wx.stoat.org/metar.html?start=2025-04-01T12:00:00&end=2025-04-02T12:00:00
|
|
|
|
if (event.hvtec_nwsli !== undefined && event.hvtec_nwsli !== null && event.hvtec_nwsli !== "00000") {
|
|
html += `
|
|
<p><strong>HVTEC NWSLI:</strong> ${event.hvtec_nwsli}</p>
|
|
`;
|
|
}
|
|
|
|
html += `
|
|
<p><strong>IAState VTEC Link:</strong> <a href="${event.url}" target="_blank">${event.url}</a></p>
|
|
<p><strong>Feature ID${hasFeatures && data.features.length > 1 ? 's' : ''}:</strong> ${featureIds}</p>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
|
|
getwwas()
|
|
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|