mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f17f69f60e | ||
|
|
fa16095355 | ||
|
|
cc3dbeb043 | ||
|
|
8ee1e954eb | ||
|
|
bfc4bddfef | ||
|
|
567325e3c5 | ||
|
|
4903b95fec | ||
|
|
b43fb32820 | ||
|
|
0d0c4ec452 | ||
|
|
49d18c2fbe | ||
|
|
1732a3381f | ||
|
|
093b6ac239 | ||
|
|
12d068d740 | ||
|
|
517c560ef6 | ||
|
|
3eb571bed4 | ||
|
|
52ca161bdb | ||
|
|
ee5690dcad | ||
|
|
c05b827593 | ||
|
|
bef42a3da2 | ||
|
|
13ff0317e6 | ||
|
|
5cc85840a9 | ||
|
|
190e50e2f3 | ||
|
|
aa7ac64827 | ||
|
|
2ab737d5a5 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,4 +11,4 @@ Please do not report issues with api.weather.gov being down. It's a new service
|
||||
|
||||
Please include:
|
||||
* Web browser and OS
|
||||
* Location for which you are viewing a forecast
|
||||
* Forecast Information text block from the very bottom of the web page
|
||||
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -26,6 +26,18 @@
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"args": [
|
||||
"--use-cache"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"name": "Data:stations-api",
|
||||
"program": "${workspaceFolder}/datagenerators/stations.mjs",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev --legacy-peer-deps
|
||||
RUN npm ci --legacy-peer-deps
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
25
README.md
25
README.md
@@ -32,6 +32,18 @@ From a learning standpoint, this codebase make use of a lot of different methods
|
||||
* Hand written CSS made easier to mange with SASS
|
||||
* A linting library to keep code style consistent
|
||||
|
||||
## Quck Start
|
||||
|
||||
Ensure you have Node installed.
|
||||
```bash
|
||||
git clone https://github.com/netbymatt/ws4kp.git
|
||||
cd ws4kp
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Open your browser and navigate to https://localhost:8080
|
||||
|
||||
## Does WeatherStar 4000+ work outside of the USA?
|
||||
|
||||
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclusive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
||||
@@ -57,14 +69,7 @@ WeatherStar 4000+ supports two deployment modes:
|
||||
* Browser-based caching
|
||||
* Used by: static file hosting and default `Dockerfile`
|
||||
|
||||
## Run Your WeatherStar
|
||||
|
||||
Ensure you have Node installed. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/netbymatt/ws4kp.git
|
||||
cd ws4kp
|
||||
npm install
|
||||
```
|
||||
## Other methods to run Ws4kp
|
||||
|
||||
### Development Mode (individual JS files, easier debugging)
|
||||
```bash
|
||||
@@ -309,11 +314,13 @@ If you're unable to pre-set the play state before entering kiosk mode (such as w
|
||||
|
||||
## Community Notes
|
||||
|
||||
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
|
||||
Thanks to the WeatherStar+ community for providing these discussions to further extend your retro forecasts!
|
||||
|
||||
* [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.
|
||||
* [SSL Certificates](https://github.com/netbymatt/ws4kp/issues/135) Discussion about how to host with an SSL certificate (enables geolocation).
|
||||
* [Changing playlists](https://github.com/netbymatt/ws4kp/issues/138) Possible ways to automatically change the playlist on a schedule.
|
||||
|
||||
## Customization
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
import * as url from 'node:url';
|
||||
|
||||
// Load station data
|
||||
const stationInfo = JSON.parse(readFileSync('./datagenerators/output/stations-raw.json', 'utf8'));
|
||||
// const regionalCities = JSON.parse(readFileSync('./datagenerators/output/regionalcities.json', 'utf8'));
|
||||
@@ -1109,139 +1111,184 @@ or where the fallback to the ICAO airport code occurred:
|
||||
jq -c '.[] | select(.name | test("^[A-Z]{3}$")) | {state, city, simple, name}'
|
||||
*/
|
||||
|
||||
const diffMode = process.argv.includes('--diff');
|
||||
const onlyProblems = process.argv.includes('--only-problems');
|
||||
const noProblems = process.argv.includes('--no-problems');
|
||||
const onlyDuplicates = process.argv.includes('--only-dupes');
|
||||
const noPriority = process.argv.includes('--no-priority');
|
||||
const noSimple = process.argv.includes('--no-simple');
|
||||
const noCoordinates = process.argv.includes('--no-coords');
|
||||
const writeFile = process.argv.includes('--write');
|
||||
const readArguments = () => ({
|
||||
diffMode: process.argv.includes('--diff'),
|
||||
onlyProblems: process.argv.includes('--only-problems'),
|
||||
noProblems: process.argv.includes('--no-problems'),
|
||||
onlyDuplicates: process.argv.includes('--only-dupes'),
|
||||
noPriority: process.argv.includes('--no-priority'),
|
||||
noSimple: process.argv.includes('--no-simple'),
|
||||
noCoordinates: process.argv.includes('--no-coords'),
|
||||
writeFile: process.argv.includes('--write'),
|
||||
});
|
||||
|
||||
// Process ALL stations at once to get the display name map
|
||||
let displayNameMap = processAllStations(stationInfo);
|
||||
const DEFAULT_OPTIONS = {
|
||||
diffMode: false,
|
||||
onlyProblems: false,
|
||||
noProblems: false,
|
||||
onlyDuplicates: false,
|
||||
noPriority: false,
|
||||
noSimple: false,
|
||||
noCoordinates: false,
|
||||
writeFile: false,
|
||||
};
|
||||
|
||||
// Apply priority-based deduplication
|
||||
displayNameMap = resolveDuplicatesByPriority(displayNameMap, stationInfo);
|
||||
const postProcessor = (_options) => {
|
||||
// combine default and provided options
|
||||
const options = { ...DEFAULT_OPTIONS, ..._options };
|
||||
|
||||
const results = [];
|
||||
// Process ALL stations at once to get the display name map
|
||||
let displayNameMap = processAllStations(stationInfo);
|
||||
|
||||
// Now iterate through stations and use the pre-computed display names
|
||||
const stations = Object.values(stationInfo);
|
||||
stations.forEach((station) => {
|
||||
const originalName = station.city;
|
||||
const processedName = processingUtils.finalCleanup(displayNameMap[station.id]); // Look up by station ID
|
||||
// Apply priority-based deduplication
|
||||
displayNameMap = resolveDuplicatesByPriority(displayNameMap, stationInfo);
|
||||
|
||||
// Get airport type and priority for this station
|
||||
const airportType = getAirportType(originalName, station.id); // Pass station ID for enhanced detection
|
||||
const priority = getAirportPriority(airportType);
|
||||
const results = [];
|
||||
|
||||
const potentialIssues = [];
|
||||
// Check if the processed name contains punctuation (a period at the end is OK)
|
||||
if (/[,;!?/:.]/.test(processedName) && !processedName.endsWith('.')) {
|
||||
potentialIssues.push('punctuation');
|
||||
}
|
||||
if (processedName.length > 12) {
|
||||
potentialIssues.push('long');
|
||||
}
|
||||
if (processedName.length > 20) {
|
||||
potentialIssues.push('reallyLong');
|
||||
}
|
||||
// check if it contains any digits
|
||||
if (/\d/.test(processedName)) {
|
||||
potentialIssues.push('digits');
|
||||
}
|
||||
// Now iterate through stations and use the pre-computed display names
|
||||
const stations = Object.values(stationInfo);
|
||||
stations.forEach((station) => {
|
||||
const originalName = station.city;
|
||||
const processedName = processingUtils.finalCleanup(displayNameMap[station.id]); // Look up by station ID
|
||||
|
||||
results.push({
|
||||
id: station.id,
|
||||
lat: station.lat,
|
||||
lon: station.lon,
|
||||
state: station.state,
|
||||
location: originalName, // original full location name
|
||||
city: processedName, // processed city name for display
|
||||
simple: originalName.match(/[^,/;\\-]*/)[0].substr(0, 12).trim(),
|
||||
type: airportType,
|
||||
priority,
|
||||
potentialIssues,
|
||||
// Get airport type and priority for this station
|
||||
const airportType = getAirportType(originalName, station.id); // Pass station ID for enhanced detection
|
||||
const priority = getAirportPriority(airportType);
|
||||
|
||||
const potentialIssues = [];
|
||||
// Check if the processed name contains punctuation (a period at the end is OK)
|
||||
if (/[,;!?/:.]/.test(processedName) && !processedName.endsWith('.')) {
|
||||
potentialIssues.push('punctuation');
|
||||
}
|
||||
if (processedName.length > 12) {
|
||||
potentialIssues.push('long');
|
||||
}
|
||||
if (processedName.length > 20) {
|
||||
potentialIssues.push('reallyLong');
|
||||
}
|
||||
// check if it contains any digits
|
||||
if (/\d/.test(processedName)) {
|
||||
potentialIssues.push('digits');
|
||||
}
|
||||
|
||||
results.push({
|
||||
id: station.id,
|
||||
lat: station.lat,
|
||||
lon: station.lon,
|
||||
state: station.state,
|
||||
location: originalName, // original full location name
|
||||
city: processedName, // processed city name for display
|
||||
simple: originalName.match(/[^,/;\\-]*/)[0].substr(0, 12).trim(),
|
||||
type: airportType,
|
||||
priority,
|
||||
potentialIssues,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check for duplicates by state
|
||||
const cleanedMapByState = new Map();
|
||||
// Check for duplicates by state
|
||||
const cleanedMapByState = new Map();
|
||||
|
||||
results.forEach((result) => {
|
||||
const { state } = result;
|
||||
if (!cleanedMapByState.has(state)) {
|
||||
cleanedMapByState.set(state, new Map());
|
||||
}
|
||||
const stateMap = cleanedMapByState.get(state);
|
||||
if (stateMap.has(result.city)) {
|
||||
stateMap.get(result.city).push(result);
|
||||
} else {
|
||||
stateMap.set(result.city, [result]);
|
||||
}
|
||||
});
|
||||
|
||||
cleanedMapByState.forEach((stateMap, _state) => {
|
||||
stateMap.forEach((originals, _cleaned) => {
|
||||
if (originals.length > 1) {
|
||||
originals.forEach((original) => {
|
||||
if (!original.potentialIssues.includes('duplicate')) {
|
||||
original.potentialIssues.push('duplicate');
|
||||
}
|
||||
});
|
||||
results.forEach((result) => {
|
||||
const { state } = result;
|
||||
if (!cleanedMapByState.has(state)) {
|
||||
cleanedMapByState.set(state, new Map());
|
||||
}
|
||||
const stateMap = cleanedMapByState.get(state);
|
||||
if (stateMap.has(result.city)) {
|
||||
stateMap.get(result.city).push(result);
|
||||
} else {
|
||||
stateMap.set(result.city, [result]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Filter results if requested
|
||||
let finalResults = results;
|
||||
if (onlyProblems) {
|
||||
finalResults = results.filter((r) => r.potentialIssues.length > 0);
|
||||
}
|
||||
if (onlyDuplicates) {
|
||||
finalResults = finalResults.filter((r) => r.potentialIssues.includes('duplicate'));
|
||||
}
|
||||
cleanedMapByState.forEach((stateMap, _state) => {
|
||||
stateMap.forEach((originals, _cleaned) => {
|
||||
if (originals.length > 1) {
|
||||
originals.forEach((original) => {
|
||||
if (!original.potentialIssues.includes('duplicate')) {
|
||||
original.potentialIssues.push('duplicate');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const outputResult = finalResults.map((result) => {
|
||||
let outputItem = result;
|
||||
|
||||
// Don't include lat or long in diff mode
|
||||
if (noCoordinates || diffMode) {
|
||||
const {
|
||||
lat: _lat, lon: _lon, ...resultWithoutLocation
|
||||
} = result;
|
||||
outputItem = resultWithoutLocation;
|
||||
// Filter results if requested
|
||||
let finalResults = results;
|
||||
if (options.onlyProblems) {
|
||||
finalResults = results.filter((r) => r.potentialIssues.length > 0);
|
||||
}
|
||||
if (options.onlyDuplicates) {
|
||||
finalResults = finalResults.filter((r) => r.potentialIssues.includes('duplicate'));
|
||||
}
|
||||
|
||||
// Don't include potentialIssues when --no-problems is specified
|
||||
if (noProblems || diffMode) {
|
||||
const { potentialIssues: _potentialIssues, ...resultWithoutIssues } = outputItem;
|
||||
outputItem = resultWithoutIssues;
|
||||
}
|
||||
const outputResult = finalResults.map((result) => {
|
||||
let outputItem = result;
|
||||
|
||||
// Remove type and priority if --no-priority is specified
|
||||
if (noPriority || diffMode) {
|
||||
const { type: _type, priority: _priority, ...resultWithoutPriority } = outputItem;
|
||||
outputItem = resultWithoutPriority;
|
||||
}
|
||||
// Don't include lat or long in diff mode
|
||||
if (options.noCoordinates || options.diffMode) {
|
||||
const {
|
||||
lat: _lat, lon: _lon, ...resultWithoutLocation
|
||||
} = result;
|
||||
outputItem = resultWithoutLocation;
|
||||
}
|
||||
|
||||
// remove simple field if --no-simple is specified
|
||||
if (noSimple || diffMode) {
|
||||
const { simple: _simple, ...resultWithoutSimple } = outputItem;
|
||||
outputItem = resultWithoutSimple;
|
||||
}
|
||||
// Don't include potentialIssues when --no-problems is specified
|
||||
if (options.noProblems || options.diffMode) {
|
||||
const { potentialIssues: _potentialIssues, ...resultWithoutIssues } = outputItem;
|
||||
outputItem = resultWithoutIssues;
|
||||
}
|
||||
|
||||
return outputItem;
|
||||
});
|
||||
// Remove type and priority if --no-priority is specified
|
||||
if (options.noPriority || options.diffMode) {
|
||||
const { type: _type, priority: _priority, ...resultWithoutPriority } = outputItem;
|
||||
outputItem = resultWithoutPriority;
|
||||
}
|
||||
|
||||
// remove simple field if --no-simple is specified
|
||||
if (options.noSimple || options.diffMode) {
|
||||
const { simple: _simple, ...resultWithoutSimple } = outputItem;
|
||||
outputItem = resultWithoutSimple;
|
||||
}
|
||||
|
||||
return outputItem;
|
||||
});
|
||||
|
||||
if (writeFile) {
|
||||
const fileResults = results.map(({
|
||||
simple: _simple, type: _type, potentialIssues: _potentialIssues, ...rest
|
||||
simple: _simple, type: _type, potentialIssues: _potentialIssues, location: _location, ...rest
|
||||
}) => rest);
|
||||
|
||||
writeFileSync('./datagenerators/output/stations.json', compactStringifyToObject(fileResults));
|
||||
console.log(`Wrote ${fileResults.length} processed stations to datagenerators/output/stations.json`);
|
||||
} else {
|
||||
console.log(compactStringifyToArray(outputResult));
|
||||
if (options.writeFile) {
|
||||
writeFileSync('./datagenerators/output/stations.json', compactStringifyToObject(fileResults));
|
||||
console.log(`Wrote ${fileResults.length} processed stations to datagenerators/output/stations.json`);
|
||||
} else {
|
||||
console.log(compactStringifyToArray(outputResult));
|
||||
}
|
||||
|
||||
// array to output object
|
||||
const returnObject = {};
|
||||
fileResults.forEach((item) => {
|
||||
returnObject[item.id] = item;
|
||||
});
|
||||
|
||||
return returnObject;
|
||||
};
|
||||
|
||||
// determine if running from command line or module
|
||||
const commandLine = (() => {
|
||||
if (import.meta.url.startsWith('file:')) { // (A)
|
||||
const modulePath = url.fileURLToPath(import.meta.url);
|
||||
if (process.argv[1] === modulePath) { // (B)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
)();
|
||||
|
||||
// run post processor if called from command line
|
||||
if (commandLine) {
|
||||
postProcessor(readArguments());
|
||||
}
|
||||
|
||||
export default postProcessor;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-loop-func */
|
||||
// list all stations in a single file
|
||||
// only find stations with 4 letter codes
|
||||
|
||||
@@ -6,67 +7,91 @@ import https from './https.mjs';
|
||||
import states from './stations-states.mjs';
|
||||
import chunk from './chunk.mjs';
|
||||
import overrides from './stations-overrides.mjs';
|
||||
import postProcessor from './stations-postprocessor.mjs';
|
||||
|
||||
// check for cached flag
|
||||
const USE_CACHE = process.argv.includes('--use-cache');
|
||||
|
||||
// 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'];
|
||||
|
||||
// chunk the list of states
|
||||
const chunkStates = chunk(states, 1);
|
||||
const chunkStates = chunk(states, 3);
|
||||
|
||||
// store output
|
||||
const output = {};
|
||||
let completed = 0;
|
||||
|
||||
// process all chunks
|
||||
for (let i = 0; i < chunkStates.length; i += 1) {
|
||||
const stateChunk = chunkStates[i];
|
||||
// loop through states
|
||||
// get data from api if desired
|
||||
if (!USE_CACHE) {
|
||||
// process all chunks
|
||||
for (let i = 0; i < chunkStates.length; i += 1) {
|
||||
const stateChunk = chunkStates[i];
|
||||
// loop through states
|
||||
|
||||
// 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;
|
||||
}
|
||||
// get any overrides if available
|
||||
const override = overrides[id] ?? {};
|
||||
output[id] = {
|
||||
id,
|
||||
city: station.properties.name,
|
||||
state,
|
||||
lat: station.geometry.coordinates[1],
|
||||
lon: station.geometry.coordinates[0],
|
||||
// finally add the overrides
|
||||
...override,
|
||||
};
|
||||
});
|
||||
next = stations?.pagination?.next;
|
||||
round += 1;
|
||||
// write the output
|
||||
writeFileSync('./datagenerators/output/stations-raw.json', JSON.stringify(output, null, 2));
|
||||
// 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-raw.json', JSON.stringify(output, null, 2));
|
||||
}
|
||||
while (next && stations.features.length > 0);
|
||||
completed += 1;
|
||||
console.log(`Complete: ${state} ${completed}/${states.length}`);
|
||||
return true;
|
||||
} catch {
|
||||
console.error(`Unable to get state: ${state}`);
|
||||
return false;
|
||||
}
|
||||
while (next && stations.features.length > 0);
|
||||
console.log(`Complete: ${state}`);
|
||||
return true;
|
||||
} catch {
|
||||
console.error(`Unable to get state: ${state}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// run the post processor
|
||||
// data is passed through the file stations-raw.json
|
||||
const postProcessed = postProcessor();
|
||||
|
||||
// apply any overrides
|
||||
Object.entries(overrides).forEach(([id, values]) => {
|
||||
// check for existing value
|
||||
if (postProcessed[id]) {
|
||||
// apply the overrides
|
||||
postProcessed[id] = {
|
||||
...postProcessed[id],
|
||||
...values,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// write final file to disk
|
||||
writeFileSync('./datagenerators/output/stations.json', JSON.stringify(postProcessed, null, 2));
|
||||
|
||||
@@ -97,23 +97,27 @@ const copyCss = () => src(cssSources)
|
||||
const htmlSources = [
|
||||
'views/*.ejs',
|
||||
];
|
||||
const compressHtml = async () => {
|
||||
const packageJson = await readFile('package.json');
|
||||
const { version } = JSON.parse(packageJson);
|
||||
|
||||
return src(htmlSources)
|
||||
.pipe(ejs({
|
||||
production: version,
|
||||
serverAvailable: false,
|
||||
version,
|
||||
OVERRIDES,
|
||||
query: {},
|
||||
}))
|
||||
.pipe(rename({ extname: '.html' }))
|
||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||
.pipe(dest('./dist'));
|
||||
const packageJson = await readFile('package.json');
|
||||
let { version } = JSON.parse(packageJson);
|
||||
const previewVersion = async () => {
|
||||
// generate a relatively unique timestamp for cache invalidation of the preview site
|
||||
const now = new Date();
|
||||
const msNow = now.getTime() % 1_000_000;
|
||||
version = msNow.toString();
|
||||
};
|
||||
|
||||
const compressHtml = async () => src(htmlSources)
|
||||
.pipe(ejs({
|
||||
production: version,
|
||||
serverAvailable: false,
|
||||
version,
|
||||
OVERRIDES,
|
||||
query: {},
|
||||
}))
|
||||
.pipe(rename({ extname: '.html' }))
|
||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||
.pipe(dest('./dist'));
|
||||
|
||||
const otherFiles = [
|
||||
'server/robots.txt',
|
||||
'server/manifest.json',
|
||||
@@ -205,7 +209,7 @@ const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, com
|
||||
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
||||
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
||||
const stageFrontend = series(buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||
|
||||
export default publishFrontend;
|
||||
|
||||
|
||||
4123
package-lock.json
generated
4123
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.9",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -56,6 +56,7 @@
|
||||
"dotenv": "^17.0.1",
|
||||
"ejs": "^3.1.5",
|
||||
"express": "^5.1.0",
|
||||
"metar-taf-parser": "^6.1.2"
|
||||
"metar-taf-parser": "^9.0.0",
|
||||
"npm": "^11.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
server/images/backgrounds/7-wide.png
Normal file
BIN
server/images/backgrounds/7-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
server/images/backgrounds/7.png
Normal file
BIN
server/images/backgrounds/7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -145,6 +145,8 @@ const init = async () => {
|
||||
document.querySelector('#spanStationId').innerHTML = '';
|
||||
document.querySelector('#spanRadarId').innerHTML = '';
|
||||
document.querySelector('#spanZoneId').innerHTML = '';
|
||||
document.querySelector('#spanOfficeId').innerHTML = '';
|
||||
document.querySelector('#spanGridPoint').innerHTML = '';
|
||||
|
||||
localStorage.removeItem('play');
|
||||
postMessage('navButton', 'play');
|
||||
|
||||
@@ -97,6 +97,12 @@ const drawScreen = async () => {
|
||||
if (elem.parentElement.id === 'progress-html') return;
|
||||
thisScreen?.classes?.forEach((cls) => elem.classList.add(cls));
|
||||
});
|
||||
// special case for red background on hazard scroll
|
||||
const mainScrollBg = document.getElementById('scroll-bg');
|
||||
mainScrollBg.className = '';
|
||||
if (thisScreen?.classes?.includes('hazard')) {
|
||||
mainScrollBg.classList.add('hazard');
|
||||
}
|
||||
|
||||
if (typeof thisScreen === 'string') {
|
||||
// only a string
|
||||
|
||||
@@ -137,10 +137,6 @@ const parse = (fullForecast, forecastUrl) => {
|
||||
}
|
||||
// get the object to modify/populate
|
||||
const fDay = forecast[destIndex];
|
||||
// high temperature will always be last in the source array so it will overwrite the low values assigned below
|
||||
fDay.icon = getLargeIcon(period.icon);
|
||||
fDay.text = shortenExtendedForecastText(period.shortForecast);
|
||||
fDay.dayName = dates[destIndex];
|
||||
|
||||
// preload the icon
|
||||
preloadImg(fDay.icon);
|
||||
@@ -148,6 +144,9 @@ const parse = (fullForecast, forecastUrl) => {
|
||||
if (period.isDaytime) {
|
||||
// day time is the high temperature
|
||||
fDay.high = period.temperature;
|
||||
fDay.icon = getLargeIcon(period.icon);
|
||||
fDay.text = shortenExtendedForecastText(period.shortForecast);
|
||||
fDay.dayName = dates[destIndex];
|
||||
// Wait for the corresponding night period to increment
|
||||
} else {
|
||||
// low temperature
|
||||
|
||||
@@ -71,6 +71,7 @@ class Hazards extends WeatherDisplay {
|
||||
// get the forecast using centralized safe handling
|
||||
const url = new URL('https://api.weather.gov/alerts/active');
|
||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||
url.searchParams.append('status', 'actual');
|
||||
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||
|
||||
if (!alerts) {
|
||||
@@ -103,7 +104,10 @@ class Hazards extends WeatherDisplay {
|
||||
// show alert indicator
|
||||
if (unViewed > 0) alert.classList.add('show');
|
||||
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
|
||||
this.drawLongCanvas();
|
||||
// unless this has been disabled
|
||||
if (this.isEnabled) {
|
||||
this.drawLongCanvas();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
@@ -115,7 +119,7 @@ class Hazards extends WeatherDisplay {
|
||||
this.getDataCallback();
|
||||
|
||||
if (!superResult) {
|
||||
this.setStatus(STATUS.loaded);
|
||||
// Don't override status - super.getData() already set it to STATUS.disabled
|
||||
return;
|
||||
}
|
||||
this.drawLongCanvas();
|
||||
|
||||
@@ -111,7 +111,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
weatherParameters.stations = stations.features;
|
||||
|
||||
// update the main process for display purposes
|
||||
populateWeatherParameters(weatherParameters);
|
||||
populateWeatherParameters(weatherParameters, point.properties);
|
||||
|
||||
// reset the scroll
|
||||
postMessage({ type: 'current-weather-scroll', method: 'reload' });
|
||||
@@ -753,12 +753,14 @@ const registerProgress = (_progress) => {
|
||||
progress = _progress;
|
||||
};
|
||||
|
||||
const populateWeatherParameters = (params) => {
|
||||
const populateWeatherParameters = (params, point) => {
|
||||
document.querySelector('#spanCity').innerHTML = `${params.city}, `;
|
||||
document.querySelector('#spanState').innerHTML = params.state;
|
||||
document.querySelector('#spanStationId').innerHTML = params.stationId;
|
||||
document.querySelector('#spanRadarId').innerHTML = params.radarId;
|
||||
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
|
||||
document.querySelector('#spanOfficeId').innerHTML = point.cwa;
|
||||
document.querySelector('#spanGridPoint').innerHTML = `${point.gridX},${point.gridY}`;
|
||||
};
|
||||
|
||||
const latLonReceived = (data, haveDataCallback) => {
|
||||
|
||||
@@ -3,13 +3,32 @@ import { parseMetar } from '../../vendor/auto/metar-taf-parser.mjs';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import en from '../../vendor/auto/locale/en.js';
|
||||
|
||||
// metar-taf-parser requires regex lookbehind
|
||||
// this does not work in iOS < 16.4
|
||||
// this is a detection algorithm for iOS versions
|
||||
const isIos = /iP(ad|od|hone)/i.test(window.navigator.userAgent);
|
||||
let iosVersionOk = false;
|
||||
if (isIos) {
|
||||
// regex match the version string
|
||||
const iosVersionRaw = /OS (\d+)_(\d+)/.exec(window.navigator.userAgent);
|
||||
// check for match
|
||||
if (iosVersionRaw) {
|
||||
// break into parts
|
||||
const iosVersionMajor = parseInt(iosVersionRaw[1], 10);
|
||||
const iosVersionMinor = parseInt(iosVersionRaw[2], 10);
|
||||
if (iosVersionMajor > 16) iosVersionOk = true;
|
||||
if (iosVersionMajor === 16 && iosVersionMinor >= 4) iosVersionOk = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment observation data by parsing METAR when API fields are missing
|
||||
* @param {Object} observation - The observation object from the API
|
||||
* @returns {Object} - Augmented observation with parsed METAR data filled in
|
||||
*/
|
||||
const augmentObservationWithMetar = (observation) => {
|
||||
if (!observation?.rawMessage) {
|
||||
// check for a metar message and for unusable ios versions
|
||||
if (!observation?.rawMessage || (isIos && !iosVersionOk)) {
|
||||
return observation;
|
||||
}
|
||||
|
||||
|
||||
13
server/scripts/vendor/auto/locale/en.js
vendored
13
server/scripts/vendor/auto/locale/en.js
vendored
@@ -94,7 +94,7 @@ var en = {
|
||||
TS: "thunderstorm",
|
||||
},
|
||||
Error: {
|
||||
prefix: "An error occured. Error code n°",
|
||||
prefix: "An error occurred. Error code n°",
|
||||
},
|
||||
ErrorCode: {
|
||||
AirportNotFound: "The airport was not found for this message.",
|
||||
@@ -136,11 +136,13 @@ var en = {
|
||||
TS: "thunderstorm",
|
||||
UP: "unknown precipitation",
|
||||
VA: "volcanic ash",
|
||||
NSW: 'no significant weather'
|
||||
},
|
||||
Remark: {
|
||||
ALQDS: "all quadrants",
|
||||
AO1: "automated stations without a precipitation discriminator",
|
||||
AO2: "automated station with a precipitation discriminator",
|
||||
AO2A: "automated station with a precipitation discriminator (augmented)",
|
||||
BASED: "based",
|
||||
Barometer: [
|
||||
"Increase, then decrease",
|
||||
@@ -156,7 +158,7 @@ var en = {
|
||||
Ceiling: {
|
||||
Height: "ceiling varying between {0} and {1} feet",
|
||||
Second: {
|
||||
Location: "ceiling of {0} feet mesured by a second sensor located at {1}",
|
||||
Location: "ceiling of {0} feet measured by a second sensor located at {1}",
|
||||
},
|
||||
},
|
||||
DSNT: "distant",
|
||||
@@ -192,6 +194,11 @@ var en = {
|
||||
LGT: "light",
|
||||
LTG: "lightning",
|
||||
MOD: "moderate",
|
||||
Next: {
|
||||
Forecast: {
|
||||
By: "next forecast by {0}, {1}:{2}Z"
|
||||
},
|
||||
},
|
||||
NXT: "next",
|
||||
ON: "on",
|
||||
Obscuration: "{0} layer at {1} feet composed of {2}",
|
||||
@@ -223,7 +230,7 @@ var en = {
|
||||
},
|
||||
Second: {
|
||||
Location: {
|
||||
Visibility: "visibility of {0} SM mesured by a second sensor located at {1}",
|
||||
Visibility: "visibility of {0} SM measured by a second sensor located at {1}",
|
||||
},
|
||||
},
|
||||
Sector: {
|
||||
|
||||
2
server/scripts/vendor/auto/luxon.js.map
vendored
2
server/scripts/vendor/auto/luxon.js.map
vendored
File diff suppressed because one or more lines are too long
228
server/scripts/vendor/auto/luxon.mjs
vendored
228
server/scripts/vendor/auto/luxon.mjs
vendored
@@ -1053,10 +1053,18 @@ class Locale {
|
||||
|
||||
months(length, format = false) {
|
||||
return listStuff(this, length, months, () => {
|
||||
// Workaround for "ja" locale: formatToParts does not label all parts of the month
|
||||
// as "month" and for this locale there is no difference between "format" and "non-format".
|
||||
// As such, just use format() instead of formatToParts() and take the whole string
|
||||
const monthSpecialCase = this.intl === "ja" || this.intl.startsWith("ja-");
|
||||
format &= !monthSpecialCase;
|
||||
const intl = format ? { month: length, day: "numeric" } : { month: length },
|
||||
formatStr = format ? "format" : "standalone";
|
||||
if (!this.monthsCache[formatStr][length]) {
|
||||
this.monthsCache[formatStr][length] = mapMonths((dt) => this.extract(dt, intl, "month"));
|
||||
const mapper = !monthSpecialCase
|
||||
? (dt) => this.extract(dt, intl, "month")
|
||||
: (dt) => this.dtFormatter(dt, intl).format();
|
||||
this.monthsCache[formatStr][length] = mapMonths(mapper);
|
||||
}
|
||||
return this.monthsCache[formatStr][length];
|
||||
});
|
||||
@@ -2040,10 +2048,24 @@ function parseMillis(fraction) {
|
||||
}
|
||||
}
|
||||
|
||||
function roundTo(number, digits, towardZero = false) {
|
||||
const factor = 10 ** digits,
|
||||
rounder = towardZero ? Math.trunc : Math.round;
|
||||
return rounder(number * factor) / factor;
|
||||
function roundTo(number, digits, rounding = "round") {
|
||||
const factor = 10 ** digits;
|
||||
switch (rounding) {
|
||||
case "expand":
|
||||
return number > 0
|
||||
? Math.ceil(number * factor) / factor
|
||||
: Math.floor(number * factor) / factor;
|
||||
case "trunc":
|
||||
return Math.trunc(number * factor) / factor;
|
||||
case "round":
|
||||
return Math.round(number * factor) / factor;
|
||||
case "floor":
|
||||
return Math.floor(number * factor) / factor;
|
||||
case "ceil":
|
||||
return Math.ceil(number * factor) / factor;
|
||||
default:
|
||||
throw new RangeError(`Value rounding ${rounding} is out of range`);
|
||||
}
|
||||
}
|
||||
|
||||
// DATE BASICS
|
||||
@@ -2151,7 +2173,7 @@ function signedOffset(offHourStr, offMinuteStr) {
|
||||
|
||||
function asNumber(value) {
|
||||
const numericValue = Number(value);
|
||||
if (typeof value === "boolean" || value === "" || Number.isNaN(numericValue))
|
||||
if (typeof value === "boolean" || value === "" || !Number.isFinite(numericValue))
|
||||
throw new InvalidArgumentError(`Invalid unit value ${value}`);
|
||||
return numericValue;
|
||||
}
|
||||
@@ -2410,8 +2432,12 @@ class Formatter {
|
||||
for (let i = 0; i < fmt.length; i++) {
|
||||
const c = fmt.charAt(i);
|
||||
if (c === "'") {
|
||||
if (currentFull.length > 0) {
|
||||
splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull });
|
||||
// turn '' into a literal signal quote instead of just skipping the empty literal
|
||||
if (currentFull.length > 0 || bracketed) {
|
||||
splits.push({
|
||||
literal: bracketed || /^\s+$/.test(currentFull),
|
||||
val: currentFull === "" ? "'" : currentFull,
|
||||
});
|
||||
}
|
||||
current = null;
|
||||
currentFull = "";
|
||||
@@ -2475,7 +2501,7 @@ class Formatter {
|
||||
return this.dtFormatter(dt, opts).resolvedOptions();
|
||||
}
|
||||
|
||||
num(n, p = 0) {
|
||||
num(n, p = 0, signDisplay = undefined) {
|
||||
// we get some perf out of doing this here, annoyingly
|
||||
if (this.opts.forceSimple) {
|
||||
return padStart(n, p);
|
||||
@@ -2486,6 +2512,9 @@ class Formatter {
|
||||
if (p > 0) {
|
||||
opts.padTo = p;
|
||||
}
|
||||
if (signDisplay) {
|
||||
opts.signDisplay = signDisplay;
|
||||
}
|
||||
|
||||
return this.loc.numberFormatter(opts).format(n);
|
||||
}
|
||||
@@ -2721,32 +2750,44 @@ class Formatter {
|
||||
}
|
||||
|
||||
formatDurationFromString(dur, fmt) {
|
||||
const invertLargest = this.opts.signMode === "negativeLargestOnly" ? -1 : 1;
|
||||
const tokenToField = (token) => {
|
||||
switch (token[0]) {
|
||||
case "S":
|
||||
return "millisecond";
|
||||
return "milliseconds";
|
||||
case "s":
|
||||
return "second";
|
||||
return "seconds";
|
||||
case "m":
|
||||
return "minute";
|
||||
return "minutes";
|
||||
case "h":
|
||||
return "hour";
|
||||
return "hours";
|
||||
case "d":
|
||||
return "day";
|
||||
return "days";
|
||||
case "w":
|
||||
return "week";
|
||||
return "weeks";
|
||||
case "M":
|
||||
return "month";
|
||||
return "months";
|
||||
case "y":
|
||||
return "year";
|
||||
return "years";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
tokenToString = (lildur) => (token) => {
|
||||
tokenToString = (lildur, info) => (token) => {
|
||||
const mapped = tokenToField(token);
|
||||
if (mapped) {
|
||||
return this.num(lildur.get(mapped), token.length);
|
||||
const inversionFactor =
|
||||
info.isNegativeDuration && mapped !== info.largestUnit ? invertLargest : 1;
|
||||
let signDisplay;
|
||||
if (this.opts.signMode === "negativeLargestOnly" && mapped !== info.largestUnit) {
|
||||
signDisplay = "never";
|
||||
} else if (this.opts.signMode === "all") {
|
||||
signDisplay = "always";
|
||||
} else {
|
||||
// "auto" and "negative" are the same, but "auto" has better support
|
||||
signDisplay = "auto";
|
||||
}
|
||||
return this.num(lildur.get(mapped) * inversionFactor, token.length, signDisplay);
|
||||
} else {
|
||||
return token;
|
||||
}
|
||||
@@ -2756,8 +2797,14 @@ class Formatter {
|
||||
(found, { literal, val }) => (literal ? found : found.concat(val)),
|
||||
[]
|
||||
),
|
||||
collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t));
|
||||
return stringifyTokens(tokens, tokenToString(collapsed));
|
||||
collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t)),
|
||||
durationInfo = {
|
||||
isNegativeDuration: collapsed < 0,
|
||||
// this relies on "collapsed" being based on "shiftTo", which builds up the object
|
||||
// in order
|
||||
largestUnit: Object.keys(collapsed.values)[0],
|
||||
};
|
||||
return stringifyTokens(tokens, tokenToString(collapsed, durationInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2818,11 +2865,11 @@ function simpleParse(...keys) {
|
||||
}
|
||||
|
||||
// ISO and SQL parsing
|
||||
const offsetRegex = /(?:(Z)|([+-]\d\d)(?::?(\d\d))?)/;
|
||||
const offsetRegex = /(?:([Zz])|([+-]\d\d)(?::?(\d\d))?)/;
|
||||
const isoExtendedZone = `(?:${offsetRegex.source}?(?:\\[(${ianaRegex.source})\\])?)?`;
|
||||
const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/;
|
||||
const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`);
|
||||
const isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`);
|
||||
const isoTimeExtensionRegex = RegExp(`(?:[Tt]${isoTimeRegex.source})?`);
|
||||
const isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/;
|
||||
const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/;
|
||||
const isoOrdinalRegex = /(\d{4})-?(\d{3})/;
|
||||
@@ -3537,9 +3584,13 @@ class Duration {
|
||||
* @param {string} fmt - the format string
|
||||
* @param {Object} opts - options
|
||||
* @param {boolean} [opts.floor=true] - floor numerical values
|
||||
* @param {'negative'|'all'|'negativeLargestOnly'} [opts.signMode=negative] - How to handle signs
|
||||
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("y d s") //=> "1 6 2"
|
||||
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("yy dd sss") //=> "01 06 002"
|
||||
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("M S") //=> "12 518402000"
|
||||
* @example Duration.fromObject({ days: 6, seconds: 2 }).toFormat("d s", { signMode: "all" }) //=> "+6 +2"
|
||||
* @example Duration.fromObject({ days: -6, seconds: -2 }).toFormat("d s", { signMode: "all" }) //=> "-6 -2"
|
||||
* @example Duration.fromObject({ days: -6, seconds: -2 }).toFormat("d s", { signMode: "negativeLargestOnly" }) //=> "-6 2"
|
||||
* @return {string}
|
||||
*/
|
||||
toFormat(fmt, opts = {}) {
|
||||
@@ -3559,21 +3610,25 @@ class Duration {
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options
|
||||
* @param {Object} opts - Formatting options. Accepts the same keys as the options parameter of the native `Intl.NumberFormat` constructor, as well as `listStyle`.
|
||||
* @param {string} [opts.listStyle='narrow'] - How to format the merged list. Corresponds to the `style` property of the options parameter of the native `Intl.ListFormat` constructor.
|
||||
* @param {boolean} [opts.showZeros=true] - Show all units previously used by the duration even if they are zero
|
||||
* @example
|
||||
* ```js
|
||||
* var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 })
|
||||
* dur.toHuman() //=> '1 day, 5 hours, 6 minutes'
|
||||
* dur.toHuman({ listStyle: "long" }) //=> '1 day, 5 hours, and 6 minutes'
|
||||
* dur.toHuman({ unitDisplay: "short" }) //=> '1 day, 5 hr, 6 min'
|
||||
* var dur = Duration.fromObject({ months: 1, weeks: 0, hours: 5, minutes: 6 })
|
||||
* dur.toHuman() //=> '1 month, 0 weeks, 5 hours, 6 minutes'
|
||||
* dur.toHuman({ listStyle: "long" }) //=> '1 month, 0 weeks, 5 hours, and 6 minutes'
|
||||
* dur.toHuman({ unitDisplay: "short" }) //=> '1 mth, 0 wks, 5 hr, 6 min'
|
||||
* dur.toHuman({ showZeros: false }) //=> '1 month, 5 hours, 6 minutes'
|
||||
* ```
|
||||
*/
|
||||
toHuman(opts = {}) {
|
||||
if (!this.isValid) return INVALID$2;
|
||||
|
||||
const showZeros = opts.showZeros !== false;
|
||||
|
||||
const l = orderedUnits$1
|
||||
.map((unit) => {
|
||||
const val = this.values[unit];
|
||||
if (isUndefined(val)) {
|
||||
if (isUndefined(val) || (val === 0 && !showZeros)) {
|
||||
return null;
|
||||
}
|
||||
return this.loc
|
||||
@@ -3933,6 +3988,17 @@ class Duration {
|
||||
return clone$1(this, { values: negated }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all units with values equal to 0 from this Duration.
|
||||
* @example Duration.fromObject({ years: 2, days: 0, hours: 0, minutes: 0 }).removeZeros().toObject() //=> { years: 2 }
|
||||
* @return {Duration}
|
||||
*/
|
||||
removeZeros() {
|
||||
if (!this.isValid) return this;
|
||||
const vals = removeZeroes(this.values);
|
||||
return clone$1(this, { values: vals }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the years.
|
||||
* @type {number}
|
||||
@@ -4243,7 +4309,8 @@ class Interval {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end of the Interval
|
||||
* Returns the end of the Interval. This is the first instant which is not part of the interval
|
||||
* (Interval is half-open).
|
||||
* @type {DateTime}
|
||||
*/
|
||||
get end() {
|
||||
@@ -5674,21 +5741,22 @@ function toTechFormat(dt, format, allowZ = true) {
|
||||
: null;
|
||||
}
|
||||
|
||||
function toISODate(o, extended) {
|
||||
function toISODate(o, extended, precision) {
|
||||
const longFormat = o.c.year > 9999 || o.c.year < 0;
|
||||
let c = "";
|
||||
if (longFormat && o.c.year >= 0) c += "+";
|
||||
c += padStart(o.c.year, longFormat ? 6 : 4);
|
||||
|
||||
if (precision === "year") return c;
|
||||
if (extended) {
|
||||
c += "-";
|
||||
c += padStart(o.c.month);
|
||||
if (precision === "month") return c;
|
||||
c += "-";
|
||||
c += padStart(o.c.day);
|
||||
} else {
|
||||
c += padStart(o.c.month);
|
||||
c += padStart(o.c.day);
|
||||
if (precision === "month") return c;
|
||||
}
|
||||
c += padStart(o.c.day);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -5698,26 +5766,39 @@ function toISOTime(
|
||||
suppressSeconds,
|
||||
suppressMilliseconds,
|
||||
includeOffset,
|
||||
extendedZone
|
||||
extendedZone,
|
||||
precision
|
||||
) {
|
||||
let c = padStart(o.c.hour);
|
||||
if (extended) {
|
||||
c += ":";
|
||||
c += padStart(o.c.minute);
|
||||
if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
|
||||
c += ":";
|
||||
}
|
||||
} else {
|
||||
c += padStart(o.c.minute);
|
||||
}
|
||||
|
||||
if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
|
||||
c += padStart(o.c.second);
|
||||
|
||||
if (o.c.millisecond !== 0 || !suppressMilliseconds) {
|
||||
c += ".";
|
||||
c += padStart(o.c.millisecond, 3);
|
||||
}
|
||||
let showSeconds = !suppressSeconds || o.c.millisecond !== 0 || o.c.second !== 0,
|
||||
c = "";
|
||||
switch (precision) {
|
||||
case "day":
|
||||
case "month":
|
||||
case "year":
|
||||
break;
|
||||
default:
|
||||
c += padStart(o.c.hour);
|
||||
if (precision === "hour") break;
|
||||
if (extended) {
|
||||
c += ":";
|
||||
c += padStart(o.c.minute);
|
||||
if (precision === "minute") break;
|
||||
if (showSeconds) {
|
||||
c += ":";
|
||||
c += padStart(o.c.second);
|
||||
}
|
||||
} else {
|
||||
c += padStart(o.c.minute);
|
||||
if (precision === "minute") break;
|
||||
if (showSeconds) {
|
||||
c += padStart(o.c.second);
|
||||
}
|
||||
}
|
||||
if (precision === "second") break;
|
||||
if (showSeconds && (!suppressMilliseconds || o.c.millisecond !== 0)) {
|
||||
c += ".";
|
||||
c += padStart(o.c.millisecond, 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeOffset) {
|
||||
@@ -5909,8 +5990,9 @@ function quickDT(obj, opts) {
|
||||
|
||||
function diffRelative(start, end, opts) {
|
||||
const round = isUndefined(opts.round) ? true : opts.round,
|
||||
rounding = isUndefined(opts.rounding) ? "trunc" : opts.rounding,
|
||||
format = (c, unit) => {
|
||||
c = roundTo(c, round || opts.calendary ? 0 : 2, true);
|
||||
c = roundTo(c, round || opts.calendary ? 0 : 2, opts.calendary ? "round" : rounding);
|
||||
const formatter = end.loc.clone(opts).relFormatter(opts);
|
||||
return formatter.format(c, unit);
|
||||
},
|
||||
@@ -7289,10 +7371,13 @@ class DateTime {
|
||||
* @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00'
|
||||
* @param {boolean} [opts.extendedZone=false] - add the time zone format extension
|
||||
* @param {string} [opts.format='extended'] - choose between the basic and extended format
|
||||
* @param {string} [opts.precision='milliseconds'] - truncate output to desired presicion: 'years', 'months', 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. When precision and suppressSeconds or suppressMilliseconds are used together, precision sets the maximum unit shown in the output, however seconds or milliseconds will still be suppressed if they are 0.
|
||||
* @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z'
|
||||
* @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00'
|
||||
* @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335'
|
||||
* @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400'
|
||||
* @example DateTime.now().toISO({ precision: 'day' }) //=> '2017-04-22Z'
|
||||
* @example DateTime.now().toISO({ precision: 'minute' }) //=> '2017-04-22T20:47Z'
|
||||
* @return {string|null}
|
||||
*/
|
||||
toISO({
|
||||
@@ -7301,16 +7386,26 @@ class DateTime {
|
||||
suppressMilliseconds = false,
|
||||
includeOffset = true,
|
||||
extendedZone = false,
|
||||
precision = "milliseconds",
|
||||
} = {}) {
|
||||
if (!this.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
precision = normalizeUnit(precision);
|
||||
const ext = format === "extended";
|
||||
|
||||
let c = toISODate(this, ext);
|
||||
c += "T";
|
||||
c += toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone);
|
||||
let c = toISODate(this, ext, precision);
|
||||
if (orderedUnits.indexOf(precision) >= 3) c += "T";
|
||||
c += toISOTime(
|
||||
this,
|
||||
ext,
|
||||
suppressSeconds,
|
||||
suppressMilliseconds,
|
||||
includeOffset,
|
||||
extendedZone,
|
||||
precision
|
||||
);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -7318,16 +7413,17 @@ class DateTime {
|
||||
* Returns an ISO 8601-compliant string representation of this DateTime's date component
|
||||
* @param {Object} opts - options
|
||||
* @param {string} [opts.format='extended'] - choose between the basic and extended format
|
||||
* @param {string} [opts.precision='day'] - truncate output to desired precision: 'years', 'months', or 'days'.
|
||||
* @example DateTime.utc(1982, 5, 25).toISODate() //=> '1982-05-25'
|
||||
* @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'
|
||||
* @example DateTime.utc(1982, 5, 25).toISODate({ precision: 'month' }) //=> '1982-05'
|
||||
* @return {string|null}
|
||||
*/
|
||||
toISODate({ format = "extended" } = {}) {
|
||||
toISODate({ format = "extended", precision = "day" } = {}) {
|
||||
if (!this.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toISODate(this, format === "extended");
|
||||
return toISODate(this, format === "extended", normalizeUnit(precision));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7348,10 +7444,12 @@ class DateTime {
|
||||
* @param {boolean} [opts.extendedZone=true] - add the time zone format extension
|
||||
* @param {boolean} [opts.includePrefix=false] - include the `T` prefix
|
||||
* @param {string} [opts.format='extended'] - choose between the basic and extended format
|
||||
* @param {string} [opts.precision='milliseconds'] - truncate output to desired presicion: 'hours', 'minutes', 'seconds' or 'milliseconds'. When precision and suppressSeconds or suppressMilliseconds are used together, precision sets the maximum unit shown in the output, however seconds or milliseconds will still be suppressed if they are 0.
|
||||
* @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime() //=> '07:34:19.361Z'
|
||||
* @example DateTime.utc().set({ hour: 7, minute: 34, seconds: 0, milliseconds: 0 }).toISOTime({ suppressSeconds: true }) //=> '07:34Z'
|
||||
* @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ format: 'basic' }) //=> '073419.361Z'
|
||||
* @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z'
|
||||
* @example DateTime.utc().set({ hour: 7, minute: 34, second: 56 }).toISOTime({ precision: 'minute' }) //=> '07:34Z'
|
||||
* @return {string}
|
||||
*/
|
||||
toISOTime({
|
||||
@@ -7361,12 +7459,14 @@ class DateTime {
|
||||
includePrefix = false,
|
||||
extendedZone = false,
|
||||
format = "extended",
|
||||
precision = "milliseconds",
|
||||
} = {}) {
|
||||
if (!this.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let c = includePrefix ? "T" : "";
|
||||
precision = normalizeUnit(precision);
|
||||
let c = includePrefix && orderedUnits.indexOf(precision) >= 3 ? "T" : "";
|
||||
return (
|
||||
c +
|
||||
toISOTime(
|
||||
@@ -7375,7 +7475,8 @@ class DateTime {
|
||||
suppressSeconds,
|
||||
suppressMilliseconds,
|
||||
includeOffset,
|
||||
extendedZone
|
||||
extendedZone,
|
||||
precision
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -7653,12 +7754,13 @@ class DateTime {
|
||||
|
||||
/**
|
||||
* Returns a string representation of a this time relative to now, such as "in two days". Can only internationalize if your
|
||||
* platform supports Intl.RelativeTimeFormat. Rounds down by default.
|
||||
* platform supports Intl.RelativeTimeFormat. Rounds towards zero by default.
|
||||
* @param {Object} options - options that affect the output
|
||||
* @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now.
|
||||
* @param {string} [options.style="long"] - the style of units, must be "long", "short", or "narrow"
|
||||
* @param {string|string[]} options.unit - use a specific unit or array of units; if omitted, or an array, the method will pick the best unit. Use an array or one of "years", "quarters", "months", "weeks", "days", "hours", "minutes", or "seconds"
|
||||
* @param {boolean} [options.round=true] - whether to round the numbers in the output.
|
||||
* @param {string} [options.rounding="trunc"] - rounding method to use when rounding the numbers in the output. Can be "trunc" (toward zero), "expand" (away from zero), "round", "floor", or "ceil".
|
||||
* @param {number} [options.padding=0] - padding in milliseconds. This allows you to round up the result if it fits inside the threshold. Don't use in combination with {round: false} because the decimal output will include the padding.
|
||||
* @param {string} options.locale - override the locale of this DateTime
|
||||
* @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this
|
||||
@@ -8025,7 +8127,7 @@ function friendlyDateTime(dateTimeish) {
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION = "3.6.1";
|
||||
const VERSION = "3.7.1";
|
||||
|
||||
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||
//# sourceMappingURL=luxon.js.map
|
||||
|
||||
627
server/scripts/vendor/auto/metar-taf-parser.mjs
vendored
627
server/scripts/vendor/auto/metar-taf-parser.mjs
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
@@ -1,5 +1,9 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
#hazards-html.weather-display {
|
||||
background-image: url('../images/backgrounds/7.png');
|
||||
}
|
||||
|
||||
.weather-display .main.hazards {
|
||||
&.main {
|
||||
@@ -7,6 +11,7 @@
|
||||
height: 480px;
|
||||
background-color: rgb(112, 35, 35);
|
||||
|
||||
|
||||
.hazard-lines {
|
||||
min-height: 400px;
|
||||
padding-top: 10px;
|
||||
@@ -26,3 +31,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wide.hazards #container {
|
||||
background: url(../images/backgrounds/7-wide.png);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/_colors'as c;
|
||||
|
||||
@font-face {
|
||||
font-family: "Star4000";
|
||||
@@ -161,6 +161,7 @@ body {
|
||||
#divTwcMain {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
position: relative;
|
||||
|
||||
.wide & {
|
||||
width: 854px;
|
||||
@@ -813,4 +814,4 @@ body.kiosk #loading .instructions {
|
||||
>*:not(#divTwc) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
.weather-display {
|
||||
width: 640px;
|
||||
@@ -116,9 +116,11 @@
|
||||
.scroll {
|
||||
@include u.text-shadow(3px, 1.5px);
|
||||
width: 640px;
|
||||
height: 70px;
|
||||
height: 77px;
|
||||
overflow: hidden;
|
||||
margin-top: 3px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&.hazard {
|
||||
background-color: rgb(112, 35, 35);
|
||||
@@ -159,3 +161,18 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#scroll-bg {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
height: 77px;
|
||||
width: 640px;
|
||||
|
||||
&.hazard {
|
||||
background-color: rgb(112, 35, 35);
|
||||
}
|
||||
}
|
||||
|
||||
.wide #scroll-bg {
|
||||
width: 854px;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ const tester = async (location, testPage) => {
|
||||
// run all the locations
|
||||
for (let i = 0; i < LOCATIONS.length; i += 1) {
|
||||
const location = LOCATIONS[i];
|
||||
console.log(location);
|
||||
console.log(`${i + 1}/${LOCATIONS.length} ${location}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await tester(location, page);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
<%- include('partials/hazards.ejs') %>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scroll-bg"></div>
|
||||
</div>
|
||||
<div id="divTwcBottom">
|
||||
<div id="divTwcBottomLeft">
|
||||
@@ -191,6 +192,8 @@
|
||||
Station Id: <span id="spanStationId"></span><br />
|
||||
Radar Id: <span id="spanRadarId"></span><br />
|
||||
Zone Id: <span id="spanZoneId"></span><br />
|
||||
Office Id: <span id="spanOfficeId"></span><br />
|
||||
Grid X,Y: <span id="spanGridPoint"></span><br />
|
||||
Music: <span id="musicTrack">Not playing</span><br />
|
||||
Ws4kp Version: <span><%- version %></span>
|
||||
</div>
|
||||
@@ -198,4 +201,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user