// ------------------------------------------------------------------------ // config.js - Pure-data configuration constants for the UTCIForecast app. // // Moved out of app.js so the main component is easier to read and edit. // Nothing here has state or side effects - just constants and lookup // tables imported by app.js (and anywhere else that needs them). // // Where to find things: // FREE_DAYS .................. how many days the free tier shows // FILTER_PROFILES ............ preset column sets (Basic, Home, etc.) // OUTDOORS_VARIANTS .......... Places & Activities sub-variants // POLLEN_TYPES ............... pollen-column pulldown options // UTCI_ENVIRONMENTS .......... environment modifiers for UTCI+P column // variantIcons ............... emoji glyph per place/activity variant // profileButtonOrder ......... left-to-right order of the profile buttons // activityVariantKeys ........ which variants appear in the Activities menu // placeVariantKeys ........... which variants appear in the Places menu // COL_DESCRIPTIONS ........... tooltip text for each table column header // ------------------------------------------------------------------------ // How many days the free tier shows. Days beyond this get a -. // Bump this number if you want to give free users more access. export const FREE_DAYS = 14; // Filter profile presets - each preset defines which columns are visible // when that profile is selected. export const FILTER_PROFILES = { basic: { label: 'Basic', icon: '🌡️', scene: "url('assets/images/profiles/basic.png') center / cover no-repeat, linear-gradient(160deg,#b8d8e8,#daeef8)", // Basic is the "just tell me" preset - it opens in Quick view (see `view` // below) with the smallest useful column set behind it. view: 'simple', cols: { hour: true, air: false, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, home: { label: 'Home', icon: '🏠', scene: "url('assets/images/profiles/home.png') center / cover no-repeat, linear-gradient(160deg,#b0d890,#d0ecb8)", cols: { hour: true, air: true, shadeT: false, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true, vis: false, aqi: true, pollen: true, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, vehicle: { label: 'Vehicle', icon: '🚗', scene: "url('assets/images/profiles/vehicle.png') center / cover no-repeat, linear-gradient(160deg,#a8bcc8,#ccd8e4)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, pets: { label: 'Pets', icon: '🐾', scene: "url('assets/images/profiles/pets.png') center / cover no-repeat, linear-gradient(160deg,#e0c0a0,#f0ddc8)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true, furSurfaceT: true, pawT: true, petShadeT: true, petHomeT: true }, }, farming: { label: 'Farming', icon: '🌾', scene: "url('assets/images/profiles/farming.png') center / cover no-repeat, linear-gradient(160deg,#d8bc90,#ece0b8)", // Farming sits in the Work menu alongside the outdoors variants, so it // opens the detailed table the same way they do. view: 'table', cols: { hour: true, air: true, shadeT: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, outdoors: { label: 'Places', icon: '🌤️', scene: "url('assets/images/profiles/places.png') center / cover no-repeat, linear-gradient(160deg,#98ccc0,#c4e4dc)", view: 'table', // Default cols match the first variant (urban). Switching variant updates visibleCols. cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, alltemps: { label: 'Temps', icon: '🌡️', scene: "url('assets/images/profiles/temps.png') center / cover no-repeat, linear-gradient(160deg,#d8b0b0,#ecd0c8)", proOnly: true, cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: false, precipProb: false, soilT: true, soilT6: false, soilM: false, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: false, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, showall: { label: 'Show All', icon: '⚙️', scene: "url('assets/images/profiles/all.png') center / cover no-repeat, linear-gradient(160deg,#b4b4d4,#d4d4ec)", proOnly: true, cols: { hour: true, air: true, shadeT: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: true, diffuse: true, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: true, soilT6: true, soilM: true, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: true, aqi: true, pollen: true, furSurfaceT: true, pawT: true, petShadeT: true, petHomeT: true }, }, custom: { label: 'Custom', icon: '✏️', scene: "url('assets/images/profiles/custom.png') center / cover no-repeat, linear-gradient(160deg,#d8b0cc,#ecd0e4)", proOnly: true, // Blank slate - only Hour is on. User enables whichever columns they want. cols: { hour: true, air: false, shadeT: false, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: false, precipProb: false, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false }, }, }; // Places sub-variants - each has its own column set. // Selecting a variant applies its cols to visibleCols. export const OUTDOORS_VARIANTS = { urban: { name: 'Urban', scene: "url('assets/images/profiles/urban.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, beach: { name: 'Beach', scene: "url('assets/images/profiles/beach.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, events: { name: 'Events', scene: "url('assets/images/profiles/events.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, festival: { name: 'Festival', scene: "url('assets/images/profiles/festival.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, wintersports: { name: 'Winter Sports', proOnly: true, scene: "url('assets/images/profiles/winter.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, naturist: { name: 'Naturist', proOnly: true, scene: "url('assets/images/profiles/naturist.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: false, diffuse: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, sailing: { name: 'Sailing', proOnly: true, scene: "url('assets/images/profiles/sailing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, // Places - free park: { name: 'Park / Picnic', scene: "url('assets/images/profiles/park.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, forest: { name: 'Forest / Woodland', scene: "url('assets/images/profiles/forest.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: true } }, camping: { name: 'Camping', scene: "url('assets/images/profiles/camping.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: false, vis: true, aqi: false, pollen: true } }, airport: { name: 'Airport / Travel', scene: "url('assets/images/profiles/airport.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, // Places - Pro construction: { name: 'Construction', proOnly: true, scene: "url('assets/images/profiles/construction.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: true, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, // Activities - free cycling: { name: 'Cycling', scene: "url('assets/images/profiles/cycling.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, running: { name: 'Running', scene: "url('assets/images/profiles/running.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: true, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: true, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, dogwalk: { name: 'Dog Walking', scene: "url('assets/images/profiles/walking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true, furSurfaceT: true, pawT: true, petShadeT: true, petHomeT: false } }, // Activities - Pro hiking: { name: 'Hiking', proOnly: true, scene: "url('assets/images/profiles/hiking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, photography: { name: 'Photography', proOnly: true, scene: "url('assets/images/profiles/photography.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: true, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, fishing: { name: 'Fishing', proOnly: true, scene: "url('assets/images/profiles/fishing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, // Work variants market: { name: 'Market Trading', proOnly: true, scene: "url('assets/images/profiles/market.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, windowcleaning: { name: 'Window Cleaning', proOnly: true, scene: "url('assets/images/profiles/window.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, office: { name: 'Office', proOnly: true, scene: "url('assets/images/profiles/office.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: true, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: true, vis: false, aqi: true, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, driver: { name: 'Driver / Trucker', proOnly: true, scene: "url('assets/images/profiles/vehicle.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, utci: false, uvA: true, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } }, }; // Pollen type options shown in the pulldown selector. export const POLLEN_TYPES = { all_pollen: { name: 'All pollen', unit: 'grains/m³' }, grass_pollen: { name: 'Grass pollen', unit: 'grains/m³' }, birch_pollen: { name: 'Birch pollen', unit: 'grains/m³' }, alder_pollen: { name: 'Alder pollen', unit: 'grains/m³' }, mugwort_pollen: { name: 'Mugwort pollen', unit: 'grains/m³' }, olive_pollen: { name: 'Olive pollen', unit: 'grains/m³' }, ragweed_pollen: { name: 'Ragweed pollen', unit: 'grains/m³' }, }; // UTCI+P environment modifiers - affect solar inputs and air temp used // in the UTCI calculation to reflect shaded or modified environments. // dirFactor - fraction of direct radiation that reaches the person (0-1) // difFactor - fraction of diffuse radiation that reaches the person (0-1) // globFactor - fraction of global shortwave radiation that reaches the person (0-1) // taOffset - offset applied to air temperature in degrees C (evapotranspiration etc.) // label - short display name // note - shown in column header tooltip to flag modified calculation // shade - per-environment microclimate inputs for the Shade column // (calcShadeAirTemp + calcShadeFeltTemp in physics.js, where // shelter doubles as the sky-view proxy). The env air-temp // shift is NOT here - it comes from taOffset via the shared // TaEnv baseline. This block only carries local sun-trap effects: // shelter 0-1 wind shelter (high = enclosed, calm pocket) // sun 0-~1.2 how sun-baked the surroundings are // type short label of the typical shade for the tooltip export const UTCI_ENVIRONMENTS = { open: { shortLabel: null, label: 'Grass / Open Field', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null, shade: { shelter: 0.45, sun: 0.50, type: 'hedge / scattered trees' } }, urban: { shortLabel: 'City', label: 'Town / City', dirFactor: 0.70, difFactor: 0.75, globFactor: 0.82, taOffset: 2.5, note: 'Urban heat island estimate - UHI raises air temp ~2–3 °C, building canyons reduce direct and diffuse solar load, reflections from concrete and glass partially offset sky-view reduction', shade: { shelter: 0.75, sun: 0.85, type: 'building shadow' } }, beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose', shade: { shelter: 0.25, sun: 0.95, type: 'beach umbrella' } }, river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp', shade: { shelter: 0.55, sun: 0.40, type: 'trees & bushes' } }, forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling', shade: { shelter: 0.80, sun: 0.20, type: 'tree canopy' } }, openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution', shade: { shelter: 0.10, sun: 0.25, type: 'boat panels' } }, alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions', shade: { shelter: 0.35, sun: 0.40, type: 'trees & rocks' } }, desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature', shade: { shelter: 0.40, sun: 1.00, type: 'cactus / rockface' } }, }; // Maps OUTDOORS_VARIANTS keys to a default UTCI_ENVIRONMENTS key. // When a variant is selected via the Places or Activities dropdown the // UTCI+P environment modifier is automatically switched to match. // Variants not listed here default to open. export const VARIANT_DEFAULT_ENV = { forest: 'forest', park: 'open', beach: 'beach', sailing: 'openwater', hiking: 'alpine', wintersports:'alpine', fishing: 'river', naturist: 'beach', urban: 'urban', farming: 'open', construction:'urban', office: 'urban', windowcleaning:'urban', driver: 'urban', }; // Left-to-right order of the profile buttons in the top filter bar. export const profileButtonOrder = ['basic', 'home', 'vehicle', 'pets', 'alltemps', 'showall', 'custom']; // Derives which config selector / table metric is the "main" thermal // selection for the active profile — the one whose calc drives the day-tab // hi/lo readout. Shared by DayTabs (day-tab numbers + summary chip), // ConfigPanel (the gold "main" badge) and app.js (the edge-tab label). export function deriveProfileMain(activeProfile, outdoorsVariant) { const isHomeOrOffice = activeProfile === 'home' || (activeProfile === 'outdoors' && outdoorsVariant === 'office'); const isVehicleProfile = activeProfile === 'vehicle' || (activeProfile === 'outdoors' && outdoorsVariant === 'driver'); const isPetsProfile = activeProfile === 'pets'; const mainConfigKey = isHomeOrOffice ? 'indoor' : isVehicleProfile ? 'vehicle' : isPetsProfile ? 'fur' : 'solar'; const mainField = isHomeOrOffice ? 'indoorT' : isVehicleProfile ? 'vehicleT' : isPetsProfile ? 'furSurfaceT' : 'utciAdj'; // Pets pairs Fur Temp with PET Shade: the human Shade column is now a UTCI // felt number on a human comfort scale, which says nothing about a cat. // Vehicle/Driver pairs the cabin temp with plain air temperature, so the // gap between outside and inside the vehicle is visible at a glance. const secondaryField = isHomeOrOffice ? 'managedT' : isPetsProfile ? 'petShadeT' : isVehicleProfile ? 'Ta' : 'shadeT'; const mainLabel = activeProfile === 'outdoors' ? (OUTDOORS_VARIANTS[outdoorsVariant]?.name || FILTER_PROFILES.outdoors.label) : (FILTER_PROFILES[activeProfile]?.label || 'SunSoak'); // Human names for the two numbers stacked in each day tab, so the label // column beside the strip can say what they are. Pulled from // COL_DESCRIPTIONS so they never drift from the table column tooltips — // every field matches its column key except utciAdj, whose column is utciP. const colKey = mainField === 'utciAdj' ? 'utciP' : mainField; const mainMetric = COL_DESCRIPTIONS[colKey] || {}; // Same mapping for the secondary field: Ta's column key is `air`. const secColKey = secondaryField === 'Ta' ? 'air' : secondaryField; const secondaryMetric = secColKey ? (COL_DESCRIPTIONS[secColKey] || {}) : null; return { isHomeOrOffice, isVehicleProfile, isPetsProfile, mainConfigKey, mainField, secondaryField, mainLabel, mainMetricLabel: mainMetric.title || '', mainMetricDesc: mainMetric.desc || '', // "Air Temperature" is too wide for the day-tab legend column, so the // vehicle pairing shows it as just "Air" (the tooltip still spells it out). secondaryMetricLabel: secColKey === 'air' ? 'Air' : (secondaryMetric?.title || ''), secondaryMetricDesc: secondaryMetric?.desc || '', }; } // Emoji glyph per place/activity variant. export const variantIcons = { urban: '🏙️', beach: '🏖️', events: '🎪', park: '🌳', forest: '🌲', camping: '🏕️', airport: '✈️', festival: '⛺', construction: '🏗️', cycling: '🚴', running: '🏃', dogwalk: '🐾', sailing: '⛵', wintersports: '🎿', naturist: '☀️', hiking: '🏔️', photography: '📸', fishing: '🎣', market: '🏪', windowcleaning: '🪟', office: '🏢', driver: '🚚', }; // Which variant keys appear in the Activities dropdown. export const activityVariantKeys = ['dogwalk', 'running', 'cycling', 'hiking', 'fishing', 'sailing', 'wintersports', 'photography', 'naturist']; // Which variant keys appear in the Places dropdown. export const placeVariantKeys = ['urban', 'park', 'forest', 'camping', 'beach', 'events', 'festival', 'airport']; // Which variant keys appear in the Work dropdown. export const workVariantKeys = ['farming', 'construction', 'market', 'windowcleaning', 'driver', 'office']; // Tooltip text shown when the user clicks/hovers a table column header. // // `short` is the one- or two-sentence summary the popup actually shows; `desc` // is the full write-up, still used for the day-tab metric descriptions via // deriveProfileMain. `link` deep-links to that column's row on the reference // page (the row ids in columns.html) for the rest of the detail. export const COL_DESCRIPTIONS = { hour: { title: 'Rows', desc: 'Local wall-clock time for this forecast row. Each row covers the interval set by the stepper in this header, clock-aligned from midnight — at 3h, a row labelled 09:00 spans 09:00–11:59. Totals like rainfall are summed across the block, risk figures show the block\'s peak, and smooth readings like air temperature are averaged.', short: "The local time each row covers. The stepper in this header sets the block size — totals are summed across the block, risks show its peak, and steady readings are averaged.", link: "./columns.html#col-hour" }, air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.', short: "Thermometer reading at 2 m above the ground. Ignores sun, wind and humidity.", link: "./columns.html#col-air" }, shadeT: { title: 'Shade', desc: 'SunSoak with the sun taken away: the felt temperature standing in the typical shade of the selected Solar Model — building shadow, beach umbrella, tree canopy, boat panels, and so on. You are still outdoors, so wind, humidity and rain hit you exactly as they do in the open; only the direct beam and part of the sky are blocked. Expect several degrees below SunSoak in daytime sun, and near-identical readings after dark, when there is no sun to stand out of. An estimate of a typical shaded spot, not a measurement.', short: "Felt temperature standing in typical shade for the selected Solar Model. Wind, humidity and rain still reach you — only the sun is blocked.", link: "./columns.html#col-shadeT" }, rh: { title: 'Relative Humidity', desc: 'How much moisture the air holds relative to its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer.', short: "Moisture in the air as a percentage of what it could hold at that temperature. High RH makes warm days stickier and cold days rawer.", link: "./columns.html#col-rh" }, dew: { title: 'Dew Point', desc: 'The temperature at which air becomes saturated and moisture begins to condense. A useful measure of absolute humidity — above 16 °C it starts to feel muggy; above 21 °C it feels oppressive.', short: "The temperature at which air saturates and moisture condenses — a good measure of absolute humidity. Above 16 °C feels muggy.", link: "./columns.html#col-dew" }, wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates Beaufort scale severity: amber = Near Gale (32+ mph), orange = Gale (39+ mph), red = Severe Gale or above (55+ mph). Banners above the table classify conditions from Strong Breeze through to Violent Storm using standard wind categories. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.', short: "Mean wind speed at 10 m, with the peak gust in brackets. Gust colour follows the Beaufort scale.", link: "./columns.html#col-wind" }, dir: { title: 'Wind Direction', desc: 'The compass direction the wind is blowing from, shown as an arrow and abbreviated label (e.g. SW = south-westerly).', short: "The compass direction the wind is blowing from, as an arrow and short label.", link: "./columns.html#col-dir" }, cloud: { title: 'Cloud Cover', desc: 'Total cloud cover as a percentage of the sky. High cloud blocks solar radiation and reduces both daytime heating and overnight cooling.', short: "Total cloud cover as a percentage of the sky. Cloud cuts both daytime heating and overnight cooling.", link: "./columns.html#col-cloud" }, sun: { title: 'Sun Elevation', desc: 'The angle of the sun above the horizon in degrees. Below 0° the sun has set. The higher the elevation, the more intense the solar radiation reaching the ground.', short: "The angle of the sun above the horizon. Below 0° the sun has set; higher angles mean stronger radiation.", link: "./columns.html#col-sun" }, direct: { title: 'Direct Radiation', desc: 'Shortwave solar radiation arriving in a direct beam from the sun (W/m²). The primary driver of skin heating on sunny days.', short: "Solar radiation arriving in a direct beam from the sun (W/m²) — the main driver of skin heating on sunny days.", link: "./columns.html#col-direct" }, diffuse: { title: 'Diffuse Radiation', desc: 'Scattered solar radiation arriving from all directions across the sky (W/m²). Present even under cloud; contributes to overall solar load.', short: "Solar radiation scattered across the sky (W/m²). Present even under cloud, and adds to the total solar load.", link: "./columns.html#col-diffuse" }, utci: { title: 'UTCI', desc: 'Universal Thermal Climate Index — the peer-reviewed standard felt temperature, combining air temp, humidity, wind, and solar radiation. Always computed from the raw open-ground reading: it does NOT move when you change the Solar Model, and it does not include precipitation. That is the point of it — a fixed benchmark to read SunSoak against, so the gap between the two columns shows exactly what your surroundings and the weather are adding.', short: "The peer-reviewed standard felt temperature. Always taken from the raw open-ground reading, so it stays fixed as a benchmark to read SunSoak against.", link: "./columns.html#col-utci" }, uvA: { title: 'UV-A Index', desc: 'Estimated UV-A radiation index. UV-A penetrates deeper into the skin and contributes to long-term ageing and some skin cancers, even through glass.', short: "Estimated UV-A index. UV-A reaches deeper into the skin and drives long-term ageing, even through glass.", link: "./columns.html#col-uvA" }, uvB: { title: 'UV-B Index', desc: 'Estimated UV-B radiation index. UV-B causes sunburn and is the main driver of vitamin D production. Intensity depends strongly on solar elevation and cloud cover.', short: "Estimated UV-B index. UV-B causes sunburn and drives vitamin D production.", link: "./columns.html#col-uvB" }, burn: { title: 'Burn Time', desc: 'Estimated time to reach one Minimal Erythemal Dose (MED) — the threshold for sunburn — based on the UV index and your selected skin type. This is a guide, not a medical measurement.', short: "Estimated time to sunburn (one MED) for your selected skin type. A guide, not a medical measurement.", link: "./columns.html#col-burn" }, utciP: { title: 'SunSoak', desc: 'SunScope\'s felt-temperature index. Based on UTCI — the standard combining air temp, humidity, wind, and solar radiation — with added adjustments for rain/snow and your chosen surroundings.', short: "SunScope's felt-temperature index: UTCI plus adjustments for rain or snow and your chosen surroundings.", link: "./columns.html#col-utciP" }, precip: { title: 'Precipitation', desc: 'Expected rainfall or snowfall in mm per hour. Snow is shown in cm. Even light drizzle meaningfully reduces felt temperature when combined with wind.', short: "Expected rainfall in mm per hour, or snow in cm. Even drizzle cuts felt temperature noticeably in wind.", link: "./columns.html#col-precip" }, precipProb: { title: 'Rain Probability', desc: 'Probability of precipitation for this hour as a percentage (0-100%). Derived from Open-Meteo ensemble model runs. A high value means rain is likely even if the expected rate is low - useful for spotting showers that the deterministic forecast may miss. A ⚡ marks a thundery hour: it appears once the Lightning Potential Index passes 5 J/kg (moderate risk) and turns bold above 25 (high risk), tinting the cell amber.', short: "Chance of precipitation this hour, from ensemble model runs. A ⚡ marks hours with lightning potential.", link: "./columns.html#col-precipProb" }, soilT: { title: 'Soil Temperature', desc: 'Temperature of the soil at the surface (0 cm depth). Useful for planting decisions — most seeds germinate above 7–10 °C.', short: "Soil temperature at the surface. Most seeds germinate above 7–10 °C.", link: "./columns.html#col-soilT" }, soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.', short: "Soil temperature at 6 cm — the root zone for many crops. Lags the surface by several hours.", link: "./columns.html#col-soilT6" }, soilM: { title: 'Soil Moisture', desc: 'Water saturation of the top 1 cm of soil as a percentage. Above 40% suggests saturated ground; below 20% indicates dry conditions.', short: "Water saturation of the top 1 cm of soil. Above 40% is saturated ground; below 20% is dry.", link: "./columns.html#col-soilM" }, concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 15–25 °C above air temperature on sunny days.', short: "Estimated temperature of sun-exposed paving, which can run 15–25 °C above air temperature.", link: "./columns.html#col-concreteT" }, vehicleT: { title: 'Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces that warm more slowly but hold heat longer, with gain through the windscreen and rooflights and a little retained living-space warmth. Accounts for ambient wind over the bodywork, road speed, and whether the windows are open. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.', short: "Estimated cabin temperature inside a parked vehicle. Dangerous for children and pets above 35 °C.", link: "./columns.html#col-vehicleT" }, indoorT: { title: 'Indoors', desc: 'Estimated temperature inside the selected building type when it is empty — windows closed, no heating, no cooling, no occupants. Accounts for window solar gain, the thermal mass of walls and floors (which smooths the daily swing and builds up over a heatwave or cold snap), heat exchange with the ground, and warmth from heated neighbours through party walls in flats and terraces.', short: "Estimated temperature inside your selected building type when empty — windows closed, no heating or cooling.", link: "./columns.html#col-indoorT" }, managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature when the building is managed sensibly for the weather. On warm days (24 °C+ outside, or 23 °C+ inside) it follows UK heatwave advice: curtains closed against the sun and windows opened whenever outdoor air is cooler, never ventilating below 20 °C. On cooler days windows stay shut, curtains are open by day to let the sun in and closed at night to hold the warmth.', short: "The same building managed for the weather — heatwave advice on warm days, curtains used to hold warmth on cool days.", link: "./columns.html#col-managedT" }, vis: { title: 'Visibility', desc: 'Horizontal visibility in kilometres, sourced from the CAMS air quality model. Values below 1 km indicate fog or very thick haze; below 10 km suggests mist, smoke, or significant pollution. Relevant for driving, flying, and photography.', short: "Horizontal visibility in km. Below 1 km means fog; below 10 km means mist, smoke or pollution.", link: "./columns.html#col-vis" }, aqi: { title: 'Air Quality Index', desc: 'European Air Quality Index (0–100+), combining PM2.5, PM10, ozone, nitrogen dioxide, and sulphur dioxide. 0–20 = Good; 20–40 = Fair; 40–60 = Moderate; 60–80 = Poor; 80–100 = Very Poor; 100+ = Extremely Poor. Sourced from Copernicus CAMS.', short: "European Air Quality Index combining PM2.5, PM10, ozone, NO₂ and SO₂. 0–20 is Good, 100+ Extremely Poor.", link: "./columns.html#col-aqi" }, pollen: { title: 'Pollen', desc: 'Pollen concentration in grains per cubic metre for the selected pollen type, sourced from the Copernicus CAMS pollen forecast. Low = <10; Moderate = 10–50; High = 50–200; Very High = 200+. Values vary by species and season.', short: "Pollen concentration in grains per m³ for the selected type. Low is under 10; Very High is 200+.", link: "./columns.html#col-pollen" }, furSurfaceT: { title: 'Fur Temp', desc: 'Estimated fur surface temperature a cat or small dog experiences in direct sun, calibrated for a thin/short coat rather than a heavily-insulated breed. Runs hotter than air temperature on sunny, still days — set fur colour with the selector alongside this column.', short: "Estimated fur surface temperature for a cat or small dog in direct sun. Set the coat colour beside this column.", link: "./columns.html#col-furSurfaceT" }, pawT: { title: 'Paw Temp', desc: 'Estimated ground/pavement surface temperature for paw contact, colour-coded by burn risk: green below 40 °C (safe), amber 40–52 °C (discomfort / possible burn on prolonged contact), red above 52 °C (can burn within 60 seconds). If you can\'t hold the back of your hand on the surface for 7 seconds, it\'s too hot to walk your pet.', short: "Estimated ground temperature for paw contact. Above 52 °C can burn within 60 seconds.", link: "./columns.html#col-pawT" }, petShadeT: { title: 'Pet Shade', desc: 'Shaded air temperature for a resting pet, using the same shade microclimate model as the Shade column but kept as an air temperature — a human felt-temperature index says little about an animal — and with a lower, pet-specific alert threshold — small pets have far less capacity to dump heat than humans.', short: "Shaded air temperature for a resting pet, with a lower, pet-specific alert threshold.", link: "./columns.html#col-petShadeT" }, petHomeT: { title: 'Pet Home', desc: 'Indoor temperature estimate for a pet left home alone, using the same model as the Indoors column but with a lower, pet-specific alert threshold (26 °C) rather than the human heatwave guidance threshold.', short: "Indoor estimate for a pet left home alone, alerting at 26 °C rather than the human threshold.", link: "./columns.html#col-petHomeT" }, }; // Seasonal crop calendar for the Farming "At a glance" sow/harvest advice. // Months are 1-12 on a UK / Northern-temperate basis (auto-flipped +6 for the // southern hemisphere in computeCropAdvice). `minSoilT` is the seedbed // germination floor in °C, tested against the DAILY MEAN soil temperature at // 6 cm (drilling depth) rather than a surface peak. // // Optional flags, all defaulting to falsy: // sow.tender - seedlings are killed by air frost, so an air frost in the // forecast week rules the crop out. // sow.underCover - sown indoors / under glass in its window, so the outdoor // seedbed temperature and wetness gates don't apply. // harvest.dry - must be brought in / cured dry (grains, rape, onions), so // needs a run of consecutive dry days. // harvest.lift - lifted by machine off the ground (root crops), so // waterlogged soil means ruts, compaction and damaged roots. // A crop with neither harvest flag (lettuce, hand-cut) passes on month alone. export const CROP_CALENDAR = [ { key: 'wheat', label: 'Wheat', icon: '🌾', sow: { months: [9, 10], minSoilT: 8 }, harvest: { months: [8, 9], dry: true } }, { key: 'barley', label: 'Barley', icon: '🌾', sow: { months: [2, 3, 9, 10], minSoilT: 6 }, harvest: { months: [7, 8], dry: true } }, { key: 'oats', label: 'Oats', icon: '🌾', sow: { months: [2, 3, 4], minSoilT: 5 }, harvest: { months: [8, 9], dry: true } }, { key: 'osr', label: 'Oilseed rape', icon: '🌻', sow: { months: [8, 9], minSoilT: 10 }, harvest: { months: [7, 8], dry: true } }, { key: 'fieldbean', label: 'Field beans', icon: '🫘', sow: { months: [2, 3, 10, 11], minSoilT: 3 }, harvest: { months: [8, 9], dry: true } }, { key: 'potato', label: 'Potatoes', icon: '🥔', sow: { months: [3, 4, 5], minSoilT: 7, tender: true }, harvest: { months: [6, 7, 8, 9], dry: false, lift: true } }, { key: 'carrot', label: 'Carrots', icon: '🥕', sow: { months: [3, 4, 5, 6, 7], minSoilT: 7 }, harvest: { months: [6, 7, 8, 9, 10], dry: false, lift: true } }, { key: 'tomato', label: 'Tomatoes', icon: '🍅', sow: { months: [3, 4], minSoilT: 14, tender: true, underCover: true }, harvest: { months: [7, 8, 9], dry: false } }, { key: 'onion', label: 'Onions', icon: '🧅', sow: { months: [3, 4], minSoilT: 7 }, harvest: { months: [7, 8], dry: true } }, { key: 'pea', label: 'Peas', icon: '🟢', sow: { months: [3, 4, 5, 6], minSoilT: 8 }, harvest: { months: [6, 7, 8], dry: false } }, { key: 'lettuce', label: 'Lettuce', icon: '🥬', sow: { months: [3, 4, 5, 6, 7, 8], minSoilT: 5 }, harvest: { months: [5, 6, 7, 8, 9], dry: false } }, { key: 'beetroot', label: 'Beetroot', icon: '🟣', sow: { months: [4, 5, 6, 7], minSoilT: 7 }, harvest: { months: [7, 8, 9, 10], dry: false, lift: true } }, ];