mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-19 01:59:31 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262ea15468 | ||
|
|
5d2e5a6d9c | ||
|
|
992258d3ce | ||
|
|
b031934022 | ||
|
|
4cc2312ffd | ||
|
|
ce99fc16e7 | ||
|
|
97f96d4091 | ||
|
|
13f08b62cc | ||
|
|
eacd82b4f4 | ||
|
|
91f669e828 | ||
|
|
cdbe3d968f | ||
|
|
0a388cdb83 | ||
|
|
8678d9f053 | ||
|
|
a592668d0d | ||
|
|
c7eb56f60c |
@@ -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
3
.vscode/launch.json
vendored
@@ -15,6 +15,9 @@
|
||||
"**/*.min.js",
|
||||
"**/vendor/**"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"--autoplay-policy=no-user-gesture-required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Data:stations",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -575,7 +575,7 @@
|
||||
"lon": -77.9447
|
||||
},
|
||||
{
|
||||
"city": "Tucsan",
|
||||
"city": "Tucson",
|
||||
"lat": 32.2216,
|
||||
"lon": -110.9698
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
737
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
// unicorn
|
||||
'unicorn/numeric-separators-style': 0,
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -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,
|
||||
|
||||
307
server/scripts/modules/autocomplete.mjs
Normal file
307
server/scripts/modules/autocomplete.mjs
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/<(\/?strong)>/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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
10716
server/scripts/vendor/auto/jquery.js
vendored
10716
server/scripts/vendor/auto/jquery.js
vendored
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
@@ -82,6 +82,14 @@
|
||||
|
||||
.like {
|
||||
left: 425px;
|
||||
|
||||
&.heat-index {
|
||||
color: #e00;
|
||||
}
|
||||
|
||||
&.wind-chill {
|
||||
color: c.$extended-low;
|
||||
}
|
||||
}
|
||||
|
||||
.wind {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user