mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 17:19:30 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4903b95fec | ||
|
|
b43fb32820 | ||
|
|
0d0c4ec452 | ||
|
|
49d18c2fbe | ||
|
|
1732a3381f | ||
|
|
093b6ac239 | ||
|
|
12d068d740 | ||
|
|
517c560ef6 | ||
|
|
3eb571bed4 | ||
|
|
52ca161bdb | ||
|
|
ee5690dcad | ||
|
|
c05b827593 | ||
|
|
bef42a3da2 | ||
|
|
13ff0317e6 | ||
|
|
5cc85840a9 | ||
|
|
190e50e2f3 | ||
|
|
aa7ac64827 | ||
|
|
2ab737d5a5 | ||
|
|
ecf0999675 | ||
|
|
6a49b7b6ce | ||
|
|
5ffff03db9 | ||
|
|
c8a25e5d9a | ||
|
|
ea58b5a9c8 | ||
|
|
4bf725413b | ||
|
|
75eb81887f |
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:
|
Please include:
|
||||||
* Web browser and OS
|
* 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": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
],
|
],
|
||||||
|
"args": [
|
||||||
|
"--use-cache"
|
||||||
|
],
|
||||||
|
"type": "node"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Data:stations-api",
|
||||||
|
"program": "${workspaceFolder}/datagenerators/stations.mjs",
|
||||||
|
"request": "launch",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
"type": "node"
|
"type": "node"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ FROM node:24-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --omit=dev --legacy-peer-deps
|
RUN npm ci --legacy-peer-deps
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -309,11 +309,13 @@ If you're unable to pre-set the play state before entering kiosk mode (such as w
|
|||||||
|
|
||||||
## Community Notes
|
## 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)
|
* [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.
|
* [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.
|
* [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
|
## Customization
|
||||||
|
|
||||||
@@ -335,6 +337,10 @@ Before reporting an issue or requesting a feature please consider that this is n
|
|||||||
|
|
||||||
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||||
|
|
||||||
|
## The full moon icon is broken
|
||||||
|
|
||||||
|
This is a known problem with the Ws4kp as it ages. It was a problem with the [actual Weatherstar hardware](https://youtu.be/rcUwlZ4pqh0?feature=shared&t=116) as well.
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
|
|
||||||
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
||||||
|
|||||||
@@ -84,8 +84,8 @@
|
|||||||
"lat": 29.7633,
|
"lat": 29.7633,
|
||||||
"lon": -95.3633,
|
"lon": -95.3633,
|
||||||
"point": {
|
"point": {
|
||||||
"x": 65,
|
"x": 63,
|
||||||
"y": 97,
|
"y": 95,
|
||||||
"wfo": "HGX"
|
"wfo": "HGX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"city": "Washington DC",
|
"city": "Washington",
|
||||||
"lat": 38.8951,
|
"lat": 38.8951,
|
||||||
"lon": -77.0364,
|
"lon": -77.0364,
|
||||||
"point": {
|
"point": {
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
"lat": 61.2181,
|
"lat": 61.2181,
|
||||||
"lon": -149.9003,
|
"lon": -149.9003,
|
||||||
"point": {
|
"point": {
|
||||||
"x": 125,
|
"x": 143,
|
||||||
"y": 236,
|
"y": 236,
|
||||||
"wfo": "AER"
|
"wfo": "AER"
|
||||||
}
|
}
|
||||||
@@ -734,8 +734,8 @@
|
|||||||
"lat": 42.9956,
|
"lat": 42.9956,
|
||||||
"lon": -71.4548,
|
"lon": -71.4548,
|
||||||
"point": {
|
"point": {
|
||||||
"x": 42,
|
"x": 38,
|
||||||
"y": 21,
|
"y": 20,
|
||||||
"wfo": "GYX"
|
"wfo": "GYX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -884,8 +884,8 @@
|
|||||||
"lat": 43.6615,
|
"lat": 43.6615,
|
||||||
"lon": -70.2553,
|
"lon": -70.2553,
|
||||||
"point": {
|
"point": {
|
||||||
"x": 76,
|
"x": 72,
|
||||||
"y": 59,
|
"y": 58,
|
||||||
"wfo": "GYX"
|
"wfo": "GYX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,7 @@
|
|||||||
"lon": -82.5329
|
"lon": -82.5329
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"city": "Washington DC",
|
"city": "Washington",
|
||||||
"lat": 38.8951,
|
"lat": 38.8951,
|
||||||
"lon": -77.0364
|
"lon": -77.0364
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
import * as url from 'node:url';
|
||||||
|
|
||||||
// Load station data
|
// Load station data
|
||||||
const stationInfo = JSON.parse(readFileSync('./datagenerators/output/stations-raw.json', 'utf8'));
|
const stationInfo = JSON.parse(readFileSync('./datagenerators/output/stations-raw.json', 'utf8'));
|
||||||
// const regionalCities = JSON.parse(readFileSync('./datagenerators/output/regionalcities.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}'
|
jq -c '.[] | select(.name | test("^[A-Z]{3}$")) | {state, city, simple, name}'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const diffMode = process.argv.includes('--diff');
|
const readArguments = () => ({
|
||||||
const onlyProblems = process.argv.includes('--only-problems');
|
diffMode: process.argv.includes('--diff'),
|
||||||
const noProblems = process.argv.includes('--no-problems');
|
onlyProblems: process.argv.includes('--only-problems'),
|
||||||
const onlyDuplicates = process.argv.includes('--only-dupes');
|
noProblems: process.argv.includes('--no-problems'),
|
||||||
const noPriority = process.argv.includes('--no-priority');
|
onlyDuplicates: process.argv.includes('--only-dupes'),
|
||||||
const noSimple = process.argv.includes('--no-simple');
|
noPriority: process.argv.includes('--no-priority'),
|
||||||
const noCoordinates = process.argv.includes('--no-coords');
|
noSimple: process.argv.includes('--no-simple'),
|
||||||
const writeFile = process.argv.includes('--write');
|
noCoordinates: process.argv.includes('--no-coords'),
|
||||||
|
writeFile: process.argv.includes('--write'),
|
||||||
|
});
|
||||||
|
|
||||||
// Process ALL stations at once to get the display name map
|
const DEFAULT_OPTIONS = {
|
||||||
let displayNameMap = processAllStations(stationInfo);
|
diffMode: false,
|
||||||
|
onlyProblems: false,
|
||||||
|
noProblems: false,
|
||||||
|
onlyDuplicates: false,
|
||||||
|
noPriority: false,
|
||||||
|
noSimple: false,
|
||||||
|
noCoordinates: false,
|
||||||
|
writeFile: false,
|
||||||
|
};
|
||||||
|
|
||||||
// Apply priority-based deduplication
|
const postProcessor = (_options) => {
|
||||||
displayNameMap = resolveDuplicatesByPriority(displayNameMap, stationInfo);
|
// 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
|
// Apply priority-based deduplication
|
||||||
const stations = Object.values(stationInfo);
|
displayNameMap = resolveDuplicatesByPriority(displayNameMap, stationInfo);
|
||||||
stations.forEach((station) => {
|
|
||||||
const originalName = station.city;
|
|
||||||
const processedName = processingUtils.finalCleanup(displayNameMap[station.id]); // Look up by station ID
|
|
||||||
|
|
||||||
// Get airport type and priority for this station
|
const results = [];
|
||||||
const airportType = getAirportType(originalName, station.id); // Pass station ID for enhanced detection
|
|
||||||
const priority = getAirportPriority(airportType);
|
|
||||||
|
|
||||||
const potentialIssues = [];
|
// Now iterate through stations and use the pre-computed display names
|
||||||
// Check if the processed name contains punctuation (a period at the end is OK)
|
const stations = Object.values(stationInfo);
|
||||||
if (/[,;!?/:.]/.test(processedName) && !processedName.endsWith('.')) {
|
stations.forEach((station) => {
|
||||||
potentialIssues.push('punctuation');
|
const originalName = station.city;
|
||||||
}
|
const processedName = processingUtils.finalCleanup(displayNameMap[station.id]); // Look up by station ID
|
||||||
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({
|
// Get airport type and priority for this station
|
||||||
id: station.id,
|
const airportType = getAirportType(originalName, station.id); // Pass station ID for enhanced detection
|
||||||
lat: station.lat,
|
const priority = getAirportPriority(airportType);
|
||||||
lon: station.lon,
|
|
||||||
state: station.state,
|
const potentialIssues = [];
|
||||||
location: originalName, // original full location name
|
// Check if the processed name contains punctuation (a period at the end is OK)
|
||||||
city: processedName, // processed city name for display
|
if (/[,;!?/:.]/.test(processedName) && !processedName.endsWith('.')) {
|
||||||
simple: originalName.match(/[^,/;\\-]*/)[0].substr(0, 12).trim(),
|
potentialIssues.push('punctuation');
|
||||||
type: airportType,
|
}
|
||||||
priority,
|
if (processedName.length > 12) {
|
||||||
potentialIssues,
|
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
|
// Check for duplicates by state
|
||||||
const cleanedMapByState = new Map();
|
const cleanedMapByState = new Map();
|
||||||
|
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
const { state } = result;
|
const { state } = result;
|
||||||
if (!cleanedMapByState.has(state)) {
|
if (!cleanedMapByState.has(state)) {
|
||||||
cleanedMapByState.set(state, new Map());
|
cleanedMapByState.set(state, new Map());
|
||||||
}
|
}
|
||||||
const stateMap = cleanedMapByState.get(state);
|
const stateMap = cleanedMapByState.get(state);
|
||||||
if (stateMap.has(result.city)) {
|
if (stateMap.has(result.city)) {
|
||||||
stateMap.get(result.city).push(result);
|
stateMap.get(result.city).push(result);
|
||||||
} else {
|
} else {
|
||||||
stateMap.set(result.city, [result]);
|
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Filter results if requested
|
cleanedMapByState.forEach((stateMap, _state) => {
|
||||||
let finalResults = results;
|
stateMap.forEach((originals, _cleaned) => {
|
||||||
if (onlyProblems) {
|
if (originals.length > 1) {
|
||||||
finalResults = results.filter((r) => r.potentialIssues.length > 0);
|
originals.forEach((original) => {
|
||||||
}
|
if (!original.potentialIssues.includes('duplicate')) {
|
||||||
if (onlyDuplicates) {
|
original.potentialIssues.push('duplicate');
|
||||||
finalResults = finalResults.filter((r) => r.potentialIssues.includes('duplicate'));
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const outputResult = finalResults.map((result) => {
|
// Filter results if requested
|
||||||
let outputItem = result;
|
let finalResults = results;
|
||||||
|
if (options.onlyProblems) {
|
||||||
// Don't include lat or long in diff mode
|
finalResults = results.filter((r) => r.potentialIssues.length > 0);
|
||||||
if (noCoordinates || diffMode) {
|
}
|
||||||
const {
|
if (options.onlyDuplicates) {
|
||||||
lat: _lat, lon: _lon, ...resultWithoutLocation
|
finalResults = finalResults.filter((r) => r.potentialIssues.includes('duplicate'));
|
||||||
} = result;
|
|
||||||
outputItem = resultWithoutLocation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't include potentialIssues when --no-problems is specified
|
const outputResult = finalResults.map((result) => {
|
||||||
if (noProblems || diffMode) {
|
let outputItem = result;
|
||||||
const { potentialIssues: _potentialIssues, ...resultWithoutIssues } = outputItem;
|
|
||||||
outputItem = resultWithoutIssues;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove type and priority if --no-priority is specified
|
// Don't include lat or long in diff mode
|
||||||
if (noPriority || diffMode) {
|
if (options.noCoordinates || options.diffMode) {
|
||||||
const { type: _type, priority: _priority, ...resultWithoutPriority } = outputItem;
|
const {
|
||||||
outputItem = resultWithoutPriority;
|
lat: _lat, lon: _lon, ...resultWithoutLocation
|
||||||
}
|
} = result;
|
||||||
|
outputItem = resultWithoutLocation;
|
||||||
|
}
|
||||||
|
|
||||||
// remove simple field if --no-simple is specified
|
// Don't include potentialIssues when --no-problems is specified
|
||||||
if (noSimple || diffMode) {
|
if (options.noProblems || options.diffMode) {
|
||||||
const { simple: _simple, ...resultWithoutSimple } = outputItem;
|
const { potentialIssues: _potentialIssues, ...resultWithoutIssues } = outputItem;
|
||||||
outputItem = resultWithoutSimple;
|
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(({
|
const fileResults = results.map(({
|
||||||
simple: _simple, type: _type, potentialIssues: _potentialIssues, ...rest
|
simple: _simple, type: _type, potentialIssues: _potentialIssues, location: _location, ...rest
|
||||||
}) => rest);
|
}) => rest);
|
||||||
|
|
||||||
writeFileSync('./datagenerators/output/stations.json', compactStringifyToObject(fileResults));
|
if (options.writeFile) {
|
||||||
console.log(`Wrote ${fileResults.length} processed stations to datagenerators/output/stations.json`);
|
writeFileSync('./datagenerators/output/stations.json', compactStringifyToObject(fileResults));
|
||||||
} else {
|
console.log(`Wrote ${fileResults.length} processed stations to datagenerators/output/stations.json`);
|
||||||
console.log(compactStringifyToArray(outputResult));
|
} 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
|
// list all stations in a single file
|
||||||
// only find stations with 4 letter codes
|
// only find stations with 4 letter codes
|
||||||
|
|
||||||
@@ -6,67 +7,91 @@ import https from './https.mjs';
|
|||||||
import states from './stations-states.mjs';
|
import states from './stations-states.mjs';
|
||||||
import chunk from './chunk.mjs';
|
import chunk from './chunk.mjs';
|
||||||
import overrides from './stations-overrides.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
|
// 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'];
|
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
|
// chunk the list of states
|
||||||
const chunkStates = chunk(states, 1);
|
const chunkStates = chunk(states, 3);
|
||||||
|
|
||||||
// store output
|
// store output
|
||||||
const output = {};
|
const output = {};
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
// process all chunks
|
// get data from api if desired
|
||||||
for (let i = 0; i < chunkStates.length; i += 1) {
|
if (!USE_CACHE) {
|
||||||
const stateChunk = chunkStates[i];
|
// process all chunks
|
||||||
// loop through states
|
for (let i = 0; i < chunkStates.length; i += 1) {
|
||||||
|
const stateChunk = chunkStates[i];
|
||||||
|
// loop through states
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await Promise.allSettled(stateChunk.map(async (state) => {
|
await Promise.allSettled(stateChunk.map(async (state) => {
|
||||||
try {
|
try {
|
||||||
let stations;
|
let stations;
|
||||||
let next = `https://api.weather.gov/stations?state=${state}`;
|
let next = `https://api.weather.gov/stations?state=${state}`;
|
||||||
let round = 0;
|
let round = 0;
|
||||||
do {
|
do {
|
||||||
console.log(`Getting: ${state}-${round}`);
|
console.log(`Getting: ${state}-${round}`);
|
||||||
// get list and parse the JSON
|
// get list and parse the JSON
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const stationsRaw = await https(next);
|
const stationsRaw = await https(next);
|
||||||
stations = JSON.parse(stationsRaw);
|
stations = JSON.parse(stationsRaw);
|
||||||
// filter stations for 4 letter identifiers
|
// filter stations for 4 letter identifiers
|
||||||
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
|
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
|
||||||
// filter against starting letter
|
// filter against starting letter
|
||||||
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
||||||
// add each resulting station to the output
|
// add each resulting station to the output
|
||||||
stationsFiltered.forEach((station) => {
|
stationsFiltered.forEach((station) => {
|
||||||
const id = station.properties.stationIdentifier;
|
const id = station.properties.stationIdentifier;
|
||||||
if (output[id]) {
|
if (output[id]) {
|
||||||
console.log(`Duplicate station: ${state}-${id}`);
|
console.log(`Duplicate station: ${state}-${id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// get any overrides if available
|
output[id] = {
|
||||||
const override = overrides[id] ?? {};
|
id,
|
||||||
output[id] = {
|
city: station.properties.name,
|
||||||
id,
|
state,
|
||||||
city: station.properties.name,
|
lat: station.geometry.coordinates[1],
|
||||||
state,
|
lon: station.geometry.coordinates[0],
|
||||||
lat: station.geometry.coordinates[1],
|
};
|
||||||
lon: station.geometry.coordinates[0],
|
});
|
||||||
// finally add the overrides
|
next = stations?.pagination?.next;
|
||||||
...override,
|
round += 1;
|
||||||
};
|
// write the output
|
||||||
});
|
writeFileSync('./datagenerators/output/stations-raw.json', JSON.stringify(output, null, 2));
|
||||||
next = stations?.pagination?.next;
|
}
|
||||||
round += 1;
|
while (next && stations.features.length > 0);
|
||||||
// write the output
|
completed += 1;
|
||||||
writeFileSync('./datagenerators/output/stations-raw.json', JSON.stringify(output, null, 2));
|
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));
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable import/no-extraneous-dependencies */
|
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import {
|
import {
|
||||||
src, dest, series, parallel,
|
src, dest, series, parallel,
|
||||||
@@ -58,15 +57,6 @@ const jsVendorSources = [
|
|||||||
'server/scripts/vendor/auto/suncalc.js',
|
'server/scripts/vendor/auto/suncalc.js',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Copy metar-taf-parser separately since it's an ES module with locale dependencies
|
|
||||||
const metarVendorSources = [
|
|
||||||
'server/scripts/vendor/auto/metar-taf-parser.mjs',
|
|
||||||
'server/scripts/vendor/auto/locale/en.js',
|
|
||||||
];
|
|
||||||
|
|
||||||
const copyMetarVendor = () => src(metarVendorSources, { base: 'server/scripts/vendor/auto' })
|
|
||||||
.pipe(dest(`${RESOURCES_PATH}/vendor/auto`));
|
|
||||||
|
|
||||||
const compressJsVendor = () => src(jsVendorSources)
|
const compressJsVendor = () => src(jsVendorSources)
|
||||||
.pipe(concat('vendor.min.js'))
|
.pipe(concat('vendor.min.js'))
|
||||||
.pipe(terser())
|
.pipe(terser())
|
||||||
@@ -107,23 +97,27 @@ const copyCss = () => src(cssSources)
|
|||||||
const htmlSources = [
|
const htmlSources = [
|
||||||
'views/*.ejs',
|
'views/*.ejs',
|
||||||
];
|
];
|
||||||
const compressHtml = async () => {
|
const packageJson = await readFile('package.json');
|
||||||
const packageJson = await readFile('package.json');
|
let { version } = JSON.parse(packageJson);
|
||||||
const { version } = JSON.parse(packageJson);
|
const previewVersion = async () => {
|
||||||
|
// generate a relatively unique timestamp for cache invalidation of the preview site
|
||||||
return src(htmlSources)
|
const now = new Date();
|
||||||
.pipe(ejs({
|
const msNow = now.getTime() % 1_000_000;
|
||||||
production: version,
|
version = msNow.toString();
|
||||||
serverAvailable: false,
|
|
||||||
version,
|
|
||||||
OVERRIDES,
|
|
||||||
query: {},
|
|
||||||
}))
|
|
||||||
.pipe(rename({ extname: '.html' }))
|
|
||||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
|
||||||
.pipe(dest('./dist'));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = [
|
const otherFiles = [
|
||||||
'server/robots.txt',
|
'server/robots.txt',
|
||||||
'server/manifest.json',
|
'server/manifest.json',
|
||||||
@@ -210,12 +204,12 @@ const buildPlaylist = async () => {
|
|||||||
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyMetarVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||||
|
|
||||||
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
// 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
|
// 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 publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
||||||
const stageFrontend = series(buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||||
|
|
||||||
export default publishFrontend;
|
export default publishFrontend;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable import/no-extraneous-dependencies */
|
|
||||||
import { src, series, dest } from 'gulp';
|
import { src, series, dest } from 'gulp';
|
||||||
import { deleteAsync } from 'del';
|
import { deleteAsync } from 'del';
|
||||||
import rename from 'gulp-rename';
|
import rename from 'gulp-rename';
|
||||||
|
|||||||
3842
package-lock.json
generated
3842
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.0.0",
|
"version": "6.1.7",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"metar-taf-parser": "^6.1.2"
|
"metar-taf-parser": "^9.0.0",
|
||||||
|
"npm": "^11.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
server/images/gimp/Full-Moon-Degraded.xcf
Normal file
BIN
server/images/gimp/Full-Moon-Degraded.xcf
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
server/images/gimp/No-Data.xcf
Normal file
BIN
server/images/gimp/No-Data.xcf
Normal file
Binary file not shown.
BIN
server/images/icons/current-conditions/No-Data.gif
Normal file
BIN
server/images/icons/current-conditions/No-Data.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/images/icons/moon-phases/Full-Moon-Degraded.gif
Normal file
BIN
server/images/icons/moon-phases/Full-Moon-Degraded.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1009 B |
@@ -145,6 +145,8 @@ const init = async () => {
|
|||||||
document.querySelector('#spanStationId').innerHTML = '';
|
document.querySelector('#spanStationId').innerHTML = '';
|
||||||
document.querySelector('#spanRadarId').innerHTML = '';
|
document.querySelector('#spanRadarId').innerHTML = '';
|
||||||
document.querySelector('#spanZoneId').innerHTML = '';
|
document.querySelector('#spanZoneId').innerHTML = '';
|
||||||
|
document.querySelector('#spanOfficeId').innerHTML = '';
|
||||||
|
document.querySelector('#spanGridPoint').innerHTML = '';
|
||||||
|
|
||||||
localStorage.removeItem('play');
|
localStorage.removeItem('play');
|
||||||
postMessage('navButton', 'play');
|
postMessage('navButton', 'play');
|
||||||
|
|||||||
@@ -9,11 +9,19 @@ class Almanac extends WeatherDisplay {
|
|||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Almanac', true);
|
super(navId, elemId, 'Almanac', true);
|
||||||
|
|
||||||
|
// occasional degraded moon icon
|
||||||
|
this.iconPaths = {
|
||||||
|
Full: imageName(Math.random() > 0.995 ? 'Degraded' : 'Full'),
|
||||||
|
Last: imageName('Last'),
|
||||||
|
New: imageName('New'),
|
||||||
|
First: imageName('First'),
|
||||||
|
};
|
||||||
|
|
||||||
// preload the moon images
|
// preload the moon images
|
||||||
preloadImg(imageName('Full'));
|
preloadImg(this.iconPaths.Full);
|
||||||
preloadImg(imageName('Last'));
|
preloadImg(this.iconPaths.Last);
|
||||||
preloadImg(imageName('New'));
|
preloadImg(this.iconPaths.New);
|
||||||
preloadImg(imageName('First'));
|
preloadImg(this.iconPaths.First);
|
||||||
|
|
||||||
this.timing.totalScreens = 1;
|
this.timing.totalScreens = 1;
|
||||||
}
|
}
|
||||||
@@ -142,7 +150,7 @@ class Almanac extends WeatherDisplay {
|
|||||||
|
|
||||||
fill.date = date;
|
fill.date = date;
|
||||||
fill.type = MoonPhase.phase;
|
fill.type = MoonPhase.phase;
|
||||||
fill.icon = { type: 'img', src: imageName(MoonPhase.phase) };
|
fill.icon = { type: 'img', src: this.iconPaths[MoonPhase.phase] };
|
||||||
|
|
||||||
return this.fillTemplate('day', fill);
|
return this.fillTemplate('day', fill);
|
||||||
});
|
});
|
||||||
@@ -169,6 +177,8 @@ const imageName = (type) => {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Full':
|
case 'Full':
|
||||||
return 'images/icons/moon-phases/Full-Moon.gif';
|
return 'images/icons/moon-phases/Full-Moon.gif';
|
||||||
|
case 'Degraded':
|
||||||
|
return 'images/icons/moon-phases/Full-Moon-Degraded.gif';
|
||||||
case 'Last':
|
case 'Last':
|
||||||
return 'images/icons/moon-phases/Last-Quarter.gif';
|
return 'images/icons/moon-phases/Last-Quarter.gif';
|
||||||
case 'New':
|
case 'New':
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ name: 'temperature', check: (props) => props.temperature?.value === null, required: true },
|
{ name: 'temperature', check: (props) => props.temperature?.value === null, required: true },
|
||||||
{ name: 'textDescription', check: (props) => props.textDescription === null || props.textDescription === '', required: true },
|
{ name: 'textDescription', check: (props) => props.textDescription === null || props.textDescription === '', required: true },
|
||||||
{ name: 'icon', check: (props) => props.icon === null, required: true },
|
|
||||||
{ name: 'windSpeed', check: (props) => props.windSpeed?.value === null, required: false },
|
{ name: 'windSpeed', check: (props) => props.windSpeed?.value === null, required: false },
|
||||||
{ name: 'dewpoint', check: (props) => props.dewpoint?.value === null, required: false },
|
{ name: 'dewpoint', check: (props) => props.dewpoint?.value === null, required: false },
|
||||||
{ name: 'barometricPressure', check: (props) => props.barometricPressure?.value === null, required: false },
|
{ name: 'barometricPressure', check: (props) => props.barometricPressure?.value === null, required: false },
|
||||||
|
|||||||
@@ -137,10 +137,6 @@ const parse = (fullForecast, forecastUrl) => {
|
|||||||
}
|
}
|
||||||
// get the object to modify/populate
|
// get the object to modify/populate
|
||||||
const fDay = forecast[destIndex];
|
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
|
// preload the icon
|
||||||
preloadImg(fDay.icon);
|
preloadImg(fDay.icon);
|
||||||
@@ -148,6 +144,9 @@ const parse = (fullForecast, forecastUrl) => {
|
|||||||
if (period.isDaytime) {
|
if (period.isDaytime) {
|
||||||
// day time is the high temperature
|
// day time is the high temperature
|
||||||
fDay.high = period.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
|
// Wait for the corresponding night period to increment
|
||||||
} else {
|
} else {
|
||||||
// low temperature
|
// low temperature
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ class Hazards extends WeatherDisplay {
|
|||||||
// show alert indicator
|
// show alert indicator
|
||||||
if (unViewed > 0) alert.classList.add('show');
|
if (unViewed > 0) alert.classList.add('show');
|
||||||
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
|
// 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) {
|
} catch (error) {
|
||||||
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
||||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
@@ -115,7 +118,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
this.getDataCallback();
|
this.getDataCallback();
|
||||||
|
|
||||||
if (!superResult) {
|
if (!superResult) {
|
||||||
this.setStatus(STATUS.loaded);
|
// Don't override status - super.getData() already set it to STATUS.disabled
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawLongCanvas();
|
this.drawLongCanvas();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`largeIcon: ${error.message}`);
|
console.warn(`largeIcon: ${error.message}`);
|
||||||
// Return a fallback icon to prevent downstream errors
|
// Return a fallback icon to prevent downstream errors
|
||||||
return addPath(_isNightTime ? 'Clear.gif' : 'Sunny.gif');
|
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the icon
|
// find the icon
|
||||||
@@ -169,7 +169,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
default: {
|
default: {
|
||||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
// Return a reasonable fallback instead of false to prevent downstream errors
|
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||||
return addPath(isNightTime ? 'Clear.gif' : 'Sunny.gif');
|
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
|||||||
weatherParameters.stations = stations.features;
|
weatherParameters.stations = stations.features;
|
||||||
|
|
||||||
// update the main process for display purposes
|
// update the main process for display purposes
|
||||||
populateWeatherParameters(weatherParameters);
|
populateWeatherParameters(weatherParameters, point.properties);
|
||||||
|
|
||||||
// reset the scroll
|
// reset the scroll
|
||||||
postMessage({ type: 'current-weather-scroll', method: 'reload' });
|
postMessage({ type: 'current-weather-scroll', method: 'reload' });
|
||||||
@@ -753,12 +753,14 @@ const registerProgress = (_progress) => {
|
|||||||
progress = _progress;
|
progress = _progress;
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateWeatherParameters = (params) => {
|
const populateWeatherParameters = (params, point) => {
|
||||||
document.querySelector('#spanCity').innerHTML = `${params.city}, `;
|
document.querySelector('#spanCity').innerHTML = `${params.city}, `;
|
||||||
document.querySelector('#spanState').innerHTML = params.state;
|
document.querySelector('#spanState').innerHTML = params.state;
|
||||||
document.querySelector('#spanStationId').innerHTML = params.stationId;
|
document.querySelector('#spanStationId').innerHTML = params.stationId;
|
||||||
document.querySelector('#spanRadarId').innerHTML = params.radarId;
|
document.querySelector('#spanRadarId').innerHTML = params.radarId;
|
||||||
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
|
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
|
||||||
|
document.querySelector('#spanOfficeId').innerHTML = point.cwa;
|
||||||
|
document.querySelector('#spanGridPoint').innerHTML = `${point.gridX},${point.gridY}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const latLonReceived = (data, haveDataCallback) => {
|
const latLonReceived = (data, haveDataCallback) => {
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
// METAR parsing utilities using metar-taf-parser library
|
// METAR parsing utilities using metar-taf-parser library
|
||||||
import { parseMetar } from '../../vendor/auto/metar-taf-parser.mjs';
|
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
|
* Augment observation data by parsing METAR when API fields are missing
|
||||||
@@ -7,14 +27,15 @@ import { parseMetar } from '../../vendor/auto/metar-taf-parser.mjs';
|
|||||||
* @returns {Object} - Augmented observation with parsed METAR data filled in
|
* @returns {Object} - Augmented observation with parsed METAR data filled in
|
||||||
*/
|
*/
|
||||||
const augmentObservationWithMetar = (observation) => {
|
const augmentObservationWithMetar = (observation) => {
|
||||||
if (!observation?.rawMessage) {
|
// check for a metar message and for unusable ios versions
|
||||||
|
if (!observation?.rawMessage || (isIos && !iosVersionOk)) {
|
||||||
return observation;
|
return observation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metar = { ...observation };
|
const metar = { ...observation };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metarData = parseMetar(observation.rawMessage);
|
const metarData = parseMetar(observation.rawMessage, { locale: en });
|
||||||
|
|
||||||
if (observation.windSpeed?.value === null && metarData.wind?.speed !== undefined) {
|
if (observation.windSpeed?.value === null && metarData.wind?.speed !== undefined) {
|
||||||
metar.windSpeed = {
|
metar.windSpeed = {
|
||||||
|
|||||||
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",
|
TS: "thunderstorm",
|
||||||
},
|
},
|
||||||
Error: {
|
Error: {
|
||||||
prefix: "An error occured. Error code n°",
|
prefix: "An error occurred. Error code n°",
|
||||||
},
|
},
|
||||||
ErrorCode: {
|
ErrorCode: {
|
||||||
AirportNotFound: "The airport was not found for this message.",
|
AirportNotFound: "The airport was not found for this message.",
|
||||||
@@ -136,11 +136,13 @@ var en = {
|
|||||||
TS: "thunderstorm",
|
TS: "thunderstorm",
|
||||||
UP: "unknown precipitation",
|
UP: "unknown precipitation",
|
||||||
VA: "volcanic ash",
|
VA: "volcanic ash",
|
||||||
|
NSW: 'no significant weather'
|
||||||
},
|
},
|
||||||
Remark: {
|
Remark: {
|
||||||
ALQDS: "all quadrants",
|
ALQDS: "all quadrants",
|
||||||
AO1: "automated stations without a precipitation discriminator",
|
AO1: "automated stations without a precipitation discriminator",
|
||||||
AO2: "automated station with a precipitation discriminator",
|
AO2: "automated station with a precipitation discriminator",
|
||||||
|
AO2A: "automated station with a precipitation discriminator (augmented)",
|
||||||
BASED: "based",
|
BASED: "based",
|
||||||
Barometer: [
|
Barometer: [
|
||||||
"Increase, then decrease",
|
"Increase, then decrease",
|
||||||
@@ -156,7 +158,7 @@ var en = {
|
|||||||
Ceiling: {
|
Ceiling: {
|
||||||
Height: "ceiling varying between {0} and {1} feet",
|
Height: "ceiling varying between {0} and {1} feet",
|
||||||
Second: {
|
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",
|
DSNT: "distant",
|
||||||
@@ -192,6 +194,11 @@ var en = {
|
|||||||
LGT: "light",
|
LGT: "light",
|
||||||
LTG: "lightning",
|
LTG: "lightning",
|
||||||
MOD: "moderate",
|
MOD: "moderate",
|
||||||
|
Next: {
|
||||||
|
Forecast: {
|
||||||
|
By: "next forecast by {0}, {1}:{2}Z"
|
||||||
|
},
|
||||||
|
},
|
||||||
NXT: "next",
|
NXT: "next",
|
||||||
ON: "on",
|
ON: "on",
|
||||||
Obscuration: "{0} layer at {1} feet composed of {2}",
|
Obscuration: "{0} layer at {1} feet composed of {2}",
|
||||||
@@ -223,7 +230,7 @@ var en = {
|
|||||||
},
|
},
|
||||||
Second: {
|
Second: {
|
||||||
Location: {
|
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: {
|
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) {
|
months(length, format = false) {
|
||||||
return listStuff(this, length, months, () => {
|
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 },
|
const intl = format ? { month: length, day: "numeric" } : { month: length },
|
||||||
formatStr = format ? "format" : "standalone";
|
formatStr = format ? "format" : "standalone";
|
||||||
if (!this.monthsCache[formatStr][length]) {
|
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];
|
return this.monthsCache[formatStr][length];
|
||||||
});
|
});
|
||||||
@@ -2040,10 +2048,24 @@ function parseMillis(fraction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundTo(number, digits, towardZero = false) {
|
function roundTo(number, digits, rounding = "round") {
|
||||||
const factor = 10 ** digits,
|
const factor = 10 ** digits;
|
||||||
rounder = towardZero ? Math.trunc : Math.round;
|
switch (rounding) {
|
||||||
return rounder(number * factor) / factor;
|
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
|
// DATE BASICS
|
||||||
@@ -2151,7 +2173,7 @@ function signedOffset(offHourStr, offMinuteStr) {
|
|||||||
|
|
||||||
function asNumber(value) {
|
function asNumber(value) {
|
||||||
const numericValue = Number(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}`);
|
throw new InvalidArgumentError(`Invalid unit value ${value}`);
|
||||||
return numericValue;
|
return numericValue;
|
||||||
}
|
}
|
||||||
@@ -2410,8 +2432,12 @@ class Formatter {
|
|||||||
for (let i = 0; i < fmt.length; i++) {
|
for (let i = 0; i < fmt.length; i++) {
|
||||||
const c = fmt.charAt(i);
|
const c = fmt.charAt(i);
|
||||||
if (c === "'") {
|
if (c === "'") {
|
||||||
if (currentFull.length > 0) {
|
// turn '' into a literal signal quote instead of just skipping the empty literal
|
||||||
splits.push({ literal: bracketed || /^\s+$/.test(currentFull), val: currentFull });
|
if (currentFull.length > 0 || bracketed) {
|
||||||
|
splits.push({
|
||||||
|
literal: bracketed || /^\s+$/.test(currentFull),
|
||||||
|
val: currentFull === "" ? "'" : currentFull,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
current = null;
|
current = null;
|
||||||
currentFull = "";
|
currentFull = "";
|
||||||
@@ -2475,7 +2501,7 @@ class Formatter {
|
|||||||
return this.dtFormatter(dt, opts).resolvedOptions();
|
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
|
// we get some perf out of doing this here, annoyingly
|
||||||
if (this.opts.forceSimple) {
|
if (this.opts.forceSimple) {
|
||||||
return padStart(n, p);
|
return padStart(n, p);
|
||||||
@@ -2486,6 +2512,9 @@ class Formatter {
|
|||||||
if (p > 0) {
|
if (p > 0) {
|
||||||
opts.padTo = p;
|
opts.padTo = p;
|
||||||
}
|
}
|
||||||
|
if (signDisplay) {
|
||||||
|
opts.signDisplay = signDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
return this.loc.numberFormatter(opts).format(n);
|
return this.loc.numberFormatter(opts).format(n);
|
||||||
}
|
}
|
||||||
@@ -2721,32 +2750,44 @@ class Formatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatDurationFromString(dur, fmt) {
|
formatDurationFromString(dur, fmt) {
|
||||||
|
const invertLargest = this.opts.signMode === "negativeLargestOnly" ? -1 : 1;
|
||||||
const tokenToField = (token) => {
|
const tokenToField = (token) => {
|
||||||
switch (token[0]) {
|
switch (token[0]) {
|
||||||
case "S":
|
case "S":
|
||||||
return "millisecond";
|
return "milliseconds";
|
||||||
case "s":
|
case "s":
|
||||||
return "second";
|
return "seconds";
|
||||||
case "m":
|
case "m":
|
||||||
return "minute";
|
return "minutes";
|
||||||
case "h":
|
case "h":
|
||||||
return "hour";
|
return "hours";
|
||||||
case "d":
|
case "d":
|
||||||
return "day";
|
return "days";
|
||||||
case "w":
|
case "w":
|
||||||
return "week";
|
return "weeks";
|
||||||
case "M":
|
case "M":
|
||||||
return "month";
|
return "months";
|
||||||
case "y":
|
case "y":
|
||||||
return "year";
|
return "years";
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tokenToString = (lildur) => (token) => {
|
tokenToString = (lildur, info) => (token) => {
|
||||||
const mapped = tokenToField(token);
|
const mapped = tokenToField(token);
|
||||||
if (mapped) {
|
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 {
|
} else {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -2756,8 +2797,14 @@ class Formatter {
|
|||||||
(found, { literal, val }) => (literal ? found : found.concat(val)),
|
(found, { literal, val }) => (literal ? found : found.concat(val)),
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t));
|
collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t)),
|
||||||
return stringifyTokens(tokens, tokenToString(collapsed));
|
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
|
// 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 isoExtendedZone = `(?:${offsetRegex.source}?(?:\\[(${ianaRegex.source})\\])?)?`;
|
||||||
const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/;
|
const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/;
|
||||||
const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`);
|
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 isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/;
|
||||||
const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/;
|
const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/;
|
||||||
const isoOrdinalRegex = /(\d{4})-?(\d{3})/;
|
const isoOrdinalRegex = /(\d{4})-?(\d{3})/;
|
||||||
@@ -3537,9 +3584,13 @@ class Duration {
|
|||||||
* @param {string} fmt - the format string
|
* @param {string} fmt - the format string
|
||||||
* @param {Object} opts - options
|
* @param {Object} opts - options
|
||||||
* @param {boolean} [opts.floor=true] - floor numerical values
|
* @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("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("yy dd sss") //=> "01 06 002"
|
||||||
* @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("M S") //=> "12 518402000"
|
* @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}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
toFormat(fmt, opts = {}) {
|
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
|
* @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 {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 {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
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* var dur = Duration.fromObject({ days: 1, hours: 5, minutes: 6 })
|
* var dur = Duration.fromObject({ months: 1, weeks: 0, hours: 5, minutes: 6 })
|
||||||
* dur.toHuman() //=> '1 day, 5 hours, 6 minutes'
|
* dur.toHuman() //=> '1 month, 0 weeks, 5 hours, 6 minutes'
|
||||||
* dur.toHuman({ listStyle: "long" }) //=> '1 day, 5 hours, and 6 minutes'
|
* dur.toHuman({ listStyle: "long" }) //=> '1 month, 0 weeks, 5 hours, and 6 minutes'
|
||||||
* dur.toHuman({ unitDisplay: "short" }) //=> '1 day, 5 hr, 6 min'
|
* dur.toHuman({ unitDisplay: "short" }) //=> '1 mth, 0 wks, 5 hr, 6 min'
|
||||||
|
* dur.toHuman({ showZeros: false }) //=> '1 month, 5 hours, 6 minutes'
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
toHuman(opts = {}) {
|
toHuman(opts = {}) {
|
||||||
if (!this.isValid) return INVALID$2;
|
if (!this.isValid) return INVALID$2;
|
||||||
|
|
||||||
|
const showZeros = opts.showZeros !== false;
|
||||||
|
|
||||||
const l = orderedUnits$1
|
const l = orderedUnits$1
|
||||||
.map((unit) => {
|
.map((unit) => {
|
||||||
const val = this.values[unit];
|
const val = this.values[unit];
|
||||||
if (isUndefined(val)) {
|
if (isUndefined(val) || (val === 0 && !showZeros)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this.loc
|
return this.loc
|
||||||
@@ -3933,6 +3988,17 @@ class Duration {
|
|||||||
return clone$1(this, { values: negated }, true);
|
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.
|
* Get the years.
|
||||||
* @type {number}
|
* @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}
|
* @type {DateTime}
|
||||||
*/
|
*/
|
||||||
get end() {
|
get end() {
|
||||||
@@ -5674,21 +5741,22 @@ function toTechFormat(dt, format, allowZ = true) {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toISODate(o, extended) {
|
function toISODate(o, extended, precision) {
|
||||||
const longFormat = o.c.year > 9999 || o.c.year < 0;
|
const longFormat = o.c.year > 9999 || o.c.year < 0;
|
||||||
let c = "";
|
let c = "";
|
||||||
if (longFormat && o.c.year >= 0) c += "+";
|
if (longFormat && o.c.year >= 0) c += "+";
|
||||||
c += padStart(o.c.year, longFormat ? 6 : 4);
|
c += padStart(o.c.year, longFormat ? 6 : 4);
|
||||||
|
if (precision === "year") return c;
|
||||||
if (extended) {
|
if (extended) {
|
||||||
c += "-";
|
c += "-";
|
||||||
c += padStart(o.c.month);
|
c += padStart(o.c.month);
|
||||||
|
if (precision === "month") return c;
|
||||||
c += "-";
|
c += "-";
|
||||||
c += padStart(o.c.day);
|
|
||||||
} else {
|
} else {
|
||||||
c += padStart(o.c.month);
|
c += padStart(o.c.month);
|
||||||
c += padStart(o.c.day);
|
if (precision === "month") return c;
|
||||||
}
|
}
|
||||||
|
c += padStart(o.c.day);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5698,26 +5766,39 @@ function toISOTime(
|
|||||||
suppressSeconds,
|
suppressSeconds,
|
||||||
suppressMilliseconds,
|
suppressMilliseconds,
|
||||||
includeOffset,
|
includeOffset,
|
||||||
extendedZone
|
extendedZone,
|
||||||
|
precision
|
||||||
) {
|
) {
|
||||||
let c = padStart(o.c.hour);
|
let showSeconds = !suppressSeconds || o.c.millisecond !== 0 || o.c.second !== 0,
|
||||||
if (extended) {
|
c = "";
|
||||||
c += ":";
|
switch (precision) {
|
||||||
c += padStart(o.c.minute);
|
case "day":
|
||||||
if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
|
case "month":
|
||||||
c += ":";
|
case "year":
|
||||||
}
|
break;
|
||||||
} else {
|
default:
|
||||||
c += padStart(o.c.minute);
|
c += padStart(o.c.hour);
|
||||||
}
|
if (precision === "hour") break;
|
||||||
|
if (extended) {
|
||||||
if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) {
|
c += ":";
|
||||||
c += padStart(o.c.second);
|
c += padStart(o.c.minute);
|
||||||
|
if (precision === "minute") break;
|
||||||
if (o.c.millisecond !== 0 || !suppressMilliseconds) {
|
if (showSeconds) {
|
||||||
c += ".";
|
c += ":";
|
||||||
c += padStart(o.c.millisecond, 3);
|
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) {
|
if (includeOffset) {
|
||||||
@@ -5909,8 +5990,9 @@ function quickDT(obj, opts) {
|
|||||||
|
|
||||||
function diffRelative(start, end, opts) {
|
function diffRelative(start, end, opts) {
|
||||||
const round = isUndefined(opts.round) ? true : opts.round,
|
const round = isUndefined(opts.round) ? true : opts.round,
|
||||||
|
rounding = isUndefined(opts.rounding) ? "trunc" : opts.rounding,
|
||||||
format = (c, unit) => {
|
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);
|
const formatter = end.loc.clone(opts).relFormatter(opts);
|
||||||
return formatter.format(c, unit);
|
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.includeOffset=true] - include the offset, such as 'Z' or '-04:00'
|
||||||
* @param {boolean} [opts.extendedZone=false] - add the time zone format extension
|
* @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.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.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() //=> '2017-04-22T20:47:05.335-04:00'
|
||||||
* @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335'
|
* @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({ 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}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
toISO({
|
toISO({
|
||||||
@@ -7301,16 +7386,26 @@ class DateTime {
|
|||||||
suppressMilliseconds = false,
|
suppressMilliseconds = false,
|
||||||
includeOffset = true,
|
includeOffset = true,
|
||||||
extendedZone = false,
|
extendedZone = false,
|
||||||
|
precision = "milliseconds",
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
precision = normalizeUnit(precision);
|
||||||
const ext = format === "extended";
|
const ext = format === "extended";
|
||||||
|
|
||||||
let c = toISODate(this, ext);
|
let c = toISODate(this, ext, precision);
|
||||||
c += "T";
|
if (orderedUnits.indexOf(precision) >= 3) c += "T";
|
||||||
c += toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone);
|
c += toISOTime(
|
||||||
|
this,
|
||||||
|
ext,
|
||||||
|
suppressSeconds,
|
||||||
|
suppressMilliseconds,
|
||||||
|
includeOffset,
|
||||||
|
extendedZone,
|
||||||
|
precision
|
||||||
|
);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7318,16 +7413,17 @@ class DateTime {
|
|||||||
* Returns an ISO 8601-compliant string representation of this DateTime's date component
|
* Returns an ISO 8601-compliant string representation of this DateTime's date component
|
||||||
* @param {Object} opts - options
|
* @param {Object} opts - options
|
||||||
* @param {string} [opts.format='extended'] - choose between the basic and extended format
|
* @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() //=> '1982-05-25'
|
||||||
* @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'
|
* @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'
|
||||||
|
* @example DateTime.utc(1982, 5, 25).toISODate({ precision: 'month' }) //=> '1982-05'
|
||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
*/
|
*/
|
||||||
toISODate({ format = "extended" } = {}) {
|
toISODate({ format = "extended", precision = "day" } = {}) {
|
||||||
if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return toISODate(this, format === "extended", normalizeUnit(precision));
|
||||||
return toISODate(this, format === "extended");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7348,10 +7444,12 @@ class DateTime {
|
|||||||
* @param {boolean} [opts.extendedZone=true] - add the time zone format extension
|
* @param {boolean} [opts.extendedZone=true] - add the time zone format extension
|
||||||
* @param {boolean} [opts.includePrefix=false] - include the `T` prefix
|
* @param {boolean} [opts.includePrefix=false] - include the `T` prefix
|
||||||
* @param {string} [opts.format='extended'] - choose between the basic and extended format
|
* @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 }).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, 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({ 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 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z'
|
||||||
|
* @example DateTime.utc().set({ hour: 7, minute: 34, second: 56 }).toISOTime({ precision: 'minute' }) //=> '07:34Z'
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
toISOTime({
|
toISOTime({
|
||||||
@@ -7361,12 +7459,14 @@ class DateTime {
|
|||||||
includePrefix = false,
|
includePrefix = false,
|
||||||
extendedZone = false,
|
extendedZone = false,
|
||||||
format = "extended",
|
format = "extended",
|
||||||
|
precision = "milliseconds",
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = includePrefix ? "T" : "";
|
precision = normalizeUnit(precision);
|
||||||
|
let c = includePrefix && orderedUnits.indexOf(precision) >= 3 ? "T" : "";
|
||||||
return (
|
return (
|
||||||
c +
|
c +
|
||||||
toISOTime(
|
toISOTime(
|
||||||
@@ -7375,7 +7475,8 @@ class DateTime {
|
|||||||
suppressSeconds,
|
suppressSeconds,
|
||||||
suppressMilliseconds,
|
suppressMilliseconds,
|
||||||
includeOffset,
|
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
|
* 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 {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 {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} [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 {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 {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 {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.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
|
* @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 };
|
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||||
//# sourceMappingURL=luxon.js.map
|
//# 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
@@ -32,7 +32,7 @@ const tester = async (location, testPage) => {
|
|||||||
// run all the locations
|
// run all the locations
|
||||||
for (let i = 0; i < LOCATIONS.length; i += 1) {
|
for (let i = 0; i < LOCATIONS.length; i += 1) {
|
||||||
const location = LOCATIONS[i];
|
const location = LOCATIONS[i];
|
||||||
console.log(location);
|
console.log(`${i + 1}/${LOCATIONS.length} ${location}`);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await tester(location, page);
|
await tester(location, page);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,6 +191,8 @@
|
|||||||
Station Id: <span id="spanStationId"></span><br />
|
Station Id: <span id="spanStationId"></span><br />
|
||||||
Radar Id: <span id="spanRadarId"></span><br />
|
Radar Id: <span id="spanRadarId"></span><br />
|
||||||
Zone Id: <span id="spanZoneId"></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 />
|
Music: <span id="musicTrack">Not playing</span><br />
|
||||||
Ws4kp Version: <span><%- version %></span>
|
Ws4kp Version: <span><%- version %></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,4 +200,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -44,7 +44,8 @@
|
|||||||
"mwood",
|
"mwood",
|
||||||
"unmuted",
|
"unmuted",
|
||||||
"dumpio",
|
"dumpio",
|
||||||
"mesonet"
|
"mesonet",
|
||||||
|
"metar"
|
||||||
],
|
],
|
||||||
"cSpell.ignorePaths": [
|
"cSpell.ignorePaths": [
|
||||||
"**/package-lock.json",
|
"**/package-lock.json",
|
||||||
@@ -82,4 +83,4 @@
|
|||||||
"j69.ejs-beautify",
|
"j69.ejs-beautify",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user