mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Compare commits
20 Commits
v6.5.8
...
994c9240b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
994c9240b8 | ||
|
|
27d75ba62d | ||
|
|
63e089451d | ||
|
|
42f1f66117 | ||
|
|
d4f648f244 | ||
|
|
71d52c0b72 | ||
|
|
d2bf8f3f99 | ||
|
|
f4289e6329 | ||
|
|
ec1169e07b | ||
|
|
eee4519095 | ||
|
|
38cdb46c85 | ||
|
|
e70639d7a6 | ||
|
|
63d27d1a26 | ||
|
|
97ac0a1656 | ||
|
|
8158afd039 | ||
|
|
5fffc495ae | ||
|
|
b2a424a64f | ||
|
|
9f6b90919c | ||
|
|
778b7f4456 | ||
|
|
443114f555 |
18
README.md
18
README.md
@@ -139,8 +139,8 @@ services:
|
|||||||
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
|
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
|
||||||
# more complete list of configuration options.
|
# more complete list of configuration options.
|
||||||
- WSQS_latLonQuery=Orlando International Airport Orlando FL USA
|
- WSQS_latLonQuery=Orlando International Airport Orlando FL USA
|
||||||
- WSQS_hazards_checkbox=false
|
- WSQS_hazards=false
|
||||||
- WSQS_current_weather_checkbox=true
|
- WSQS_current_weather=true
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080 # change the first 8080 to meet your local network needs
|
- 8080:8080 # change the first 8080 to meet your local network needs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -192,13 +192,13 @@ Selected displays, the forecast city and widescreen setting are sticky from one
|
|||||||
|
|
||||||
Your permalink will be very long. Here is an example for the Orlando International Airport:
|
Your permalink will be very long. Here is an example for the Orlando International Airport:
|
||||||
```
|
```
|
||||||
https://weatherstar.netbymatt.com/?hazards-checkbox=false¤t-weather-checkbox=true&latest-observations-checkbox=true&hourly-checkbox=false&hourly-graph-checkbox=true&travel-checkbox=false®ional-forecast-checkbox=true&local-forecast-checkbox=true&extended-forecast-checkbox=true&almanac-checkbox=false&spc-outlook-checkbox=true&radar-checkbox=true&settings-wide-checkbox=false&settings-kiosk-checkbox=false&settings-scanLines-checkbox=false&settings-speed-select=1.00&settings-units-select=us&latLonQuery=Orlando+International+Airport%2C+Orlando%2C+FL%2C+USA&latLon=%7B%22lat%22%3A28.431%2C%22lon%22%3A-81.3076%7D
|
https://weatherstar.netbymatt.com/?hazards=false¤t-weather=true&latest-observations=true&hourly=false&hourly-graph=true&travel=false®ional-forecast=true&local-forecast=true&extended-forecast=true&almanac=false&spc-outlook=true&radar=true&wide=false&kiosk=false&scanLines=false&speed-select=1.00&units-select=us&latLonQuery=Orlando+International+Airport%2C+Orlando%2C+FL%2C+USA&latLon=%7B%22lat%22%3A28.431%2C%22lon%22%3A-81.3076%7D
|
||||||
```
|
```
|
||||||
You can also build your own permalink. Any omitted settings will be filled with defaults. Here are a few examples:
|
You can also build your own permalink. Any omitted settings will be filled with defaults. Here are a few examples:
|
||||||
```
|
```
|
||||||
https://weatherstar.netbymatt.com/?latLonQuery=Orlando+International+Airport
|
https://weatherstar.netbymatt.com/?latLonQuery=Orlando+International+Airport
|
||||||
https://weatherstar.netbymatt.com/?kiosk=true
|
https://weatherstar.netbymatt.com/?kiosk=true
|
||||||
https://weatherstar.netbymatt.com/?settings-units-select=metric
|
https://weatherstar.netbymatt.com/?units-select=metric
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kiosk mode
|
### Kiosk mode
|
||||||
@@ -213,7 +213,7 @@ When serving this via the built-in Express server, it's possible to define envir
|
|||||||
|
|
||||||
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
|
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
|
||||||
|
|
||||||
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, its matching environment variable becomes `WSQS_travel_checkbox=true`.
|
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, its matching environment variable becomes `WSQS_travel=true`.
|
||||||
|
|
||||||
When using the Docker container, these environment variables are read on container start-up to generate the static redirect HTML.
|
When using the Docker container, these environment variables are read on container start-up to generate the static redirect HTML.
|
||||||
|
|
||||||
@@ -221,7 +221,13 @@ When using the Docker container, these environment variables are read on contain
|
|||||||
|
|
||||||
**Speed:** Controls the playback speed multiplier of the displays, from "Very Fast" (1.5x) to "Very Slow" (0.5x) with "Normal" being 1x
|
**Speed:** Controls the playback speed multiplier of the displays, from "Very Fast" (1.5x) to "Very Slow" (0.5x) with "Normal" being 1x
|
||||||
|
|
||||||
**Widescreen:** Stretches the background to 16:9 to avoid "pillarboxing" on modern displays
|
**Display Mode:**
|
||||||
|
- Standard: Classic 4:3 display with the classic (not enhanced, below) screen layouts.
|
||||||
|
- Widescreen: Stretches the background to 16:9 to avoid "pillarboxing" on modern displays
|
||||||
|
- Widescreen Enhanced: Stretches as above, and makes use of the additional space to provide wider maps, more weather data and/or additional days in the forecast
|
||||||
|
- Portrait Enhanced: (in progress) Rotates the screen to a 16:9 portrait orientation and enhances the original displays by adjusting them to fit the new orientation.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Kiosk:** Immediately activates kiosk mode, which hides all settings. Exit by refreshing the page or using `Ctrl-K`. (Kiosk mode is similar to clicking the "Fullscreen" icon, but scales to the current browser viewport instead of activating the browser's actual "Fullscreen" mode.)
|
**Kiosk:** Immediately activates kiosk mode, which hides all settings. Exit by refreshing the page or using `Ctrl-K`. (Kiosk mode is similar to clicking the "Fullscreen" icon, but scales to the current browser viewport instead of activating the browser's actual "Fullscreen" mode.)
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import { readFile } from 'fs/promises';
|
|||||||
import file from 'gulp-file';
|
import file from 'gulp-file';
|
||||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||||
import log from 'fancy-log';
|
import log from 'fancy-log';
|
||||||
import dartSass from 'sass';
|
import * as dartSass from 'sass';
|
||||||
import gulpSass from 'gulp-sass';
|
import gulpSass from 'gulp-sass';
|
||||||
import sourceMaps from 'gulp-sourcemaps';
|
import sourceMaps from 'gulp-sourcemaps';
|
||||||
import OVERRIDES from '../src/overrides.mjs';
|
import OVERRIDES from '../src/overrides.mjs';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
// get cloudfront
|
// get cloudfront
|
||||||
import reader from '../src/playlist-reader.mjs';
|
import reader from '../src/playlist-reader.mjs';
|
||||||
@@ -110,10 +111,9 @@ const htmlSources = [
|
|||||||
const packageJson = await readFile('package.json');
|
const packageJson = await readFile('package.json');
|
||||||
let { version } = JSON.parse(packageJson);
|
let { version } = JSON.parse(packageJson);
|
||||||
const previewVersion = async () => {
|
const previewVersion = async () => {
|
||||||
// generate a relatively unique timestamp for cache invalidation of the preview site
|
// generate a unique timestamp for cache invalidation of the preview site
|
||||||
const now = new Date();
|
const now = DateTime.utc();
|
||||||
const msNow = now.getTime() % 1_000_000;
|
version = now.toFormat('yyyyLLddHHmm').substring(3);
|
||||||
version = msNow.toString();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const compressHtml = async () => src(htmlSources)
|
const compressHtml = async () => src(htmlSources)
|
||||||
@@ -222,7 +222,7 @@ const buildDist = series(clean, parallel(buildJs, compressJsVendor, buildCss, co
|
|||||||
// 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, logVersion);
|
const publishFrontend = series(buildDist, uploadImages, upload, invalidate, logVersion);
|
||||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview, logVersion);
|
||||||
|
|
||||||
export default publishFrontend;
|
export default publishFrontend;
|
||||||
|
|
||||||
|
|||||||
BIN
server/images/backgrounds/1-chart-wide.png
Normal file
BIN
server/images/backgrounds/1-chart-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
server/images/backgrounds/1-wide-enhanced.png
Normal file
BIN
server/images/backgrounds/1-wide-enhanced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
server/images/backgrounds/3-wide-enhanced.png
Normal file
BIN
server/images/backgrounds/3-wide-enhanced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
server/images/gimp/1-chart-wide.xcf
Normal file
BIN
server/images/gimp/1-chart-wide.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/1-wide-enhanced.xcf
Normal file
BIN
server/images/gimp/1-wide-enhanced.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/1.xcf
Normal file
BIN
server/images/gimp/1.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/3-wide-enhanced.xcf
Normal file
BIN
server/images/gimp/3-wide-enhanced.xcf
Normal file
Binary file not shown.
@@ -141,7 +141,7 @@ const init = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle kiosk mode initialization
|
// Handle kiosk mode initialization
|
||||||
const urlKioskCheckbox = parsedParameters['settings-kiosk-checkbox'];
|
const urlKioskCheckbox = parsedParameters?.kiosk ?? parsedParameters['settings-kiosk-checkbox'];
|
||||||
|
|
||||||
// If kiosk=false is specified, disable kiosk mode and clear any stored value
|
// If kiosk=false is specified, disable kiosk mode and clear any stored value
|
||||||
if (urlKioskCheckbox === 'false') {
|
if (urlKioskCheckbox === 'false') {
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ class Almanac extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
calcSunMoonData(weatherParameters) {
|
calcSunMoonData(weatherParameters) {
|
||||||
const sun = [
|
const sun = [0, 1, 2, 3, 4, 5, 6].map((days) => SunCalc.getTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
|
||||||
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
|
|
||||||
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
|
|
||||||
];
|
|
||||||
|
|
||||||
// brute force the moon phases by scanning the next 30 days
|
// brute force the moon phases by scanning the next 30 days
|
||||||
const moon = [];
|
const moon = [];
|
||||||
@@ -72,7 +69,7 @@ class Almanac extends WeatherDisplay {
|
|||||||
|
|
||||||
// stop after 30 days or 4 moon phases
|
// stop after 30 days or 4 moon phases
|
||||||
iterations += 1;
|
iterations += 1;
|
||||||
} while (iterations <= 30 && moon.length < 4);
|
} while (iterations <= 45 && moon.length < 5);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sun,
|
sun,
|
||||||
@@ -126,21 +123,16 @@ class Almanac extends WeatherDisplay {
|
|||||||
|
|
||||||
// Set day names
|
// Set day names
|
||||||
const Today = DateTime.local();
|
const Today = DateTime.local();
|
||||||
const Tomorrow = Today.plus({ days: 1 });
|
// fill all three days, even if some are hidden by non-enhanced
|
||||||
this.elem.querySelector('.day-1').textContent = Today.toLocaleString({ weekday: 'long' });
|
for (let i = 0; i < 3; i += 1) {
|
||||||
this.elem.querySelector('.day-2').textContent = Tomorrow.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector(`.day-${i}`).textContent = Today.plus({ days: i }).toLocaleString({ weekday: 'long' });
|
||||||
|
|
||||||
const todaySunrise = DateTime.fromJSDate(info.sun[0].sunrise);
|
const sunrise = DateTime.fromJSDate(info.sun[i].sunrise);
|
||||||
const todaySunset = DateTime.fromJSDate(info.sun[0].sunset);
|
const sunset = DateTime.fromJSDate(info.sun[i].sunset);
|
||||||
const [todaySunriseFormatted, todaySunsetFormatted] = formatTimesForColumn([todaySunrise, todaySunset]);
|
const [sunriseFormatted, sunsetFormatted] = formatTimesForColumn([sunrise, sunset]);
|
||||||
this.elem.querySelector('.rise-1').textContent = todaySunriseFormatted;
|
this.elem.querySelector(`.rise-${i}`).textContent = sunriseFormatted;
|
||||||
this.elem.querySelector('.set-1').textContent = todaySunsetFormatted;
|
this.elem.querySelector(`.set-${i}`).textContent = sunsetFormatted;
|
||||||
|
}
|
||||||
const tomorrowSunrise = DateTime.fromJSDate(info.sun[1].sunrise);
|
|
||||||
const tomorrowSunset = DateTime.fromJSDate(info.sun[1].sunset);
|
|
||||||
const [tomorrowSunriseFormatted, tomorrowSunsetformatted] = formatTimesForColumn([tomorrowSunrise, tomorrowSunset]);
|
|
||||||
this.elem.querySelector('.rise-2').textContent = tomorrowSunriseFormatted;
|
|
||||||
this.elem.querySelector('.set-2').textContent = tomorrowSunsetformatted;
|
|
||||||
|
|
||||||
// Moon data
|
// Moon data
|
||||||
const days = info.moon.map((MoonPhase) => {
|
const days = info.moon.map((MoonPhase) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { debugFlag } from './utils/debug.mjs';
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
|
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
class CurrentWeather extends WeatherDisplay {
|
class CurrentWeather extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -193,7 +194,9 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
||||||
|
|
||||||
// get location (city name) from StationInfo if available (allows for overrides)
|
// get location (city name) from StationInfo if available (allows for overrides)
|
||||||
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, 20);
|
// longer name allowed if in wide-enhanced
|
||||||
|
const locationLimit = (settings.wide?.value && settings.enhanced?.value) ? 25 : 20;
|
||||||
|
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, locationLimit);
|
||||||
|
|
||||||
const fill = {
|
const fill = {
|
||||||
temp: this.data.Temperature + String.fromCharCode(176),
|
temp: this.data.Temperature + String.fromCharCode(176),
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
super(navId, elemId, 'Extended Forecast', true);
|
super(navId, elemId, 'Extended Forecast', true);
|
||||||
|
|
||||||
// set timings
|
// set timings
|
||||||
this.timing.totalScreens = 2;
|
if (settings.portrait?.value) {
|
||||||
|
this.timing.totalScreens = 1;
|
||||||
|
this.perPage = 4;
|
||||||
|
} else {
|
||||||
|
this.timing.totalScreens = 2;
|
||||||
|
this.perPage = 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
@@ -54,7 +60,7 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
|
|
||||||
// determine bounds
|
// determine bounds
|
||||||
// grab the first three or second set of three array elements
|
// grab the first three or second set of three array elements
|
||||||
const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
|
const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + this.perPage * this.screenIndex, this.perPage + this.screenIndex * this.perPage);
|
||||||
|
|
||||||
// create each day template
|
// create each day template
|
||||||
const days = forecast.map((Day) => {
|
const days = forecast.map((Day) => {
|
||||||
|
|||||||
@@ -5,10 +5,30 @@ import getHourlyData from './hourly.mjs';
|
|||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
// get available space
|
// set up spacing and scales
|
||||||
const availableWidth = 532;
|
const scaling = () => {
|
||||||
const availableHeight = 285;
|
const available = {
|
||||||
|
width: 532,
|
||||||
|
height: 285,
|
||||||
|
};
|
||||||
|
const dataLength = {
|
||||||
|
hours: 36,
|
||||||
|
xTicks: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
available.width = available.width + 107 + 107;
|
||||||
|
available.height = 285;
|
||||||
|
dataLength.hours = 48;
|
||||||
|
dataLength.xTicks = 6;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
available,
|
||||||
|
dataLength,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
class HourlyGraph extends WeatherDisplay {
|
class HourlyGraph extends WeatherDisplay {
|
||||||
constructor(navId, elemId, defaultActive) {
|
constructor(navId, elemId, defaultActive) {
|
||||||
@@ -46,28 +66,43 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get the data length for current settings
|
||||||
|
const { dataLength } = scaling();
|
||||||
|
|
||||||
|
// clamp down the data to the allowed size
|
||||||
|
Object.entries(this.data).forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
this.data[key] = value.slice(0, dataLength.hours);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawCanvas() {
|
drawCanvas() {
|
||||||
|
// get scaling parameters
|
||||||
|
const { dataLength, available } = scaling();
|
||||||
|
|
||||||
|
// get the image
|
||||||
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
||||||
|
|
||||||
this.image.width = availableWidth;
|
// set up image
|
||||||
this.image.height = availableHeight;
|
this.image.width = available.width;
|
||||||
|
this.image.height = available.height;
|
||||||
|
|
||||||
// get context
|
// get context
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = availableWidth;
|
canvas.width = available.width;
|
||||||
canvas.height = availableHeight;
|
canvas.height = available.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
// calculate time scale
|
// calculate time scale
|
||||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, available.width);
|
||||||
const timeStep = this.data.temperature.length / 4;
|
const timeStep = this.data.temperature.length / (dataLength.xTicks);
|
||||||
const startTime = DateTime.now().startOf('hour');
|
const startTime = DateTime.now().startOf('hour');
|
||||||
let prevTime = startTime;
|
let prevTime = startTime;
|
||||||
Array(5).fill().forEach((val, idx) => {
|
Array(dataLength.xTicks + 1).fill().forEach((val, idx) => {
|
||||||
// track the previous label so a day of week can be added when it changes
|
// track the previous label so a day of week can be added when it changes
|
||||||
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
||||||
prevTime = label.ts;
|
prevTime = label.ts;
|
||||||
@@ -77,7 +112,7 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
|
|
||||||
// order is important last line drawn is on top
|
// order is important last line drawn is on top
|
||||||
// clouds
|
// clouds
|
||||||
const percentScale = calcScale(0, availableHeight - 10, 100, 10);
|
const percentScale = calcScale(0, available.height - 10, 100, 10);
|
||||||
const cloud = createPath(this.data.skyCover, timeScale, percentScale);
|
const cloud = createPath(this.data.skyCover, timeScale, percentScale);
|
||||||
drawPath(cloud, ctx, {
|
drawPath(cloud, ctx, {
|
||||||
strokeStyle: 'lightgrey',
|
strokeStyle: 'lightgrey',
|
||||||
@@ -97,7 +132,7 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
const thirdScale = (maxScale - minScale) / 3;
|
const thirdScale = (maxScale - minScale) / 3;
|
||||||
const midScale1 = Math.round(minScale + thirdScale);
|
const midScale1 = Math.round(minScale + thirdScale);
|
||||||
const midScale2 = Math.round(minScale + (thirdScale * 2));
|
const midScale2 = Math.round(minScale + (thirdScale * 2));
|
||||||
const tempScale = calcScale(minScale, availableHeight - 10, maxScale, 10);
|
const tempScale = calcScale(minScale, available.height - 10, maxScale, 10);
|
||||||
|
|
||||||
// dewpoint
|
// dewpoint
|
||||||
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
|
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// expand a set of values with durations to an hour-by-hour array
|
// expand a set of values with durations to an hour-by-hour array
|
||||||
const expand = (data, maxHours = 36) => {
|
const expand = (data, maxHours = 48) => {
|
||||||
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
||||||
const result = []; // resulting expanded values
|
const result = []; // resulting expanded values
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
|
|||||||
@@ -159,12 +159,17 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||||
|
|
||||||
const Temperature = temperatureConverter(condition.temperature.value);
|
const Temperature = temperatureConverter(condition.temperature.value);
|
||||||
|
const Like = likeTemperature(condition.heatIndex?.value, condition.windChill?.value, Temperature, temperatureConverter);
|
||||||
const WindSpeed = windConverter(condition.windSpeed.value);
|
const WindSpeed = windConverter(condition.windSpeed.value);
|
||||||
|
|
||||||
|
const locationLimit = (settings.wide?.value && settings.enhanced?.value) ? 20 : 14;
|
||||||
|
const weatherLimit = (settings.wide?.value && settings.enhanced?.value) ? 10 : 9;
|
||||||
|
|
||||||
const fill = {
|
const fill = {
|
||||||
location: locationCleanup(condition.city).substr(0, 14),
|
location: locationCleanup(condition.city).substr(0, locationLimit),
|
||||||
temp: Temperature,
|
temp: Temperature,
|
||||||
weather: shortenCurrentConditions(condition.textDescription).substr(0, 9),
|
like: Like.value,
|
||||||
|
weather: shortenCurrentConditions(condition.textDescription).substr(0, weatherLimit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (WindSpeed > 0) {
|
if (WindSpeed > 0) {
|
||||||
@@ -175,7 +180,12 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
fill.wind = 'Calm';
|
fill.wind = 'Calm';
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.fillTemplate('observation-row', fill);
|
const filledRow = this.fillTemplate('observation-row', fill);
|
||||||
|
|
||||||
|
// add the feels like class
|
||||||
|
if (Like.cssClass) filledRow.querySelector('.like').classList.add(Like.cssClass);
|
||||||
|
|
||||||
|
return filledRow;
|
||||||
});
|
});
|
||||||
|
|
||||||
const linesContainer = this.elem.querySelector('.observation-lines');
|
const linesContainer = this.elem.querySelector('.observation-lines');
|
||||||
@@ -186,6 +196,25 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generate a "feels like" temperature from heat index and wind chill.
|
||||||
|
const likeTemperature = (heat, wind, actual, converter) => {
|
||||||
|
// figure out the feels like value
|
||||||
|
let value = '';
|
||||||
|
if (heat) value = converter(heat);
|
||||||
|
if (wind) value = converter(wind);
|
||||||
|
|
||||||
|
// determine if there's a red/blue color class to add
|
||||||
|
let cssClass;
|
||||||
|
if (value !== '') {
|
||||||
|
if (value > actual) cssClass = 'heat-index';
|
||||||
|
if (value < actual) cssClass = 'wind-chill';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
cssClass,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const shortenCurrentConditions = (_condition) => {
|
const shortenCurrentConditions = (_condition) => {
|
||||||
let condition = _condition;
|
let condition = _condition;
|
||||||
condition = condition.replace(/Light/, 'L');
|
condition = condition.replace(/Light/, 'L');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { text } from './utils/fetch.mjs';
|
import { text } from './utils/fetch.mjs';
|
||||||
import Setting from './utils/setting.mjs';
|
import Setting from './utils/setting.mjs';
|
||||||
import { registerHiddenSetting } from './share.mjs';
|
|
||||||
|
|
||||||
let playlist;
|
let playlist;
|
||||||
let currentTrack = 0;
|
let currentTrack = 0;
|
||||||
@@ -33,9 +32,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// get the playlist
|
// get the playlist
|
||||||
getMedia();
|
getMedia();
|
||||||
|
|
||||||
// register the volume setting
|
|
||||||
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const scanMusicDirectory = async () => {
|
const scanMusicDirectory = async () => {
|
||||||
@@ -246,6 +242,7 @@ const mediaVolume = new Setting('mediaVolume', {
|
|||||||
[0.25, '25%'],
|
[0.25, '25%'],
|
||||||
],
|
],
|
||||||
changeAction: setVolume,
|
changeAction: setVolume,
|
||||||
|
visible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initializePlayer = () => {
|
const initializePlayer = () => {
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ const init = async () => {
|
|||||||
resizeTimeout = setTimeout(() => resize(), 100);
|
resizeTimeout = setTimeout(() => resize(), 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// redraw current screen (typically from enhanced setting change)
|
||||||
|
window.addEventListener('redraw', () => {
|
||||||
|
currentDisplay()?.drawCanvas();
|
||||||
|
});
|
||||||
|
|
||||||
// Handle orientation changes (Mobile Safari doesn't always fire resize events on orientation change)
|
// Handle orientation changes (Mobile Safari doesn't always fire resize events on orientation change)
|
||||||
window.addEventListener('orientationchange', () => {
|
window.addEventListener('orientationchange', () => {
|
||||||
if (debugFlag('resize')) {
|
if (debugFlag('resize')) {
|
||||||
|
|||||||
@@ -1,5 +1,56 @@
|
|||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
|
const radarFinalSize = () => {
|
||||||
|
const size = {
|
||||||
|
width: 640, height: 367,
|
||||||
|
};
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
size.width = 854;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
};
|
||||||
|
|
||||||
|
const radarSourceSize = () => {
|
||||||
|
const size = {
|
||||||
|
width: 240,
|
||||||
|
height: 163,
|
||||||
|
};
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
size.width = 240 / 640 * 854; // original size of 640 scaled up to wide at 854
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
};
|
||||||
|
|
||||||
|
const radarOffset = () => {
|
||||||
|
const offset = {
|
||||||
|
x: 240,
|
||||||
|
y: 138,
|
||||||
|
};
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
// 107 is the margins shift, 640/854 is the scaling factor normal => wide, /2 is because of the fixed 2:1 scaling between source radar and map tiles
|
||||||
|
offset.x = 240 + (107 * 640 / 854 / 2); // original size of 640 scaled up to wide at 854;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// shift the base coordinates to align with enhanced radar window sizes
|
||||||
|
const radarShift = () => {
|
||||||
|
const shift = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
shift.x = 107;
|
||||||
|
}
|
||||||
|
return shift;
|
||||||
|
};
|
||||||
|
|
||||||
export const TILE_SIZE = { x: 680, y: 387 };
|
export const TILE_SIZE = { x: 680, y: 387 };
|
||||||
export const TILE_COUNT = { x: 10, y: 11 };
|
export const TILE_COUNT = { x: 10, y: 11 };
|
||||||
export const TILE_FULL_SIZE = { x: 6800, y: 4255 };
|
export const TILE_FULL_SIZE = { x: 6800, y: 4255 };
|
||||||
export const RADAR_FULL_SIZE = { width: 2550, height: 1600 };
|
export const RADAR_FULL_SIZE = { width: 2550, height: 1600 };
|
||||||
export const RADAR_FINAL_SIZE = { width: 640, height: 367 };
|
export const RADAR_FINAL_SIZE = radarFinalSize;
|
||||||
|
export const RADAR_SOURCE_SIZE = radarSourceSize;
|
||||||
|
export const RADAR_OFFSET = radarOffset;
|
||||||
|
export const RADAR_SHIFT = radarShift;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { removeDopplerRadarImageNoise } from './radar-utils.mjs';
|
import { removeDopplerRadarImageNoise } from './radar-utils.mjs';
|
||||||
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE } from './radar-constants.mjs';
|
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE, RADAR_SOURCE_SIZE } from './radar-constants.mjs';
|
||||||
|
|
||||||
// process a single radar image and place it on the provided canvas
|
// process a single radar image and place it on the provided canvas
|
||||||
const processRadar = async (data) => {
|
const processRadar = async (data) => {
|
||||||
@@ -13,8 +13,8 @@ const processRadar = async (data) => {
|
|||||||
|
|
||||||
// calculate offsets and sizes
|
// calculate offsets and sizes
|
||||||
const radarSource = {
|
const radarSource = {
|
||||||
width: 240,
|
width: RADAR_SOURCE_SIZE().width,
|
||||||
height: 163,
|
height: RADAR_SOURCE_SIZE().height,
|
||||||
x: Math.round(radarSourceXY.x / 2),
|
x: Math.round(radarSourceXY.x / 2),
|
||||||
y: Math.round(radarSourceXY.y / 2),
|
y: Math.round(radarSourceXY.y / 2),
|
||||||
};
|
};
|
||||||
@@ -52,11 +52,11 @@ const processRadar = async (data) => {
|
|||||||
|
|
||||||
// stretch the radar image
|
// stretch the radar image
|
||||||
const stretchCanvas = document.createElement('canvas');
|
const stretchCanvas = document.createElement('canvas');
|
||||||
stretchCanvas.width = RADAR_FINAL_SIZE.width;
|
stretchCanvas.width = RADAR_FINAL_SIZE().width;
|
||||||
stretchCanvas.height = RADAR_FINAL_SIZE.height;
|
stretchCanvas.height = RADAR_FINAL_SIZE().height;
|
||||||
const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true });
|
const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
stretchContext.imageSmoothingEnabled = false;
|
stretchContext.imageSmoothingEnabled = false;
|
||||||
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE.width, RADAR_FINAL_SIZE.height);
|
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE().width, RADAR_FINAL_SIZE().height);
|
||||||
|
|
||||||
return stretchCanvas.toDataURL();
|
return stretchCanvas.toDataURL();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,28 +31,33 @@ const setTiles = (data) => {
|
|||||||
|
|
||||||
// determine the basemap images needed
|
// determine the basemap images needed
|
||||||
const baseMapTiles = [
|
const baseMapTiles = [
|
||||||
pixelToFile(sourceXY.x, sourceXY.y),
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y),
|
||||||
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y),
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y),
|
||||||
pixelToFile(sourceXY.x, sourceXY.y + TILE_SIZE.y),
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y),
|
||||||
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y + TILE_SIZE.y),
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y + TILE_SIZE.y),
|
||||||
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y + TILE_SIZE.y),
|
||||||
|
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y + TILE_SIZE.y),
|
||||||
];
|
];
|
||||||
|
|
||||||
// do some calculations
|
// do some calculations
|
||||||
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
|
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
|
||||||
// T[0] T[1]
|
// T[0] T[1] T[2]
|
||||||
// T[2] T[3]
|
// T[3] T[4] T[5]
|
||||||
|
|
||||||
// calculate the shift of tile 0 (upper left)
|
// calculate the shift of tile 0 (upper left)
|
||||||
const tileShift = modTile(sourceXY.x, sourceXY.y);
|
const tileShift = modTile(sourceXY.x, sourceXY.y);
|
||||||
|
|
||||||
// determine which tiles are used
|
// determine which tiles are used
|
||||||
|
const secondRow = tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE().height;
|
||||||
const usedTiles = [
|
const usedTiles = [
|
||||||
true,
|
true,
|
||||||
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE.width,
|
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE().width,
|
||||||
tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE.height,
|
tileShift.x + (TILE_SIZE.x * 2) > RADAR_FINAL_SIZE().width,
|
||||||
|
secondRow,
|
||||||
];
|
];
|
||||||
// if we need t[1] and t[2] then we also need t[3]
|
// second row is a copy of the first row when in use
|
||||||
usedTiles.push(usedTiles[1] && usedTiles[2]);
|
// calculate T[4] and T[5]
|
||||||
|
usedTiles.push(secondRow && usedTiles[1], secondRow && usedTiles[2]);
|
||||||
|
|
||||||
// helper function for populating tiles
|
// helper function for populating tiles
|
||||||
const populateTile = (tileName) => (elem, index) => {
|
const populateTile = (tileName) => (elem, index) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { TILE_SIZE, TILE_FULL_SIZE } from './radar-constants.mjs';
|
import {
|
||||||
|
TILE_SIZE, TILE_FULL_SIZE, RADAR_OFFSET, RADAR_SHIFT,
|
||||||
|
} from './radar-constants.mjs';
|
||||||
|
|
||||||
// limit a value to within a range
|
// limit a value to within a range
|
||||||
const coerce = (low, value, high) => Math.max(Math.min(value, high), low);
|
const coerce = (low, value, high) => Math.max(Math.min(value, high), low);
|
||||||
@@ -9,16 +11,16 @@ const getXYFromLatitudeLongitudeMap = (pos) => {
|
|||||||
// 589 466 -122.3615246 47.63177832
|
// 589 466 -122.3615246 47.63177832
|
||||||
// 5288 3638 -80.18297384 25.77018996
|
// 5288 3638 -80.18297384 25.77018996
|
||||||
|
|
||||||
// map position is calculated as a regresion from the above values (=/- a manual adjustment factor)
|
// map position is calculated as a regresion from the above values (+/- a manual adjustment factor) and shifting for enhanced views
|
||||||
// then shifted by half of the tile size (to center the map)
|
// then shifted by half of the tile size (to center the map)
|
||||||
// then they are limited to values between 0 and the width or height of the map
|
// then they are limited to values between 0 and the width or height of the map
|
||||||
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2), TILE_FULL_SIZE.y - (TILE_SIZE.y));
|
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2) - RADAR_SHIFT().y, TILE_FULL_SIZE.y - (TILE_SIZE.y));
|
||||||
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2), TILE_FULL_SIZE.x - (TILE_SIZE.x));
|
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2) - RADAR_SHIFT().x, TILE_FULL_SIZE.x - (TILE_SIZE.x));
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
|
const getXYFromLatitudeLongitudeDoppler = (pos) => {
|
||||||
const imgHeight = 6000;
|
const imgHeight = 6000;
|
||||||
const imgWidth = 2800;
|
const imgWidth = 2800;
|
||||||
|
|
||||||
@@ -26,8 +28,8 @@ const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
|
|||||||
// then shifted by half of the tile size (to center the map)
|
// then shifted by half of the tile size (to center the map)
|
||||||
// then they are limited to values between 0 and the width or height of the map
|
// then they are limited to values between 0 and the width or height of the map
|
||||||
|
|
||||||
const y = coerce(0, (51 - pos.latitude) * 61.4481 - offsetY, imgHeight);
|
const y = coerce(0, (51 - pos.latitude) * 61.4481 - RADAR_OFFSET().y, imgHeight);
|
||||||
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - offsetX, imgWidth);
|
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - RADAR_OFFSET().x, imgWidth);
|
||||||
|
|
||||||
return { x: x * 2, y: y * 2 };
|
return { x: x * 2, y: y * 2 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,10 +128,8 @@ class Radar extends WeatherDisplay {
|
|||||||
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
||||||
|
|
||||||
// calculate offsets and sizes
|
// calculate offsets and sizes
|
||||||
const offsetX = 120 * 2;
|
|
||||||
const offsetY = 69 * 2;
|
|
||||||
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters);
|
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters);
|
||||||
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
|
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters);
|
||||||
|
|
||||||
// set up the base map and overlay tiles
|
// set up the base map and overlay tiles
|
||||||
setTiles({
|
setTiles({
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ const getMinMaxLatitudeLongitudeHI = (X, Y, OffsetX, OffsetY) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
|
const getXYForCity = (City, MaxLatitude, MinLongitude, state, maxX = 580) => {
|
||||||
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
|
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
|
||||||
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
|
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
|
||||||
let x = (City.lon - MinLongitude) * 57;
|
let x = (City.lon - MinLongitude) * 57;
|
||||||
@@ -219,7 +219,7 @@ const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
|
|||||||
if (y > 282) y = 282;
|
if (y > 282) y = 282;
|
||||||
|
|
||||||
if (x < 40) x = 40;
|
if (x < 40) x = 40;
|
||||||
if (x > 580) x = 580;
|
if (x > maxX) x = maxX;
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,11 +14,29 @@ import * as utils from './regionalforecast-utils.mjs';
|
|||||||
import { getPoint } from './utils/weather.mjs';
|
import { getPoint } from './utils/weather.mjs';
|
||||||
import { debugFlag } from './utils/debug.mjs';
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
||||||
|
import settings from './settings.mjs';
|
||||||
|
|
||||||
// map offset
|
// set up spacing and scales
|
||||||
const mapOffsetXY = {
|
const scaling = () => {
|
||||||
x: 240,
|
// available space
|
||||||
y: 117,
|
const available = {
|
||||||
|
x: 640,
|
||||||
|
};
|
||||||
|
|
||||||
|
// map offset
|
||||||
|
const mapOffsetXY = {
|
||||||
|
x: 240,
|
||||||
|
y: 117,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settings.wide?.value && settings.enhanced?.value) {
|
||||||
|
mapOffsetXY.x = 320;
|
||||||
|
available.x = 854;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mapOffsetXY,
|
||||||
|
available,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
class RegionalForecast extends WeatherDisplay {
|
class RegionalForecast extends WeatherDisplay {
|
||||||
@@ -45,6 +63,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
this.elem.querySelector('.map img').src = baseMap;
|
this.elem.querySelector('.map img').src = baseMap;
|
||||||
|
|
||||||
// get user's location in x/y
|
// get user's location in x/y
|
||||||
|
const { available, mapOffsetXY } = scaling();
|
||||||
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
||||||
|
|
||||||
// get latitude and longitude limits
|
// get latitude and longitude limits
|
||||||
@@ -102,7 +121,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get XY on map for city
|
// get XY on map for city
|
||||||
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
|
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state, available - 60);
|
||||||
|
|
||||||
// wait for the regional observation if it's not done yet
|
// wait for the regional observation if it's not done yet
|
||||||
const observation = await observationPromise;
|
const observation = await observationPromise;
|
||||||
@@ -188,7 +207,8 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw the map
|
// draw the map
|
||||||
const scale = 640 / (mapOffsetXY.x * 2);
|
const { available, mapOffsetXY } = scaling();
|
||||||
|
const scale = available.x / (mapOffsetXY.x * 2);
|
||||||
const map = this.elem.querySelector('.map');
|
const map = this.elem.querySelector('.map');
|
||||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ const deferredDomSettings = new Set();
|
|||||||
// don't show checkboxes for these settings
|
// don't show checkboxes for these settings
|
||||||
const hiddenSettings = [
|
const hiddenSettings = [
|
||||||
'scanLines',
|
'scanLines',
|
||||||
|
|
||||||
|
// wide, portrait and enhanced are handled by a dropdown which sets these individual settings accordingly
|
||||||
|
'wide',
|
||||||
|
'portrait',
|
||||||
|
'enhanced',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ)
|
// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ)
|
||||||
@@ -32,6 +37,69 @@ const wideScreenChange = (value) => {
|
|||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const portraitChange = (value) => {
|
||||||
|
const container = document.querySelector('#divTwc');
|
||||||
|
if (!container) {
|
||||||
|
// DOM not ready; defer enabling if set
|
||||||
|
if (value) {
|
||||||
|
deferredDomSettings.add('portrait');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
container.classList.add('portrait');
|
||||||
|
} else {
|
||||||
|
container.classList.remove('portrait');
|
||||||
|
}
|
||||||
|
// Trigger resize to recalculate scaling for new width
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhancedChange = (value) => {
|
||||||
|
const container = document.querySelector('#divTwc');
|
||||||
|
if (!container) {
|
||||||
|
// DOM not ready; defer enabling if set
|
||||||
|
if (value) {
|
||||||
|
deferredDomSettings.add('enhanced');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
container.classList.add('enhanced');
|
||||||
|
} else {
|
||||||
|
container.classList.remove('enhanced');
|
||||||
|
}
|
||||||
|
// Trigger resize to recalculate scaling for new width
|
||||||
|
window.dispatchEvent(new Event('redraw'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewModeChange = (value) => {
|
||||||
|
// set the appropriate mode bits which triggers change actions above
|
||||||
|
switch (value) {
|
||||||
|
case 'wide':
|
||||||
|
settings.wide.value = true;
|
||||||
|
settings.enhanced.value = false;
|
||||||
|
settings.portrait.value = false;
|
||||||
|
break;
|
||||||
|
case 'wide-enhanced':
|
||||||
|
settings.wide.value = true;
|
||||||
|
settings.enhanced.value = true;
|
||||||
|
settings.portrait.value = false;
|
||||||
|
break;
|
||||||
|
case 'portrait-enhanced':
|
||||||
|
settings.wide.value = false;
|
||||||
|
settings.enhanced.value = true;
|
||||||
|
settings.portrait.value = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
settings.wide.value = false;
|
||||||
|
settings.enhanced.value = false;
|
||||||
|
settings.portrait.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const kioskChange = (value) => {
|
const kioskChange = (value) => {
|
||||||
const body = document.querySelector('body');
|
const body = document.querySelector('body');
|
||||||
if (!body) {
|
if (!body) {
|
||||||
@@ -130,6 +198,39 @@ const init = () => {
|
|||||||
changeAction: wideScreenChange,
|
changeAction: wideScreenChange,
|
||||||
sticky: true,
|
sticky: true,
|
||||||
});
|
});
|
||||||
|
settings.portrait = new Setting('portrait', {
|
||||||
|
name: 'Allow Portrait',
|
||||||
|
changeAction: portraitChange,
|
||||||
|
defaultValue: false,
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
settings.enhanced = new Setting('enhanced', {
|
||||||
|
name: 'Enhanced Screens',
|
||||||
|
defaultValue: false,
|
||||||
|
changeAction: enhancedChange,
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
// widescreen, portrait and enhanced are handled by a dropdown
|
||||||
|
// the dropdown change action sets the above bits accordingly
|
||||||
|
// first, figure out the default value based on other settings
|
||||||
|
// this also enforces rules on how these can be combined
|
||||||
|
let viewModeDefault = 'standard';
|
||||||
|
if (settings.wide.value && !settings.enhanced.value) viewModeDefault = 'wide';
|
||||||
|
if (settings.wide.value && settings.enhanced.value) viewModeDefault = 'wide-enhanced';
|
||||||
|
if (settings.portrait.value) viewModeDefault = 'portrait-enhanced';
|
||||||
|
settings.viewMode = new Setting('viewMode', {
|
||||||
|
name: 'Display mode',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: viewModeDefault,
|
||||||
|
changeAction: viewModeChange,
|
||||||
|
sticky: false, // not sticky because the above 3 settings are sticky and define this item's starting state
|
||||||
|
values: [
|
||||||
|
['standard', 'Standard'],
|
||||||
|
['wide', 'Widescreen'],
|
||||||
|
['wide-enhanced', 'Widescreen enhanced'],
|
||||||
|
['portrait-enhanced', 'Portrait enhanced'],
|
||||||
|
],
|
||||||
|
});
|
||||||
settings.kiosk = new Setting('kiosk', {
|
settings.kiosk = new Setting('kiosk', {
|
||||||
name: 'Kiosk',
|
name: 'Kiosk',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
@@ -172,6 +273,7 @@ const init = () => {
|
|||||||
['medium', 'Medium (2x)'],
|
['medium', 'Medium (2x)'],
|
||||||
['thick', 'Thick (3x)'],
|
['thick', 'Thick (3x)'],
|
||||||
],
|
],
|
||||||
|
visible: false,
|
||||||
});
|
});
|
||||||
settings.units = new Setting('units', {
|
settings.units = new Setting('units', {
|
||||||
name: 'Units',
|
name: 'Units',
|
||||||
@@ -221,7 +323,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const settingHtml = Object.values(settings).map((setting) => {
|
const settingHtml = Object.values(settings).map((setting) => {
|
||||||
if (hiddenSettings.includes(setting.shortName)) {
|
if (hiddenSettings.includes(setting.shortName)) {
|
||||||
// setting is hidden, register it
|
// setting is hidden, register it
|
||||||
registerHiddenSetting(setting.elemId, setting);
|
registerHiddenSetting(setting.shortName, setting);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// generate HTML for setting
|
// generate HTML for setting
|
||||||
@@ -239,7 +341,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} else if (modeSelect) {
|
} else if (modeSelect) {
|
||||||
modeSelect.style.display = 'none';
|
modeSelect.style.display = 'none';
|
||||||
}
|
}
|
||||||
registerHiddenSetting('settings-scanLineMode-select', settings.scanLineMode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
|
|||||||
@@ -25,22 +25,28 @@ const createLink = async (e) => {
|
|||||||
const queryStringElements = {};
|
const queryStringElements = {};
|
||||||
|
|
||||||
elemForEach('input[type=checkbox]', (elem) => {
|
elemForEach('input[type=checkbox]', (elem) => {
|
||||||
if (elem?.id) {
|
// use name, and fallback to id (older prefix/suffix permalinks)
|
||||||
queryStringElements[elem.id] = elem?.checked ?? false;
|
const key = elem?.name ?? elem?.id;
|
||||||
|
if (key) {
|
||||||
|
queryStringElements[key] = elem?.checked ?? false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get all select boxes
|
// get all select boxes
|
||||||
elemForEach('select', (elem) => {
|
elemForEach('select', (elem) => {
|
||||||
if (elem?.id) {
|
// use name, and fallback to id (older prefix/suffix permalinks)
|
||||||
queryStringElements[elem.id] = encodeURIComponent(elem?.value ?? '');
|
const key = elem?.name ?? elem?.id;
|
||||||
|
if (key) {
|
||||||
|
queryStringElements[key] = encodeURIComponent(elem?.value ?? '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get all text boxes
|
// get all text boxes
|
||||||
elemForEach('input[type=text]', ((elem) => {
|
elemForEach('input[type=text]', ((elem) => {
|
||||||
if (elem?.id) {
|
// use name, and fallback to id (older prefix/suffix permalinks)
|
||||||
queryStringElements[elem.id] = elem?.value ?? 0;
|
const key = elem?.name ?? elem?.id;
|
||||||
|
if (key && key !== '') {
|
||||||
|
queryStringElements[key] = elem?.value ?? 0;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ class Setting {
|
|||||||
this.elemId = `settings-${shortName}-${this.type}`;
|
this.elemId = `settings-${shortName}-${this.type}`;
|
||||||
|
|
||||||
// get value from url
|
// get value from url
|
||||||
const urlValue = parseQueryString()?.[this.elemId];
|
// includes a fallback to the older prefix/suffix version
|
||||||
|
const queryString = parseQueryString();
|
||||||
|
const urlValue = queryString?.[shortName] ?? queryString?.[this.elemId];
|
||||||
let urlState;
|
let urlState;
|
||||||
if (this.type === 'checkbox' && urlValue !== undefined) {
|
if (this.type === 'checkbox' && urlValue !== undefined) {
|
||||||
urlState = urlValue === 'true';
|
urlState = urlValue === 'true';
|
||||||
@@ -92,7 +94,7 @@ class Setting {
|
|||||||
|
|
||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.id = `settings-${this.shortName}-select`;
|
select.id = `settings-${this.shortName}-select`;
|
||||||
select.name = `settings-${this.shortName}-select`;
|
select.name = this.shortName;
|
||||||
select.addEventListener('change', (e) => this.selectChange(e));
|
select.addEventListener('change', (e) => this.selectChange(e));
|
||||||
|
|
||||||
this.values.forEach(([value, text]) => {
|
this.values.forEach(([value, text]) => {
|
||||||
@@ -125,7 +127,7 @@ class Setting {
|
|||||||
checkbox.type = 'checkbox';
|
checkbox.type = 'checkbox';
|
||||||
checkbox.value = true;
|
checkbox.value = true;
|
||||||
checkbox.id = `settings-${this.shortName}-checkbox`;
|
checkbox.id = `settings-${this.shortName}-checkbox`;
|
||||||
checkbox.name = `settings-${this.shortName}-checkbox`;
|
checkbox.name = this.shortName;
|
||||||
checkbox.checked = this.myValue;
|
checkbox.checked = this.myValue;
|
||||||
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
@@ -148,14 +150,14 @@ class Setting {
|
|||||||
textInput.type = 'text';
|
textInput.type = 'text';
|
||||||
textInput.value = this.myValue;
|
textInput.value = this.myValue;
|
||||||
textInput.id = `settings-${this.shortName}-string`;
|
textInput.id = `settings-${this.shortName}-string`;
|
||||||
textInput.name = `settings-${this.shortName}-string`;
|
textInput.name = this.shortName;
|
||||||
textInput.placeholder = this.placeholder;
|
textInput.placeholder = this.placeholder;
|
||||||
// set button
|
// set button
|
||||||
const setButton = document.createElement('input');
|
const setButton = document.createElement('input');
|
||||||
setButton.type = 'button';
|
setButton.type = 'button';
|
||||||
setButton.value = 'Set';
|
setButton.value = 'Set';
|
||||||
setButton.id = `settings-${this.shortName}-button`;
|
setButton.id = `settings-${this.shortName}-button`;
|
||||||
setButton.name = `settings-${this.shortName}-button`;
|
setButton.name = this.shortName;
|
||||||
setButton.addEventListener('click', () => {
|
setButton.addEventListener('click', () => {
|
||||||
this.stringChange({ target: { value: textInput.value } });
|
this.stringChange({ target: { value: textInput.value } });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ class WeatherDisplay {
|
|||||||
// no checkbox if progress
|
// no checkbox if progress
|
||||||
if (this.elemId === 'progress') return false;
|
if (this.elemId === 'progress') return false;
|
||||||
|
|
||||||
// get url provided state
|
// get url provided state, and fall back to the older suffix naming convention
|
||||||
const urlValue = parseQueryString()?.[`${this.elemId}-checkbox`];
|
const queryString = parseQueryString();
|
||||||
|
const urlValue = queryString?.[this.elemId] ?? queryString?.[`${this.elemId}-checkbox`];
|
||||||
let urlState;
|
let urlState;
|
||||||
if (urlValue !== undefined) {
|
if (urlValue !== undefined) {
|
||||||
urlState = urlValue === 'true';
|
urlState = urlValue === 'true';
|
||||||
@@ -78,7 +79,7 @@ class WeatherDisplay {
|
|||||||
checkbox.type = 'checkbox';
|
checkbox.type = 'checkbox';
|
||||||
checkbox.value = true;
|
checkbox.value = true;
|
||||||
checkbox.id = `${this.elemId}-checkbox`;
|
checkbox.id = `${this.elemId}-checkbox`;
|
||||||
checkbox.name = `${this.elemId}-checkbox`;
|
checkbox.name = this.elemId;
|
||||||
checkbox.checked = this.isEnabled;
|
checkbox.checked = this.isEnabled;
|
||||||
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
#almanac-html.weather-display {
|
#almanac-html.weather-display {
|
||||||
background-image: url('../images/backgrounds/3.png');
|
background-image: url('../images/backgrounds/3.png');
|
||||||
|
|
||||||
|
// repeat the background if wide-enhanced
|
||||||
|
.wide.enhanced & {
|
||||||
|
background-image: url('../images/backgrounds/3-wide-enhanced.png');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-display .main.almanac {
|
.weather-display .main.almanac {
|
||||||
@@ -14,13 +19,17 @@
|
|||||||
// Use CSS Grid for cross-browser consistency
|
// Use CSS Grid for cross-browser consistency
|
||||||
// Grid is populated in reading order (left-to-right, top-to-bottom):
|
// Grid is populated in reading order (left-to-right, top-to-bottom):
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto auto;
|
grid-template-columns: repeat(3, auto);
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: repeat(3, auto);
|
||||||
gap: 0px 90px;
|
gap: 0px 90px;
|
||||||
margin: 3px auto 5px auto; // align the bottom of the div with the background
|
margin: 3px auto 5px auto; // align the bottom of the div with the background
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
}
|
||||||
|
|
||||||
.grid-item {
|
.grid-item {
|
||||||
// Reset inherited styles that interfere with grid layout
|
// Reset inherited styles that interfere with grid layout
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -45,6 +54,14 @@
|
|||||||
&.time {
|
&.time {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.wide-enhanced {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +75,10 @@
|
|||||||
padding-left: 13px;
|
padding-left: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.days {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.day {
|
.day {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -77,4 +98,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
.weather-display .main.current-weather {
|
.weather-display .main.current-weather {
|
||||||
&.main {
|
&.main {
|
||||||
|
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@@ -12,12 +14,17 @@
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
width: 300px;
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
@include u.text-shadow();
|
@include u.text-shadow();
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
font-family: 'Star4000 Extended';
|
font-family: 'Star4000 Extended';
|
||||||
font-size: 24pt;
|
font-size: 24pt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
@@ -92,4 +99,4 @@
|
|||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
#hazards-html.weather-display {
|
#hazards-html.weather-display {
|
||||||
background-image: url('../images/backgrounds/7.png');
|
background-image: url('../images/backgrounds/7.png');
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
.weather-display .main.hazards {
|
.weather-display .main.hazards {
|
||||||
&.main {
|
&.main {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
background-color: rgb(112, 35, 35);
|
background-color: rgb(112, 35, 35);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
#hourly-graph-html {
|
#hourly-graph-html {
|
||||||
background-image: url(../images/backgrounds/1-chart.png);
|
background-image: url(../images/backgrounds/1-chart.png);
|
||||||
|
|
||||||
|
// change background for wide-enhanced
|
||||||
|
.wide.enhanced & {
|
||||||
|
background-image: url(../images/backgrounds/1-chart-wide.png);
|
||||||
|
background-position-x: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
.right {
|
.right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -84,10 +90,51 @@
|
|||||||
&.l-5 {
|
&.l-5 {
|
||||||
left: calc(532px / 4 * 4);
|
left: calc(532px / 4 * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adjust when enhanced
|
||||||
|
.wide.enhanced & {
|
||||||
|
|
||||||
|
&.l-1 {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-2 {
|
||||||
|
left: calc(726px / 6 * 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-3 {
|
||||||
|
left: calc(726px / 6 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-4 {
|
||||||
|
left: calc(726px / 6 * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-5 {
|
||||||
|
left: calc(726px / 6 * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-6 {
|
||||||
|
left: calc(726px / 6 * 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-7 {
|
||||||
|
left: calc(726px / 6 * 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only in wide + enhanced
|
||||||
|
&.l-6,
|
||||||
|
&.l-7 {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
@@ -97,6 +144,11 @@
|
|||||||
img {
|
img {
|
||||||
width: 532px;
|
width: 532px;
|
||||||
height: 285px;
|
height: 285px;
|
||||||
|
|
||||||
|
// wide and enhanced
|
||||||
|
.wide.enhanced & {
|
||||||
|
width: 746px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,32 +180,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-headers {
|
|
||||||
background-color: c.$column-header;
|
|
||||||
height: 20px;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-headers {
|
|
||||||
position: sticky;
|
|
||||||
top: 0px;
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
|
|
||||||
.temp {
|
|
||||||
left: 355px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.like {
|
|
||||||
left: 435px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind {
|
|
||||||
left: 535px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
left: 425px;
|
left: 425px;
|
||||||
|
|
||||||
&.heat-index {
|
&.heat-index {
|
||||||
color: #e00;
|
color: c.$heat-index;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.wind-chill {
|
&.wind-chill {
|
||||||
color: c.$extended-low;
|
color: c.$wind-chill;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,41 @@
|
|||||||
left: 430px;
|
left: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.like {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wide and enhanced moves the columns and enables the like column
|
||||||
|
.wide.enhanced & {
|
||||||
|
.temp {
|
||||||
|
left: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like {
|
||||||
|
left: 380px;
|
||||||
|
display: hidden;
|
||||||
|
|
||||||
|
&.heat-index {
|
||||||
|
color: c.$heat-index;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wind-chill {
|
||||||
|
display: block;
|
||||||
|
color: c.$wind-chill;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather {
|
||||||
|
left: 470px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind {
|
||||||
|
left: 630px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.observation-lines {
|
.observation-lines {
|
||||||
min-height: 338px;
|
min-height: 338px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
.weather-display .local-forecast {
|
.weather-display .local-forecast {
|
||||||
|
|
||||||
|
// clamp width to standard
|
||||||
|
&.main {
|
||||||
|
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Star4000";
|
font-family: "Star4000";
|
||||||
@@ -33,7 +34,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#divQuery {
|
#divQuery {
|
||||||
max-width: 640px;
|
max-width: p.$standard-width;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
@@ -146,11 +147,11 @@ body {
|
|||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 640px;
|
max-width: p.$standard-width;
|
||||||
margin: 0; // Ensure edge-to-edge display
|
margin: 0; // Ensure edge-to-edge display
|
||||||
|
|
||||||
&.wide {
|
&.wide {
|
||||||
max-width: 854px;
|
max-width: p.$wide-width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +160,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#divTwcMain {
|
#divTwcMain {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.wide & {
|
.wide & {
|
||||||
width: 854px;
|
width: p.$wide-width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +210,10 @@ body {
|
|||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
|
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
|
|
||||||
.wide & {
|
.wide & {
|
||||||
width: 854px;
|
width: p.$wide-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -274,7 +275,7 @@ body {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
max-width: 640px;
|
max-width: p.$standard-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwcNav>div {
|
#divTwcNav>div {
|
||||||
@@ -336,8 +337,8 @@ body {
|
|||||||
|
|
||||||
#container {
|
#container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
// overflow: hidden;
|
// overflow: hidden;
|
||||||
background-image: url(../images/backgrounds/1.png);
|
background-image: url(../images/backgrounds/1.png);
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
@@ -345,8 +346,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wide #container {
|
.wide #container {
|
||||||
padding-left: 107px;
|
width: p.$wide-width;
|
||||||
padding-right: 107px;
|
|
||||||
background: url(../images/backgrounds/1-wide.png);
|
background: url(../images/backgrounds/1-wide.png);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
@@ -359,8 +359,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#loading {
|
#loading {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-shadow: 4px 4px black;
|
text-shadow: 4px 4px black;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -368,6 +368,10 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
.wide & {
|
||||||
|
margin-left: p.$wide-margin;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-family: Star4000 Large;
|
font-family: Star4000 Large;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
.weather-display .progress {
|
.weather-display .progress {
|
||||||
@include u.text-shadow();
|
@include u.text-shadow();
|
||||||
font-family: 'Star4000 Extended';
|
font-family: 'Star4000 Extended';
|
||||||
font-size: 19pt;
|
font-size: 19pt;
|
||||||
|
|
||||||
|
// clamp width to standard
|
||||||
|
&.main {
|
||||||
|
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
margin: 0px 10px;
|
margin: 0px 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 310px;
|
height: p.$standard-scroll-height;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
|
|
||||||
@@ -118,4 +124,4 @@
|
|||||||
transition: width 1s steps(6);
|
transition: width 1s steps(6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
#radar-html.weather-display {
|
#radar-html.weather-display {
|
||||||
background-image: url('../images/backgrounds/4.png');
|
background-image: url('../images/backgrounds/4.png');
|
||||||
|
|
||||||
|
.wide & {
|
||||||
|
background: url(../images/backgrounds/4-wide.png);
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
height: 83px;
|
height: 83px;
|
||||||
|
|
||||||
@@ -104,12 +109,13 @@
|
|||||||
.weather-display .main.radar {
|
.weather-display .main.radar {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 367px;
|
height: 367px;
|
||||||
|
width: p.$standard-width;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
||||||
.tiles {
|
.tiles {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1400px;
|
width: 2040px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -120,8 +126,4 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.wide.radar #container {
|
|
||||||
background: url(../images/backgrounds/4-wide.png);
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
#regional-forecast-html.weather-display {
|
|
||||||
background-image: url('../images/backgrounds/5.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
.weather-display .main.regional-forecast {
|
.weather-display .main.regional-forecast {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
#spc-outlook-html.weather-display {
|
#spc-outlook-html.weather-display {
|
||||||
background-image: url('../images/backgrounds/6.png');
|
background-image: url('../images/backgrounds/6.png');
|
||||||
|
|||||||
@@ -1,29 +1,50 @@
|
|||||||
@use 'shared/_colors'as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils'as u;
|
@use 'shared/_utils'as u;
|
||||||
|
@use 'shared/positions'as p;
|
||||||
|
|
||||||
.weather-display {
|
.weather-display {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-image: url(../images/backgrounds/1.png);
|
background-image: url(../images/backgrounds/1.png);
|
||||||
|
|
||||||
|
// adjust for wide
|
||||||
|
.wide & {
|
||||||
|
width: p.$wide-width;
|
||||||
|
background-position-x: p.$wide-margin;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
&:has(.can-enhance) {
|
||||||
|
background-image: url(../images/backgrounds/1-wide-enhanced.png);
|
||||||
|
background-position-x: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* this method is required to hide blocks so they can be measured while off screen */
|
/* this method is required to hide blocks so they can be measured while off screen */
|
||||||
height: 0px;
|
height: 0px;
|
||||||
|
|
||||||
&.show {
|
&.show {
|
||||||
height: 480px;
|
height: p.$standard-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template {
|
.template {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
>.header {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
position: relative;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
|
|
||||||
|
// adjust for wide
|
||||||
|
.wide & {
|
||||||
|
left: p.$wide-margin;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: c.$title-color;
|
color: c.$title-color;
|
||||||
@include u.text-shadow(3px, 1.5px);
|
@include u.text-shadow(3px, 1.5px);
|
||||||
@@ -92,10 +113,23 @@
|
|||||||
.main {
|
.main {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
// adjust for wide
|
||||||
|
.wide & {
|
||||||
|
left: p.$wide-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust for enhanced when possible
|
||||||
|
.wide.enhanced & {
|
||||||
|
&.can-enhance {
|
||||||
|
left: 0px;
|
||||||
|
width: p.$wide-width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.has-scroll {
|
&.has-scroll {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
height: 310px;
|
height: p.$standard-scroll-height;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.no-header {
|
&.no-header {
|
||||||
@@ -105,9 +139,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.has-box {
|
&.has-box {
|
||||||
margin-left: 64px;
|
margin-left: p.$blue-box-margin;
|
||||||
margin-right: 64px;
|
margin-right: p.$blue-box-margin;
|
||||||
width: calc(100% - 128px);
|
width: calc(100% - 128px);
|
||||||
|
|
||||||
|
.wide.enhanced & {
|
||||||
|
&.can-enhance {
|
||||||
|
width: calc(p.$wide-width - p.$blue-box-margin - p.$blue-box-margin)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -117,7 +157,7 @@
|
|||||||
#container>.scroll {
|
#container>.scroll {
|
||||||
display: none;
|
display: none;
|
||||||
@include u.text-shadow(3px, 1.5px);
|
@include u.text-shadow(3px, 1.5px);
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
height: 77px;
|
height: 77px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
@@ -125,12 +165,17 @@
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
// adjust for wide
|
||||||
|
.wide & {
|
||||||
|
left: p.$wide-margin;
|
||||||
|
}
|
||||||
|
|
||||||
&.hazard {
|
&.hazard {
|
||||||
background-color: rgb(112, 35, 35);
|
background-color: rgb(112, 35, 35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-container {
|
.scroll-container {
|
||||||
width: 640px;
|
width: p.$standard-width;
|
||||||
|
|
||||||
.fixed,
|
.fixed,
|
||||||
.scroll-header {
|
.scroll-header {
|
||||||
@@ -156,7 +201,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
// the following added by js code as it is dependent on the content of the element
|
// the following added by js code as it is dependent on the content of the element
|
||||||
// transition: left (x)s;
|
// transition: left (x)s;
|
||||||
// left: calc((elem width) - 640px);
|
// left: calc((elem width) - p.$standard-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,10 +211,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wide #container>.scroll {
|
.wide #container>.scroll {
|
||||||
width: 854px;
|
width: p.$wide-width;
|
||||||
margin-left: -107px;
|
margin-left: -1*p.$wide-margin;
|
||||||
|
|
||||||
.scroll-container {
|
.scroll-container {
|
||||||
margin-left: 107px;
|
margin-left: p.$wide-margin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,7 @@ $gradient-loading-3: #4f99f9;
|
|||||||
$gradient-loading-4: #8ffdfa;
|
$gradient-loading-4: #8ffdfa;
|
||||||
|
|
||||||
$extended-low: #8080FF;
|
$extended-low: #8080FF;
|
||||||
|
$wind-chill: #8080FF;
|
||||||
|
$heat-index: #e00;
|
||||||
|
|
||||||
$blue-box: #26235a;
|
$blue-box: #26235a;
|
||||||
14
server/styles/scss/shared/_positions.scss
Normal file
14
server/styles/scss/shared/_positions.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// standard positioning
|
||||||
|
$standard-width: 640px;
|
||||||
|
$standard-height: 480px;
|
||||||
|
|
||||||
|
// height with scroll
|
||||||
|
$standard-scroll-height: 310px;
|
||||||
|
|
||||||
|
// blue box size
|
||||||
|
$blue-box-margin: 64px;
|
||||||
|
|
||||||
|
// wide screen positioning
|
||||||
|
$wide-padding: 107px;
|
||||||
|
$wide-margin: 107px;
|
||||||
|
$wide-width: 854px;
|
||||||
2
server/styles/ws.min.css
vendored
2
server/styles/ws.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,10 +68,10 @@
|
|||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body <% if (query && query['settings-kiosk-checkbox'] === 'true' ) { %>class="kiosk" <% }%>>
|
<body <% if (query && (query['kiosk'] === true || query['settings-kiosk-checkbox'] === 'true' )) { %>class="kiosk" <% }%>>
|
||||||
|
|
||||||
<div id="divQuery">
|
<div id="divQuery">
|
||||||
<input id="txtLocation" type="text" value="" placeholder="ZIP Code or City, State" data-1p-ignore />
|
<input id="txtLocation" name="txtLocation" type="text" value="" placeholder="ZIP Code or City, State" data-1p-ignore />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light" />
|
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light" />
|
||||||
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark" />
|
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark" />
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
|
<%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
|
||||||
<div class="main has-scroll almanac">
|
<div class="main has-scroll almanac can-enhance">
|
||||||
<div class="sun">
|
<div class="sun">
|
||||||
<div class="grid-item empty"></div>
|
<div class="grid-item empty"></div>
|
||||||
|
<div class="grid-item header day-0"></div>
|
||||||
<div class="grid-item header day-1"></div>
|
<div class="grid-item header day-1"></div>
|
||||||
<div class="grid-item header day-2"></div>
|
<div class="grid-item header day-2 wide-enhanced"></div>
|
||||||
<div class="grid-item row-label">Sunrise:</div>
|
<div class="grid-item row-label">Sunrise:</div>
|
||||||
|
<div class="grid-item time rise-0"></div>
|
||||||
<div class="grid-item time rise-1"></div>
|
<div class="grid-item time rise-1"></div>
|
||||||
<div class="grid-item time rise-2"></div>
|
<div class="grid-item time rise-2 wide-enhanced"></div>
|
||||||
<div class="grid-item row-label">Sunset:</div>
|
<div class="grid-item row-label">Sunset:</div>
|
||||||
|
<div class="grid-item time set-0"></div>
|
||||||
<div class="grid-item time set-1"></div>
|
<div class="grid-item time set-1"></div>
|
||||||
<div class="grid-item time set-2"></div>
|
<div class="grid-item time set-2 wide-enhanced"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="moon">
|
<div class="moon">
|
||||||
<div class="title">Moon Data:</div>
|
<div class="title">Moon Data:</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
||||||
<div class="main has-scroll has-box current-weather">
|
<div class="main has-scroll has-box current-weather can-enhance">
|
||||||
<div class="weather template">
|
<div class="weather template">
|
||||||
<div class="left col">
|
<div class="left col">
|
||||||
<div class="temp center"></div>
|
<div class="temp center"></div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
|
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
|
||||||
<div class="main has-scroll hourly-graph">
|
<div class="main has-scroll hourly-graph can-enhance">
|
||||||
<div class="top-right template ">
|
<div class="top-right template ">
|
||||||
<div class="temperature">Temperature</div>
|
<div class="temperature">Temperature</div>
|
||||||
<div class="dewpoint">Dewpoint</div>
|
<div class="dewpoint">Dewpoint</div>
|
||||||
<div class="cloud">Cloud %</div>
|
<div class="cloud">Cloud %</div>
|
||||||
<div class="rain">Precip %</div>
|
<div class="rain">Precip %</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="label l-1">75</div>
|
<div class="label l-1">75</div>
|
||||||
<div class="label l-2">65</div>
|
<div class="label l-2">65</div>
|
||||||
<div class="label l-3">55</div>
|
<div class="label l-3">55</div>
|
||||||
<div class="label l-4">45</div>
|
<div class="label l-4">45</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<img id="chart-area"></img>
|
<img id="chart-area"></img>
|
||||||
@@ -21,5 +21,7 @@
|
|||||||
<div class="label l-3">12p</div>
|
<div class="label l-3">12p</div>
|
||||||
<div class="label l-4">6p</div>
|
<div class="label l-4">6p</div>
|
||||||
<div class="label l-5">12a</div>
|
<div class="label l-5">12a</div>
|
||||||
|
<div class="label l-6">6a</div>
|
||||||
|
<div class="label l-7">12p</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
||||||
<div class="main has-scroll latest-observations has-box">
|
<div class="main has-scroll latest-observations has-box can-enhance">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="column-headers">
|
<div class="column-headers">
|
||||||
<div class="temp english">°F</div>
|
<div class="temp english">°F</div>
|
||||||
<div class="temp metric">°C</div>
|
<div class="temp metric">°C</div>
|
||||||
|
<div class="like">Like</div>
|
||||||
<div class="weather">Weather</div>
|
<div class="weather">Weather</div>
|
||||||
<div class="wind">Wind</div>
|
<div class="wind">Wind</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
<div class="observation-row template">
|
<div class="observation-row template">
|
||||||
<div class="location"></div>
|
<div class="location"></div>
|
||||||
<div class="temp"></div>
|
<div class="temp"></div>
|
||||||
|
<div class="like"></div>
|
||||||
<div class="weather"></div>
|
<div class="weather"></div>
|
||||||
<div class="wind"></div>
|
<div class="wind"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %>
|
||||||
<div class="main has-scroll has-box local-forecast">
|
<div class="main has-scroll has-box local-forecast can-enhance">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="forecasts">
|
<div class="forecasts">
|
||||||
<div class="forecast template">
|
<div class="forecast template">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('scroll.ejs') %>
|
<%- include('scroll.ejs') %>
|
||||||
@@ -1,45 +1,45 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
|
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
|
||||||
<div class="title dual">
|
<div class="title dual">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
Local
|
Local
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
Radar
|
Radar
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="precip">
|
<div class="precip">
|
||||||
<div class="precip-header">PRECIP</div>
|
<div class="precip-header">PRECIP</div>
|
||||||
<div class="scale">
|
<div class="scale">
|
||||||
<div class="text">Light</div>
|
<div class="text">Light</div>
|
||||||
<div class="scale-table">
|
<div class="scale-table">
|
||||||
<div class="box box-1"></div>
|
<div class="box box-1"></div>
|
||||||
<div class="box box-2"></div>
|
<div class="box box-2"></div>
|
||||||
<div class="box box-3"></div>
|
<div class="box box-3"></div>
|
||||||
<div class="box box-4"></div>
|
<div class="box box-4"></div>
|
||||||
<div class="box box-5"></div>
|
<div class="box box-5"></div>
|
||||||
<div class="box box-6"></div>
|
<div class="box box-6"></div>
|
||||||
<div class="box box-7"></div>
|
<div class="box box-7"></div>
|
||||||
<div class="box box-7"></div>
|
<div class="box box-7"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text">Heavy</div>
|
<div class="text">Heavy</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time"></div>
|
<div class="time"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main radar">
|
<div class="main radar can-enhance">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="map-tiles tiles"><img/><img/><img/><img/></div>
|
<div class="map-tiles tiles"><img /><img /><img /><img /><img /><img /></div>
|
||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<div class="frame template">
|
<div class="frame template">
|
||||||
<div class="map">
|
<div class="map">
|
||||||
<img/>
|
<img />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overlay-tiles tiles"><img/><img/><img/><img/></div>
|
<div class="overlay-tiles tiles"><img /><img /><img /><img /><img /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
||||||
<div class="main has-scroll regional-forecast">
|
<div class="main has-scroll regional-forecast can-enhance">
|
||||||
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
||||||
<div class="location-container">
|
<div class="location-container">
|
||||||
<div class="location template">
|
<div class="location template">
|
||||||
|
|||||||
Reference in New Issue
Block a user