JoelDueck.com Source

windfinger.js at tip
Login

File static/res/windfinger.js from the latest check-in


     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
   100
   101
   102
   103
   104
   105
   106
   107
   108
   109
   110
   111
   112
   113
   114
   115
   116
   117
   118
   119
   120
   121
   122
   123
   124
   125
   126
   127
   128
   129
   130
   131
   132
   133
   134
   135
   136
   137
const nwsEndpoint = 'https://api.weather.gov/gridpoints/MPX/105,74/forecast/hourly?units=us';
const nwsApiNewsLink = 'https://github.com/weather-gov/api/discussions/688';

function setCSSVars(degF) {
    // The temperature → hue formula
    var tempHue = 210 - (degF * 1.5);
    if(tempHue < 0) tempHue = 360 + tempHue;
    var accent = tempHue - 26;
    if(accent < 0) accent = accent + 360;

    document.documentElement.style.setProperty('--base-temp', tempHue);
    document.documentElement.style.setProperty('--accent', accent);
}

async function updateWeatherDiv() {
    const weatherDiv = document.getElementById('weather');
    const slider = document.getElementById("myRange");
    weatherDiv.innerHTML = 'Getting weather…';
    try {
        const weather = await loadWeather();
        
        setCSSVars(weather.temp);
        if(weather.status == "guess") {
            weatherDiv.innerHTML = `Robbinsdale, MN: <abbr title="National Weather Service API is on break right now">${weather.conditions}</abbr> <a href="/mnwx.html">📸</a>  `;
        } else {
            weatherDiv.innerHTML = `Robbinsdale, MN: ${properTemp(weather.temp)}, ${weather.conditions} <a href="/mnwx.html">📸</a>`;
        }
        slider.value = weather.temp;
    } catch (e) {
        weatherDiv.innerHTML = e.message;
    }
}

const updateButton = document.getElementById("reset-weather");
updateButton.onclick = updateWeatherDiv;

async function loadWeather() {
    let w = JSON.parse(localStorage.getItem('weather'));
    if(w === null || Date.now() - w.timestamp > 3600000) {
        w = await fetchWeather();
        localStorage.setItem('weather', JSON.stringify(w));
    }
    return w;
}

const delay = (retryCount) => new Promise((res) => setTimeout(res, 10 ** retryCount));

async function fetchWeather(retryCount = 0) {
    console.log('Updating weather data cache…');
    try {
        const response = await fetch('https://api.weather.gov/gridpoints/MPX/105,74/forecast/hourly?units=us');
        if(!response.ok) {
            const e = new Error(response.statusText);
            e.code = response.status;
            throw e; 
        }
        
        const weatherJson = await response.json();
        const weather = weatherJson.properties.periods[0];
        return { 
            "status": "success",
            "timestamp" : Date.now(),
            "temp" : weather.temperature,
            "conditions" : weather.shortForecast 
        };
    } catch (e) {
        console.log(`Attempt ${retryCount} failed: ${e.message} / ${e.code}`);
        if (e.code >= 500 || e.message.match(/(?:Load failed|NetworkError)/gi) || retryCount > 3) {
            return guessConditions(); 
        } else {
            await delay(retryCount + 1);
            return fetchWeather(retryCount + 1);
        } 
    }
}

updateWeatherDiv();

document.getElementById("myRange").oninput = function() {
    const weatherDiv = document.getElementById('weather');
    setCSSVars(this.value);
    weatherDiv.innerHTML = "Let’s pretend it’s " + properTemp(this.value);
}

function properTemp(degF) {
    const temp = probablyInAmerica() ? degF + "°F" : 
                                       Math.round((degF - 32) * 5 / 9) + "°C";
    return temp.replaceAll("-", "&#x2212;"); // proper minus sign
}

function guessConditions() {
    // https://www.weather.gov/wrh/Climate?wfo=mpx
    const avgTemps = [16,20,33,47,59,69,74,71,63,49,34,15];
    const temperatureWords = [  
        ["January", "frosty", "frigid", "icy", "freezing"],
        ["February", "chill", "frostbitten", "snowy", "arctic"],
        ["March", "cold", "nippy", "sleety", "blustery"],
        ["April", "cool", "crisp", "rainy", "muddy"],
        ["May", "mild", "temperate", "gentle", "refreshing"],
        ["June", "warm", "balmy", "sunny", "humid"],
        ["July", "hot", "sultry", "sweltering", "muggy"],
        ["August", "scorching", "boiling", "roasting", "stifling"],
        ["September", "warm", "pleasant", "temperate", "golden"],
        ["October", "crisp", "cool", "refreshing", "autumnal"],
        ["November", "chilly", "frosty", "wintry", "dreary"],
        ["December", "frigid", "snowy", "icy", "frosty"]
    ];
    const d = new Date();
    console.log(avgTemps[d.getMonth()]);
    let tempWord = temperatureWords[d.getMonth()][Math.floor(Math.random() * 4) + 1];
    return {
        "status": "guess",
        "temp": avgTemps[d.getMonth()],
        "timestamp" : Date.now() - 3300000,
        "conditions": `Probably ${tempWord}<a href="${nwsApiNewsLink}"><sup>‡</sup></a>`
    };
}

const USTimeZoneCities = [
    "Wake", /* US minor outlying islands */
    "New_York", "Detroit", "Louisville", "Monticello", "Indianapolis", "Vincennes",
    "Winamac", "Marengo", "Petersburg", "Vevay", "Chicago", "Tell_City", "Knox",
    "Menominee", "Center", "New_Salem", "Beulah", "Denver", "Boise", "Phoenix",
    "Los_Angeles", "Anchorage", "Juneau", "Sitka", "Metlakatla", "Yakutat", "Nome",
    "Adak", "Honolulu"
];

function probablyInAmerica() {
    if (Intl) {
        const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
        const tzArr = userTimeZone.split("/");
        userCity = tzArr[tzArr.length - 1];
        return USTimeZoneCities.includes(userCity);
    } else {
        return false;
    }
}