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 '¤t=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 '¤t=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 '¤t=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.