PageSourceSearch

https://hiraeth.club/assets/js/weather.js?v=a6ca355b5f

js hiraeth.club collected 2026-10-04 08:04:05 UTC 15,449 bytes, 369 lines download raw bytes

1(function () {
2  'use strict';
3
4  var LAT = 51.5714, LON = -3.9771;
5
6  var DIRS = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'];
7  var WX_CODES = {
8    0:'Clear sky', 1:'Mainly clear', 2:'Partly cloudy', 3:'Overcast',
9    45:'Fog', 48:'Icy fog', 51:'Light drizzle', 53:'Drizzle', 55:'Heavy drizzle',
10    61:'Light rain', 63:'Rain', 65:'Heavy rain',
11    71:'Light snow', 73:'Snow', 75:'Heavy snow',
12    80:'Rain showers', 81:'Showers', 82:'Heavy showers',
13    95:'Thunderstorm', 96:'Thunderstorm'
14  };
15
16  function degToDir(d) { return DIRS[Math.round(d / 22.5) % 16]; }
17
18  function beaufort(mph) {
19    var b = [1,3,7,12,18,24,31,38,46,54,63];
20    for (var i = 0; i < b.length; i++) { if (mph <= b[i]) return i; }
21    return 12;
22  }
23
24  function bftDesc(b) {
25    var names = ['Calm','Light air','Light breeze','Gentle breeze','Moderate breeze',
26      'Fresh breeze','Strong breeze','Near gale','Gale','Severe gale','Storm','Violent storm','Hurricane'];
27    return names[b] || '';
28  }
29
30  function uvLabel(u) {
31    if (u <= 2) return 'Low';
32    if (u <= 5) return 'Moderate';
33    if (u <= 7) return 'High';
34    if (u <= 10) return 'Very high';
35    return 'Extreme';
36  }
37
38  function dewPoint(t, rh) { return Math.round(t - (100 - rh) / 5); }
39
40  function fmtTime(iso) {
41    return new Date(iso).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
42  }
43
44  function fmtDur(ms) {
45    var h = Math.floor(ms / 3600000);
46    var m = Math.round((ms % 3600000) / 60000);
47    return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
48  }
49
50  function setText(id, val) {
51    var el = document.getElementById(id);
52    if (el) el.textContent = val;
53  }
54
55  function setHTML(id, val) {
56    var el = document.getElementById(id);
57    if (el) el.innerHTML = val;
58  }
59
60  // --- Tide calculation ---
61  function tideCalc() {
62    var M2 = { amp: 3.8, phase: 195 };
63    var S2 = { amp: 1.2, phase: 220 };
64    var N2 = { amp: 0.7, phase: 185 };
65
66    var now = new Date();
67    var nowMs = now.getTime();
68    var day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
69    var epoch = new Date('2000-01-01T00:00:00Z');
70
71    var points = 576;
72    var heights = [], times = [];
73    for (var i = 0; i < points; i++) {
74      var t = i * (48 * 60 / points);
75      var d = new Date(day.getTime() + t * 60000);
76      var T = (d - epoch) / (1000 * 3600);
77      var m2 = 2 * Math.PI * (T / 12.42 + M2.phase / 360);
78      var s2 = 2 * Math.PI * (T / 12.00 + S2.phase / 360);
79      var n2 = 2 * Math.PI * (T / 12.66 + N2.phase / 360);
80      heights.push(4.5 + M2.amp * Math.cos(m2) + S2.amp * Math.cos(s2) + N2.amp * Math.cos(n2));
81      times.push(d);
82    }
83
84    var mn = Math.min.apply(null, heights);
85    var mx = Math.max.apply(null, heights);
86
87    // SVG: today only (first half of points)
88    var todayPts = points / 2;
89    var pts = heights.slice(0, todayPts).map(function (h, i) {
90      var x = (i / (todayPts - 1) * 600).toFixed(1);
91      var y = (80 - ((h - mn) / (mx - mn)) * 65 - 7).toFixed(1);
92      return x + ',' + y;
93    }).join(' ');
94
95    var minsToday = now.getHours() * 60 + now.getMinutes();
96    var nowIdx = Math.min(Math.round(minsToday / (24 * 60) * todayPts), todayPts - 1);
97    var nowH = heights[nowIdx];
98    var rising = nowH > heights[Math.max(0, nowIdx - 1)];
99
100    var nowX = (nowIdx / (todayPts - 1) * 600).toFixed(1);
101    var nowY = (80 - ((nowH - mn) / (mx - mn)) * 65 - 7).toFixed(1);
102
103    var svg = document.getElementById('tide-svg');
104    if (svg) {
105      svg.innerHTML =
106        '<polyline points="' + pts + '" fill="none" stroke="#c9b8d8" stroke-width="2"/>' +
107        '<circle cx="' + nowX + '" cy="' + nowY + '" r="4" fill="#7a5a8a"/>';
108    }
109
110    // Find extremes across full 48h
111    var extremes = [];
112    for (var j = 1; j < heights.length - 1; j++) {
113      if (heights[j] > heights[j-1] && heights[j] > heights[j+1]) {
114        extremes.push({ type: 'High', h: heights[j], t: times[j] });
115      }
116      if (heights[j] < heights[j-1] && heights[j] < heights[j+1]) {
117        extremes.push({ type: 'Low', h: heights[j], t: times[j] });
118      }
119    }
120
121    // Today's extremes for tide times display
122    var midnight = new Date(day.getTime() + 24 * 60 * 60000);
123    var todayExtremes = extremes.filter(function (e) {
124      return e.t.getTime() < midnight.getTime();
125    }).slice(0, 4);
126
127    var tideTimesEl = document.getElementById('tide-times');
128    if (tideTimesEl) {
129      tideTimesEl.innerHTML = todayExtremes.map(function (e) {
130        return '<span>' + e.type + ' tide: ' +
131          e.t.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) +
132          ' (' + e.h.toFixed(1) + ' m)</span>';
133      }).join('');
134    }
135
136    setText('fp-tide-now', nowH.toFixed(1) + ' m');
137    setText('fp-tide-state', rising ? 'Rising' : 'Falling');
138
139    // Next tide: first extreme strictly in the future
140    var future = extremes.filter(function (e) { return e.t.getTime() > nowMs; });
141    if (future.length > 0) {
142      var next = future[0];
143      setText('fp-tide-next', next.type + ' tide');
144      setText('fp-tide-countdown', 'in ' + fmtDur(next.t.getTime() - nowMs) + ' \u00B7 ' + next.h.toFixed(1) + ' m');
145    }
146
147    return { nowH: nowH, rising: rising };
148  }
149
150  // --- Moon calculation ---
151  function moonCalc() {
152    var now = new Date();
153    var known = new Date('2000-01-06T18:14:00Z');
154    var cycle = 29.53058867;
155    var age = (((now - known) / 86400000) % cycle + cycle) % cycle;
156    var illum = Math.round((1 - Math.cos(2 * Math.PI * age / cycle)) / 2 * 100);
157
158    var phase, icon;
159    if (age < 1.85)       { phase = 'New moon';        icon = '\uD83C\uDF11'; }
160    else if (age < 7.38)  { phase = 'Waxing crescent'; icon = '\uD83C\uDF12'; }
161    else if (age < 9.22)  { phase = 'First quarter';   icon = '\uD83C\uDF13'; }
162    else if (age < 14.76) { phase = 'Waxing gibbous';  icon = '\uD83C\uDF14'; }
163    else if (age < 16.61) { phase = 'Full moon';       icon = '\uD83C\uDF15'; }
164    else if (age < 22.15) { phase = 'Waning gibbous';  icon = '\uD83C\uDF16'; }
165    else if (age < 23.99) { phase = 'Last quarter';    icon = '\uD83C\uDF17'; }
166    else                  { phase = 'Waning crescent'; icon = '\uD83C\uDF18'; }
167
168    var daysToFull = age <= 14.76 ? 14.76 - age : cycle - age + 14.76;
169    var nextFull = new Date(now.getTime() + daysToFull * 86400000);
170
171    setText('moon-icon', icon);
172    setText('moon-phase', phase);
173    setText('moon-illum', illum + '% illuminated');
174    setText('moon-next', 'Next full moon: ' + nextFull.toLocaleDateString('en-GB', { day: 'numeric', month: 'long' }));
175  }
176
177  // --- Outdoor verdict ---
178  function verdict(wxCode, windMph, gustMph, humid, visibility) {
179    var fogRisk = humid > 92 && visibility < 2000;
180    var icon, text;
181
182    if (fogRisk || wxCode === 45 || wxCode === 48) {
183      icon = '\uD83C\uDF2B'; text = 'Sea fog possible \u2014 poor visibility likely on the bay';
184    } else if (wxCode === 95 || wxCode === 96) {
185      icon = '\u26C8'; text = 'Thunderstorms \u2014 stay indoors';
186    } else if (gustMph > 50) {
187      icon = '\uD83C\uDF2C'; text = 'Storm-force gusts \u2014 not suitable for outdoor activities';
188    } else if (gustMph > 35) {
189      icon = '\uD83D\uDCA8'; text = 'Strong gusts \u2014 exposed headlands will be rough';
190    } else if ((wxCode === 61 || wxCode === 63 || wxCode === 65 || wxCode === 80 || wxCode === 81 || wxCode === 82) && windMph > 20) {
191      icon = '\uD83C\uDF27'; text = 'Wet and windy \u2014 waterproofs essential';
192    } else if (wxCode === 61 || wxCode === 63 || wxCode === 65 || wxCode === 80 || wxCode === 81 || wxCode === 82) {
193      icon = '\uD83C\uDF27'; text = 'Rainy \u2014 good day for a sheltered walk along the prom';
194    } else if (windMph <= 10 && (wxCode === 0 || wxCode === 1 || wxCode === 2)) {
195      icon = '\uD83C\uDF3F'; text = 'Calm and clear \u2014 fine conditions for the headland or bay';
196    } else if (windMph <= 15) {
197      icon = '\uD83D\uDEB6'; text = 'Pleasant conditions for walking';
198    } else {
199      icon = '\uD83C\uDF24'; text = 'Mixed conditions \u2014 reasonable for getting out';
200    }
201
202    setText('verdict-icon', icon);
203    setText('verdict-text', text);
204  }
205
206  // --- Weather ---
207  function loadWeather() {
208    var url = 'https://api.open-meteo.com/v1/forecast' +
209      '?latitude=' + LAT + '&longitude=' + LON +
210      '&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code' +
211      ',wind_speed_10m,wind_direction_10m,wind_gusts_10m,surface_pressure,uv_index,visibility' +
212      '&hourly=surface_pressure,visibility,cloud_cover' +
213      '&daily=sunrise,sunset,precipitation_sum,precipitation_probability_max,uv_index_max' +
214      '&wind_speed_unit=mph&timezone=Europe%2FLondon&forecast_days=2';
215
216    fetch(url).then(function (r) { return r.json(); }).then(function (d) {
217      var c = d.current, dd = d.daily;
218      var hour = new Date().getHours();
219
220      var temp    = Math.round(c.temperature_2m);
221      var feels   = Math.round(c.apparent_temperature);
222      var windMph = Math.round(c.wind_speed_10m);
223      var gustMph = Math.round(c.wind_gusts_10m);
224      var humid   = Math.round(c.relative_humidity_2m);
225      var wdir    = degToDir(c.wind_direction_10m);
226      var uv      = Math.round(dd.uv_index_max[0]);
227      var rain    = (dd.precipitation_sum[0] || 0).toFixed(1);
228      var pprob   = Math.round(dd.precipitation_probability_max[0] || 0);
229      var pres    = Math.round(c.surface_pressure);
230      var dew     = dewPoint(temp, humid);
231      var bft     = beaufort(windMph);
232      var bftG    = beaufort(gustMph);
233      var cloud   = Math.round(d.hourly.cloud_cover[hour] || 0);
234      var visMi   = (c.visibility / 1609.34).toFixed(1);
235      var fogRisk = humid > 92 && c.visibility < 2000;
236
237      var prev3h      = d.hourly.surface_pressure[Math.max(0, hour - 3)];
238      var presDiff    = pres - Math.round(prev3h);
239      var presTrend   = presDiff > 1.5 ? 'Rising' : presDiff < -1.5 ? 'Falling' : 'Steady';
240      var presDiffStr = (presDiff > 0 ? '+' : '') + presDiff + ' hPa / 3h';
241
242      var sunrise = fmtTime(dd.sunrise[0]);
243      var sunset  = fmtTime(dd.sunset[0]);
244      var dayMins = (new Date(dd.sunset[0]) - new Date(dd.sunrise[0])) / 60000;
245      var dayHrs  = (dayMins / 60).toFixed(1);
246
247      var propNote = dew > temp - 3
248        ? 'High humidity \u2014 may affect HF propagation'
249        : pres > 1020 ? 'High pressure \u2014 generally favourable for HF'
250        : pres < 1000 ? 'Low pressure system \u2014 variable propagation'
251        : 'Typical propagation conditions';
252
253      setText('fp-temp',          temp + '\u00B0C');
254      setText('fp-feels',         'Feels like ' + feels + '\u00B0C');
255      setText('fp-dew',           'Dew point ' + dew + '\u00B0C');
256      setText('fp-wind',          windMph + ' mph');
257      setText('fp-winddir',       wdir);
258      setText('fp-beaufort',      'Beaufort ' + bft + ' \u00B7 ' + bftDesc(bft));
259      setText('fp-gusts',         gustMph + ' mph');
260      setText('fp-gust-bft',      'Beaufort ' + bftG + ' \u00B7 ' + bftDesc(bftG));
261      setText('fp-pressure',      pres + ' hPa');
262      setHTML('fp-pressure-trend','<span class="wx-trend-' 
262+ presTrend.toLowerCase() + '">' + presTrend + '</span> (' + presDiffStr + ')');
263      setText('fp-rain',          rain + ' mm');
264      setText('fp-precip-prob',   pprob + '% chance');
265      setText('fp-humid',         humid + '%');
266      setText('fp-fog',           fogRisk ? '\u26A0 Sea fog risk' : 'No fog risk');
267      setText('fp-uv',            uv);
268      setText('fp-uv-label',      uvLabel(uv));
269      setText('fp-vis',           visMi + ' mi');
270      setText('fp-cloud',         cloud + '% cloud');
271      setText('fp-sun',           sunrise + ' / ' + sunset);
272      setText('fp-daylen',        dayHrs + ' hrs daylight');
273      setText('fp-prop',          propNote);
274
275      if (fogRisk) {
276        var hcard = document.getElementById('fp-humid-card');
277        if (hcard) hcard.classList.add('wx-card-fog');
278      }
279
280      setText('wx-updated', 'Weather data from Open-Meteo \u00B7 updated ' +
281        new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }));
282
283      verdict(c.weather_code, windMph, gustMph, humid, c.visibility);
284      updateSidebarWidget(c.weather_code, temp, windMph, wdir, pres);
285
286    }).catch(function () {
287      setText('fp-temp', 'Unavailable');
288    });
289  }
290
291  // --- Marine ---
292  function loadMarine() {
293    var url = 'https://marine-api.open-meteo.com/v1/marine' +
294      '?latitude=' + LAT + '&longitude=' + LON +
295      '&current=wave_height,wave_direction,wave_period,wind_wave_height,wind_wave_direction,swell_wave_direction,sea_surface_temperature' +
296      '&timezone=Europe%2FLondon';
297
298    fetch(url).then(function (r) { return r.json(); }).then(function (d) {
299      var c = d.current;
300      var swellDir    = degToDir(c.swell_wave_direction);
301      var windWaveDir = degToDir(c.wind_wave_direction);
302      var same        = swellDir === windWaveDir;
303
304      setText('fp-wave',          c.wave_height.toFixed(1) + ' m');
305      setText('fp-wave-dir',      'from ' + degToDir(c.wave_direction));
306      setText('fp-swell-dir',     swellDir);
307      setText('fp-wave-vs-swell', same ? 'aligned with swell' : 'crossing from ' + windWaveDir);
308      setText('fp-period',        Math.round(c.wave_period) + ' s');
309      setText('fp-sst',           Math.round(c.sea_surface_temperature) + '\u00B0C');
310      setText('fp-ww',            c.wind_wave_height.toFixed(1) + ' m');
311      setText('fp-ww-dir',        'from ' + windWaveDir);
312    }).catch(function () {});
313  }
314
315  // --- Sidebar widget ---
316  function updateSidebarWidget(wxCode, temp, windMph, wdir, pres) {
317    var WX_ICONS = {
318      0:'\u2600\uFE0F', 1:'\uD83C\uDF24', 2:'\u26C5', 3:'\u2601\uFE0F',
319      45:'\uD83C\uDF2B', 48:'\uD83C\uDF2B',
320      51:'\uD83C\uDF26', 53:'\uD83C\uDF26', 55:'\uD83C\uDF27',
321      61:'\uD83C\uDF26', 63:'\uD83C\uDF27', 65:'\uD83C\uDF27',
322      71:'\uD83C\uDF28', 73:'\u2744\uFE0F', 75:'\u2744\uFE0F',
323      80:'\uD83C\uDF26', 81:'\uD83C\uDF27', 82:'\u26C8\uFE0F',
324      95:'\u26C8\uFE0F', 96:'\u26C8\uFE0F'
325    };
326    setText('wx-widget-icon',     WX_ICONS[wxCode] || '\uD83C\uDF21\uFE0F');
327    setText('wx-widget-temp',     temp + '\u00B0C');
328    setText('wx-widget-desc',     WX_CODES[wxCode] || 'Variable');
329    setText('wx-widget-wind',     windMph + ' mph ' + wdir);
330    setText('wx-widget-pressure', pres + ' hPa');
331  }
332
333  // --- Sidebar-only load ---
334  function loadSidebarOnly() {
335    var url = 'https://api.open-meteo.com/v1/forecast' +
336      '?latitude=' + LAT + '&longitude=' + LON +
337      '&current=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m,surface_pressure' +
338      '&wind_speed_unit=mph&timezone=Europe%2FLondon&forecast_days=1';
339
340    fetch(url).then(function (r) { return r.json(); }).then(function (d) {
341      var c = d.current;
342      updateSidebarWidget(
343        c.weather_code,
344        Math.round(c.temperature_2m),
345        Math.round(c.wind_speed_10m),
346        degToDir(c.wind_direction_10m),
347        Math.round(c.surface_pressure)
348      );
349    }).catch(function () {
350      setText('wx-widget-desc', 'Unavailable');
351    });
352  }
353
354  // --- Init ---
355  document.addEventListener('DOMContentLoaded', function () {
356    var isWeatherPage = !!document.querySelector('.weather-page');
357
358    tideCalc();
359    moonCalc();
360
361    if (isWeatherPage) {
362      loadWeather();
363      loadMarine();
364    } else {
365      loadSidebarOnly();
366    }
367  });
368
369})();

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.