mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Add content-aware transition timing; remove expired forecasts
- DOM-based measurement system for accurate forecast lines - Replace fixed-timing with dynamic timing based on actual forecast lines - Filter out expired forecasts - Improve error handling and only set failed state if enabled - Debug logging for timing calculations and content measurement - Switch from json() to safeJson() for centralized error handling
This commit is contained in:
@@ -1,17 +1,21 @@
|
|||||||
// display text based local forecast
|
// display text based local forecast
|
||||||
|
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { safeJson } from './utils/fetch.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
import settings from './settings.mjs';
|
import settings from './settings.mjs';
|
||||||
|
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
||||||
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
|
|
||||||
class LocalForecast extends WeatherDisplay {
|
class LocalForecast extends WeatherDisplay {
|
||||||
|
static BASE_FORECAST_DURATION_MS = 5000; // Base duration (in ms) for a standard 3-5 line forecast page
|
||||||
|
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Local Forecast', true);
|
super(navId, elemId, 'Local Forecast', true);
|
||||||
|
|
||||||
// set timings
|
// set timings
|
||||||
this.timing.baseDelay = 5000;
|
this.timing.baseDelay = LocalForecast.BASE_FORECAST_DURATION_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
@@ -22,13 +26,13 @@ class LocalForecast extends WeatherDisplay {
|
|||||||
// check for data, or if there's old data available
|
// check for data, or if there's old data available
|
||||||
if (!rawData && !this.data) {
|
if (!rawData && !this.data) {
|
||||||
// fail for no old or new data
|
// fail for no old or new data
|
||||||
this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// store the data
|
// store the data
|
||||||
this.data = rawData || this.data;
|
this.data = rawData || this.data;
|
||||||
// parse raw data
|
// parse raw data and filter out expired periods
|
||||||
const conditions = parse(this.data);
|
const conditions = parse(this.data, this.weatherParameters.forecast);
|
||||||
|
|
||||||
// read each text
|
// read each text
|
||||||
this.screenTexts = conditions.map((condition) => {
|
this.screenTexts = conditions.map((condition) => {
|
||||||
@@ -46,34 +50,32 @@ class LocalForecast extends WeatherDisplay {
|
|||||||
forecastsElem.innerHTML = '';
|
forecastsElem.innerHTML = '';
|
||||||
forecastsElem.append(...templates);
|
forecastsElem.append(...templates);
|
||||||
|
|
||||||
// increase each forecast height to a multiple of container height
|
// Get page height for screen calculations
|
||||||
this.pageHeight = forecastsElem.parentNode.offsetHeight;
|
this.pageHeight = forecastsElem.parentNode.offsetHeight;
|
||||||
templates.forEach((forecast) => {
|
|
||||||
const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight;
|
|
||||||
forecast.style.height = `${newHeight}px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.timing.totalScreens = forecastsElem.scrollHeight / this.pageHeight;
|
this.calculateContentAwareTiming(templates);
|
||||||
|
|
||||||
this.calcNavTiming();
|
this.calcNavTiming();
|
||||||
|
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the unformatted data (also used by extended forecast)
|
// get the unformatted data (also used by extended forecast)
|
||||||
async getRawData(weatherParameters) {
|
async getRawData(weatherParameters) {
|
||||||
// request us or si units
|
// request us or si units using centralized safe handling
|
||||||
try {
|
const data = await safeJson(weatherParameters.forecast, {
|
||||||
return await json(weatherParameters.forecast, {
|
data: {
|
||||||
data: {
|
units: settings.units.value,
|
||||||
units: settings.units.value,
|
},
|
||||||
},
|
retryCount: 3,
|
||||||
retryCount: 3,
|
stillWaiting: () => this.stillWaiting(),
|
||||||
stillWaiting: () => this.stillWaiting(),
|
});
|
||||||
});
|
|
||||||
} catch (error) {
|
if (!data) {
|
||||||
console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
|
|
||||||
console.error(error.status, error.responseJSON);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
@@ -84,14 +86,180 @@ class LocalForecast extends WeatherDisplay {
|
|||||||
|
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate dynamic timing based on height measurement template approach
|
||||||
|
calculateContentAwareTiming(templates) {
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
this.timing.delay = 1; // fallback to single delay if no templates
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the original base duration constant for timing calculations
|
||||||
|
const originalBaseDuration = LocalForecast.BASE_FORECAST_DURATION_MS;
|
||||||
|
this.timing.baseDelay = 250; // use 250ms per count for precise timing control
|
||||||
|
|
||||||
|
// Get line height from CSS for accurate calculations
|
||||||
|
const sampleForecast = templates[0];
|
||||||
|
const computedStyle = window.getComputedStyle(sampleForecast);
|
||||||
|
const lineHeight = parseInt(computedStyle.lineHeight, 10);
|
||||||
|
|
||||||
|
// Calculate the actual width that forecast text uses
|
||||||
|
// Use the forecast container that's already been set up
|
||||||
|
const forecastContainer = this.elem.querySelector('.local-forecast .container');
|
||||||
|
let effectiveWidth;
|
||||||
|
|
||||||
|
if (!forecastContainer) {
|
||||||
|
console.error('LocalForecast: Could not find forecast container for width calculation, using fallback width');
|
||||||
|
effectiveWidth = 492; // "magic number" from manual calculations as fallback
|
||||||
|
} else {
|
||||||
|
const containerStyle = window.getComputedStyle(forecastContainer);
|
||||||
|
const containerWidth = forecastContainer.offsetWidth;
|
||||||
|
const paddingLeft = parseInt(containerStyle.paddingLeft, 10) || 0;
|
||||||
|
const paddingRight = parseInt(containerStyle.paddingRight, 10) || 0;
|
||||||
|
effectiveWidth = containerWidth - paddingLeft - paddingRight;
|
||||||
|
|
||||||
|
if (debugFlag('localforecast')) {
|
||||||
|
console.log(`LocalForecast: Using measurement width of ${effectiveWidth}px (container=${containerWidth}px, padding=${paddingLeft}+${paddingRight}px)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure each forecast period to get actual line counts
|
||||||
|
const forecastLineCounts = [];
|
||||||
|
templates.forEach((template, index) => {
|
||||||
|
const currentHeight = template.offsetHeight;
|
||||||
|
const currentLines = Math.round(currentHeight / lineHeight);
|
||||||
|
|
||||||
|
if (currentLines > 7) {
|
||||||
|
// Multi-page forecasts measure correctly, so use the measurement directly
|
||||||
|
forecastLineCounts.push(currentLines);
|
||||||
|
|
||||||
|
if (debugFlag('localforecast')) {
|
||||||
|
console.log(`LocalForecast: Forecast ${index} measured ${currentLines} lines (${currentHeight}px direct measurement, ${lineHeight}px line-height)`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If may be 7 lines or less, we need to pad the content to ensure proper height measurement
|
||||||
|
// Short forecasts are capped by CSS min-height: 280px (7 lines)
|
||||||
|
// Add 7 <br> tags to force height beyond the minimum, then subtract the padding
|
||||||
|
const originalHTML = template.innerHTML;
|
||||||
|
const paddingBRs = '<br/>'.repeat(7);
|
||||||
|
template.innerHTML = originalHTML + paddingBRs;
|
||||||
|
|
||||||
|
// Measure the padded height
|
||||||
|
const paddedHeight = template.offsetHeight;
|
||||||
|
const paddedLines = Math.round(paddedHeight / lineHeight);
|
||||||
|
|
||||||
|
// Calculate actual content lines by subtracting the 7 BR lines we added
|
||||||
|
const actualLines = Math.max(1, paddedLines - 7);
|
||||||
|
|
||||||
|
// Restore original content
|
||||||
|
template.innerHTML = originalHTML;
|
||||||
|
|
||||||
|
forecastLineCounts.push(actualLines);
|
||||||
|
|
||||||
|
if (debugFlag('localforecast')) {
|
||||||
|
console.log(`LocalForecast: Forecast ${index} measured ${actualLines} lines (${paddedHeight}px with padding - ${7 * lineHeight}px = ${actualLines * lineHeight}px actual, ${lineHeight}px line-height)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply height padding for proper scrolling display (keep existing system working)
|
||||||
|
templates.forEach((forecast) => {
|
||||||
|
const newHeight = Math.ceil(forecast.offsetHeight / this.pageHeight) * this.pageHeight;
|
||||||
|
forecast.style.height = `${newHeight}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total screens based on padded height (for navigation system)
|
||||||
|
const forecastsElem = templates[0].parentNode;
|
||||||
|
const totalHeight = forecastsElem.scrollHeight;
|
||||||
|
this.timing.totalScreens = Math.round(totalHeight / this.pageHeight);
|
||||||
|
|
||||||
|
// Now calculate timing based on actual measured line counts, ignoring padding
|
||||||
|
const maxLinesPerScreen = 7; // 280px / 40px line height
|
||||||
|
const screenTimings = []; forecastLineCounts.forEach((lines, forecastIndex) => {
|
||||||
|
if (lines <= maxLinesPerScreen) {
|
||||||
|
// Single screen for this forecast
|
||||||
|
screenTimings.push({ forecastIndex, lines, type: 'single' });
|
||||||
|
} else {
|
||||||
|
// Multiple screens for this forecast
|
||||||
|
let remainingLines = lines;
|
||||||
|
let isFirst = true;
|
||||||
|
|
||||||
|
while (remainingLines > 0) {
|
||||||
|
const linesThisScreen = Math.min(remainingLines, maxLinesPerScreen);
|
||||||
|
const type = isFirst ? 'first-of-multi' : 'remainder';
|
||||||
|
|
||||||
|
screenTimings.push({ forecastIndex, lines: linesThisScreen, type });
|
||||||
|
|
||||||
|
remainingLines -= linesThisScreen;
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create timing array based on measured line counts
|
||||||
|
const screenDelays = screenTimings.map((screenInfo, screenIndex) => {
|
||||||
|
const screenLines = screenInfo.lines;
|
||||||
|
|
||||||
|
// Apply timing rules based on actual screen content lines
|
||||||
|
let timingMultiplier;
|
||||||
|
if (screenLines === 1) {
|
||||||
|
timingMultiplier = 0.6; // 1 line = shortest (3.0s at normal speed)
|
||||||
|
} else if (screenLines === 2) {
|
||||||
|
timingMultiplier = 0.8; // 2 lines = shorter (4.0s at normal speed)
|
||||||
|
} else if (screenLines >= 6) {
|
||||||
|
timingMultiplier = 1.4; // 6+ lines = longer (7.0s at normal speed)
|
||||||
|
} else {
|
||||||
|
timingMultiplier = 1.0; // 3-5 lines = normal (5.0s at normal speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to base counts
|
||||||
|
const desiredDurationMs = timingMultiplier * originalBaseDuration;
|
||||||
|
const baseCounts = Math.round(desiredDurationMs / this.timing.baseDelay);
|
||||||
|
|
||||||
|
if (debugFlag('localforecast')) {
|
||||||
|
console.log(`LocalForecast: Screen ${screenIndex}: ${screenLines} lines, ${timingMultiplier.toFixed(2)}x multiplier, ${desiredDurationMs}ms desired, ${baseCounts} counts (forecast ${screenInfo.forecastIndex}, ${screenInfo.type})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseCounts;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust timing array to match actual screen count if needed
|
||||||
|
while (screenDelays.length < this.timing.totalScreens) {
|
||||||
|
// Add fallback timing for extra screens
|
||||||
|
const fallbackCounts = Math.round(originalBaseDuration / this.timing.baseDelay);
|
||||||
|
screenDelays.push(fallbackCounts);
|
||||||
|
console.warn(`LocalForecast: using fallback timing for Screen ${screenDelays.length - 1}: 5 lines, 1.00x multiplier, ${fallbackCounts} counts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate if we have too many calculated screens
|
||||||
|
if (screenDelays.length > this.timing.totalScreens) {
|
||||||
|
const removed = screenDelays.splice(this.timing.totalScreens);
|
||||||
|
console.warn(`LocalForecast: Truncated ${removed.length} excess screen timings`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the timing array based on screen content
|
||||||
|
this.timing.delay = screenDelays;
|
||||||
|
|
||||||
|
if (debugFlag('localforecast')) {
|
||||||
|
console.log(`LocalForecast: Final screen count - calculated: ${screenTimings.length}, actual: ${this.timing.totalScreens}, timing array: ${screenDelays.length}`);
|
||||||
|
const multipliers = screenDelays.map((counts) => counts * this.timing.baseDelay / originalBaseDuration);
|
||||||
|
console.log('LocalForecast: Screen multipliers:', multipliers);
|
||||||
|
console.log('LocalForecast: Expected durations (ms):', screenDelays.map((counts) => counts * this.timing.baseDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// format the forecast
|
// format the forecast
|
||||||
// only use the first 6 lines
|
// filter out expired periods, then use the first 6 forecasts
|
||||||
const parse = (forecast) => forecast.properties.periods.slice(0, 6).map((text) => ({
|
const parse = (forecast, forecastUrl) => {
|
||||||
// format day and text
|
const allPeriods = forecast.properties.periods;
|
||||||
DayName: text.name.toUpperCase(),
|
const activePeriods = filterExpiredPeriods(allPeriods, forecastUrl);
|
||||||
Text: text.detailedForecast,
|
|
||||||
}));
|
return activePeriods.slice(0, 6).map((text) => ({
|
||||||
|
// format day and text
|
||||||
|
DayName: text.name.toUpperCase(),
|
||||||
|
Text: text.detailedForecast,
|
||||||
|
}));
|
||||||
|
};
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new LocalForecast(7, 'local-forecast'));
|
registerDisplay(new LocalForecast(7, 'local-forecast'));
|
||||||
|
|||||||
Reference in New Issue
Block a user