mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 07:39:29 -07:00
318 lines
9.1 KiB
JavaScript
318 lines
9.1 KiB
JavaScript
import { locationCleanup } from './utils/string.mjs';
|
|
import getCurrentWeather from './currentweather.mjs';
|
|
import { currentDisplay } from './navigation.mjs';
|
|
import getHazards from './hazards.mjs';
|
|
import settings from './settings.mjs';
|
|
|
|
// constants
|
|
const degree = String.fromCharCode(176);
|
|
const SCROLL_SPEED = 100; // pixels/second
|
|
const TICK_INTERVAL_MS = 500; // milliseconds per tick
|
|
const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_MS);
|
|
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
|
|
|
|
// items on page
|
|
let mainScroll;
|
|
let fixedScroll;
|
|
let header;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
mainScroll = document.querySelector('#container>.scroll');
|
|
fixedScroll = document.querySelector('#container>.scroll .fixed');
|
|
header = document.querySelector('#container>.scroll .scroll-header');
|
|
});
|
|
|
|
// local variables
|
|
let interval;
|
|
let screenIndex = 0;
|
|
let sinceLastUpdate = 0;
|
|
let nextUpdate = DEFAULT_UPDATE;
|
|
let resetFlag;
|
|
let defaultScreensLoaded = true;
|
|
|
|
// start drawing conditions
|
|
// reset starts from the first item in the text scroll list
|
|
const start = () => {
|
|
// show the block
|
|
show();
|
|
// if already started, draw the screen on a reset flag and return
|
|
if (interval) {
|
|
if (resetFlag) drawScreen();
|
|
resetFlag = false;
|
|
return;
|
|
}
|
|
resetFlag = false;
|
|
// set up the interval if needed
|
|
if (!interval) {
|
|
interval = setInterval(incrementInterval, TICK_INTERVAL_MS);
|
|
}
|
|
|
|
// draw the data
|
|
drawScreen();
|
|
};
|
|
|
|
const stop = (reset) => {
|
|
if (reset) {
|
|
screenIndex = 0;
|
|
resetFlag = true;
|
|
}
|
|
};
|
|
|
|
// increment interval, roll over
|
|
// 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');
|
|
hide();
|
|
return;
|
|
}
|
|
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
|
|
|
// draw new text
|
|
drawScreen();
|
|
};
|
|
|
|
const drawScreen = async () => {
|
|
// get the conditions
|
|
const { data, parameters } = await getCurrentWeather();
|
|
|
|
// create a data object (empty if no valid current weather conditions)
|
|
const scrollData = data || {};
|
|
|
|
// add the hazards if on screen 0
|
|
if (screenIndex === 0) {
|
|
const hazards = await getHazards();
|
|
if (hazards && hazards.length > 0) {
|
|
scrollData.hazards = hazards;
|
|
}
|
|
}
|
|
|
|
// if we have no current weather and no hazards, there's nothing to display
|
|
if (!data && (!scrollData.hazards || scrollData.hazards.length === 0)) return;
|
|
|
|
const thisScreen = workingScreens[screenIndex](scrollData, parameters);
|
|
|
|
// update classes on the scroll area
|
|
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
|
|
thisScreen?.classes?.forEach((cls) => mainScroll.classList.add(cls));
|
|
|
|
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);
|
|
}
|
|
// add the header if available
|
|
if (thisScreen.header) {
|
|
setHeader(thisScreen.header);
|
|
} else {
|
|
setHeader('');
|
|
}
|
|
} else {
|
|
// can't identify screen, get another one
|
|
incrementInterval(true);
|
|
}
|
|
};
|
|
|
|
const hazards = (data) => {
|
|
// test for data
|
|
if (!data.hazards || data.hazards.length === 0) return false;
|
|
|
|
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
|
|
|
|
return {
|
|
text: hazard,
|
|
type: 'scroll',
|
|
classes: ['hazard'],
|
|
header: data.hazards[0].properties.event,
|
|
};
|
|
};
|
|
|
|
// additional screens are stored in a separate for simple clearing/resettings
|
|
let additionalScreens = [];
|
|
// the "screens" are stored in an array for easy addition and removal
|
|
const baseScreens = [
|
|
// hazards
|
|
hazards,
|
|
// station name
|
|
(data) => {
|
|
const location = (StationInfo[data.station.properties.stationIdentifier]?.city ?? locationCleanup(data.station.properties.name)).substr(0, 20);
|
|
return `Conditions at ${location}`;
|
|
},
|
|
|
|
// temperature
|
|
(data) => {
|
|
let text = `Temp: ${data.Temperature}${degree}${data.TemperatureUnit}`;
|
|
if (data.observations.heatIndex.value) {
|
|
text += ` Heat Index: ${data.HeatIndex}${degree}${data.TemperatureUnit}`;
|
|
} else if (data.observations.windChill.value) {
|
|
text += ` Wind Chill: ${data.WindChill}${degree}${data.TemperatureUnit}`;
|
|
}
|
|
return text;
|
|
},
|
|
|
|
// humidity
|
|
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
|
|
|
// barometric pressure
|
|
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
|
|
|
// wind
|
|
(data) => {
|
|
let text = data.WindSpeed > 0
|
|
? `Wind: ${data.WindDirection} ${data.WindSpeed} ${data.WindUnit}`
|
|
: 'Wind: Calm';
|
|
|
|
if (data.WindGust > 0) {
|
|
text += ` Gusts to ${data.WindGust}`;
|
|
}
|
|
return text;
|
|
},
|
|
|
|
// visibility
|
|
(data) => {
|
|
const distance = `${data.Ceiling} ${data.CeilingUnit}`;
|
|
return `Visib: ${data.Visibility} ${data.VisibilityUnit} Ceiling: ${data.Ceiling === 0 ? 'Unlimited' : distance}`;
|
|
},
|
|
];
|
|
|
|
// working screens are the combination of base screens (when active) and additional screens
|
|
let workingScreens = [...baseScreens, ...additionalScreens];
|
|
|
|
// internal draw function with preset parameters
|
|
const drawCondition = (text) => {
|
|
fixedScroll.innerHTML = text;
|
|
setHeader('');
|
|
};
|
|
|
|
const setHeader = (text) => {
|
|
header.innerHTML = text ?? '';
|
|
};
|
|
|
|
// reset the screens back to the original set
|
|
const reset = () => {
|
|
workingScreens = [...baseScreens];
|
|
additionalScreens = [];
|
|
defaultScreensLoaded = true;
|
|
};
|
|
|
|
// add screen, keepBase keeps the regular weather crawl
|
|
const addScreen = (screen, keepBase = true) => {
|
|
defaultScreensLoaded = false;
|
|
additionalScreens.push(screen);
|
|
workingScreens = [...(keepBase ? baseScreens : []), ...additionalScreens];
|
|
};
|
|
|
|
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
|
|
fixedScroll.innerHTML = scrollElement.outerHTML;
|
|
// grab the width
|
|
const { scrollWidth, clientWidth } = document.querySelector('#container>.scroll .fixed .scroll-area');
|
|
|
|
// calculate the scroll distance and set a minimum scroll
|
|
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
|
|
// calculate the scroll time (scaled by global speed setting), minimum 2s (4s when added to start and end delays)
|
|
const scrollTime = Math.max(scrollDistance / SCROLL_SPEED * settings.speed.value, 2);
|
|
// add 1 second pause at the end of the scroll animation
|
|
const endPauseTime = 1.0;
|
|
const totalAnimationTime = scrollTime + endPauseTime;
|
|
// calculate total on-screen time: animation time + start delay + end pause
|
|
const startDelayTime = 1.0; // setTimeout delay below
|
|
const totalDisplayTime = totalAnimationTime + startDelayTime;
|
|
nextUpdate = secondsToTicks(totalDisplayTime);
|
|
|
|
// update the element with initial position and transition
|
|
scrollElement.style.transform = 'translateX(0px)';
|
|
scrollElement.style.transition = `transform ${scrollTime.toFixed(1)}s linear`;
|
|
scrollElement.style.willChange = 'transform'; // Hint to browser for hardware acceleration
|
|
scrollElement.style.backfaceVisibility = 'hidden'; // Force hardware acceleration
|
|
scrollElement.style.perspective = '1000px'; // Enable 3D rendering context
|
|
|
|
fixedScroll.innerHTML = '';
|
|
fixedScroll.append(scrollElement.cloneNode(true));
|
|
|
|
// start the scroll after the specified delay
|
|
setTimeout(() => {
|
|
// change the transform to trigger the scroll
|
|
document.querySelector('#container>.scroll .fixed .scroll-area').style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
|
|
}, startDelayTime * 1000);
|
|
};
|
|
|
|
const parseMessage = (event) => {
|
|
if (event?.data?.type === 'current-weather-scroll') {
|
|
if (event.data?.method === 'start') start();
|
|
if (event.data?.method === 'reload') stop(true);
|
|
if (event.data?.method === 'non-display') nonDisplay();
|
|
if (event.data?.method === 'show') show();
|
|
if (event.data?.method === 'hide') hide();
|
|
}
|
|
};
|
|
|
|
const show = () => {
|
|
mainScroll.style.display = 'block';
|
|
};
|
|
|
|
const hide = () => {
|
|
mainScroll.style.display = 'none';
|
|
};
|
|
|
|
const nonDisplay = () => {
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
interval = null;
|
|
stop();
|
|
// if greater than default update (typically long scroll) skip to the next weather screen
|
|
if (nextUpdate > DEFAULT_UPDATE) {
|
|
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
|
sinceLastUpdate = 0;
|
|
nextUpdate = DEFAULT_UPDATE;
|
|
}
|
|
}
|
|
};
|
|
|
|
const screenCount = () => workingScreens.length;
|
|
const atDefault = () => defaultScreensLoaded;
|
|
|
|
// add event listener for start message
|
|
window.addEventListener('message', parseMessage);
|
|
|
|
window.CurrentWeatherScroll = {
|
|
addScreen,
|
|
reset,
|
|
start,
|
|
show,
|
|
hide,
|
|
screenCount,
|
|
atDefault,
|
|
};
|
|
|
|
export {
|
|
addScreen,
|
|
reset,
|
|
start,
|
|
show,
|
|
hide,
|
|
screenCount,
|
|
atDefault,
|
|
hazards,
|
|
};
|