mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f6efe801c | ||
|
|
bc77a1891c | ||
|
|
4666878250 | ||
|
|
5813dd9a92 | ||
|
|
8cb8873760 | ||
|
|
323c175936 | ||
|
|
85e2553cb2 | ||
|
|
101d0ac9ea | ||
|
|
834d68f9e3 | ||
|
|
0ee7fdc9f8 | ||
|
|
d75121e894 | ||
|
|
4cdced3659 | ||
|
|
1a5548d135 | ||
|
|
11c826a2af | ||
|
|
7a129c1cd3 | ||
|
|
867657a965 | ||
|
|
e89dc52541 | ||
|
|
317883fc04 | ||
|
|
a4a601a387 | ||
|
|
375812c024 | ||
|
|
6af8b58f14 | ||
|
|
6287db7483 | ||
|
|
7a196ac64a | ||
|
|
5946ee495a |
@@ -109,7 +109,7 @@ The resulting files will be in the /dist folder in the root of the project. Thes
|
||||
## Music
|
||||
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
||||
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks will be posted in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
|
||||
### Customizing the music
|
||||
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
||||
@@ -141,6 +141,10 @@ Please do not report issues with api.weather.gov being down. It's a new service
|
||||
|
||||
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||
|
||||
## Related Projects
|
||||
|
||||
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||
|
||||
@@ -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',
|
||||
@@ -158,6 +159,9 @@ const uploadImages = () => src(imageSources, { base: './server', encoding: false
|
||||
s3({
|
||||
Bucket: process.env.BUCKET,
|
||||
StorageClass: 'STANDARD',
|
||||
maps: {
|
||||
CacheControl: () => 'max-age=31536000',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -188,4 +192,5 @@ export default publishFrontend;
|
||||
|
||||
export {
|
||||
buildDist,
|
||||
invalidate,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import updateVendor from './gulp/update-vendor.mjs';
|
||||
import publishFrontend, { buildDist } from './gulp/publish-frontend.mjs'
|
||||
import publishFrontend, { buildDist, invalidate } from './gulp/publish-frontend.mjs';
|
||||
|
||||
export {
|
||||
updateVendor,
|
||||
publishFrontend,
|
||||
buildDist,
|
||||
invalidate,
|
||||
};
|
||||
|
||||
18
index.mjs
18
index.mjs
@@ -60,16 +60,34 @@ const index = (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
const geoip = (req, res) => {
|
||||
res.set({
|
||||
'x-geoip-city': 'Orlando',
|
||||
'x-geoip-country': 'US',
|
||||
'x-geoip-country-name': 'United States',
|
||||
'x-geoip-country-region': 'FL',
|
||||
'x-geoip-country-region-name': 'Florida',
|
||||
'x-geoip-latitude': '28.52135',
|
||||
'x-geoip-longitude': '-81.41079',
|
||||
'x-geoip-postal-code': '32789',
|
||||
'x-geoip-time-zone': 'America/New_York',
|
||||
'content-type': 'application/json',
|
||||
});
|
||||
res.json({});
|
||||
};
|
||||
|
||||
// debugging
|
||||
if (process.env?.DIST === '1') {
|
||||
// distribution
|
||||
app.use('/images', express.static('./server/images'));
|
||||
app.use('/fonts', express.static('./server/fonts'));
|
||||
app.use('/scripts', express.static('./server/scripts'));
|
||||
app.use('/geoip', geoip);
|
||||
app.use('/', express.static('./dist'));
|
||||
} else {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.use('/geoip', geoip);
|
||||
app.get('/', index);
|
||||
app.get('*name', express.static('./server'));
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.18.1",
|
||||
"version": "5.20.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ws4kp",
|
||||
"version": "5.18.1",
|
||||
"version": "5.20.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.18.1",
|
||||
"version": "5.20.3",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
--Star 3000--
|
||||
|
||||
Star3000.ttf - Standard text style for most screens (and Travel Cities title header)
|
||||
Star3000 Small.ttf - Time/Date and some page headers
|
||||
Star3000 Large.ttf - Travel Cities Forecast (Forecast portion only)
|
||||
Star3000 Extra Large.ttf - Only used on some advertiser text
|
||||
Star3000 Extended.ttf - Only used on some advertiser text
|
||||
"Heavy" style is an emboldened version of the standard font (used on some STARs)
|
||||
|
||||
Star3000 Outline.ttf - A contrast border (stroke) that surrounds the Star3000.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
|
||||
Star3000 Small Outline.ttf - A contrast border (stroke) that surrounds the Star3000 Small.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
|
||||
Star3000 Large Outline.ttf - A contrast border (stroke) that surrounds the Star3000 Large.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
|
||||
|
||||
***Outlines for other font styles are not currently available.
|
||||
|
||||
--Star 4000--
|
||||
|
||||
Star4000.ttf - Standard text style for zone forecast, observation tables, regional map cities, almanac, extended forecast day/weather/temperature headers, Current Conditions right half data and most page header titles (also Travel Cities title header before Nov. 1992)
|
||||
Star4000 Small.ttf - Time/Date, NWS Local Update page header, temperature header for Travel Cities Forecast (after Nov. 1992)
|
||||
Star4000 Large.ttf - City names and temperature data on Travel Cities Forecast (after Nov. 1992), Extended forecast temperature values (after Feb. 1991), Current Conditions temperature value (after Mar. 1991)
|
||||
Star4000 Large Compressed - Travel Cities Forecast (before Nov. 1992), regional map temperatures
|
||||
Star4000 Large Compressed Numbers - Temperature values on regional forecast/observation maps
|
||||
Star4000 Extended - A proportional width font used for the Current Conditions present weather description and wind data
|
||||
Star 4 Radar.ttf - Radar airport I.D.
|
||||
|
||||
--Star Jr.--
|
||||
|
||||
StarJr.ttf - Standard text style for most screens (and Travel Cities title header)
|
||||
StarJr Small.ttf - Time/Date and some page headers
|
||||
StarJr Compressed.ttf - Travel Cities Forecast
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
|
Before Width: | Height: | Size: 234 KiB |
BIN
server/images/maps/basemap.webp
Normal file
BIN
server/images/maps/basemap.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
BIN
server/images/maps/radar.webp
Normal file
BIN
server/images/maps/radar.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -78,6 +78,7 @@ const init = () => {
|
||||
onSelect(suggestion) { autocompleteOnSelect(suggestion); },
|
||||
width: 490,
|
||||
});
|
||||
window.autoComplete = autoComplete;
|
||||
|
||||
// attempt to parse the url parameters
|
||||
const parsedParameters = parseQueryString();
|
||||
@@ -109,9 +110,6 @@ const init = () => {
|
||||
document.querySelector('#spanRadarId').innerHTML = '';
|
||||
document.querySelector('#spanZoneId').innerHTML = '';
|
||||
|
||||
document.querySelector('#chkAutoRefresh').checked = true;
|
||||
localStorage.removeItem('autoRefresh');
|
||||
|
||||
localStorage.removeItem('play');
|
||||
postMessage('navButton', 'play');
|
||||
|
||||
@@ -374,6 +372,10 @@ const btnGetGpsClick = async () => {
|
||||
const position = await getPosition();
|
||||
const { latitude, longitude } = position.coords;
|
||||
|
||||
getForecastFromLatLon(latitude, longitude, true);
|
||||
};
|
||||
|
||||
const getForecastFromLatLon = (latitude, longitude, fromGps = false) => {
|
||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||
txtAddress.value = `${round2(latitude, 4)}, ${round2(longitude, 4)}`;
|
||||
|
||||
@@ -383,7 +385,7 @@ const btnGetGpsClick = async () => {
|
||||
const query = `${location.city}, ${location.state}`;
|
||||
localStorage.setItem('latLon', JSON.stringify({ lat: latitude, lon: longitude }));
|
||||
localStorage.setItem('latLonQuery', query);
|
||||
localStorage.setItem('latLonFromGPS', true);
|
||||
localStorage.setItem('latLonFromGPS', fromGps);
|
||||
txtAddress.value = `${location.city}, ${location.state}`;
|
||||
});
|
||||
};
|
||||
@@ -414,3 +416,6 @@ const getCustomCode = async () => {
|
||||
document.body.append(customElem);
|
||||
}
|
||||
};
|
||||
|
||||
// expose functions for external use
|
||||
window.getForecastFromLatLon = getForecastFromLatLon;
|
||||
|
||||
@@ -172,6 +172,11 @@ class AutoComplete {
|
||||
}
|
||||
}
|
||||
|
||||
setValue(newValue) {
|
||||
this.currentValue = newValue;
|
||||
this.elem.value = newValue;
|
||||
}
|
||||
|
||||
onValueChange() {
|
||||
clearTimeout(this.onValueChange);
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ import { currentDisplay } from './navigation.mjs';
|
||||
|
||||
// constants
|
||||
const degree = String.fromCharCode(176);
|
||||
const SCROLL_SPEED = 75; // pixels/second
|
||||
const DEFAULT_UPDATE = 8; // 0.5s ticks
|
||||
|
||||
// local variables
|
||||
let interval;
|
||||
let screenIndex = 0;
|
||||
let sinceLastUpdate = 0;
|
||||
let nextUpdate = DEFAULT_UPDATE;
|
||||
|
||||
// start drawing conditions
|
||||
// reset starts from the first item in the text scroll list
|
||||
@@ -17,7 +21,7 @@ const start = () => {
|
||||
|
||||
// set up the interval if needed
|
||||
if (!interval) {
|
||||
interval = setInterval(incrementInterval, 4000);
|
||||
interval = setInterval(incrementInterval, 500);
|
||||
}
|
||||
|
||||
// draw the data
|
||||
@@ -29,14 +33,24 @@ const stop = (reset) => {
|
||||
};
|
||||
|
||||
// increment interval, roll over
|
||||
const incrementInterval = () => {
|
||||
// forcing is used when drawScreen receives an invalid screen and needs to request the next one in line
|
||||
const incrementInterval = (force) => {
|
||||
if (!force) {
|
||||
// test for elapsed time (0.5s ticks);
|
||||
sinceLastUpdate += 1;
|
||||
if (sinceLastUpdate < nextUpdate) return;
|
||||
}
|
||||
// reset flags
|
||||
sinceLastUpdate = 0;
|
||||
nextUpdate = DEFAULT_UPDATE;
|
||||
|
||||
// test current screen
|
||||
const display = currentDisplay();
|
||||
if (!display?.okToDrawCurrentConditions) {
|
||||
stop(display?.elemId === 'progress');
|
||||
return;
|
||||
}
|
||||
screenIndex = (screenIndex + 1) % (screens.length);
|
||||
screenIndex = (screenIndex + 1) % (lastScreen);
|
||||
// draw new text
|
||||
drawScreen();
|
||||
};
|
||||
@@ -48,7 +62,22 @@ const drawScreen = async () => {
|
||||
// nothing to do if there's no data yet
|
||||
if (!data) return;
|
||||
|
||||
drawCondition(screens[screenIndex](data));
|
||||
const thisScreen = screens[screenIndex](data);
|
||||
if (typeof thisScreen === 'string') {
|
||||
// only a string
|
||||
drawCondition(thisScreen);
|
||||
} else if (typeof thisScreen === 'object') {
|
||||
// an object was provided with additional parameters
|
||||
switch (thisScreen.type) {
|
||||
case 'scroll':
|
||||
drawScrollCondition(thisScreen);
|
||||
break;
|
||||
default: drawCondition(thisScreen);
|
||||
}
|
||||
} else {
|
||||
// can't identify screen, get another one
|
||||
incrementInterval(true);
|
||||
}
|
||||
};
|
||||
|
||||
// the "screens" are stored in an array for easy addition and removal
|
||||
@@ -71,7 +100,7 @@ const screens = [
|
||||
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
||||
|
||||
// barometric pressure
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
@@ -102,3 +131,56 @@ const drawCondition = (text) => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
start();
|
||||
});
|
||||
|
||||
// store the original number of screens
|
||||
const originalScreens = screens.length;
|
||||
let lastScreen = originalScreens;
|
||||
|
||||
// reset the number of screens
|
||||
const reset = () => {
|
||||
lastScreen = originalScreens;
|
||||
};
|
||||
|
||||
// add screen
|
||||
const addScreen = (screen) => {
|
||||
screens.push(screen);
|
||||
lastScreen += 1;
|
||||
};
|
||||
|
||||
const drawScrollCondition = (screen) => {
|
||||
// create the scroll element
|
||||
const scrollElement = document.createElement('div');
|
||||
scrollElement.classList.add('scroll-area');
|
||||
scrollElement.innerHTML = screen.text;
|
||||
// add it to the page to get the width
|
||||
document.querySelector('.weather-display .scroll .fixed').innerHTML = scrollElement.outerHTML;
|
||||
// grab the width
|
||||
const { scrollWidth, clientWidth } = document.querySelector('.weather-display .scroll .fixed .scroll-area');
|
||||
|
||||
// calculate the scroll distance and set a minimum scroll
|
||||
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
|
||||
// calculate the scroll time
|
||||
const scrollTime = scrollDistance / SCROLL_SPEED;
|
||||
// calculate a new minimum on-screen time +1.0s at start and end
|
||||
nextUpdate = Math.round(Math.ceil(scrollTime / 0.5) + 4);
|
||||
|
||||
// update the element transition and set initial left position
|
||||
scrollElement.style.left = '0px';
|
||||
scrollElement.style.transition = `left linear ${scrollTime.toFixed(1)}s`;
|
||||
elemForEach('.weather-display .scroll .fixed', (elem) => {
|
||||
elem.innerHTML = '';
|
||||
elem.append(scrollElement.cloneNode(true));
|
||||
});
|
||||
// start the scroll after a short delay
|
||||
setTimeout(() => {
|
||||
// change the left position to trigger the scroll
|
||||
elemForEach('.weather-display .scroll .fixed .scroll-area', (elem) => {
|
||||
elem.style.left = `-${scrollDistance.toFixed(0)}px`;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
window.CurrentWeatherScroll = {
|
||||
addScreen,
|
||||
reset,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ class Hazards extends WeatherDisplay {
|
||||
// get the forecast
|
||||
const url = new URL('https://api.weather.gov/alerts/active');
|
||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||
url.searchParams.append('limit', 5);
|
||||
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||
const unsortedAlerts = alerts.features ?? [];
|
||||
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class LocalForecast extends WeatherDisplay {
|
||||
forecastsElem.append(...templates);
|
||||
|
||||
// increase each forecast height to a multiple of container height
|
||||
this.pageHeight = forecastsElem.parentNode.scrollHeight;
|
||||
this.pageHeight = forecastsElem.parentNode.offsetHeight;
|
||||
templates.forEach((forecast) => {
|
||||
const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight;
|
||||
forecast.style.height = `${newHeight}px`;
|
||||
|
||||
@@ -52,7 +52,7 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// get the base map
|
||||
const src = 'images/maps/radar.jpg';
|
||||
const src = 'images/maps/radar.webp';
|
||||
this.baseMap = await loadImg(src);
|
||||
|
||||
const baseUrl = 'https://mesonet.agron.iastate.edu/archive/data/';
|
||||
@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(10, 'radar'));
|
||||
registerDisplay(new Radar(11, 'radar'));
|
||||
|
||||
@@ -28,7 +28,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// there are enough other cities available to populate the map sufficiently even if some do not load
|
||||
|
||||
// pre-load the base map
|
||||
let baseMap = 'images/maps/basemap.png';
|
||||
let baseMap = 'images/maps/basemap.webp';
|
||||
if (weatherParameters.state === 'HI') {
|
||||
baseMap = 'images/maps/radar-hawaii.png';
|
||||
} else if (weatherParameters.state === 'AK') {
|
||||
|
||||
127
server/scripts/modules/spc-outlook.mjs
Normal file
127
server/scripts/modules/spc-outlook.mjs
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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;
|
||||
|
||||
// initial data does not need to be reloaded on a location change, only during silent refresh
|
||||
if (!this.initialData || refresh) {
|
||||
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
|
||||
this.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.initialData) {
|
||||
this.setStatus(STATUS.failed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// do the initial parsing of the data
|
||||
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.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
@@ -90,7 +90,9 @@
|
||||
|
||||
.location {
|
||||
color: c.$title-color;
|
||||
max-height: 32px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
@font-face {
|
||||
font-family: "Star4000";
|
||||
src: url('../fonts/Star4000.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -251,39 +252,22 @@ body {
|
||||
width: 475px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Star4000";
|
||||
src: url('../fonts/Star4000.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Star 4 Radar";
|
||||
src: url('../fonts/Star 4 Radar.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Star4000 Extended';
|
||||
src: url('../fonts/Star4000 Extended.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Star4000LCN';
|
||||
src: url('../fonts/Star4000LCN.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Star4000 Large Compressed';
|
||||
src: url('../fonts/Star4000 Large Compressed.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Star4000 Large';
|
||||
src: url('../fonts/Star4000 Large.ttf') format('truetype');
|
||||
src: url('../fonts/Star4000 Large.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Star4000 Small';
|
||||
src: url('../fonts/Star4000 Small.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
#display {
|
||||
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
.scroll {
|
||||
@include u.text-shadow(3px, 1.5px);
|
||||
width: 640px;
|
||||
width: calc(640px - 2 * 30px);
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
@@ -122,6 +122,15 @@
|
||||
font-family: 'Star4000';
|
||||
font-size: 24pt;
|
||||
margin-left: 55px;
|
||||
overflow: hidden;
|
||||
|
||||
.scroll-area {
|
||||
text-wrap: nowrap;
|
||||
position: relative;
|
||||
// the following added by js code as it is dependent on the content of the element
|
||||
// transition: left (x)s;
|
||||
// left: calc((elem width) - 640px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@
|
||||
@use 'regional-forecast';
|
||||
@use 'almanac';
|
||||
@use 'hazards';
|
||||
@use 'media';
|
||||
@use 'media';
|
||||
@use 'spc-outlook';
|
||||
@@ -18,6 +18,10 @@
|
||||
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="627">
|
||||
<link rel="preload" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
|
||||
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
|
||||
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" type="font/woff" crossorigin>
|
||||
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
|
||||
|
||||
<% if (production) { %>
|
||||
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
||||
@@ -33,6 +37,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 +114,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>
|
||||
@@ -171,6 +179,7 @@
|
||||
Station Id: <span id="spanStationId"></span><br />
|
||||
Radar Id: <span id="spanRadarId"></span><br />
|
||||
Zone Id: <span id="spanZoneId"></span><br />
|
||||
Ws4kp Version: <span><%- version %></span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="scroll-area">
|
||||
<div class="frame template">
|
||||
<div class="map">
|
||||
<img src="images/maps/radar.jpg" />
|
||||
<img src="images/maps/radar.webp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
||||
<div class="main has-scroll regional-forecast">
|
||||
<div class="map"><img src="images/maps/basemap.png" /></div>
|
||||
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
||||
<div class="location-container">
|
||||
<div class="location template">
|
||||
<div class="icon">
|
||||
|
||||
20
views/partials/spc-outlook.ejs
Normal file
20
views/partials/spc-outlook.ejs
Normal file
@@ -0,0 +1,20 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: 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