You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

364 lines
12 KiB
JavaScript

// 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];
// Event type
let event_type = walk_entry.event_type[window.current_local.substring(0,2)];
// --- add the class
$cal_entry.addClass("confirmed");
$cal_entry.find(".reservation .not-confirmed").remove();
// --- fill infos
$cal_entry.find(".event-type").html(event_type);
$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 = [event_type, phase_name, formated_date['date']].join(", ");
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();
});