{
  "version": 3,
  "sources": ["map_timelapse.js"],
  "sourcesContent": ["/*\n * static/js/map_timelapse.js \u2014 the five season-playback transport buttons.\n *\n * SNOW-610, step 4: extracted verbatim from map.js. Runs at parse time and\n * binds its own DOM, so it loads AFTER map.js in the position it held\n * inside that file \u2014 document order is execution order for deferred\n * classic scripts. Shared state (`MAP`, `FEATURE_BY_REGION_ID`,\n * `COUNTRY_STATE`, \u2026) comes from static/js/map_state.js; shared helpers\n * (`getSeasonRatings`, `repaintRegionsForDate`, the localStorage trio)\n * from static/js/map_shared.js. Both load first.\n */\n\n// Season timelapse \u2014 five transport buttons on the scrubber:\n//   |<  skip to season start\n//   <   play in reverse from current thumb position (second press = stop)\n//   >   play forward from current thumb position (second press = stop)\n//   >|  skip to season end\n//\n// Each frame repaints region colours via feature-state and announces a\n// snowdesk:date-changed event so the date readout stays in sync. Pressing\n// the opposite play button mid-playback flips direction from the current\n// frame without resetting the frame index.\n//\n// SNOW-660: playback chooses a day like any other surface, so the URL has\n// to say which \u2014 ``?d=`` is written at the points playback SETTLES (a stop,\n// and the two skip jumps), never per frame. See ``writeUrlDateParam`` in\n// map_shared.js for why the rate differs from the scrubber's.\n(function timelapseInit() {\n  const playButton = document.getElementById('scrubber-play');\n  if (!playButton) return;\n\n  // BASE_FRAME_MS gives ~5 fps \u2014 consistent in both forward and reverse.\n  const BASE_FRAME_MS = 200;\n\n  // Drive the scrubber thumb so playback position is visible.\n  const scrubber = document.getElementById('season-scrubber');\n  const scrubberThumb = scrubber ? scrubber.querySelector('.season-scrubber-thumb') : null;\n  // SNOW-794: the scrubber's TRACK span (twelve months ending today), not the\n  // avalanche season \u2014 the two used to be one pair under one name. Playback\n  // still steps through ``sortedDates``, real data days; this is only how a\n  // frame becomes a thumb position.\n  const seasonStartMs = scrubber ? Date.parse(scrubber.dataset.seasonStart) : NaN;\n  const seasonEndMs = scrubber ? Date.parse(scrubber.dataset.seasonEnd) : NaN;\n  const seasonSpanMs = seasonEndMs - seasonStartMs;\n\n  const reverseButton = document.getElementById('scrubber-reverse');\n  const skipStartButton = document.getElementById('scrubber-skip-start');\n  const skipEndButton = document.getElementById('scrubber-skip-end');\n\n  // Active playback direction: 1 = forward, -1 = reverse.\n  let direction = 1;\n\n  const moveScrubber = (dateKey) => {\n    if (!scrubberThumb || !Number.isFinite(seasonSpanMs) || seasonSpanMs <= 0) return;\n    const dateMs = Date.parse(dateKey);\n    if (Number.isNaN(dateMs)) return;\n    const pct = Math.max(0, Math.min(100, ((dateMs - seasonStartMs) / seasonSpanMs) * 100));\n    scrubberThumb.style.left = pct + '%';\n    // Keep aria-valuenow in lock-step with the visual thumb position so\n    // ``currentFrameIdx`` resumes from the right spot when the user stops\n    // playback mid-season and then presses play again \u2014 without this,\n    // the next start() falls back to the last user-committed pct rather\n    // than the last frame painted by the timelapse.\n    if (scrubber) {\n      scrubber.setAttribute('aria-valuenow', String(Math.round(pct)));\n    }\n  };\n\n  // Determine the frame index to start from so playback begins at the\n  // current thumb position rather than always rewinding to frame 0.\n  const currentFrameIdx = () => {\n    const ariaNow = scrubber ? parseFloat(scrubber.getAttribute('aria-valuenow')) : NaN;\n    if (window.pwaScrubberCore) {\n      return window.pwaScrubberCore.nearestFrameIndex(sortedDates, ariaNow, seasonStartMs, seasonSpanMs);\n    }\n    if (!sortedDates || sortedDates.length === 0) return 0;\n    if (!Number.isFinite(ariaNow) || !Number.isFinite(seasonSpanMs) || seasonSpanMs <= 0) {\n      return 0;\n    }\n    const targetMs = seasonStartMs + (ariaNow / 100) * seasonSpanMs;\n    // Find the nearest sortedDates entry to the current thumb position.\n    let best = 0;\n    let bestDelta = Math.abs(Date.parse(sortedDates[0]) - targetMs);\n    for (let i = 1; i < sortedDates.length; i++) {\n      const delta = Math.abs(Date.parse(sortedDates[i]) - targetMs);\n      if (delta < bestDelta) { best = i; bestDelta = delta; }\n    }\n    return best;\n  };\n\n  let cache = null;        // {date_iso: {region_id: int}}\n  let sortedDates = null;  // ascending list of date keys\n  let frameIdx = 0;\n  let timer = null;\n\n  // SNOW-660: the last frame actually painted \u2014 the day currently on screen.\n  //\n  // ``stop()`` cannot ask ``frameIdx`` for it: the boundary stop runs with\n  // the index already stepped out of range (``nextFrame`` returns the\n  // incremented value alongside ``done``), so ``sortedDates[frameIdx]``\n  // there is undefined. This is set where the paint happens, which is the\n  // only place that knows what the map is showing.\n  let lastPaintedDate = null;\n\n  const announce = (dateKey) => {\n    document.dispatchEvent(new CustomEvent('snowdesk:date-changed', {\n      detail: { date: dateKey, source: 'timelapse' },\n    }));\n  };\n\n  const applyFrame = (dateKey) => {\n    repaintRegionsForDate(dateKey, cache);\n    moveScrubber(dateKey);\n    lastPaintedDate = dateKey;\n    announce(dateKey);\n  };\n\n  // Hoisted so start() can re-arm setInterval at a new direction without\n  // losing the current frame index.\n  const tick = () => {\n    if (window.pwaScrubberCore) {\n      const result = window.pwaScrubberCore.nextFrame(frameIdx, direction, sortedDates.length);\n      frameIdx = result.frameIdx;\n      if (result.done) {\n        // Boundary reached (forward end or reverse start): last valid frame\n        // already painted \u2014 stop so the value settles. SNOW-660: playback\n        // ran to its own end, so the frame it settled on is the day the\n        // visitor is left looking at, and the URL says so.\n        stop({ syncUrl: true });\n        return;\n      }\n      applyFrame(sortedDates[frameIdx]);\n      return;\n    }\n    frameIdx += direction;\n    if (direction === 1 && frameIdx >= sortedDates.length) {\n      // Forward end: last frame already painted \u2014 stop so the value settles.\n      stop({ syncUrl: true });\n      return;\n    }\n    if (direction === -1 && frameIdx < 0) {\n      // Reverse start: first frame already painted \u2014 stop.\n      stop({ syncUrl: true });\n      return;\n    }\n    applyFrame(sortedDates[frameIdx]);\n  };\n\n  /**\n   * Halt playback, leaving the map on the frame it reached.\n   *\n   * SNOW-660: `opts.syncUrl` writes `?d=` for that frame \u2014 playback chooses\n   * a day just as the scrubber does, so the URL has to say which, or a\n   * reload comes back to a map with no day chosen at all. It is written HERE\n   * and not per frame because `history.replaceState` is throttled by Safari\n   * (~100 calls per 30s, and it THROWS on the 101st), and five frames a\n   * second would hit that inside half a minute of playback \u2014 see\n   * `writeUrlDateParam` in map_shared.js.\n   *\n   * Off by default because two of this function's four callers are not the\n   * user ending playback: the `snowdesk:date-changed` listener stops in\n   * deference to a scrub that has ALREADY written its own, later date, and\n   * the basemap-swap listener stops for a reason that is not about dates at\n   * all. Writing in either case would replace the current URL with a stale\n   * frame's.\n   *\n   * @param {{syncUrl?: boolean}} [opts]\n   */\n  const stop = (opts = {}) => {\n    if (timer !== null) {\n      clearInterval(timer);\n      timer = null;\n    }\n    if (opts.syncUrl && lastPaintedDate) writeUrlDateParam(lastPaintedDate);\n    // Reset data-state on both play transport buttons.\n    playButton.dataset.state = 'stopped';\n    playButton.setAttribute('aria-label', MAP_STRINGS['timelapse-play']);\n    if (reverseButton) {\n      reverseButton.dataset.state = 'stopped';\n      reverseButton.setAttribute('aria-label', MAP_STRINGS['timelapse-play-reverse']);\n    }\n    // Leave the map painted on the current frame \u2014 do not clear\n    // feature-state or reset the thumb. The user sees what was playing.\n    IS_PLAYING = false;\n    document.dispatchEvent(new CustomEvent('snowdesk:timelapse-state', { detail: { playing: false } }));\n  };\n\n  // start(directionArg) \u2014 begins playback from the current thumb position.\n  // If timer is already running (direction flip mid-playback), re-arms the\n  // interval at the new direction without resetting frameIdx so position is\n  // preserved.\n  const start = async (directionArg) => {\n    if (!MAP || !MAP.isStyleLoaded()) return;\n    if (cache === null) {\n      try {\n        cache = await getSeasonRatings();\n        sortedDates = Object.keys(cache).sort();\n      } catch (_err) {\n        return;\n      }\n    }\n    if (sortedDates.length === 0) return;\n\n    direction = directionArg;\n\n    // Only update frameIdx when starting fresh (not a direction flip).\n    if (timer === null) {\n      frameIdx = currentFrameIdx();\n    }\n\n    // Clear any existing timer before arming the new one.\n    if (timer !== null) {\n      clearInterval(timer);\n      timer = null;\n    }\n\n    // Update button states to reflect which transport is now active.\n    if (directionArg === 1) {\n      playButton.dataset.state = 'playing';\n      playButton.setAttribute('aria-label', MAP_STRINGS['timelapse-stop']);\n      if (reverseButton) {\n        reverseButton.dataset.state = 'stopped';\n        reverseButton.setAttribute('aria-label', MAP_STRINGS['timelapse-play-reverse']);\n      }\n    } else {\n      if (reverseButton) {\n        reverseButton.dataset.state = 'playing';\n        reverseButton.setAttribute('aria-label', MAP_STRINGS['timelapse-stop-reverse']);\n      }\n      playButton.dataset.state = 'stopped';\n      playButton.setAttribute('aria-label', MAP_STRINGS['timelapse-play']);\n    }\n\n    IS_PLAYING = true;\n    document.dispatchEvent(new CustomEvent('snowdesk:timelapse-state', { detail: { playing: true } }));\n    applyFrame(sortedDates[frameIdx]);\n    timer = setInterval(tick, BASE_FRAME_MS);\n  };\n\n  // When the scrubber commits a new date, the timelapse must surrender\n  // control \u2014 both paint via feature-state on the same source, so a\n  // running timer would fight a user scrub.\n  document.addEventListener('snowdesk:date-changed', (e) => {\n    if (timer !== null && (!e.detail || e.detail.source !== 'timelapse')) {\n      stop();\n    }\n  });\n\n  // SNOW-58: a basemap swap wipes the regions source mid-frame \u2014 stop\n  // so setInterval doesn't paint into a half-loaded style.\n  document.addEventListener('snowdesk:basemap-changing', () => {\n    if (timer !== null) stop();\n  });\n\n  // Play-forward button: running forward \u2192 stop; else \u2192 start(1).\n  // Pressing while reverse is playing flips direction from the current frame.\n  playButton.addEventListener('click', () => {\n    if (timer !== null && direction === 1) {\n      // SNOW-660: the user ending playback IS a choice of day \u2014 the frame\n      // they stopped on is what they want to look at, so the URL says so.\n      stop({ syncUrl: true });\n    } else {\n      start(1);\n    }\n  });\n\n  // Play-reverse button: running reverse \u2192 stop; else \u2192 start(-1).\n  // Pressing while forward is playing flips direction from the current frame.\n  if (reverseButton) {\n    reverseButton.addEventListener('click', () => {\n      if (timer !== null && direction === -1) {\n        stop({ syncUrl: true });\n      } else {\n        start(-1);\n      }\n    });\n  }\n\n  // Skip-to-start / skip-to-end: jump the thumb to the first/last data\n   // day. Falls back to ``data-season-start``/``data-season-end`` before\n  // the ratings cache resolves. moveScrubber owns the thumb position +\n  // aria-valuenow update; the synthetic date-changed event (source:\n  // 'scrubber') causes the running timelapse to surrender control via\n  // its own listener.\n  //\n  // SNOW-660: one discrete user action, one settled day \u2014 so it writes\n  // ``?d=`` exactly as a scrubber commit does. It announces itself as\n  // ``source: 'scrubber'`` and the listener above therefore stops playback\n  // WITHOUT its own URL write, which is right: this write is the later one\n  // and names the day the jump landed on, not the frame it interrupted.\n  const commitJump = (target) => {\n    if (!target) return;\n    if (cache) repaintRegionsForDate(target, cache);\n    moveScrubber(target);\n    lastPaintedDate = target;\n    writeUrlDateParam(target);\n    document.dispatchEvent(new CustomEvent('snowdesk:date-changed', {\n      detail: { date: target, source: 'scrubber' },\n    }));\n  };\n\n  if (skipStartButton) {\n    skipStartButton.addEventListener('click', () => {\n      commitJump(sortedDates ? sortedDates[0] : (scrubber ? scrubber.dataset.seasonStart : null));\n    });\n  }\n\n  if (skipEndButton) {\n    skipEndButton.addEventListener('click', () => {\n      commitJump(sortedDates ? sortedDates[sortedDates.length - 1] : (scrubber ? scrubber.dataset.seasonEnd : null));\n    });\n  }\n})();\n"],
  "mappings": "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",
  "names": ["playButton", "BASE_FRAME_MS", "scrubber", "scrubberThumb", "seasonStartMs", "seasonSpanMs", "reverseButton", "skipStartButton", "skipEndButton", "direction", "moveScrubber", "dateKey", "dateMs", "pct", "currentFrameIdx", "ariaNow", "sortedDates", "targetMs", "best", "bestDelta", "i", "delta", "cache", "frameIdx", "timer", "lastPaintedDate", "announce", "applyFrame", "tick", "result", "stop", "opts", "start", "directionArg", "commitJump", "target"]
}
