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
   138
   139
   140
   141
   142
   143
   144
   145
   146
   147
   148
   149
   150
   151
   152
   153
   154
   155
   156
   157
   158
   159
   160
   161
   162
   163
   164
   165
   166
   167
   168
   169
   170
   171
   172
   173
   174
   175
   176
   177
   178
   179
   180
   181
   182
   183
   184
   185
   186
   187
//const nwsEndpoint = 'https://api.weather.gov/gridpoints/MPX/105,74/forecast/hourly?units=us';
const nwsStationId = 'KMIC';
const nwsApiNewsLink = '#'; //'https://github.com/weather-gov/api/discussions/688';
const CURRENT_SCHEMA = 3;

const CLOUD_COVERAGE = [
    ['CLR', 0, 'Clear', 0],
    ['SKC', 9, 'Clear (Observed)', 1],
    ['FEW', 19, 'Few clouds', 2],
    ['SCT', 38, 'Scattered clouds', 3],
    ['BKN', 69, 'Broken clouds', 4],
    ['OVC', 94, 'Overcast', 5]
];

const COVERAGE_BY_CODE = Object.fromEntries(CLOUD_COVERAGE.map(([code, pct]) => [code, pct]));
const COVERAGE_CODES = CLOUD_COVERAGE.map(([code]) => code);
const COVERAGE_VALUES = CLOUD_COVERAGE.map(([, pct]) => pct);
const COVERAGE_DESCRIPTIONS = CLOUD_COVERAGE.map(([, , desc]) => desc);
const COVERAGE_SLIDER_BY_VALUE = Object.fromEntries(CLOUD_COVERAGE.map(([, pct, , slider]) => [pct, slider]));

function setCSSVars(degF, cloudCoverage) {
    // 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;

    var chroma = 85 - (cloudCoverage * 0.6);

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

async function updateWeatherDiv() {
    const weatherDiv = document.getElementById('weather');
    const tempSlider = document.getElementById("tempRange");
    const cloudSlider = document.getElementById("cloudRange");

    weatherDiv.innerHTML = 'Getting weather…';
    try {
        const weather = await loadWeather();
        
        setCSSVars(weather.temp, weather.cloudCoverage);
        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>`;
        }
        tempSlider.value = weather.temp;
        cloudRange.value = COVERAGE_SLIDER_BY_VALUE[weather.cloudCoverage];
    } 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.schema || w.schema < CURRENT_SCHEMA) {
        w = await fetchWeather();
        localStorage.setItem('weather', JSON.stringify(w));
    }
    return w;
}

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

function getMaxCloudCoverage(cloudLayers) {
    if (!cloudLayers || cloudLayers.length === 0) return 0;
    
    let maxCoverage = 0;
    for (const layer of cloudLayers) {
        const amount = layer.amount;
        if (amount && COVERAGE_BY_CODE[amount] !== undefined) {
            maxCoverage = Math.max(maxCoverage, COVERAGE_BY_CODE[amount]);
        }
    }
    return maxCoverage;
}

async function fetchWeather(retryCount = 0) {
    console.log('Updating weather data cache…');
    try {
        const response = await fetch(`https://api.weather.gov/stations/${nwsStationId}/observations/latest`);
        if(!response.ok) {
            const e = new Error(response.statusText);
            e.code = response.status;
            throw e; 
        }
        
        const weatherJson = await response.json();
        const weather = weatherJson.properties;
        const cloudCoverage = getMaxCloudCoverage(weather.cloudLayers);
        return { 
            "status": "success",
            "timestamp" : Date.now(),
            "temp" : Math.round(weather.temperature.value * 9/5 + 32),
            "conditions" : weather.textDescription || "Unknown conditions",
            "cloudCoverage": cloudCoverage,
            "schema": CURRENT_SCHEMA
        };
    } 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("tempRange").oninput = function() {
    const weatherDiv = document.getElementById('weather');
    const cloudSlider = document.getElementById('cloudRange');
    setCSSVars(this.value, COVERAGE_VALUES[cloudRange.value]);
    weatherDiv.innerHTML = "Let’s pretend it’s " + properTemp(this.value) + ", " + COVERAGE_DESCRIPTIONS[cloudRange.value];
}

document.getElementById("cloudRange").oninput = function() {
    const weatherDiv = document.getElementById('weather');
    const tempSlider = document.getElementById("tempRange");
    
    setCSSVars(tempSlider.value, COVERAGE_VALUES[this.value]);
    weatherDiv.innerHTML = "Let's pretend it's " + properTemp(tempSlider.value) + ", " + COVERAGE_DESCRIPTIONS[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 avgCloudCover = [68,65,60,55,50,40,31,31,45,55,65,68];
    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>`,
        "cloudCoverage": avgCloudCover[d.getMonth()]
    };
}

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;
    }
}