Files
sunscope/assets/js/events.js
T
fraxle 916efca55e 3.5
UI Overhaul
Profiles now in popup
Current profile always on screen
Hidden columns only when edit neeeded
Add pulldowns in to profile config
Customised the day tabs for vehicle, indoor and pet
2026-07-18 15:45:20 +01:00

155 lines
6.2 KiB
JavaScript

// ------------------------------------------------------------------------
// events.js - Cosmic & weather event engine for SunScope.
//
// This is the public-facing barrel module. It composes the smaller files
// under ./events/ and exposes the same API as before, so callers
// (app.js, components.js) keep working without changes.
//
// Returns the "active event" (or null) based on:
// 1. PROMO_OVERRIDE - manually set a message for promotions etc.
// 2. Hardcoded cosmic calendar (eclipses, meteor showers, alignments)
// 3. Weather-derived events (stargazing, sunset, heat spike, storm)
//
// To set a promo banner, edit PROMO_OVERRIDE below.
// Set it back to null when done.
//
// Each event object:
// { id, emoji, title, message, color, textColor, type }
//
// type: 'cosmic' | 'weather' | 'promo'
//
// Where to find things:
// Cosmic calendar data ........ ./events/cosmic-calendar.js
// Almanac calendar data ....... ./events/almanac-calendar.js
// Weather check functions ..... ./events/weather-checks.js
// Dynamic message helper ...... ./events/dynamic-message.js
// Lens overlay SVG renderer ... ./events/lens-overlay.js
// ------------------------------------------------------------------------
import { COSMIC_CALENDAR } from './events/cosmic-calendar.js';
import {
checkStargazing,
checkSunset,
checkHeatSpike,
checkWind,
checkWet,
checkStorm,
checkFrost,
} from './events/weather-checks.js';
import { dynamicCosmicMessage } from './events/dynamic-message.js';
import { activityChance } from './events/activity-profile.js';
// Cosmic events only surface as a banner once estimated viewing conditions
// are at least this good (see ./events/activity-profile.js for the model).
const ACTIVITY_THRESHOLD = 80;
// Re-export so callers that imported these from events.js keep working.
export { getUpcomingEvents } from './events/almanac-calendar.js';
export { getLensOverlaySVG } from './events/lens-overlay.js';
// --- PROMO OVERRIDE ------------------------------------------------------
// Set this to show a custom banner regardless of weather or cosmic events.
// Leave as null for automatic event detection.
//
// Example:
// export const PROMO_OVERRIDE = {
// id: 'promo-summer',
// emoji: '--',
// title: 'Summer Sale',
// message: 'SunScope Extra - 20% off this weekend only.',
// color: '#c8922a',
// textColor: '#fff',
// type: 'promo',
// };
//
export const PROMO_OVERRIDE = null;
// --- CELL TAG LOGIC ------------------------------------------------------
// Returns the subset of active events that should show an icon for this row.
export function getCellTagEvents(events, row) {
if (!events || events.length === 0) return [];
return events.filter(ev => {
if (ev.type === 'promo') return false;
if (ev.nightOnly && row.elev >= -5) return false;
if (ev.id === 'heat-spike' && row.elev < 0) return false;
// Sunset/sunrise events: only show icon during the actual twilight window.
// For bucketed table rows the span is [row.iso .. row.isoEnd]; show the
// icon if the event window overlaps that span at all.
if (ev.isoRange) {
const hi = row.isoEnd || row.iso;
if (hi < ev.isoRange[0] || row.iso > ev.isoRange[1]) return false;
}
return true;
});
}
// --- BANNER PRIORITY -----------------------------------------------------
// Weather event IDs that are safety-relevant and should always appear as the
// first banner slide, ahead of cosmic events and lower-priority weather items.
const PRIORITY_WEATHER_IDS = new Set(['heat-spike', 'frost', 'storm', 'wind', 'wet']);
// --- MAIN EXPORT ---------------------------------------------------------
// Returns ALL active events for the given rows/date as an array.
// Empty array = nothing active.
//
// Order within array:
// 1. PROMO_OVERRIDE (if set, returned alone)
// 2. Priority weather events (heat spike, frost, storm) — always slide 1
// 3. All matching cosmic calendar events for this date
// 4. Remaining weather-derived events (sunset, stargazing, …)
export function getActiveEvents(rows, location) {
// 1. Manual promo override - shown alone, no mixing with other events
if (PROMO_OVERRIDE) return [PROMO_OVERRIDE];
// 2. Cosmic calendar - use the date of the rows being viewed, not today.
const dateStr = (rows && rows.length > 0)
? rows[0].iso.slice(0, 10)
: new Date().toISOString().slice(0, 10);
const cosmicHits = COSMIC_CALENDAR
.filter(ev => dateStr >= ev.start && dateStr <= ev.end)
.map(ev => Object.assign({}, ev, { chance: activityChance(ev, dateStr) }))
.filter(ev => ev.chance >= ACTIVITY_THRESHOLD)
.map(ev => Object.assign(ev, { message: dynamicCosmicMessage(ev, dateStr) }));
// 3. Weather-derived events (all that match, not just first)
const weatherEvents = [];
if (rows && rows.length > 0) {
const checks = [
checkHeatSpike(rows),
checkStorm(rows),
checkWind(rows),
checkWet(rows),
checkFrost(rows),
checkSunset(rows, location),
checkStargazing(rows),
];
checks.forEach(ev => { if (ev) weatherEvents.push(ev); });
}
// Sort so priority weather warnings (heat, frost, storm) always lead,
// then cosmic events, then the remaining weather items.
const priorityWeather = weatherEvents.filter(ev => PRIORITY_WEATHER_IDS.has(ev.id));
const otherWeather = weatherEvents.filter(ev => !PRIORITY_WEATHER_IDS.has(ev.id));
return [...priorityWeather, ...cosmicHits, ...otherWeather];
}
// --- LENS EVENT PICKER ---------------------------------------------------
// From an array of active events, returns the single one closest to its peak
// (most "intense"). Weather events without a peak use today's date.
// Returns null if events array is empty.
export function getLensEvent(events) {
if (!events || events.length === 0) return null;
if (events.length === 1) return events[0];
const today = new Date().toISOString().slice(0, 10);
return events.reduce((best, ev) => {
const peakB = best.peak || today;
const peakE = ev.peak || today;
const distB = Math.abs(new Date(today) - new Date(peakB));
const distE = Math.abs(new Date(today) - new Date(peakE));
return distE < distB ? ev : best;
});
}