layers_environment_NOAA_RealtimeWeather.js

import * as THREE from 'three';
import { CloudCoverageWorker } from './CloudCoverage.worker.js';
import { LinkedHashMap } from 'js-utils-z';

const cloudsWorkerUrl = URL.createObjectURL(new Blob([CloudCoverageWorker.getScript()], { type: 'application/javascript' }));
const cloudsWorker = new Worker(cloudsWorkerUrl);
cloudsWorker.onmessage = handleCloudWorkerResponse;
cloudsWorker.onerror = handleWorkerError;

let cloudsUniforms;
let cloudsTexture;


let localTimeUTC;
let loaded3hourBefore;
let loaded3hourAfter;

const cloudsBufferCache = new LinkedHashMap();
const cacheMaxSize = 10;
const requestedTimes = new Set();

/**
 * Sets the current time and adjusts the lerp uniform relative to the loaded forecast times
 */
function setCurrentTime(localUTC) {
    if(!!localUTC){
        localTimeUTC = localUTC;
    }
    if (!cloudsUniforms || !loaded3hourBefore || !loaded3hourAfter || !localTimeUTC) return;
    const startTime = loaded3hourBefore.date.getTime();
    const endTime = loaded3hourAfter.date.getTime();

    const t = localTimeUTC.getTime();

    if (t <= startTime) {
        cloudsUniforms.realTimeLerp.value = 0.0;
        return;
    }
    if (t >= endTime) {
        cloudsUniforms.realTimeLerp.value = 1.0;
        return;
    }

    cloudsUniforms.realTimeLerp.value = (t - startTime) / (endTime - startTime);
}

/**
 * Checks if, according to local time, there are better suited forecast times available than those currently loaded.
 * If so, loads the adequate forecasts and adjusts the lerp time uniform.
 */
function checkReloadTexture() {
    let nearest3hourBefore = toUTCAndRoundToNearest3HoursStrictlyBefore(localTimeUTC);
    let nearest3hourAfter = toUTCAndRoundToNearest3HoursStrictlyAfter(localTimeUTC);
    if (!loaded3hourBefore || !loaded3hourAfter ||
        nearest3hourBefore.isoStringYYMMDDHH !== loaded3hourBefore.isoStringYYMMDDHH ||
        nearest3hourAfter.isoStringYYMMDDHH !== loaded3hourAfter.isoStringYYMMDDHH) {

        if (!cloudsBufferCache.has(nearest3hourBefore.isoStringYYMMDDHH) || !cloudsBufferCache.has(nearest3hourAfter.isoStringYYMMDDHH)) {
            return; // unavailable textures
        }

        const texData = new Uint8Array(2 * 4 * 721 * 361);

        // fetch from cache and move to tail
        const before = cloudsBufferCache.remove(nearest3hourBefore.isoStringYYMMDDHH);
        cloudsBufferCache.put(nearest3hourBefore.isoStringYYMMDDHH, before);

        const after = cloudsBufferCache.remove(nearest3hourAfter.isoStringYYMMDDHH);
        cloudsBufferCache.put(nearest3hourAfter.isoStringYYMMDDHH, after);
        //self.cache.put(entry.key, entry.value);
        texData.set(before, 0);
        texData.set(after, 4 * 721 * 361);
        if (!cloudsTexture) {
            cloudsTexture = new THREE.Data3DTexture(texData, 721, 361, 2);
            cloudsTexture.format = THREE.RGBAFormat;
            cloudsTexture.type = THREE.UnsignedByteType;
            cloudsTexture.minFilter = THREE.LinearFilter;
            cloudsTexture.magFilter = THREE.LinearFilter;
            cloudsTexture.wrapS = THREE.RepeatWrapping;
            cloudsTexture.wrapT = THREE.RepeatWrapping;
            cloudsTexture.wrapR = THREE.RepeatWrapping;
            cloudsTexture.unpackAlignment = 1;
            cloudsTexture.needsUpdate = true;

            cloudsUniforms.realTimeCoverage.value = cloudsTexture;
        } else {
            cloudsTexture.image.data.set(texData);
            cloudsTexture.needsUpdate = true;
        }

        loaded3hourBefore = nearest3hourBefore;
        loaded3hourAfter = nearest3hourAfter;
        setCurrentTime();
    }

}


function handleWorkerError() {
    console.log("no weather information for requested time");
}

function handleCloudWorkerResponse(e) {
    cloudsBufferCache.put(e.data.dateBefore,new Uint8Array(e.data.cloudsArrayBuffer1));
    cloudsBufferCache.put(e.data.dateAfter,new Uint8Array(e.data.cloudsArrayBuffer2));
    while(cloudsBufferCache.size()>cacheMaxSize){
        const key = cloudsBufferCache.head().key;
        cloudsBufferCache.remove(key);
        requestedTimes.delete(key);
    }
    checkReloadTexture();

}


async function realtimeWeather(aCloudsUniforms, ultraClock) {
    let mostRecentReportTime = await getNearestAvailableNOAAReportDate();

    cloudsUniforms = aCloudsUniforms;


    let time;

    function redraw(date){
        date = new Date(date.toUTCString());
        let reportTime = mostRecentReportTime;
        if (date < mostRecentReportTime.date) {
            reportTime = toUTCAndRoundToNearest6HoursStrictlyBefore(date);
        }


        setCurrentTime(date);
        let nearest3hourBefore = toUTCAndRoundToNearest3HoursStrictlyBefore(date);
        let nearest3hourAfter = toUTCAndRoundToNearest3HoursStrictlyAfter(date);

        if (cloudsBufferCache.has(nearest3hourBefore.isoStringYYMMDDHH) && cloudsBufferCache.has(nearest3hourAfter.isoStringYYMMDDHH)) {
            checkReloadTexture(localTimeUTC, nearest3hourBefore, nearest3hourAfter);
            return;
        }

        if (requestedTimes.has(nearest3hourBefore.isoStringYYMMDDHH) && requestedTimes.has(nearest3hourAfter.isoStringYYMMDDHH)) {
            return; // the time has already been requested so it hasn't finished loading yet, we just need to wait.. 
        }

        let time1 = nearest3hourBefore;
        let time2 = nearest3hourAfter;
        if(requestedTimes.has(time1.isoStringYYMMDDHH)){
            time1 = time2;
            time2 = toUTCAndRoundToNearest3HoursStrictlyAfter(time2.date);
        }
        
        requestedTimes.add(time1.isoStringYYMMDDHH);
        requestedTimes.add(time2.isoStringYYMMDDHH);

        cloudsWorker.postMessage({
            dateBefore: time1.isoStringYYMMDDHH,
            dateAfter: time2.isoStringYYMMDDHH,
            reportTime: reportTime,
            beforeIndex: calculate3HourIncrementsDifference(reportTime.date, time1.date),
            afterIndex: calculate3HourIncrementsDifference(reportTime.date, time2.date),
        });
    }

    ultraClock.addListener(redraw);
    redraw(ultraClock.getDate());
}

async function getNearestAvailableNOAAReportDate() {
    let date = new Date();
    let timeNearest;
    let response;
    let i = 0;
    do {
        i++;
        timeNearest = toUTCAndRoundToNearest6HoursStrictlyBefore(date);
        let url = `https://europe-west1-jdultra.cloudfunctions.net/corsproxy?https://nomads.ncep.noaa.gov/dods/gfs_0p50/gfs${timeNearest.isoStringYYMMDD}/gfs_0p50_${String(timeNearest.nearest6HourIncrementHour).padStart(2, '0')}z.info`;
        response = await fetch(url);
        let responseData = await response.text();
        if (!response.ok || responseData.includes('is not an available dataset')) {
            date.setHours(date.getHours() - 6);
        } else {
            break;
        }
    } while (i < 10);

    if (!response.ok) throw "NOAA unavailable";

    return timeNearest;
}
function toUTCAndRoundToNearest6HoursStrictlyBefore(date) {
    // Convert local date to UTC
    const dateInUTC = new Date(date.toUTCString());

    // Milliseconds for calculation
    const sixHoursInMilliseconds = 6 * 60 * 60 * 1000;

    // Calculate the difference in milliseconds from the nearest lower 6-hour mark
    const remainder = dateInUTC.getTime() % sixHoursInMilliseconds;

    // If the date is exactly on a 6-hour mark, subtract 6 hours to ensure it's strictly before
    const adjustment = remainder === 0 ? sixHoursInMilliseconds : 0;

    // Round down to the nearest x-hour increment in UTC and adjust if necessary
    const roundedTime = dateInUTC.getTime() - remainder - adjustment;
    const roundedDate = new Date(roundedTime);

    // Format to 'YYMMDD'
    const year = String(roundedDate.getUTCFullYear()).slice(-4); // Get last 4 digits of the year
    const month = String(roundedDate.getUTCMonth() + 1).padStart(2, '0');
    const day = String(roundedDate.getUTCDate()).padStart(2, '0');

    const isoStringYYMMDD = `${year}${month}${day}`;

    // Calculate the nearest x-hour increment hour (0, x, 12, 18) before the current hour
    const hour = roundedDate.getUTCHours();

    return {
        isoStringYYMMDD,
        nearest6HourIncrementHour: hour,
        date: roundedDate
    };
}

function toUTCAndRoundToNearest3HoursStrictlyBefore(date) {
    // Convert local date to UTC
    const dateInUTC = new Date(date.toUTCString());

    // Milliseconds for calculation
    const threeHoursInMilliseconds = 3 * 60 * 60 * 1000;

    // Calculate the difference in milliseconds from the nearest lower 6-hour mark
    const remainder = dateInUTC.getTime() % threeHoursInMilliseconds;

    // If the date is exactly on a 6-hour mark, subtract 6 hours to ensure it's strictly before
    const adjustment = remainder === 0 ? threeHoursInMilliseconds : 0;

    // Round down to the nearest x-hour increment in UTC and adjust if necessary
    const roundedTime = dateInUTC.getTime() - remainder - adjustment;
    const roundedDate = new Date(roundedTime);

    // Format to 'YYMMDD'
    const year = String(roundedDate.getUTCFullYear()).slice(-4); // Get last 4 digits of the year
    const month = String(roundedDate.getUTCMonth() + 1).padStart(2, '0');
    const day = String(roundedDate.getUTCDate()).padStart(2, '0');
    const hour = String(roundedDate.getUTCHours()).padStart(2, '0');

    const isoStringYYMMDD = `${year}${month}${day}`;
    const isoStringYYMMDDHH = `${year}${month}${day}${hour}`;

    // Calculate the nearest x-hour increment hour (0, x, 12, 18) before the current hour
    

    return {
        isoStringYYMMDD,
        isoStringYYMMDDHH,
        nearest3HourIncrementHour: roundedDate.getUTCHours(),
        date: roundedDate
    };
}

function toUTCAndRoundToNearest3HoursStrictlyAfter(date) {
    // Convert local date to UTC
    const dateInUTC = new Date(date.toUTCString());

    // Milliseconds for calculation
    const threeHoursInMilliseconds = 3 * 60 * 60 * 1000;

    // Calculate the difference in milliseconds from the nearest lower 6-hour mark
    const remainder = dateInUTC.getTime() % threeHoursInMilliseconds;

    // If the date is exactly on a 6-hour mark, subtract 6 hours to ensure it's strictly before
    const adjustment = remainder === 0 ? threeHoursInMilliseconds : threeHoursInMilliseconds - remainder;

    // Round down to the nearest x-hour increment in UTC and adjust if necessary
    const roundedTime = dateInUTC.getTime() + adjustment;
    const roundedDate = new Date(roundedTime);

    // Format to 'YYMMDD'
    const year = String(roundedDate.getUTCFullYear()).slice(-4); // Get last 4 digits of the year
    const month = String(roundedDate.getUTCMonth() + 1).padStart(2, '0');
    const day = String(roundedDate.getUTCDate()).padStart(2, '0');
    const hour = String(roundedDate.getUTCHours()).padStart(2, '0');

    const isoStringYYMMDD = `${year}${month}${day}`;
    const isoStringYYMMDDHH = `${year}${month}${day}${hour}`;

    // Calculate the nearest x-hour increment hour (0, x, 12, 18) before the current hour
    
    return {
        isoStringYYMMDD,
        isoStringYYMMDDHH,
        nearest3HourIncrementHour: roundedDate.getUTCHours(),
        date: roundedDate
    };
}

function calculate3HourIncrementsDifference(date1, date2) {
    // Ensure date1 is the earlier and date2 is the later date
    const startTime = Math.min(date1.getTime(), date2.getTime());
    const endTime = Math.max(date1.getTime(), date2.getTime());

    // Calculate the difference in milliseconds
    const differenceInMilliseconds = endTime - startTime;

    // Convert milliseconds to hours
    const differenceInHours = differenceInMilliseconds / (1000 * 60 * 60);

    // Calculate the number of 3-hour increments
    const increments = differenceInHours / 3;

    // Return the number of 3-hour increments, rounded down since partial increments are not counted fully
    return Math.floor(increments);
}

/* function toUTC(date) {
    // Convert local date to UTC
    const dateInUTC = new Date(date.toUTCString());

    // Format to 'YYMMDD'
    const year = String(dateInUTC.getUTCFullYear()).slice(-4); // Get last 4 digits of the year
    const month = String(dateInUTC.getUTCMonth() + 1).padStart(2, '0');
    const day = String(dateInUTC.getUTCDate()).padStart(2, '0');
    const hour = String(dateInUTC.getUTCHours()).padStart(2, '0');

    const isoStringYYMMDD = `${year}${month}${day}`;

    // Calculate the nearest x-hour increment hour (0, x, 12, 18) before the current hour
    const hour = dateInUTC.getUTCHours();

    return {
        isoStringYYMMDD,
        nearest6HourIncrementHour: hour,
        date: dateInUTC
    };
} */

export { realtimeWeather }