mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
initial data and graph
This commit is contained in:
@@ -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',
|
||||
|
||||
BIN
server/images/backgrounds/6.png
Normal file
BIN
server/images/backgrounds/6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
server/images/gimp/Background 6.xcf
Normal file
BIN
server/images/gimp/Background 6.xcf
Normal file
Binary file not shown.
@@ -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');
|
||||
|
||||
|
||||
@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(10, 'radar'));
|
||||
registerDisplay(new Radar(11, 'radar'));
|
||||
|
||||
115
server/scripts/modules/spc-outlook.mjs
Normal file
115
server/scripts/modules/spc-outlook.mjs
Normal 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'));
|
||||
51
server/scripts/modules/utils/polygon.mjs
Normal file
51
server/scripts/modules/utils/polygon.mjs
Normal 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
61
server/styles/scss/_spc-outlook.scss
Normal file
61
server/styles/scss/_spc-outlook.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,4 @@
|
||||
@use 'almanac';
|
||||
@use 'hazards';
|
||||
@use 'media';
|
||||
@use 'spc-outlook';
|
||||
@@ -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>
|
||||
|
||||
20
views/partials/spc-outlook.ejs
Normal file
20
views/partials/spc-outlook.ejs
Normal 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') %>
|
||||
Reference in New Issue
Block a user