initial data and graph

This commit is contained in:
Matt Walsh
2025-05-15 16:04:57 -05:00
parent 93ac03acd4
commit 5946ee495a
13 changed files with 259 additions and 4 deletions

View File

@@ -76,6 +76,7 @@ const mjsSources = [
'server/scripts/modules/hazards.mjs',
'server/scripts/modules/currentweather.mjs',
'server/scripts/modules/almanac.mjs',
'server/scripts/modules/spc-outlook.mjs',
'server/scripts/modules/icons.mjs',
'server/scripts/modules/extendedforecast.mjs',
'server/scripts/modules/hourly.mjs',

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

View File

@@ -113,9 +113,11 @@ const smallIcon = (link, _isNightTime) => {
return addPath('Thunderstorm.gif');
case 'wind':
case 'wind_':
case 'wind_few':
case 'wind_sct':
case 'wind-n':
case 'wind_-n':
case 'wind_few-n':
return addPath('Wind.gif');

View File

@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(10, 'radar'));
registerDisplay(new Radar(11, 'radar'));

View File

@@ -0,0 +1,115 @@
// display spc outlook in a bar graph
import STATUS from './status.mjs';
import { json } from './utils/fetch.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import testPolygon from './utils/polygon.mjs';
// list of interesting files ordered [0] = today, [1] = tomorrow...
const urlPattern = (day) => `https://www.spc.noaa.gov/products/outlook/day${day}otlk_cat.nolyr.geojson`;
const testAllPoints = (point, data) => {
// returns all points where the data matches as an array of days and then matches of the properties of the data
const result = [];
// start with a loop of days
data.forEach((day, index) => {
// initialize the result
result[index] = false;
// loop through each category
day.features.forEach((feature) => {
if (!feature.geometry.coordinates) return;
const inPolygon = testPolygon(point, feature.geometry);
if (inPolygon) result[index] = feature.properties;
});
});
return result;
};
const barSizes = {
TSTM: 60,
MRGL: 150,
SLGT: 210,
ENH: 270,
MDT: 330,
HIGH: 390,
};
class SpcOutlook extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'SPC Outlook', true);
// calculate file names
this.files = [null, null, null].map((v, i) => urlPattern(i + 1));
// set timings
this.timing.totalScreens = 1;
}
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
let initialData;
try {
// get the three categorical files to get started
const filePromises = await Promise.allSettled(this.files.map((file) => json(file)));
// store the data, promise will always be fulfilled
initialData = filePromises.map((outlookDay) => outlookDay.value);
} catch (error) {
console.error('Unable to get spc outlook');
console.error(error.status, error.responseJSON);
// if there's no previous data, fail
if (!this.data) {
this.setStatus(STATUS.failed);
return;
}
}
// do the initial parsing of the data
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], initialData);
// we only get here if there was no error above
this.screenIndex = 0;
this.setStatus(STATUS.loaded);
}
async drawCanvas() {
super.drawCanvas();
// analyze each day
const days = this.data.map((day, index) => {
// get the day name
const dayName = DateTime.now().plus({ days: index }).toLocaleString({ weekday: 'long' });
// fill the name
const fill = {};
fill['day-name'] = dayName;
// create the element
const elem = this.fillTemplate('day', fill);
// update the bar length
const bar = elem.querySelector('.risk-bar');
if (day.LABEL) {
bar.style.width = `${barSizes[day.LABEL]}px`;
} else {
bar.style.display = 'none';
}
return elem;
});
// add the days to the display
const dayContainer = this.elem.querySelector('.days');
dayContainer.innerHTML = '';
dayContainer.append(...days);
// finish drawing
this.finishDraw();
}
}
// register display
registerDisplay(new SpcOutlook(10, 'spc-outlook'));

View File

@@ -0,0 +1,51 @@
// handle multi-polygon and holes
const testPolygon = (point, _polygons) => {
// turn everything into a multi polygon for ease of processing
let polygons = [[..._polygons.coordinates]];
if (_polygons.type === 'MultiPolygon') polygons = [..._polygons.coordinates];
let inArea = false;
polygons.forEach((_polygon) => {
// copy the polygon
const polygon = [..._polygon];
// if a match has been found don't do anything more
if (inArea) return;
// polygons are defined as [[area], [optional hole 1], [optional hole 2], ...]
const area = polygon.shift();
// test if inside the initial area
inArea = pointInPolygon(point, area);
// if not in the area return false
if (!inArea) return;
// test the holes, if in any hole return false
polygon.forEach((hole) => {
if (pointInPolygon(point, hole)) {
inArea = false;
}
});
});
return inArea;
};
const pointInPolygon = (point, polygon) => {
// ray casting method from https://github.com/substack/point-in-polygon
const x = point[0];
const y = point[1];
let inside = false;
// eslint-disable-next-line no-plusplus
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect = ((yi > y) !== (yj > y))
&& (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
export default testPolygon;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#spc-outlook-html.weather-display {
background-image: url('../images/backgrounds/6.png');
}
.weather-display .spc-outlook {
.container {
position: relative;
top: 0px;
margin: 0px 10px;
box-sizing: border-box;
height: 300px;
overflow: hidden;
}
.risk-levels {
position: absolute;
left: 206px;
font-family: 'Star4000 Small';
font-size: 32px;
@include u.text-shadow();
.risk-level {
position: relative;
top: -14px;
height: 20px;
}
}
.days {
position: absolute;
top: 120px;
.day {
height: 60px;
.day-name {
position: absolute;
font-family: 'Star4000';
font-size: 24pt;
width: 200px;
text-align: right;
@include u.text-shadow();
padding-top: 20px;
}
.risk-bar {
position: absolute;
width: 150px;
height: 40px;
left: 210px;
margin-top: 20px;
border: 3px outset hsl(0, 0%, 70%);
background: linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%);
}
}
}
}

View File

@@ -12,4 +12,5 @@
@use 'regional-forecast';
@use 'almanac';
@use 'hazards';
@use 'media';
@use 'media';
@use 'spc-outlook';

View File

@@ -33,6 +33,7 @@
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
<script type="module" src="scripts/modules/currentweather.mjs"></script>
<script type="module" src="scripts/modules/almanac.mjs"></script>
<script type="module" src="scripts/modules/spc-outlook.mjs"></script>
<script type="module" src="scripts/modules/icons.mjs"></script>
<script type="module" src="scripts/modules/extendedforecast.mjs"></script>
<script type="module" src="scripts/modules/hourly-graph.mjs"></script>
@@ -109,6 +110,9 @@
<div id="almanac-html" class="weather-display">
<%- include('partials/almanac.ejs') %>
</div>
<div id="spc-outlook-html" class="weather-display">
<%- include('partials/spc-outlook.ejs') %>
</div>
<div id="extended-forecast-html" class="weather-display">
<%- include('partials/extended-forecast.ejs') %>
</div>

View File

@@ -0,0 +1,20 @@
<%- include('header.ejs', {title: 'SPC Outlook', hasTime: true, noaaLogo: true}) %>
<div class="main has-scroll spc-outlook">
<div class="container">
<div class="risk-levels">
<div class="risk-level">High</div>
<div class="risk-level">Moderate</div>
<div class="risk-level">Enhanced</div>
<div class="risk-level">Slight</div>
<div class="risk-level">Marginal</div>
<div class="risk-level">T'Storm</div>
</div>
<div class="days">
<div class="day template">
<div class="day-name">Monday</div>
<div class="risk-bar"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>