Compare commits

...

15 Commits

Author SHA1 Message Date
Matt Walsh
262ea15468 5.17.0 2025-05-13 21:13:37 -05:00
Matt Walsh
5d2e5a6d9c update versions 2025-05-13 14:59:54 -05:00
Matt Walsh
992258d3ce css/sass cleanup 2025-05-13 13:59:12 -05:00
Matt Walsh
b031934022 autocomplete working 2025-05-13 13:57:50 -05:00
Matt Walsh
4cc2312ffd Merge branch 'main' into remove-jquery 2025-05-12 13:35:01 -05:00
Matt Walsh
ce99fc16e7 5.16.6 2025-05-12 10:48:12 -05:00
Matt Walsh
97f96d4091 fix missing states in station list 2025-05-12 10:47:43 -05:00
Matt Walsh
13f08b62cc 5.16.5 2025-05-02 23:22:39 -05:00
Matt Walsh
eacd82b4f4 fix hourly graph not updating close #77 2025-05-02 23:22:24 -05:00
Matt Walsh
91f669e828 add streaming to readme 2025-05-02 10:52:32 -05:00
Matt Walsh
cdbe3d968f city spelling corrections 2025-04-29 16:38:54 -05:00
Matt Walsh
0a388cdb83 clean up settings constructor 2025-04-08 10:40:36 -05:00
Matt Walsh
8678d9f053 5.16.4 2025-04-07 22:13:33 -05:00
Matt Walsh
a592668d0d hourly color coded feels like temperatures close #69 2025-04-07 22:13:20 -05:00
Matt Walsh
c7eb56f60c non-jquery autocomplete, needs more keyboard integration 2024-10-21 23:03:34 -05:00
31 changed files with 23577 additions and 47094 deletions

View File

@@ -2,8 +2,7 @@
"env": {
"browser": true,
"es6": true,
"node": true,
"jquery": true
"node": true
},
"extends": [
"airbnb-base"
@@ -12,7 +11,8 @@
"TravelCities": "readonly",
"RegionalCities": "readonly",
"StationInfo": "readonly",
"SunCalc": "readonly"
"SunCalc": "readonly",
"NoSleep": "readonly"
},
"parserOptions": {
"ecmaVersion": 2023,

3
.vscode/launch.json vendored
View File

@@ -15,6 +15,9 @@
"**/*.min.js",
"**/vendor/**"
],
"runtimeArgs": [
"--autoplay-policy=no-user-gesture-required"
]
},
{
"name": "Data:stations",

View File

@@ -129,6 +129,7 @@ Thanks to the WeatherStar community for providing these discussions to further e
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
## Customization
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.

View File

@@ -1150,7 +1150,7 @@
}
},
{
"city": "Tucsan",
"city": "Tucson",
"lat": 32.2216,
"lon": -110.9698,
"point": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -575,7 +575,7 @@
"lon": -77.9447
},
{
"city": "Tucsan",
"city": "Tucson",
"lat": 32.2216,
"lon": -110.9698
}

View File

@@ -9,61 +9,59 @@ import chunk from './chunk.mjs';
// skip stations starting with these letters
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
// immediately invoked function so we can access async/await
const start = async () => {
// chunk the list of states
const chunkStates = chunk(states, 5);
// chunk the list of states
const chunkStates = chunk(states, 1);
// store output
const output = {};
// store output
const output = {};
// process all chunks
for (let i = 0; i < chunkStates.length; i += 1) {
const stateChunk = chunkStates[i];
// loop through states
// process all chunks
for (let i = 0; i < chunkStates.length; i += 1) {
const stateChunk = chunkStates[i];
// loop through states
stateChunk.forEach(async (state) => {
try {
let stations;
let next = `https://api.weather.gov/stations?state=${state}`;
do {
// get list and parse the JSON
// eslint-disable-next-line no-await-in-loop
const stationsRaw = await https(next);
stations = JSON.parse(stationsRaw);
// filter stations for 4 letter identifiers
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
// filter against starting letter
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// add each resulting station to the output
stationsFiltered.forEach((station) => {
const id = station.properties.stationIdentifier;
if (output[id]) {
console.log(`Duplicate station: ${state}-${id}`);
return;
}
output[id] = {
id,
city: station.properties.name,
state,
lat: station.geometry.coordinates[1],
lon: station.geometry.coordinates[0],
};
});
next = stations?.pagination?.next;
// write the output
writeFileSync('./datagenerators/output/stations.json', JSON.stringify(output, null, 2));
}
while (next && stations.features.length > 0);
console.log(`Complete: ${state}`);
return true;
} catch (e) {
console.error(`Unable to get state: ${state}`);
return false;
// eslint-disable-next-line no-await-in-loop
await Promise.allSettled(stateChunk.map(async (state) => {
try {
let stations;
let next = `https://api.weather.gov/stations?state=${state}`;
let round = 0;
do {
console.log(`Getting: ${state}-${round}`);
// get list and parse the JSON
// eslint-disable-next-line no-await-in-loop
const stationsRaw = await https(next);
stations = JSON.parse(stationsRaw);
// filter stations for 4 letter identifiers
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
// filter against starting letter
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// add each resulting station to the output
stationsFiltered.forEach((station) => {
const id = station.properties.stationIdentifier;
if (output[id]) {
console.log(`Duplicate station: ${state}-${id}`);
return;
}
output[id] = {
id,
city: station.properties.name,
state,
lat: station.geometry.coordinates[1],
lon: station.geometry.coordinates[0],
};
});
next = stations?.pagination?.next;
round += 1;
// write the output
writeFileSync('./datagenerators/output/stations.json', JSON.stringify(output, null, 2));
}
});
}
};
// immediately invoked function allows access to async
await start();
while (next && stations.features.length > 0);
console.log(`Complete: ${state}`);
return true;
} catch (e) {
console.error(`Unable to get state: ${state}`);
return false;
}
}));
}

View File

@@ -62,8 +62,6 @@ const compressJsData = () => src(jsSourcesData)
.pipe(dest(RESOURCES_PATH));
const jsVendorSources = [
'server/scripts/vendor/auto/jquery.js',
'server/scripts/vendor/jquery.autocomplete.min.js',
'server/scripts/vendor/auto/nosleep.js',
'server/scripts/vendor/auto/swiped-events.js',
'server/scripts/vendor/auto/suncalc.js',

View File

@@ -9,7 +9,6 @@ const vendorFiles = [
'./node_modules/luxon/build/es6/luxon.js',
'./node_modules/luxon/build/es6/luxon.js.map',
'./node_modules/nosleep.js/dist/NoSleep.js',
'./node_modules/jquery/dist/jquery.js',
'./node_modules/suncalc/suncalc.js',
'./node_modules/swiped-events/src/swiped-events.js',
];

737
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.16.3",
"version": "5.17.0",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",
@@ -37,8 +37,6 @@
"gulp-s3-upload": "^1.7.3",
"gulp-sass": "^6.0.0",
"gulp-terser": "^2.0.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"sass": "^1.54.0",

View File

@@ -1,6 +0,0 @@
module.exports = {
rules: {
// unicorn
'unicorn/numeric-separators-style': 0,
},
};

View File

@@ -1151,7 +1151,7 @@ const RegionalCities = [
}
},
{
"city": "Tucsan",
"city": "Tucson",
"lat": 32.2216,
"lon": -110.9698,
"point": {

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import {
import { round2 } from './modules/utils/units.mjs';
import { parseQueryString } from './modules/share.mjs';
import settings from './modules/settings.mjs';
import AutoComplete from './modules/autocomplete.mjs';
document.addEventListener('DOMContentLoaded', () => {
init();
@@ -49,13 +50,12 @@ const init = () => {
window.addEventListener('resize', fullScreenResizeCheck);
fullScreenResizeCheck.wasFull = false;
document.querySelector(TXT_ADDRESS_SELECTOR).addEventListener('keydown', (key) => { if (key.code === 'Enter') formSubmit(); });
document.querySelector('#btnGetLatLng').addEventListener('click', () => formSubmit());
document.querySelector('#btnGetLatLng').addEventListener('click', () => autoComplete.directFormSubmit());
document.addEventListener('keydown', documentKeydown);
document.addEventListener('touchmove', (e) => { if (document.fullscreenElement) e.preventDefault(); });
$(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete({
const autoComplete = new AutoComplete(document.querySelector(TXT_ADDRESS_SELECTOR), {
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
deferRequestBy: 300,
paramName: 'text',
@@ -75,16 +75,10 @@ const init = () => {
minChars: 3,
showNoSuggestionNotice: true,
noSuggestionNotice: 'No results found. Please try a different search string.',
onSelect(suggestion) { autocompleteOnSelect(suggestion, this); },
onSelect(suggestion) { autocompleteOnSelect(suggestion); },
width: 490,
});
const formSubmit = () => {
const ac = $(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete();
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).trigger('click');
return false;
};
// attempt to parse the url parameters
const parsedParameters = parseQueryString();
@@ -132,10 +126,7 @@ const init = () => {
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
};
const autocompleteOnSelect = async (suggestion, elem) => {
// Do not auto get the same city twice.
if (elem.previousSuggestionValue === suggestion.value) return;
const autocompleteOnSelect = async (suggestion) => {
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: suggestion.value,

View File

@@ -0,0 +1,307 @@
/* eslint-disable default-case */
import { json } from './utils/fetch.mjs';
const KEYS = {
ESC: 27,
TAB: 9,
RETURN: 13,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
};
const DEFAULT_OPTIONS = {
autoSelectFirst: false,
serviceUrl: null,
lookup: null,
onSelect: () => { },
onHint: null,
width: 'auto',
minChars: 3,
maxHeight: 300,
deferRequestBy: 0,
params: {},
delimiter: null,
zIndex: 9999,
type: 'GET',
noCache: false,
preserveInput: false,
containerClass: 'autocomplete-suggestions',
tabDisabled: false,
dataType: 'text',
currentRequest: null,
triggerSelectOnValidInput: true,
preventBadQueries: true,
paramName: 'query',
transformResult: (a) => a,
showNoSuggestionNotice: false,
noSuggestionNotice: 'No results',
orientation: 'bottom',
forceFixPosition: false,
};
const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
const formatResult = (suggestion, search) => {
// Do not replace anything if the current value is empty
if (!search) {
return suggestion;
}
const pattern = `(${escapeRegExChars(search)})`;
return suggestion
.replace(new RegExp(pattern, 'gi'), '<strong>$1</strong>')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&lt;(\/?strong)&gt;/g, '<$1>');
};
class AutoComplete {
constructor(elem, options) {
this.options = { ...DEFAULT_OPTIONS, ...options };
this.elem = elem;
this.selectedItem = -1;
this.onChangeTimeout = null;
this.currentValue = '';
this.suggestions = [];
this.cachedResponses = {};
// create and add the results container
const results = document.createElement('div');
results.style.display = 'none';
results.classList.add(this.options.containerClass);
results.style.width = (typeof this.options.width === 'string') ? this.options.width : `${this.options.width}px`;
results.style.zIndex = this.options.zIndex;
results.style.maxHeight = `${this.options.maxHeight}px`;
results.style.overflowX = 'hidden';
results.addEventListener('mouseover', (e) => this.mouseOver(e));
results.addEventListener('mouseout', (e) => this.mouseOut(e));
results.addEventListener('click', (e) => this.click(e));
this.results = results;
this.elem.after(results);
// add handlers for typing text and submitting the form
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());
}
mouseOver(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
e.target.classList.add('selected');
this.selectedItem = parseInt(e.target.dataset.item, 10);
}
}
mouseOut(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
e.target.classList.remove('selected');
this.selectedItem = -1;
}
}
click(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
// get the entire suggestion
const suggestion = this.suggestions[parseInt(e.target.dataset.item, 10)];
this.options.onSelect(suggestion);
this.elem.value = suggestion.value;
this.hideSuggestions();
}
}
hideSuggestions() {
this.results.style.display = 'none';
}
showSuggestions() {
this.results.style.removeProperty('display');
}
clearSuggestions() {
this.results.innerHTML = '';
}
keyUp(e) {
// reset the change timeout
clearTimeout(this.onChangeTimeout);
// up/down direction
switch (e.which) {
case KEYS.UP:
case KEYS.DOWN:
// move up or down the selection list
this.keySelect(e.which);
return;
case KEYS.ENTER:
// if the text entry field is active call direct form submit
// if there is a suggestion highlighted call the click function on that element
if (this.getSelected() !== undefined) {
this.click({ target: this.results.querySelector('.suggestion.selected') });
return;
}
if (document.activeElement.id === this.elem.id) {
// call the direct submit routine
this.directFormSubmit();
}
return;
}
if (this.currentValue !== this.elem.value) {
if (this.options.deferRequestBy > 0) {
// defer lookup during rapid key presses
this.onChangeTimeout = setTimeout(() => {
this.onValueChange();
}, this.options.deferRequestBy);
}
}
}
onValueChange() {
clearTimeout(this.onValueChange);
// confirm value actually changed
if (this.currentValue === this.elem.value) return;
// store new value
this.currentValue = this.elem.value;
// clear the selected index
this.selectedItem = -1;
this.results.querySelectorAll('div').forEach((elem) => elem.classList.remove('selected'));
// if less than minimum don't query api
if (this.currentValue.length < this.options.minChars) {
this.hideSuggestions();
return;
}
this.getSuggestions(this.currentValue);
}
async getSuggestions(search, skipHtml = false) {
// assemble options
const searchOptions = { ...this.options.params };
searchOptions[this.options.paramName] = search;
// build search url
const url = new URL(this.options.serviceUrl);
Object.entries(searchOptions).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
let result = this.cachedResponses[search];
if (!result) {
// make the request
const resultRaw = await json(url);
// use the provided parser
result = this.options.transformResult(resultRaw);
}
// store suggestions
this.cachedResponses[search] = result;
this.suggestions = result.suggestions;
if (skipHtml) return;
// populate the suggestion area
this.populateSuggestions();
}
populateSuggestions() {
if (this.suggestions.length === 0) {
if (this.options.showNoSuggestionNotice) {
this.noSuggestionNotice();
} else {
this.hideSuggestions();
}
return;
}
// build the list
const suggestionElems = this.suggestions.map((suggested, idx) => {
const elem = document.createElement('div');
elem.classList.add('suggestion');
elem.dataset.item = idx;
elem.innerHTML = (formatResult(suggested.value, this.currentValue));
return elem.outerHTML;
});
this.results.innerHTML = suggestionElems.join('');
this.showSuggestions();
}
noSuggestionNotice() {
this.results.innerHTML = `<div>${this.options.noSuggestionNotice}</div>`;
this.showSuggestions();
}
// the submit button has been pressed and we'll just use the first suggestion found
async directFormSubmit() {
// check for minimum length
if (this.currentValue.length < this.options.minChars) return;
await this.getSuggestions(this.elem.value, true);
const suggestion = this.suggestions?.[0];
if (suggestion) {
this.options.onSelect(suggestion);
this.elem.value = suggestion.value;
this.hideSuggestions();
}
}
// return the index of the selected item in suggestions
getSelected() {
const index = this.results.querySelector('.selected')?.dataset?.item;
if (index !== undefined) return parseInt(index, 10);
return index;
}
// move the selection highlight up or down
keySelect(key) {
// if the suggestions are hidden do nothing
if (this.results.style.display === 'none') return;
// if there are no suggestions do nothing
if (this.suggestions.length <= 0) return;
// get the currently selected index (or default to off the top of the list)
let index = this.getSelected();
// adjust the index per the key
// and include defaults in case no index is selected
switch (key) {
case KEYS.UP:
index = (index ?? 0) - 1;
break;
case KEYS.DOWN:
index = (index ?? -1) + 1;
break;
}
// wrap the index (and account for negative)
index = ((index % this.suggestions.length) + this.suggestions.length) % this.suggestions.length;
// set this index
this.deselectAll();
this.mouseOver({
target: this.results.querySelectorAll('.suggestion')[index],
});
}
deselectAll() {
// clear other selected indexes
[...this.results.querySelectorAll('.suggestion.selected')].forEach((elem) => elem.classList.remove('selected'));
this.selectedItem = 0;
}
}
export default AutoComplete;

View File

@@ -130,6 +130,8 @@ class CurrentWeather extends WeatherDisplay {
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentWeather(stillWaiting) {
// an external caller has requested data, set up auto reload
this.setAutoReload();
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.data);

View File

@@ -76,9 +76,9 @@ class Hourly extends WeatherDisplay {
const temperature = data.temperature.toString().padStart(3);
const feelsLike = data.apparentTemperature.toString().padStart(3);
fillValues.temp = temperature;
// only plot apparent temperature if there is a difference
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
if (temperature !== feelsLike) fillValues.like = feelsLike;
// apparent temperature is color coded if different from actual temperature (after fill is applied)
fillValues.like = feelsLike;
// wind
let wind = 'Calm';
@@ -91,7 +91,17 @@ class Hourly extends WeatherDisplay {
// image
fillValues.icon = { type: 'img', src: data.icon };
return this.fillTemplate('hourly-row', fillValues);
const filledRow = this.fillTemplate('hourly-row', fillValues);
// alter the color of the feels like column to reflect wind chill or heat index
if (feelsLike < temperature) {
filledRow.querySelector('.like').classList.add('wind-chill');
}
if (feelsLike > temperature) {
filledRow.querySelector('.like').classList.add('heat-index');
}
return filledRow;
});
list.append(...lines);
@@ -129,6 +139,8 @@ class Hourly extends WeatherDisplay {
// promise allows for data to be requested before it is available
async getCurrentData(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
// an external caller has requested data, set up auto reload
this.setAutoReload();
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue

View File

@@ -5,7 +5,12 @@ let playlist;
let currentTrack = 0;
let player;
const mediaPlaying = new Setting('mediaPlaying', 'Media Playing', 'boolean', false, null, true);
const mediaPlaying = new Setting('mediaPlaying', {
name: 'Media Playing',
type: 'boolean',
defaultValue: false,
sticky: true,
});
document.addEventListener('DOMContentLoaded', () => {
// add the event handler to the page

View File

@@ -8,27 +8,54 @@ document.addEventListener('DOMContentLoaded', () => {
const settings = { speed: { value: 1.0 } };
const init = () => {
// create settings
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
settings.kiosk = new Setting('kiosk', 'Kiosk', 'checkbox', false, kioskChange, false);
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
[0.5, 'Very Fast'],
[0.75, 'Fast'],
[1.0, 'Normal'],
[1.25, 'Slow'],
[1.5, 'Very Slow'],
]);
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
['us', 'US'],
['si', 'Metric'],
]);
settings.refreshTime = new Setting('refreshTime', 'Refresh Time', 'select', 600_000, null, false, [
[30_000, 'TESTING'],
[300_000, '5 minutes'],
[600_000, '10 minutes'],
[900_000, '15 minutes'],
[1_800_000, '30 minutes'],
]);
// create settings see setting.mjs for defaults
settings.wide = new Setting('wide', {
name: 'Widescreen',
defaultValue: false,
changeAction: wideScreenChange,
sticky: true,
});
settings.kiosk = new Setting('kiosk', {
name: 'Kiosk',
defaultValue: false,
changeAction: kioskChange,
sticky: false,
});
settings.speed = new Setting('speed', {
name: 'Speed',
type: 'select',
defaultValue: 1.0,
values: [
[0.5, 'Very Fast'],
[0.75, 'Fast'],
[1.0, 'Normal'],
[1.25, 'Slow'],
[1.5, 'Very Slow'],
],
});
settings.units = new Setting('units', {
name: 'Units',
type: 'select',
defaultValue: 'us',
changeAction: unitChange,
values: [
['us', 'US'],
['si', 'Metric'],
],
});
settings.refreshTime = new Setting('refreshTime', {
type: 'select',
defaultValue: 600_000,
sticky: false,
values: [
[30_000, 'TESTING'],
[300_000, '5 minutes'],
[600_000, '10 minutes'],
[900_000, '15 minutes'],
[1_800_000, '30 minutes'],
],
visible: false,
});
// generate html objects
const settingHtml = Object.values(settings).map((d) => d.generate());

View File

@@ -2,41 +2,58 @@ import { parseQueryString } from '../share.mjs';
const SETTINGS_KEY = 'Settings';
const DEFAULTS = {
shortName: undefined,
name: undefined,
type: 'checkbox',
defaultValue: undefined,
changeAction: () => { },
sticky: true,
values: [],
visible: true,
};
class Setting {
constructor(shortName, name, type, defaultValue, changeAction, sticky, values) {
// store values
constructor(shortName, _options) {
if (shortName === undefined) {
throw new Error('No name provided for setting');
}
// merge options with defaults
const options = { ...DEFAULTS, ...(_options ?? {}) };
// store values and combine with defaults
this.shortName = shortName;
this.name = name;
this.defaultValue = defaultValue;
this.myValue = defaultValue;
this.type = type ?? 'checkbox';
this.sticky = sticky;
this.values = values;
// a default blank change function is provided
this.changeAction = changeAction ?? (() => { });
this.name = options.name ?? shortName;
this.defaultValue = options.defaultValue;
this.myValue = this.defaultValue;
this.type = options?.type;
this.sticky = options.sticky;
this.values = options.values;
this.visible = options.visible;
this.changeAction = options.changeAction;
// get value from url
const urlValue = parseQueryString()?.[`settings-${shortName}-${type}`];
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
let urlState;
if (type === 'checkbox' && urlValue !== undefined) {
if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
if (type === 'select' && urlValue !== undefined) {
if (this.type === 'select' && urlValue !== undefined) {
urlState = parseFloat(urlValue);
}
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
if (this.type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
// couldn't parse as a float, store as a string
urlState = urlValue;
}
// get existing value if present
const storedValue = urlState ?? this.getFromLocalStorage();
if (sticky && storedValue !== null) {
if ((this.sticky || urlValue !== undefined) && storedValue !== null) {
this.myValue = storedValue;
}
// call the change function on startup
switch (type) {
switch (this.type) {
case 'select':
this.selectChange({ target: { value: this.myValue } });
break;
@@ -142,6 +159,7 @@ class Setting {
if (storedValue !== undefined) {
switch (this.type) {
case 'boolean':
case 'checkbox':
return storedValue;
case 'select':
return storedValue;
@@ -187,6 +205,9 @@ class Setting {
}
generate() {
// don't generate a control for not visible items
if (!this.visible) return '';
// call the appropriate control generator
switch (this.type) {
case 'select':
return this.generateSelect();

View File

@@ -134,10 +134,9 @@ class WeatherDisplay {
// refresh doesn't delete existing data, and is reused if the silent refresh fails
if (!refresh) {
this.data = undefined;
// clear any refresh timers
this.clearAutoReload();
}
// clear any refresh timers
clearTimeout(this.autoRefreshHandle);
this.autoRefreshHandle = null;
// store weatherParameters locally in case we need them later
if (weatherParameters) this.weatherParameters = weatherParameters;
@@ -150,8 +149,8 @@ class WeatherDisplay {
return false;
}
// set up auto reload
this.autoRefreshHandle = setTimeout(() => this.getData(false, true), settings.refreshTime.value);
// set up auto reload if necessary
this.setAutoReload();
// recalculate navigation timing (in case it was modified in the constructor)
this.calcNavTiming();
@@ -435,6 +434,15 @@ class WeatherDisplay {
this.stillWaitingCallbacks.forEach((callback) => callback());
this.stillWaitingCallbacks = [];
}
clearAutoReload() {
clearInterval(this.autoRefreshHandle);
this.autoRefreshHandle = null;
}
setAutoReload() {
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), settings.refreshTime.value);
}
}
export default WeatherDisplay;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -82,6 +82,14 @@
.like {
left: 425px;
&.heat-index {
color: #e00;
}
&.wind-chill {
color: c.$extended-low;
}
}
.wind {

View File

@@ -40,13 +40,13 @@ body {
button {
font-size: 16pt;
border: 1px solid darkgray;
@media (prefers-color-scheme: dark) {
background-color: #000000;
color: white;
}
border: 1px solid darkgray;
}
#btnGetGps {
@@ -106,23 +106,26 @@ body {
.autocomplete-suggestions {
background-color: #ffffff;
border: 1px solid #000000;
position: absolute;
z-index: 9999;
@media (prefers-color-scheme: dark) {
background-color: #000000;
}
.autocomplete-suggestion {
div {
/*padding: 2px 5px;*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16pt;
&.selected {
background-color: #0000ff;
color: #ffffff;
}
}
.autocomplete-selected {
background-color: #0000ff;
color: #ffffff;
}
}
#divTwc {
@@ -175,12 +178,13 @@ body {
flex-direction: row;
background-color: #000000;
color: #ffffff;
width: 100%;
@media (prefers-color-scheme: dark) {
background-color: rgb(48, 48, 48);
}
color: #ffffff;
width: 100%;
}
#divTwcBottom>div {

View File

@@ -1,15 +1,15 @@
@import 'page';
@import 'weather-display';
@import 'current-weather';
@import 'extended-forecast';
@import 'hourly';
@import 'hourly-graph';
@import 'travel';
@import 'latest-observations';
@import 'local-forecast';
@import 'progress';
@import 'radar';
@import 'regional-forecast';
@import 'almanac';
@import 'hazards';
@import 'media';
@use 'page';
@use 'weather-display';
@use 'current-weather';
@use 'extended-forecast';
@use 'hourly';
@use 'hourly-graph';
@use 'travel';
@use 'latest-observations';
@use 'local-forecast';
@use 'progress';
@use 'radar';
@use 'regional-forecast';
@use 'almanac';
@use 'hazards';
@use 'media';

View File

@@ -26,8 +26,6 @@
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
<% } else { %>
<link rel="stylesheet" type="text/css" href="styles/main.css" />
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script>
<script type="text/javascript" src="scripts/vendor/jquery.autocomplete.min.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
@@ -60,25 +58,26 @@
<body>
<div id="divQuery">
<div id="divQuery">
<input id="txtAddress" type="text" value="" placeholder="Zip or City, State" />
<div class="buttons">
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light"/>
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark"/>
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png"
class="light" />
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark" />
</button>
<button id="btnGetLatLng" type="submit">GO</button>
<button id="btnClearQuery" type="reset">Reset</button>
</div>
</div>
<div id="version" style="display:none">
<%- version %>
</div>
</div>
<div id="version" style="display:none">
<%- version %>
</div>
<div id="divTwc">
<div id="container">
<div id="loading" width="640" height="480">
<div>
<div class="title">WeatherStar 4000+</div>
<div id="divTwc">
<div id="container">
<div id="loading" width="640" height="480">
<div>
<div class="title">WeatherStar 4000+</div>
<div class="version">v<%- version %></div>
<div class="instructions">Enter your location above to continue</div>
</div>
@@ -140,17 +139,17 @@
</div>
</div>
<br />
<br />
<div class="info">
<a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a>
</div>
<div class="media"></div>
<div class='heading'>Selected displays</div>
<div id='enabledDisplays'>
<div class='heading'>Selected displays</div>
<div id='enabledDisplays'>
</div>
</div>
<div class='heading'>Settings</div>
<div id='settings'>
@@ -161,17 +160,18 @@
<a href='' id='share-link'>Copy Permalink</a> <span id="share-link-copied">Link copied to clipboard!</span>
<div id="share-link-instructions">
Copy this long URL:
<input type='text' id="share-link-url"></div>
<input type='text' id="share-link-url">
</div>
</div>
</div>
<div class='heading'>Forecast Information</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
</div>
</body>