// TODO: // * check what does really mean this illumination or the other one ! // like is it really how much the moon is lit ? // NOTE: // 1. the date and time DO coincide with // https://www.timeanddate.com/moon/phases/?year=2022 // to the minute ! // unlike the "self computed values" // (which can differ of 1 day ~ 12h and thus endup in the wrong daybox, even if we don't show the time value) // 2. iCalendar give date and time in CET/CEST // so we have to use luxon to parse the string as CET, and convert it to a proper date object // then we can print this object back according to where the walk happens // test zone for other date // first new moon of 2000 according to: // https://www.calendar-12.com/moon_phases/2000 // let now = new Date(Date.parse('06 Jan 2000 19:14:00 GMT+1')); // let now = new Date(Date.parse('13 Jan 2000 05:41:00 GMT+1')); // let now = new Date(Date.parse('21 Jan 2000 05:41:00 GMT+1')); let now = new Date(); let months_ahead = window.walk_calendar["months_ahead"]; let month_to_get = now; let queue = []; // UTILITIES // ========================================================================== function nextMonth(date, i=1){ // return the i-th next month let nextMonth = new Date(date); // make a copy of date object nextMonth.setMonth(nextMonth.getMonth() + i); // add i month return nextMonth; } function prevMonth(date, i=1){ // return the i-th next month let prevMonth = new Date(date); // make a copy of date object prevMonth.setMonth(prevMonth.getMonth() - i); // add i month return prevMonth; } function zeroPad(num, places) { var zero = places - num.toString().length + 1; return Array(+(zero > 0 && zero)).join("0") + num; } function translatePhaseDutch(phase_name){ if (phase_name == 'Full moon'){ return 'Volle maan'; } else if (phase_name == 'New Moon'){ return 'Nieuwe maan'; } } // FILL CALENDAR WITH DATA // ========================================================================== function addDateToCalendar(date, moon){ // given a moon state object from the API, // fill in the template and add it to the calendar // --- data we need from the yaml let lat = window.walk_calendar["latitude"]; let long = window.walk_calendar["longitude"]; let timezone = window.walk_calendar["timezone"]; let calendar = window.walk_calendar["calendar"]; let contact_mail = window.walk_calendar["reservation_mail"]; // --- converting the date to walk timezone date = date.setZone(timezone); // console.log(date.toString()); // --- clone template let template = $('#cal-entry__template').html(); let $cal_entry = $($.parseHTML(template)); // --- get suncalc moon object // for complementary info that are not in the moon API object, such as: // earth-moon distance, and illumination let moon_calc = getMoonStateFromDate(date, lat, long); let distance = moon_calc["distance"]; // let illumination = (moon_calc["illumination"]*100).toFixed(2); // --- get suncalc sun object let walk_sunset = getSunStateFromDate(date, lat, long, timezone)[1]; // --- formating the info let phase_name = moon.phaseName; let phase = moon.isPhaseLimit === 1 ? 0 : 0.5; // API is missing dutch phase name if (window.current_local == "nl-be"){ phase_name = translatePhaseDutch(phase_name); } // convert those two luxon dates in 'timezone' to human readable strings // whith 'timezone' indicated let walk_sunset_time = format_luxon_date(walk_sunset); let formated_date = format_luxon_date(date); // let time_id = date.toUnixInteger(); let time_id =formated_date['date'].split(".").join("-"); // --- filling the info $cal_entry.attr("id", time_id); // --- PHASENAME $cal_entry.find(".phase-name").html(phase_name); // --- EVENT $cal_entry.find(".day").html(formated_date['day']); $cal_entry.find(".date").html(formated_date['date']); // --- SECONDARY INFO --- $cal_entry.find(".sunset-time").html(walk_sunset_time['time']); $cal_entry.find(".moon-time").html(formated_date['time']); // $cal_entry.find(".cal-entry__distance span").html(distance.toFixed(0)); // --- updating the font updateAxis($cal_entry, phase); // --- adding walk calendar info if (calendar[time_id]){ let walk_entry = calendar[time_id]; // --- add the class $cal_entry.addClass("confirmed"); $cal_entry.find(".reservation .not-confirmed").remove(); // --- fill infos $cal_entry.find(".start-time").html(walk_entry.start_time); $cal_entry.find(".start-location a").html(walk_entry.start_location); $cal_entry.find(".start-location a").attr("href", walk_entry.start_location_link); // --- mail subject // only added if event has an entry in yaml AND not a past event let mail_subject = $cal_entry.find(".reservation a").attr("data-mail-subject"); mail_subject = mail_subject.replace("$", phase_name) + " " + formated_date['date']; let uri = "mailto:" + contact_mail + "?subject="; uri += encodeURIComponent(mail_subject); $cal_entry.find(".reservation a").attr("href", uri); } else{ $cal_entry.find(".cal-entry__description").remove(); $cal_entry.find(".reservation .signup").remove(); } // --- adding classes if(date < now){ $cal_entry.addClass("past"); $cal_entry.find(".reservation .signup").remove(); $cal_entry.find(".reservation .not-confirmed").remove(); } else{ $cal_entry.find(".reservation .past").remove(); } if(phase === 0){ $cal_entry.addClass("newmoon"); } else{ $cal_entry.addClass("fullmoon"); } // --- make it clickable $cal_entry.on( "click", function() { let yaml_entry = '#'+ phase_name + " walk on the " + formated_date['date'] + '\n' + time_id + ":"; navigator.clipboard.writeText(yaml_entry); }); // --- append to body $('#calendar-section .calendar-content').append($cal_entry); } function postProcessCalendar(){ // is called when calendar is entierly finished console.log("calendar processed"); $('.cal-entry.past').last().addClass('last-past'); $('.cal-loading').remove(); // --- add animation listener $('.cal-entry').bind("webkitAnimationEnd mozAnimationEnd animationend", function(){ $(this).removeClass("animated") }) $('.cal-entry').mouseenter(function(){ $(this).addClass("animated"); }) } function writeTimezone(){ let timezone = window.walk_calendar["timezone"]; let temp_date = luxon.DateTime.now().setZone(timezone); temp_date = temp_date.setLocale(window.current_local); let offset = luxon.DateTime.now().setZone(timezone).offsetNameShort; $('.date-time-info span').html(offset + " (" + timezone + ")"); } // PROCESSING API RESPONSE // ========================================================================== function processMonthResponse(response){ // process the API response // in this API the new/full moon event last exactly // on the day in which the precise event happens let month_config = response.config; let month_calendar = response.calendar; console.log("adding", month_config.month + " " + month_config.year); // iterate through every day for(let i = 1; i <= month_calendar.daysMonth; i++){ let moon = month_calendar.phase[i]; // if a new/full moon event happens that day // 2 and 4 are first/last quarter, // false are the in between waning/waxing days if (moon.isPhaseLimit == 1 || moon.isPhaseLimit == 3){ // create back a date object from i and month // to have easier control on it's display // + getting back the exact time of the event from the API // NOTE: in which UTC does icalc37 is giving us the calendar? // it seems it's given in CET : https://en.wikipedia.org/wiki/Central_European_Time // which oscillate between UTC+1 and UTC+2 according to winter/summer time // Note that: "When the time zone offset is absent, // date-only forms are interpreted as a UTC time and date-time forms are interpreted as local time." // so here it's working because MY local time is == to CET // but a computer in china would not parse the date correctly... // we can not precise CET and it's non-trivial to convert from: CET -> GMT -> USER TIME // because it has to take in account the rules of when CET = UTC+1 pr UTC+2 // https://stackoverflow.com/questions/20867562/create-a-date-object-with-cet-timezone // old code // let date_str = [month_config.month, i, month_config.year, moon.timeEvent].join(" "); // let date = new Date(date_str); // IANA for the CET date we got from the API: Europe/Brussels // IANA for the place where the walk happens: Europe/Brussels // those are conceptually independant !! // see luxon documentation: https://moment.github.io/luxon/#/zones let dateISO = [month_config.year, zeroPad(month_config.month,2), zeroPad(i,2)].join("-"); let timeISO = moon.timeEvent.split(":").map(x => zeroPad(x,2)).join(":"); let dateTimeISO = dateISO+"T"+timeISO; let date = luxon.DateTime.fromISO(dateTimeISO, { zone: "Europe/Brussels" }); // console.log(dateTimeISO); // console.log(date.zoneName); addDateToCalendar(date, moon); } } } // API CALLS & QUEUE HANDLING // ========================================================================== //based on http://www.wdisseny.com/lluna/?lang=en function requestMonthMoonPhases(obj, callback){ // write the request let gets=[]; for (let i in obj){ gets.push(i + "=" +encodeURIComponent(obj[i])); } gets.push("LDZ=" + new Date(obj.year,obj.month-1,1) / 1000); let xmlhttp = new XMLHttpRequest(); let url = "https://www.icalendar37.net/lunar/api/?" + gets.join("&"); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { queue.push({config: obj, calendar: JSON.parse(xmlhttp.responseText)}) callback(); } } xmlhttp.open("GET", url, true); xmlhttp.send(); } function processQueue() { // Sort the queue according to config month queue.sort(function(a, b) { if(a.config.year != b.config.year){ return a.config.year - b.config.year; } else{ return a.config.month - b.config.month; } }); // let current_queue = queue.map(x => x.config.month + " " + x.config.year); // console.log(current_queue); // Test if the queue is not empty // and if it's first element is the next one to add if ( queue.length > 0 && queue[0].config.month === month_to_get.getMonth() + 1 && queue[0].config.year === month_to_get.getFullYear()) { // add to calendar processMonthResponse(queue[0]); // Remove the first item from the queue as it's already processed queue.splice(0, 1); // update the next month to get month_to_get = nextMonth(month_to_get); months_ahead = months_ahead - 1; // Process the queue again processQueue(); } else if ( months_ahead == 0 ) { postProcessCalendar(); } } // MAIN // ========================================================================== function createCalendar(months_ahead){ let new_month = now; for (var i = 0; i < months_ahead; i++) { let lang = window.current_local.substring(0,2) // config to send to the API let config_month = { lang : lang, month : new_month.getMonth() + 1, year : new_month.getFullYear() } console.log("doing request", config_month.month + " " + config_month.year + " in " + lang); requestMonthMoonPhases(config_month, processQueue); // increase the month by 1 new_month = nextMonth(new_month); } } $(document).ready(function(){ createCalendar(months_ahead); writeTimezone(); });