class static code cleanup

This commit is contained in:
Matt Walsh 2022-12-09 13:51:51 -06:00
parent 0c8db4f38e
commit f633631532
14 changed files with 630 additions and 629 deletions

View file

@ -133,7 +133,7 @@ class Almanac extends WeatherDisplay {
fill.date = date;
fill.type = MoonPhase.phase;
fill.icon = { type: 'img', src: Almanac.imageName(MoonPhase.Phase) };
fill.icon = { type: 'img', src: imageName(MoonPhase.Phase) };
return this.fillTemplate('day', fill);
});
@ -145,20 +145,6 @@ class Almanac extends WeatherDisplay {
this.finishDraw();
}
static imageName(type) {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
}
// make sun and moon data available outside this class
// promise allows for data to be requested before it is available
async getSun() {
@ -170,6 +156,20 @@ class Almanac extends WeatherDisplay {
}
}
const imageName = (type) => {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
};
// register display
const display = new Almanac(8, 'almanac');
registerDisplay(display);

View file

@ -128,7 +128,7 @@ class CurrentWeather extends WeatherDisplay {
let Conditions = data.observations.textDescription;
if (Conditions.length > 15) {
Conditions = CurrentWeather.shortConditions(Conditions);
Conditions = shortConditions(Conditions);
}
fill.condition = Conditions;
@ -170,8 +170,9 @@ class CurrentWeather extends WeatherDisplay {
this.getDataCallbacks.push(() => resolve(this.parseData()));
});
}
}
static shortConditions(_condition) {
const shortConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/g, 'L');
condition = condition.replace(/Heavy/g, 'H');
@ -188,8 +189,8 @@ class CurrentWeather extends WeatherDisplay {
condition = condition.replace(/L Snow Fog/g, 'L Snw/Fog');
condition = condition.replace(/ with /g, '/');
return condition;
}
}
};
const display = new CurrentWeather(0, 'current-weather');
registerDisplay(display);

View file

@ -36,13 +36,48 @@ class ExtendedForecast extends WeatherDisplay {
return;
}
// we only get here if there was no error above
this.data = ExtendedForecast.parse(forecast.properties.periods);
this.data = parse(forecast.properties.periods);
this.screenIndex = 0;
this.setStatus(STATUS.loaded);
}
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
static parse(fullForecast) {
async drawCanvas() {
super.drawCanvas();
// determine bounds
// grab the first three or second set of three array elements
const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
// create each day template
const days = forecast.map((Day) => {
const fill = {};
fill.date = Day.dayName;
const { low } = Day;
if (low !== undefined) {
fill['value-lo'] = Math.round(low);
}
const { high } = Day;
fill['value-hi'] = Math.round(high);
fill.condition = Day.text;
// draw the icon
fill.icon = { type: 'img', src: Day.icon };
// return the filled template
return this.fillTemplate('day', fill);
});
// empty and update the container
const dayContainer = this.elem.querySelector('.day-container');
dayContainer.innerHTML = '';
dayContainer.append(...days);
this.finishDraw();
}
}
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
const parse = (fullForecast) => {
// create a list of days starting with today
const Days = [0, 1, 2, 3, 4, 5, 6];
@ -65,7 +100,7 @@ class ExtendedForecast extends WeatherDisplay {
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 = getWeatherIconFromIconLink(period.icon);
fDay.text = ExtendedForecast.shortenExtendedForecastText(period.shortForecast);
fDay.text = shortenExtendedForecastText(period.shortForecast);
fDay.dayName = dates[destIndex];
// preload the icon
@ -82,9 +117,9 @@ class ExtendedForecast extends WeatherDisplay {
});
return forecast;
}
};
static shortenExtendedForecastText(long) {
const shortenExtendedForecastText = (long) => {
const regexList = [
[/ and /ig, ' '],
[/Slight /ig, ''],
@ -123,42 +158,7 @@ class ExtendedForecast extends WeatherDisplay {
}
return result;
}
async drawCanvas() {
super.drawCanvas();
// determine bounds
// grab the first three or second set of three array elements
const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
// create each day template
const days = forecast.map((Day) => {
const fill = {};
fill.date = Day.dayName;
const { low } = Day;
if (low !== undefined) {
fill['value-lo'] = Math.round(low);
}
const { high } = Day;
fill['value-hi'] = Math.round(high);
fill.condition = Day.text;
// draw the icon
fill.icon = { type: 'img', src: Day.icon };
// return the filled template
return this.fillTemplate('day', fill);
});
// empty and update the container
const dayContainer = this.elem.querySelector('.day-container');
dayContainer.innerHTML = '';
dayContainer.append(...days);
this.finishDraw();
}
}
};
// register display
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));

View file

@ -43,7 +43,7 @@ class Hourly extends WeatherDisplay {
return;
}
this.data = await Hourly.parseForecast(forecast.properties);
this.data = await parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
@ -51,66 +51,6 @@ class Hourly extends WeatherDisplay {
this.drawLongCanvas();
}
// extract specific values from forecast and format as an array
static async parseForecast(data) {
const temperature = Hourly.expand(data.temperature.values);
const apparentTemperature = Hourly.expand(data.apparentTemperature.values);
const windSpeed = Hourly.expand(data.windSpeed.values);
const windDirection = Hourly.expand(data.windDirection.values);
const skyCover = Hourly.expand(data.skyCover.values); // cloud icon
const weather = Hourly.expand(data.weather.values); // fog icon
const iceAccumulation = Hourly.expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = Hourly.expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = Hourly.expand(data.snowfallAmount.values); // snow icon
const icons = await Hourly.determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
}
// given forecast paramaters determine a suitable icon
static async determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) {
const startOfHour = DateTime.local().startOf('hour');
const sunTimes = (await getSun()).sun;
const overnight = Interval.fromDateTimes(DateTime.fromJSDate(sunTimes[0].sunset), DateTime.fromJSDate(sunTimes[1].sunrise));
const tomorrowOvernight = DateTime.fromJSDate(sunTimes[1].sunset);
return skyCover.map((val, idx) => {
const hour = startOfHour.plus({ hours: idx });
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
return getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
});
}
// expand a set of values with durations to an hour-by-hour array
static expand(data) {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
const duration = Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
const endTime = startTime + duration;
// loop through duration at one hour intervals
do {
// test for timestamp greater than now
if (startTime >= startOfHour && result.length < 24) {
result.push(item.value); // push data array
} // timestamp is after now
// increment start time by 1 hour
startTime += 3600000;
} while (startTime < endTime && result.length < 24);
}); // for each value
return result;
}
async drawLongCanvas() {
// get the list element and populate
const list = this.elem.querySelector('.hourly-lines');
@ -178,19 +118,6 @@ class Hourly extends WeatherDisplay {
this.elem.querySelector('.main').scrollTo(0, offsetY);
}
static getTravelCitiesDayName(cities) {
// effectively returns early on the first found date
return cities.reduce((dayName, city) => {
if (city && dayName === '') {
// today or tomorrow
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
// return the day
return day.toLocaleString({ weekday: 'long' });
}
return dayName;
}, '');
}
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentData() {
@ -202,6 +129,66 @@ class Hourly extends WeatherDisplay {
}
}
// extract specific values from forecast and format as an array
const parseForecast = async (data) => {
const temperature = expand(data.temperature.values);
const apparentTemperature = expand(data.apparentTemperature.values);
const windSpeed = expand(data.windSpeed.values);
const windDirection = expand(data.windDirection.values);
const skyCover = expand(data.skyCover.values); // cloud icon
const weather = expand(data.weather.values); // fog icon
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
};
// given forecast paramaters determine a suitable icon
const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) => {
const startOfHour = DateTime.local().startOf('hour');
const sunTimes = (await getSun()).sun;
const overnight = Interval.fromDateTimes(DateTime.fromJSDate(sunTimes[0].sunset), DateTime.fromJSDate(sunTimes[1].sunrise));
const tomorrowOvernight = DateTime.fromJSDate(sunTimes[1].sunset);
return skyCover.map((val, idx) => {
const hour = startOfHour.plus({ hours: idx });
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
return getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
});
};
// expand a set of values with durations to an hour-by-hour array
const expand = (data) => {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
const duration = Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
const endTime = startTime + duration;
// loop through duration at one hour intervals
do {
// test for timestamp greater than now
if (startTime >= startOfHour && result.length < 24) {
result.push(item.value); // push data array
} // timestamp is after now
// increment start time by 1 hour
startTime += 3600000;
} while (startTime < endTime && result.length < 24);
}); // for each value
return result;
};
// register display
const display = new Hourly(2, 'hourly', false);
registerDisplay(display);

View file

@ -82,7 +82,7 @@ class LatestObservations extends WeatherDisplay {
const fill = {};
fill.location = locationCleanup(condition.city).substr(0, 14);
fill.temp = Temperature;
fill.weather = LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9);
fill.weather = shortenCurrentConditions(condition.textDescription).substr(0, 9);
if (WindSpeed > 0) {
fill.wind = windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString();
} else if (WindSpeed === 'NA') {
@ -100,8 +100,8 @@ class LatestObservations extends WeatherDisplay {
this.finishDraw();
}
static shortenCurrentConditions(_condition) {
}
const shortenCurrentConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/, 'L');
condition = condition.replace(/Heavy/, 'H');
@ -118,7 +118,6 @@ class LatestObservations extends WeatherDisplay {
condition = condition.replace(/L Snow Fog/, 'L Snw/Fog');
condition = condition.replace(/ with /, '/');
return condition;
}
}
};
// register display
registerDisplay(new LatestObservations(1, 'latest-observations'));

View file

@ -25,7 +25,7 @@ class LocalForecast extends WeatherDisplay {
return;
}
// parse raw data
const conditions = LocalForecast.parse(rawData);
const conditions = parse(rawData);
// read each text
this.screenTexts = conditions.map((condition) => {
@ -80,17 +80,14 @@ class LocalForecast extends WeatherDisplay {
this.finishDraw();
}
}
// format the forecast
static parse(forecast) {
// only use the first 6 lines
return forecast.properties.periods.slice(0, 6).map((text) => ({
// format the forecast
// only use the first 6 lines
const parse = (forecast) => forecast.properties.periods.slice(0, 6).map((text) => ({
// format day and text
DayName: text.name.toUpperCase(),
Text: text.detailedForecast,
}));
}
}
}));
// register display
registerDisplay(new LocalForecast(6, 'local-forecast'));

View file

@ -0,0 +1,185 @@
const getXYFromLatitudeLongitudeMap = (pos, offsetX, offsetY) => {
let y = 0;
let x = 0;
const imgHeight = 3200;
const imgWidth = 5100;
y = (51.75 - pos.latitude) * 55.2;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-130.37 - pos.longitude) * 41.775) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
};
const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
let y = 0;
let x = 0;
const imgHeight = 6000;
const imgWidth = 2800;
y = (51 - pos.latitude) * 61.4481;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-129.138 - pos.longitude) * 42.1768) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
};
const removeDopplerRadarImageNoise = (RadarContext) => {
const RadarImageData = RadarContext.getImageData(0, 0, RadarContext.canvas.width, RadarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < RadarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
let R = RadarImageData.data[i];
let G = RadarImageData.data[i + 1];
let B = RadarImageData.data[i + 2];
let A = RadarImageData.data[i + 3];
// is this pixel the old rgb?
if ((R === 0 && G === 0 && B === 0)
|| (R === 0 && G === 236 && B === 236)
|| (R === 1 && G === 160 && B === 246)
|| (R === 0 && G === 0 && B === 246)) {
// change to your new rgb
// Transparent
R = 0;
G = 0;
B = 0;
A = 0;
} else if ((R === 0 && G === 255 && B === 0)) {
// Light Green 1
R = 49;
G = 210;
B = 22;
A = 255;
} else if ((R === 0 && G === 200 && B === 0)) {
// Light Green 2
R = 0;
G = 142;
B = 0;
A = 255;
} else if ((R === 0 && G === 144 && B === 0)) {
// Dark Green 1
R = 20;
G = 90;
B = 15;
A = 255;
} else if ((R === 255 && G === 255 && B === 0)) {
// Dark Green 2
R = 10;
G = 40;
B = 10;
A = 255;
} else if ((R === 231 && G === 192 && B === 0)) {
// Yellow
R = 196;
G = 179;
B = 70;
A = 255;
} else if ((R === 255 && G === 144 && B === 0)) {
// Orange
R = 190;
G = 72;
B = 19;
A = 255;
} else if ((R === 214 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 0)) {
// Red
R = 171;
G = 14;
B = 14;
A = 255;
} else if ((R === 192 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 255)) {
// Brown
R = 115;
G = 31;
B = 4;
A = 255;
}
RadarImageData.data[i] = R;
RadarImageData.data[i + 1] = G;
RadarImageData.data[i + 2] = B;
RadarImageData.data[i + 3] = A;
}
RadarContext.putImageData(RadarImageData, 0, 0);
};
const mergeDopplerRadarImage = (mapContext, radarContext) => {
const mapImageData = mapContext.getImageData(0, 0, mapContext.canvas.width, mapContext.canvas.height);
const radarImageData = radarContext.getImageData(0, 0, radarContext.canvas.width, radarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < radarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
// is this pixel the old rgb?
if ((mapImageData.data[i] < 116 && mapImageData.data[i + 1] < 116 && mapImageData.data[i + 2] < 116)) {
// change to your new rgb
// Transparent
radarImageData.data[i] = 0;
radarImageData.data[i + 1] = 0;
radarImageData.data[i + 2] = 0;
radarImageData.data[i + 3] = 0;
}
}
radarContext.putImageData(radarImageData, 0, 0);
mapContext.drawImage(radarContext.canvas, 0, 0);
};
export {
getXYFromLatitudeLongitudeDoppler,
getXYFromLatitudeLongitudeMap,
removeDopplerRadarImageNoise,
mergeDopplerRadarImage,
};

View file

@ -6,6 +6,7 @@ import { text } from './utils/fetch.mjs';
import { rewriteUrl } from './utils/cors.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import * as utils from './radar-utils.mjs';
class Radar extends WeatherDisplay {
constructor(navId, elemId) {
@ -109,7 +110,7 @@ class Radar extends WeatherDisplay {
const height = 1600;
offsetX *= 2;
offsetY *= 2;
const sourceXY = Radar.getXYFromLatitudeLongitudeMap(weatherParameters, offsetX, offsetY);
const sourceXY = utils.getXYFromLatitudeLongitudeMap(weatherParameters, offsetX, offsetY);
// create working context for manipulation
const workingCanvas = document.createElement('canvas');
@ -121,7 +122,7 @@ class Radar extends WeatherDisplay {
// calculate radar offsets
const radarOffsetX = 120;
const radarOffsetY = 70;
const radarSourceXY = Radar.getXYFromLatitudeLongitudeDoppler(weatherParameters, offsetX, offsetY);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(weatherParameters, offsetX, offsetY);
const radarSourceX = radarSourceXY.x / 2;
const radarSourceY = radarSourceXY.y / 2;
@ -179,10 +180,10 @@ class Radar extends WeatherDisplay {
cropContext.imageSmoothingEnabled = false;
cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367);
// clean the image
Radar.removeDopplerRadarImageNoise(cropContext);
utils.removeDopplerRadarImageNoise(cropContext);
// merge the radar and map
Radar.mergeDopplerRadarImage(context, cropContext);
utils.mergeDopplerRadarImage(context, cropContext);
const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } });
@ -218,185 +219,6 @@ class Radar extends WeatherDisplay {
this.finishDraw();
}
static getXYFromLatitudeLongitudeMap(pos, offsetX, offsetY) {
let y = 0;
let x = 0;
const imgHeight = 3200;
const imgWidth = 5100;
y = (51.75 - pos.latitude) * 55.2;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-130.37 - pos.longitude) * 41.775) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
}
static getXYFromLatitudeLongitudeDoppler(pos, offsetX, offsetY) {
let y = 0;
let x = 0;
const imgHeight = 6000;
const imgWidth = 2800;
y = (51 - pos.latitude) * 61.4481;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-129.138 - pos.longitude) * 42.1768) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
}
static removeDopplerRadarImageNoise(RadarContext) {
const RadarImageData = RadarContext.getImageData(0, 0, RadarContext.canvas.width, RadarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < RadarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
let R = RadarImageData.data[i];
let G = RadarImageData.data[i + 1];
let B = RadarImageData.data[i + 2];
let A = RadarImageData.data[i + 3];
// is this pixel the old rgb?
if ((R === 0 && G === 0 && B === 0)
|| (R === 0 && G === 236 && B === 236)
|| (R === 1 && G === 160 && B === 246)
|| (R === 0 && G === 0 && B === 246)) {
// change to your new rgb
// Transparent
R = 0;
G = 0;
B = 0;
A = 0;
} else if ((R === 0 && G === 255 && B === 0)) {
// Light Green 1
R = 49;
G = 210;
B = 22;
A = 255;
} else if ((R === 0 && G === 200 && B === 0)) {
// Light Green 2
R = 0;
G = 142;
B = 0;
A = 255;
} else if ((R === 0 && G === 144 && B === 0)) {
// Dark Green 1
R = 20;
G = 90;
B = 15;
A = 255;
} else if ((R === 255 && G === 255 && B === 0)) {
// Dark Green 2
R = 10;
G = 40;
B = 10;
A = 255;
} else if ((R === 231 && G === 192 && B === 0)) {
// Yellow
R = 196;
G = 179;
B = 70;
A = 255;
} else if ((R === 255 && G === 144 && B === 0)) {
// Orange
R = 190;
G = 72;
B = 19;
A = 255;
} else if ((R === 214 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 0)) {
// Red
R = 171;
G = 14;
B = 14;
A = 255;
} else if ((R === 192 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 255)) {
// Brown
R = 115;
G = 31;
B = 4;
A = 255;
}
RadarImageData.data[i] = R;
RadarImageData.data[i + 1] = G;
RadarImageData.data[i + 2] = B;
RadarImageData.data[i + 3] = A;
}
RadarContext.putImageData(RadarImageData, 0, 0);
}
static mergeDopplerRadarImage(mapContext, radarContext) {
const mapImageData = mapContext.getImageData(0, 0, mapContext.canvas.width, mapContext.canvas.height);
const radarImageData = radarContext.getImageData(0, 0, radarContext.canvas.width, radarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < radarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
// is this pixel the old rgb?
if ((mapImageData.data[i] < 116 && mapImageData.data[i + 1] < 116 && mapImageData.data[i + 2] < 116)) {
// change to your new rgb
// Transparent
radarImageData.data[i] = 0;
radarImageData.data[i + 1] = 0;
radarImageData.data[i + 2] = 0;
radarImageData.data[i + 3] = 0;
}
}
radarContext.putImageData(radarImageData, 0, 0);
mapContext.drawImage(radarContext.canvas, 0, 0);
}
}
// register display

View file

@ -0,0 +1,205 @@
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { json } from './utils/fetch.mjs';
const buildForecast = (forecast, city, cityXY) => ({
daytime: forecast.isDaytime,
temperature: forecast.temperature || 0,
name: formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
});
const getRegionalObservation = async (point, city) => {
try {
// get stations
const stations = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
// get the first station
const station = stations.features[0].id;
// get the observation data
const observation = await json(`${station}/observations/latest`);
// preload the image
if (!observation.properties.icon) return false;
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation
return observation.properties;
} catch (e) {
console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
console.error(e.status, e.responseJSON);
return false;
}
};
// utility latitude/pixel conversions
const getXYFromLatitudeLongitude = (Latitude, Longitude, OffsetX, OffsetY, state) => {
if (state === 'AK') return getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0;
let x = 0;
const ImgHeight = 1600;
const ImgWidth = 2550;
y = (50.5 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-127.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getXYFromLatitudeLongitudeAK = (Latitude, Longitude, OffsetX, OffsetY) => {
let y = 0;
let x = 0;
const ImgHeight = 1142;
const ImgWidth = 1200;
y = (73.0 - Latitude) * 56;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-175.0 - Longitude) * 25.0) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getXYFromLatitudeLongitudeHI = (Latitude, Longitude, OffsetX, OffsetY) => {
let y = 0;
let x = 0;
const ImgHeight = 571;
const ImgWidth = 600;
y = (25 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-164.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getMinMaxLatitudeLongitude = (X, Y, OffsetX, OffsetY, state) => {
if (state === 'AK') return getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 127.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getMinMaxLatitudeLongitudeAK = (X, Y, OffsetX, OffsetY) => {
const maxLat = ((Y / 56) - 73.0) * -1;
const minLat = (((Y + (OffsetY * 2)) / 56) - 73.0) * -1;
const minLon = (((X * -1) / 25) + 175.0) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 25) + 175.0) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getMinMaxLatitudeLongitudeHI = (X, Y, OffsetX, OffsetY) => {
const maxLat = ((Y / 55.2) - 25) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 25) * -1;
const minLon = (((X * -1) / 41.775) + 164.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 164.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
const getXYForCityAK = (City, MaxLatitude, MinLongitude) => {
let x = (City.lon - MinLongitude) * 37;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
const getXYForCityHI = (City, MaxLatitude, MinLongitude) => {
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
// to fit on the map, remove anything after punctuation and then limit to 15 characters
const formatCity = (city) => city.match(/[^-;/\\,]*/)[0].substr(0, 12);
export {
buildForecast,
getRegionalObservation,
getXYFromLatitudeLongitude,
getMinMaxLatitudeLongitude,
getXYForCity,
formatCity,
};

View file

@ -10,6 +10,7 @@ import { preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import * as utils from './regionalforecast-utils.mjs';
class RegionalForecast extends WeatherDisplay {
constructor(navId, elemId) {
@ -38,10 +39,10 @@ class RegionalForecast extends WeatherDisplay {
y: 117,
};
// get user's location in x/y
const sourceXY = RegionalForecast.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
const sourceXY = utils.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits
const minMaxLatLon = RegionalForecast.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
// get a target distance
let targetDistance = 2.5;
@ -75,12 +76,12 @@ class RegionalForecast extends WeatherDisplay {
if (!city.point) throw new Error('No pre-loaded point');
// start off the observation task
const observationPromise = RegionalForecast.getRegionalObservation(city.point, city);
const observationPromise = utils.getRegionalObservation(city.point, city);
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
// get XY on map for city
const cityXY = RegionalForecast.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
@ -88,7 +89,7 @@ class RegionalForecast extends WeatherDisplay {
const regionalObservation = {
daytime: !!observation.icon.match(/\/day\//),
temperature: celsiusToFahrenheit(observation.temperature.value),
name: RegionalForecast.formatCity(city.city),
name: utils.formatCity(city.city),
icon: observation.icon,
x: cityXY.x,
y: cityXY.y,
@ -104,8 +105,8 @@ class RegionalForecast extends WeatherDisplay {
// always skip the first forecast index because it's what's going on right now
return [
regionalObservation,
RegionalForecast.buildForecast(forecast.properties.periods[1], city, cityXY),
RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY),
utils.buildForecast(forecast.properties.periods[1], city, cityXY),
utils.buildForecast(forecast.properties.periods[2], city, cityXY),
];
} catch (e) {
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
@ -133,203 +134,6 @@ class RegionalForecast extends WeatherDisplay {
this.setStatus(STATUS.loaded);
}
static buildForecast(forecast, city, cityXY) {
return {
daytime: forecast.isDaytime,
temperature: forecast.temperature || 0,
name: RegionalForecast.formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
};
}
static async getRegionalObservation(point, city) {
try {
// get stations
const stations = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
// get the first station
const station = stations.features[0].id;
// get the observation data
const observation = await json(`${station}/observations/latest`);
// preload the image
if (!observation.properties.icon) return false;
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation
return observation.properties;
} catch (e) {
console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
console.error(e.status, e.responseJSON);
return false;
}
}
// utility latitude/pixel conversions
static getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0;
let x = 0;
const ImgHeight = 1600;
const ImgWidth = 2550;
y = (50.5 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-127.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY) {
let y = 0;
let x = 0;
const ImgHeight = 1142;
const ImgWidth = 1200;
y = (73.0 - Latitude) * 56;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-175.0 - Longitude) * 25.0) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY) {
let y = 0;
let x = 0;
const ImgHeight = 571;
const ImgWidth = 600;
y = (25 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-164.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 127.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY) {
const maxLat = ((Y / 56) - 73.0) * -1;
const minLat = (((Y + (OffsetY * 2)) / 56) - 73.0) * -1;
const minLon = (((X * -1) / 25) + 175.0) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 25) + 175.0) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY) {
const maxLat = ((Y / 55.2) - 25) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 25) * -1;
const minLon = (((X * -1) / 41.775) + 164.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 164.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getXYForCity(City, MaxLatitude, MinLongitude, state) {
if (state === 'AK') RegionalForecast.getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') RegionalForecast.getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
static getXYForCityAK(City, MaxLatitude, MinLongitude) {
let x = (City.lon - MinLongitude) * 37;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
static getXYForCityHI(City, MaxLatitude, MinLongitude) {
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
// to fit on the map, remove anything after punctuation and then limit to 15 characters
static formatCity(city) {
return city.match(/[^-;/\\,]*/)[0].substr(0, 12);
}
drawCanvas() {
super.drawCanvas();
// break up data into useful values

View file

@ -112,7 +112,7 @@ class TravelForecast extends WeatherDisplay {
// set up variables
const cities = this.data;
this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${TravelForecast.getTravelCitiesDayName(cities)}`;
this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${getTravelCitiesDayName(cities)}`;
this.finishDraw();
}
@ -140,9 +140,14 @@ class TravelForecast extends WeatherDisplay {
this.elem.querySelector('.main').scrollTo(0, offsetY);
}
static getTravelCitiesDayName(cities) {
// effectively returns early on the first found date
return cities.reduce((dayName, city) => {
// necessary to get the lastest long canvas when scrolling
getLongCanvas() {
return this.longCanvas;
}
}
// effectively returns early on the first found date
const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
if (city && dayName === '') {
// today or tomorrow
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
@ -150,14 +155,7 @@ class TravelForecast extends WeatherDisplay {
return day.toLocaleString({ weekday: 'long' });
}
return dayName;
}, '');
}
// necessary to get the lastest long canvas when scrolling
getLongCanvas() {
return this.longCanvas;
}
}
}, '');
// register display, not active by default
registerDisplay(new TravelForecast(4, 'travel', false));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -73,7 +73,10 @@ button {
.autocomplete-suggestions {
background-color: #ffffff;
border: 1px solid #000000;
/*overflow: auto;*/
@media (prefers-color-scheme: dark) {
background-color: #000000;
}
}
.autocomplete-suggestion {