{
  "version": 3,
  "sources": ["scrubber_core.js"],
  "sourcesContent": ["/*\n * static/js/scrubber_core.js \u2014 Pure, context-free season-scrubber/timelapse\n * helpers (SNOW-496).\n *\n * Extracted from the inline closures in static/js/map.js's\n * ``seasonScrubberInit``, ``timelapseInit`` and ``seasonRibbonInit`` IIFEs so\n * they can be unit-tested directly (see ``tests/js/test_scrubber_core.js``) \u2014\n * MapLibre never fires ``map.on('load')`` in headless Playwright, so this\n * transport math was previously exercised only indirectly (or not at all).\n * ``map.js`` itself is otherwise unchanged: each extracted definition is\n * replaced with a thin local delegator that forwards the same closure values\n * to the function of the same name here \u2014 every call site, control flow, and\n * DOM/MapLibre interaction is untouched.\n *\n * Attached to ``window`` (not ``self``) \u2014 unlike ``mutation_queue_core.js``,\n * every consumer is a page script; there is no service-worker context to\n * share with.\n *\n * Deliberately dependency-free: every export is a pure function of its\n * arguments. Season bounds, the ratings cache, the active-country map and\n * the rating-int\u2192key lookup table are all passed in explicitly rather than\n * read from module-scope globals.\n *\n * SNOW-794: the span these functions map percentages over is the avalanche\n * season, and only that. The ticket briefly gave the scrubber a twelve-month\n * window with the season inset inside it as a band, so it could place any day\n * the calendar offers \u2014 and reverted: one control trying to be two is what\n * let the thumb and ``?d=`` disagree in the first place. The calendar is\n * \"pick a date, any date\"; this is a season scrubber, and it is simply not\n * shown for a day outside the season (static/js/map_scrubber_reveal.js).\n *\n * Public API \u2014 attached to ``window.pwaScrubberCore``:\n *\n *   pctToDateKey(pct, seasonStartMs, seasonSpanMs)\n *     Thumb percentage (0..100) \u2192 ISO date string, snapped to UTC midnight.\n *   dateKeyToPct(dateKey, seasonStartMs, seasonSpanMs, fallbackPct)\n *     ISO date string \u2192 thumb percentage, clamped to [0, 100]. Returns\n *     ``fallbackPct`` when ``dateKey`` doesn't parse or the season span is\n *     non-positive/non-finite.\n *   snapToNearestDataDay(dateKey, sortedDates)\n *     Nearest entry in ``sortedDates`` to ``dateKey`` by absolute ms delta.\n *     Returns ``dateKey`` unchanged when ``sortedDates`` is empty/absent.\n *   nearestFrameIndex(sortedDates, ariaValueNow, seasonStartMs, seasonSpanMs)\n *     Index into ``sortedDates`` nearest the position implied by\n *     ``ariaValueNow`` (a 0..100 thumb percentage) \u2014 used to resume\n *     timelapse playback from the current thumb position. Returns 0 when\n *     the inputs can't place a position.\n *   nextFrame(frameIdx, direction, totalFrames)\n *     Advance one timelapse tick. Returns ``{frameIdx, done}`` \u2014\n *     ``frameIdx`` is ``frameIdx + direction`` unconditionally (matching the\n *     original inline behaviour of leaving the counter one step past the\n *     boundary when playback stops); ``done`` is true once that new index\n *     runs off either end of ``[0, totalFrames)``.\n *   deriveEffectiveTodayKey(dates, cache, countryState, fallbackKey)\n *     Latest entry in ``dates`` whose ``cache[date]`` payload contains at\n *     least one region belonging to an active country (per ``countryState``,\n *     a ``{prefix: boolean}`` map). Falls back to the last date in ``dates``\n *     if none match, or to ``fallbackKey`` if ``dates`` is empty. Logs an\n *     unexpected-prefix warning (deduped per call) exactly as the original\n *     inline version did.\n *   isSelectableDate(dateKey, maxMs)\n *     True when ``dateKey`` parses and is not past ``maxMs`` \u2014 the same set\n *     of days the calendar offers (SNOW-794). SNOW-927: the bound is the\n *     caller's and follows the data; it is no longer always today.\n *   intToKey(n, ratingTable)\n *     Rating int \u2192 EAWS key string via ``ratingTable`` (index lookup);\n *     ``'no_rating'`` for ``null``/``undefined``/out-of-range ``n``.\n */\n\n// @ts-check\n\n(function () {\n  'use strict';\n\n  /**\n   * Thumb percentage (0..100) \u2192 ISO date string, UTC-midnight snapped.\n   *\n   * @param {number} pct\n   * @param {number} seasonStartMs\n   * @param {number} seasonSpanMs\n   * @returns {string}\n   */\n  function pctToDateKey(pct, seasonStartMs, seasonSpanMs) {\n    var ms = seasonStartMs + (pct / 100) * seasonSpanMs;\n    var day = new Date(ms);\n    var y = day.getUTCFullYear();\n    var m = String(day.getUTCMonth() + 1).padStart(2, '0');\n    var d = String(day.getUTCDate()).padStart(2, '0');\n    return y + '-' + m + '-' + d;\n  }\n\n  /**\n   * ISO date string \u2192 thumb percentage, clamped to [0, 100].\n   *\n   * @param {string} dateKey\n   * @param {number} seasonStartMs\n   * @param {number} seasonSpanMs\n   * @param {number} fallbackPct Returned when ``dateKey`` doesn't parse or\n   *   ``seasonSpanMs`` is non-finite/non-positive.\n   * @returns {number}\n   */\n  function dateKeyToPct(dateKey, seasonStartMs, seasonSpanMs, fallbackPct) {\n    var ms = Date.parse(dateKey);\n    if (Number.isNaN(ms) || !Number.isFinite(seasonSpanMs) || seasonSpanMs <= 0) {\n      return fallbackPct;\n    }\n    return Math.max(0, Math.min(100, ((ms - seasonStartMs) / seasonSpanMs) * 100));\n  }\n\n  /**\n   * Nearest entry in ``sortedDates`` to ``dateKey`` by absolute ms delta.\n   *\n   * @param {string} dateKey\n   * @param {string[]|null} sortedDates\n   * @returns {string} ``dateKey`` unchanged when ``sortedDates`` is empty/null.\n   */\n  function snapToNearestDataDay(dateKey, sortedDates) {\n    if (!sortedDates || sortedDates.length === 0) return dateKey;\n    // SNOW-614: hoisted out of the loop. ``dateKey`` does not change across\n    // it, and this runs on every pointermove of a scrubber drag over a\n    // season's worth of dates \u2014 so it was one redundant date parse per\n    // candidate per frame.\n    var targetMs = Date.parse(dateKey);\n    var best = sortedDates[0];\n    var bestDelta = Math.abs(Date.parse(best) - targetMs);\n    for (var i = 0; i < sortedDates.length; i++) {\n      var d = sortedDates[i];\n      var delta = Math.abs(Date.parse(d) - targetMs);\n      if (delta < bestDelta) {\n        best = d;\n        bestDelta = delta;\n      }\n    }\n    return best;\n  }\n\n  /**\n   * Index into ``sortedDates`` nearest the position implied by\n   * ``ariaValueNow`` (a 0..100 thumb percentage) \u2014 resumes timelapse\n   * playback from the current thumb position rather than always\n   * rewinding to frame 0.\n   *\n   * @param {string[]|null} sortedDates\n   * @param {number} ariaValueNow\n   * @param {number} seasonStartMs\n   * @param {number} seasonSpanMs\n   * @returns {number}\n   */\n  function nearestFrameIndex(sortedDates, ariaValueNow, seasonStartMs, seasonSpanMs) {\n    if (!sortedDates || sortedDates.length === 0) return 0;\n    if (\n      !Number.isFinite(ariaValueNow) ||\n      !Number.isFinite(seasonSpanMs) ||\n      seasonSpanMs <= 0\n    ) {\n      return 0;\n    }\n    var targetMs = seasonStartMs + (ariaValueNow / 100) * seasonSpanMs;\n    var best = 0;\n    var bestDelta = Math.abs(Date.parse(sortedDates[0]) - targetMs);\n    for (var i = 1; i < sortedDates.length; i++) {\n      var delta = Math.abs(Date.parse(sortedDates[i]) - targetMs);\n      if (delta < bestDelta) {\n        best = i;\n        bestDelta = delta;\n      }\n    }\n    return best;\n  }\n\n  /**\n   * Advance one timelapse tick.\n   *\n   * @param {number} frameIdx\n   * @param {1|-1} direction\n   * @param {number} totalFrames\n   * @returns {{frameIdx: number, done: boolean}} ``frameIdx`` is\n   *   ``frameIdx + direction`` unconditionally \u2014 matching the original\n   *   inline behaviour of leaving the counter one step past the boundary\n   *   when playback stops; ``done`` is true once that new index runs off\n   *   either end of ``[0, totalFrames)``.\n   */\n  function nextFrame(frameIdx, direction, totalFrames) {\n    var next = frameIdx + direction;\n    var done =\n      (direction === 1 && next >= totalFrames) || (direction === -1 && next < 0);\n    return { frameIdx: next, done: done };\n  }\n\n  /**\n   * Latest entry in ``dates`` whose ``cache[date]`` payload contains at\n   * least one region belonging to an active country.\n   *\n   * @param {string[]|null} dates Ascending date keys.\n   * @param {Object<string, Object<string, number>>} cache\n   * @param {Object<string, boolean>} countryState ``{prefix: active}``.\n   * @param {string} fallbackKey Returned when ``dates`` is empty/null.\n   * @returns {string}\n   */\n  function deriveEffectiveTodayKey(dates, cache, countryState, fallbackKey) {\n    if (!dates || dates.length === 0) return fallbackKey;\n    // Object.create(null) rather than {} \u2014 a plain object's prototype\n    // chain would make `!warnedPrefixes[prefix]` return falsy (already\n    // \"warned\") for any prefix that collides with an inherited Object.prototype\n    // name (e.g. 'constructor', 'toString'), silently suppressing that\n    // prefix's first warning.\n    var warnedPrefixes = Object.create(null);\n    for (var i = dates.length - 1; i >= 0; i--) {\n      var dateKey = dates[i];\n      var frame = cache[dateKey] || {};\n      var regionIds = Object.keys(frame);\n      for (var j = 0; j < regionIds.length; j++) {\n        var prefix = regionIds[j].split('-')[0].toLowerCase();\n        if (countryState[prefix] === true) return dateKey;\n        if (!(prefix in countryState) && !warnedPrefixes[prefix]) {\n          warnedPrefixes[prefix] = true;\n          console.warn('[map] SNOW-236: unexpected region prefix', prefix, 'in ratings payload');\n        }\n      }\n    }\n    return dates[dates.length - 1];\n  }\n\n  /**\n   * True when ``dateKey`` is a date the map can answer for: parseable, and\n   * not past the caller's ceiling.\n   *\n   * SNOW-794: this replaced an ``isInSeason`` guard on the scrubber's boot\n   * and popstate paths. That guard was narrower than what the calendar\n   * offers \u2014 the ratings payload is the whole archive, so the picker pages\n   * back years \u2014 and a ``?d=`` it refused left the map uncoloured while the\n   * URL and the calendar grid both named a day. Both surfaces answer to one\n   * bound (the calendar's ``buildMonthGrid`` ``max``) so they accept exactly\n   * the same set of days. A date outside the track's twelve-month window\n   * still parks the thumb at whichever end it is past \u2014 ``dateKeyToPct``\n   * clamps \u2014 which is the honest reading of \"that is before this track\n   * starts\".\n   *\n   * SNOW-927: that bound used to be today, hardcoded at both call sites, and\n   * this function's parameter was named for it. It is now whatever the\n   * CALLER derives from the data (``pwaCalendarCore.latestKnownDate``), which\n   * from about 16:00 is tomorrow \u2014 the evening issue forecasts the next day,\n   * and the payload has always carried it. The comparison is unchanged; only\n   * the name was ever wrong.\n   *\n   * @param {string} dateKey\n   * @param {number} maxMs Latest reachable day, UTC midnight. Today until\n   *   the ratings payload proves a later day exists.\n   * @returns {boolean}\n   */\n  function isSelectableDate(dateKey, maxMs) {\n    var ms = Date.parse(dateKey);\n    return Number.isFinite(ms) && ms <= maxMs;\n  }\n\n  /**\n   * Rating int \u2192 EAWS key string via ``ratingTable`` index lookup.\n   *\n   * @param {number|null|undefined} n\n   * @param {string[]} ratingTable\n   * @returns {string} ``'no_rating'`` for ``null``/``undefined``/out-of-range.\n   */\n  function intToKey(n, ratingTable) {\n    if (n == null || n < 0 || n >= ratingTable.length) return 'no_rating';\n    return ratingTable[n];\n  }\n\n  window.pwaScrubberCore = Object.freeze({\n    pctToDateKey: pctToDateKey,\n    dateKeyToPct: dateKeyToPct,\n    snapToNearestDataDay: snapToNearestDataDay,\n    nearestFrameIndex: nearestFrameIndex,\n    nextFrame: nextFrame,\n    deriveEffectiveTodayKey: deriveEffectiveTodayKey,\n    isSelectableDate: isSelectableDate,\n    intToKey: intToKey,\n  });\n})();\n"],
  "mappings": "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",
  "names": ["pctToDateKey", "pct", "seasonStartMs", "seasonSpanMs", "ms", "day", "y", "m", "d", "dateKeyToPct", "dateKey", "fallbackPct", "snapToNearestDataDay", "sortedDates", "targetMs", "best", "bestDelta", "delta", "nearestFrameIndex", "ariaValueNow", "i", "nextFrame", "frameIdx", "direction", "totalFrames", "next", "done", "deriveEffectiveTodayKey", "dates", "cache", "countryState", "fallbackKey", "warnedPrefixes", "frame", "regionIds", "j", "prefix", "isSelectableDate", "maxMs", "intToKey", "n", "ratingTable"]
}
