PageSourceSearch

https://www.fujisan-climb.jp/wp-content/themes/mtfuji-climbing-off…/assets/js/top-weather-live.js?ver=1.0

js fujisan-climb.jp collected 2026-10-02 03:18:35 UTC 10,193 bytes, 388 lines download raw bytes

1(() => {
2  const API_URL = '/wp-content/weather-data/live.json';
3  const DEFAULT_INDEX = 2;
4  const AUTOPLAY_INTERVAL = 500;
5
6  const imageEl = () => document.getElementById('weather-live-image');
7  const timeEl = () => document.getElementById('weather-live-time');
8  const legendEl = () => document.getElementById('weather-live-legend');
9  const scaleEl = () => document.getElementById('weather-live-scale');
10  const sliderEl = () => document.getElementById('weather-live-slider');
11  const handleEl = () => document.getElementById('weather-live-handle');
12  const prevEl = () => document.getElementById('weather-live-prev');
13  const nextEl = () => document.getElementById('weather-live-next');
14  const playEl = () => document.getElementById('weather-live-play');
15  const stopEl = () => document.getElementById('weather-live-stop');
16  const isJapanese =
17  document.documentElement.lang === 'ja' ||
18  document.documentElement.lang.startsWith('ja-');
19
20  let frames = [];
21  let currentIndex = 0;
22  let timer = null;
23  let dragging = false;
24
25  function stopAutoPlay() {
26    if (timer) {
27      clearInterval(timer);
28      timer = null;
29    }
30  }
31
32  function normalizeIndex(index) {
33    if (!frames.length) return 0;
34    return Math.max(0, Math.min(index, frames.length - 1));
35  }
36
37  function parseFrameLabel(name) {
38    const match = String(name || '').match(/KN00(\d{4})w\.png/i);
39    if (!match) return name || '';
40    const hhmm = match[1];
41    return `${hhmm.slice(0, 2)}:${hhmm.slice(2, 4)}`;
42  }
43
44  function getFrameText(frame) {
45    const timeText = frame.time_text || parseFrameLabel(frame.name);
46    const typeText = frame.type === 'rain' ? '雨量' : 'レーダー';
47    return `${timeText} ${typeText}`;
48  }
49
50  // 旧 radar.js の custom slider 相当
51  // index 0,1,2 = 28px刻み
52  // index 3以降 = 12.214px刻み + 84px開始
53  function getLegacySliderPx(index) {
54    const safeIndex = normalizeIndex(index);
55
56    if (safeIndex < 3) {
57      return 28 * safeIndex;
58    }
59
60    return (12.214 * (safeIndex - 3)) + 84;
61  }
62
63  function getLegacySliderMaxPx(lastIndex) {
64    return getLegacySliderPx(lastIndex);
65  }
66
67  function getSliderLeftPercent(index) {
68    if (!frames.length) return 0;
69
70    const lastIndex = frames.length - 1;
71    const maxPx = getLegacySliderMaxPx(lastIndex);
72
73    if (maxPx <= 0) return 0;
74
75    return (getLegacySliderPx(index) / maxPx) * 100;
76  }
77
78  function getIndexFromClientX(clientX) {
79    const slider = sliderEl();
80    if (!slider || !frames.length) return 0;
81
82    const rect = slider.getBoundingClientRect();
83    const rawX = clientX - rect.left;
84    const clampedX = Math.max(0, Math.min(rect.width, rawX));
85    const lastIndex = frames.length - 1;
86    const maxPx = getLegacySliderMaxPx(lastIndex);
87
88    if (rect.width <= 0 || maxPx <= 0) return 0;
89
90    const legacyPx = (clampedX / rect.width) * maxPx;
91
92    let index = 0;
93    if (legacyPx < 84) {
94      index = Math.round(legacyPx / 28);
95    } else {
96      index = Math.round((legacyPx - 84) / 12.214) + 3;
97    }
98
99    return normalizeIndex(index);
100  }
101
102  function updateHandlePosition() {
103    const handle = handleEl();
104    if (!handle || !frames.length) return;
105
106    handle.style.left = `${getSliderLeftPercent(currentIndex)}%`;
107  }
108
109  function buildScale() {
110    const scale = scaleEl();
111    if (!scale || !frames.length) return;
112
113    scale.innerHTML = '';
114
115    const marks = isJapanese
116      ? [
117          { index: 0, label: '2時間前' },
118          { index: 2, label: '現在', now: true },
119          { index: 3, label: '1時間後' },
120          { index: 5, label: '3時間後' },
121          { index: 8, label: '6時間後' },
122          { index: 17, label: '15時間後' }
123        ]
124      : [
125          { index: 0, label: '-2h' },
126          { index: 2, label: 'Now', now: true },
127          { index: 3, label: '+1h' },
128          { index: 5, label: '+3h' },
129          { index: 8, label: '+6h' },
130          { index: 17, label: '+15h' }
131        ];
132
133    const used = new Set();
134
135    marks.forEach((mark) => {
136      const index = normalizeIndex(mark.index);
137      if (used.has(index)) return;
138      used.add(index);
139
140      const item = document.createElement('span');
141      item.className = 'weather-live__scale-item';
142
143      if (mark.now) {
144        item.classList.add('is-now');
145      }
146
147      item.style.left = `${getSliderLeftPercent(index)}%`;
148      item.textContent = mark.label;
149
150      scale.appendChild(item);
151    });
152  }
153
154  function updateButtonState() {
155    const prev = prevEl();
156    const next = nextEl();
157    const play = playEl();
158    const stop = stopEl();
159
160    if (prev) prev.disabled = currentIndex <= 0;
161    if (next) next.disabled = currentIndex >= frames.length - 1;
162
163    if (play) play.disabled = frames.length <= 1;
164    if (stop) stop.disabled = !timer;
165  }
166
167  function renderFrame(index) {
168    if (!frames.length) return;
169
170    currentIndex = normalizeIndex(index);
171    const frame = frames[currentIndex];
172
173    const img = imageEl();
174    const time = timeEl();
175
176    if (img) {
177      img.src = frame.url;
178      img.alt = `雨雲の動き ${frame.time_text || parseFrameLabel(frame.name)}`;
179    }
180
181    if (time) {
182      time.textContent = getFrameText(frame);
183    }
184
185    updateHandlePosition();
186    updateButtonState();
187  }
188
189  function startAutoPlay() {
190    if (!frames.length) return;
191
192    stopAutoPlay();
193
194    timer = setInterval(() => {
195      if (currentIndex >= frames.length - 1) {
196        stopAutoPlay();
197        updateButtonState();
198        return;
199      }
200
201      renderFrame(currentIndex + 1);
202    }, AUTOPLAY_INTERVAL);
203
204    updateButtonState();
205  }
206
207  function bindButtons() {
208    const prev = prevEl();
209    const next = nextEl();
210    const play = playEl();
211    const stop = stopEl();
212
213    if (prev) {
214      prev.addEventListener('click', () => {
215        stopAutoPlay();
216        renderFrame(currentIndex - 1);
217      });
218    }
219
220    if (next) {
221      next.addEventListener('click', () => {
222        stopAutoPlay();
223        renderFrame(currentIndex + 1);
224      });
225    }
226
227    if (play) {
228      play.addEventListener('click', () => {
229        startAutoPlay();
230      });
231    }
232
233    if (stop) {
234      stop.addEventListener('click', () => {
235        stopAutoPlay();
236        updateButtonState();
237      });
238    }
239  }
240
241  function bindSlider() {
242    const slider = sliderEl();
243    const handle = handleEl();
244    if (!slider || !handle) return;
245
246    const updateFromPointer = (clientX) => {
247      stopAutoPlay();
248      renderFrame(getIndexFromClientX(clientX));
249    };
250
251    slider.addEventListener('click', (e) => {
252      if (e.target === handle) return;
253      updateFromPointer(e.clientX);
254    });
255
256    const startDrag = (clientX) => {
257      dragging = true;
258      updateFromPointer(clientX);
259    };
260
261    const moveDrag = (clientX) => {
262      if (!dragging) return;
263      updateFromPointer(clientX);
264    };
265
266    const endDrag = () => {
267      dragging = false;
268    };
269
270    handle.addEventListener('mousedown', (e) => {
271      e.preventDefault();
272      startDrag(e.clientX);
273    });
274
275    slider.addEventListener('mousedown', (e) => {
276      if (e.target === handle) return;
277      startDrag(e.clientX);
278    });
279
280    window.addEventListener('mousemove', (e) => {
281      moveDrag(e.clientX);
282    });
283
284    window.addEventListener('mouseup', () => {
285      endDrag();
286    });
287
288    handle.addEventListener('touchstart', (e) => {
289      const touch = e.changedTouches && e.changedTouches[0];
290      if (!touch) return;
291      startDrag(touch.clientX);
292    }, { passive: true });
293
294    slider.addEventListener('touchstart', (e) => {
295      if (e.target === handle) return;
296      const touch = e.changedTouches && e.changedTouches[0];
297      if (!touch) return;
298      startDrag(touch.clientX);
299    }, { passive: true });
300
301    window.addEventListener('touchmove', (e) => {
302      const touch = e.touches && e.touches[0];
303      if (!touch) return;
304      moveDrag(touch.clientX);
305    }, { passive: true });
306
307    window.addEventListener('touchend', () => {
308      endDrag();
309    });
310  }
311
312  async function initWeatherLive() {
313    try {
314      const res = await fetch(API_URL, { cache: 'no-store' });
315      if (!res.ok) {
316        throw new Error('Failed to fetch live data');
317      }
318
319      const json = await res.json();
320      const rainImages = Array.isArray(json.rain_images) ? json.rain_images : [];
321      const radarImages = Array.isArray(json.radar_images) ? json.radar_images : [];
322
323      frames = [
324        ...rainImages.map((item) => ({
325          type: 'rain',
326          name: item.name,
327          url: item.url,
328          label: item.label,
329          time_text: item.time_text
330        })),
331        ...radarImages.map((item) => ({
332          type: 'radar',
333          name: item.name,
334          url: item.url,
335          label: item.label,
336          time_text: item.time_text
337        }))
338      ].filter((item) => item && item.url);
339
340      if (!frames.length) {
341        if (timeEl()) {
342          timeEl().textContent = '画像データがありません';
343        }
344        return;
345      }
346
347      if (legendEl() && typeof mtfujiWeather !== 'undefined' && mtfujiWeather.themeUrl) {
348        legendEl().src = `${mtfujiWeather.themeUrl}/assets/img/${
349          isJapanese ? 'rd_hanrei.png' : 'rd_hanrei_en.png'
350        }`;
351      }
352
353      function setControlLabels() {
354        const play = playEl();
355        const stop = stopEl();
356
357        if (play) {
358          play.textContent = isJapanese ? '再生' : 'Play';
359          play.setAttribute(
360            'aria-label',
361            isJapanese ? '雨雲の動きを再生' : 'Play weather animation'
362          );
363        }
364
365        if (stop) {
366          stop.textContent = isJapanese ? '停止' : 'Stop';
367          stop.setAttribute(
368            'aria-label',
369            isJapanese ? '雨雲の動きを停止' : 'Stop weather animation'
370          );
371        }
372      }
373      
374      setControlLabels();
375      bindButtons();
376      bindSlider();
377      buildScale();
378      renderFrame(Math.min(DEFAULT_INDEX, frames.length - 1));
379    } catch (error) {
380      console.error(error);
381      if (timeEl()) {
382        timeEl().textContent = '雨雲データの取得に失敗しました';
383      }
384    }
385  }
386
387  document.addEventListener('DOMContentLoaded', initWeatherLive);
388})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.