mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-19 01:59:31 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4a601a387 | ||
|
|
375812c024 | ||
|
|
6af8b58f14 | ||
|
|
6287db7483 | ||
|
|
46a8fa470c | ||
|
|
8489b7e2be | ||
|
|
7a196ac64a | ||
|
|
5946ee495a | ||
|
|
93ac03acd4 |
@@ -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',
|
||||
@@ -151,6 +152,7 @@ const upload = () => src(uploadSources, { base: './dist', encoding: false })
|
||||
const imageSources = [
|
||||
'server/fonts/**',
|
||||
'server/images/**',
|
||||
'!server/images/gimp/**',
|
||||
];
|
||||
const uploadImages = () => src(imageSources, { base: './server', encoding: false })
|
||||
.pipe(
|
||||
|
||||
541
package-lock.json
generated
541
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.18.0",
|
||||
"version": "5.19.1",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -42,12 +42,12 @@
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0"
|
||||
"webpack-stream": "^7.0.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
"ejs": "^3.1.5",
|
||||
"express": "^5.1.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0"
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
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.8 KiB |
BIN
server/images/gimp/Background 6.xcf
Normal file
BIN
server/images/gimp/Background 6.xcf
Normal file
Binary file not shown.
@@ -90,7 +90,9 @@ class AutoComplete {
|
||||
this.elem.addEventListener('keyup', (e) => this.keyUp(e));
|
||||
this.elem.closest('form')?.addEventListener('submit', (e) => this.directFormSubmit(e));
|
||||
this.elem.addEventListener('click', () => this.deselectAll());
|
||||
this.elem.addEventListener('focusout', () => this.hideSuggestions());
|
||||
|
||||
// clicking outside the suggestion box requires a bit of work to determine if suggestions should be hidden
|
||||
document.addEventListener('click', (e) => this.checkOutsideClick(e));
|
||||
}
|
||||
|
||||
mouseOver(e) {
|
||||
@@ -138,6 +140,9 @@ class AutoComplete {
|
||||
|
||||
// up/down direction
|
||||
switch (e.which) {
|
||||
case KEYS.ESC:
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
case KEYS.UP:
|
||||
case KEYS.DOWN:
|
||||
// move up or down the selection list
|
||||
@@ -302,6 +307,13 @@ class AutoComplete {
|
||||
[...this.results.querySelectorAll('.suggestion.selected')].forEach((elem) => elem.classList.remove('selected'));
|
||||
this.selectedItem = 0;
|
||||
}
|
||||
|
||||
// if a click is detected on the page, generally we hide the suggestions, unless the click was within the autocomplete elements
|
||||
checkOutsideClick(e) {
|
||||
if (e.target.id === 'txtAddress') return;
|
||||
if (e.target?.parentNode?.classList.contains(this.options.containerClass)) return;
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
export default AutoComplete;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
125
server/scripts/modules/spc-outlook.mjs
Normal file
125
server/scripts/modules/spc-outlook.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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);
|
||||
// don't display on progress/navigation screen
|
||||
this.showOnProgress = false;
|
||||
|
||||
// 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);
|
||||
|
||||
// if all the data returns false the there's nothing to do, skip this screen
|
||||
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {
|
||||
this.timing.totalScreens = 1;
|
||||
} else {
|
||||
this.timing.totalScreens = 0;
|
||||
}
|
||||
this.calcNavTiming();
|
||||
|
||||
// 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
86
server/styles/scss/_spc-outlook.scss
Normal file
86
server/styles/scss/_spc-outlook.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
@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;
|
||||
|
||||
&:nth-child(1) {
|
||||
left: calc(20px * 5);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
left: calc(20px * 4);
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
left: calc(20px * 3);
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
left: calc(20px * 2);
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
left: calc(20px * 1);
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
left: calc(20px * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@
|
||||
@use 'regional-forecast';
|
||||
@use 'almanac';
|
||||
@use 'hazards';
|
||||
@use 'media';
|
||||
@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