User:Shisma/wikidata2ical.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* jshint esversion: 8 */

class iCal {
  constructor(uid) {
    this.UID = uid;
    const now = new Date();
    this.DTSTAMP = this.encodeDate(now);
    this.validProperties = [
    	'UID', 
    	'TZOFFSETFROM', 
    	'TZOFFSETTO', 
    	'DTSTAMP', 
    	'DTSTART', 
    	'DTEND', 
    	'SUMMARY', 
    	'DESCRIPTION',
    	'GEO',
    	'LOCATION', 
    	'URL'
    ];
    this.startTime = '000000';
    this.endTime = '000000';
  }
  encodeDate(unencoded) {
  	if (unencoded instanceof Date) {
  	  unencoded = unencoded.toISOString();
  	}
  	return unencoded.substr(0, 19).replace(/[\+\-\:Z]/g, '');
  }
  encodeTime(unencoded) {
  	return unencoded.replace(/\:/g, '');
  }
  setStartDate(date) {
  	this.startDate = this.encodeDate(date);
  }
  setEndDate(date) {
  	this.endDate = this.encodeDate(date);
  }
  setStartTime(time) {
  	this.startTime = this.encodeTime(time);
  }
  setEndTime(time) {
  	this.endTime = this.encodeTime(time);
  }
  makeEventDates() {
  	if (this.startDate) {
  		this.DTSTART = `${this.startDate.substr(0, 8)}T${this.startTime}`;
  	}
  	if (this.endDate) {
  		this.DTEND = `${this.endDate.substr(0, 8)}T${this.endTime}`;
  	}
  }
  set(key, value) {
  	if (this.validProperties.includes(key)) {
  		this[key] = value;	
  	}
  }
  toString() {
  	this.makeEventDates();
  	let lines = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//wikidata2ical', 'BEGIN:VEVENT'];
  	for (const key of this.validProperties) {
  		if (this.hasOwnProperty(key)) {
  			lines.push(`${key}:${this[key]}`)	
  		}
  	}
  	lines = lines.concat(['END:VEVENT', 'END:VCALENDAR'])
  	return lines.join("\n");
  }
}


(async function() {
	const filterValues = function (v) {
        return v.mainsnak.hasOwnProperty('datavalue') && ['normal', 'preferred'].includes(v.rank);
    };
    
    const formatDate = function(value) {
    	return value.mainsnak.datavalue.value.time.replace(/[\+\-\:Z]/g, '');
    } 
    
    const u_btoa = function u_btoa(str) {
		return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (_match, pl) {
			return String.fromCharCode('0x' + pl);
		}));
    };


	const getTimeZones = async function  () {
		const query = `
			SELECT ?t ?i WHERE {
			  ?ti wdt:P31 wd:Q17272482.
			  ?ti p:P2907 ?p.
			  ?p psv:P2907 ?psw.
			  ?psw wikibase:quantityAmount ?i.
			  ?psw wikibase:quantityUnit wd:Q25235.
			  BIND (REPLACE(STR(?ti), "http://www.wikidata.org/entity/Q", "") AS ?t)
			} ORDER BY (?i)
		`;
		let url = `https://query.wikidata.org/sparql?format=json&query=${encodeURIComponent(query)}`;
		try {
			const response = await fetch(url, {cache: "force-cache"});
			if (response.status !== 200) {
				throw 'Status Code: ' + response.status;
			}
		let json = JSON.parse(await response.text());
		if (json.results) {
			let output = {};
			for (const item of json.results.bindings) {
				const hInt = parseInt(item.i.value);
				const h = Math.abs(hInt);
				const prefix = hInt < 0 ? '-' : '';
				const clockHours = Math.floor(h).toString().padStart(2, '0');
				const clockMinutes = ((h - Math.floor(h)) * 60).toString().padStart(2, '0');
				output[`Q${item.t.value}`] = `${prefix}${clockHours}${clockMinutes}`
			}
			return output;
		} else {
			return json;
		}
		} catch(error) {
			throw ['Fetch Error :-S', error];
		}
	}

	const getTimeIndices = async function  () {
		const query = `
			SELECT ?t ?i WHERE {
			  ?ti wdt:P31 wd:Q1260524.
			  ?ti p:P4895 ?p.
			  ?p psv:P4895 ?psw.
			  ?psw wikibase:quantityAmount ?i.
			  ?psw wikibase:quantityUnit wd:Q7727.
			  BIND (REPLACE(STR(?ti), "http://www.wikidata.org/entity/Q", "") AS ?t)
			} ORDER BY (?i)
		`;
		let url = `https://query.wikidata.org/sparql?format=json&query=${encodeURIComponent(query)}`;
		try {
			const response = await fetch(url, {cache: "force-cache"});
			if (response.status !== 200) {
				throw 'Status Code: ' + response.status;
			}
		let json = JSON.parse(await response.text());
		if (json.results) {
			let output = {};
			for (const item of json.results.bindings) {
				const min = parseInt(item.i.value);
				const clockHours = Math.floor(min / 60).toString().padStart(2, '0');
				const clockMinutes = (min % 6).toString().padStart(2, '0');
				output[`Q${item.t.value}`] = `${clockHours}:${clockMinutes}:00`
			}
			return output;
		} else {
			return json;
		}
		} catch(error) {
			throw ['Fetch Error :-S', error];
		}
	}
	
	const generateCalendarLink = function calendarLink(title, ical) {
		var frag = document.createDocumentFragment();
		var link = document.createElement('a');
		link.setAttribute('href', `data:text/calendar;base64,${u_btoa(ical)}`);
		link.setAttribute('download', title);
		link.appendChild(document.createTextNode('🗓️'));
		frag.appendChild(document.createTextNode(' '));
		frag.appendChild(link);
		return frag;
	};
    
    const alt = document.querySelector('link[rel="alternate"][type="application/json"]');
    if(alt) {
		const ejson = await fetch(alt.href);
		const e = await ejson.json();
		const etitle = document.querySelector('#firstHeading .wikibase-title-label');
		if (!e?.entities?.[RLCONF.wgPageName]?.claims) {
			return;
		}
		claims = e.entities[RLCONF.wgPageName].claims;
		const indcies = await getTimeIndices();
		const timezones = await getTimeZones();
		if (claims.hasOwnProperty('P580')) {
			const edesc = document.querySelector('.wikibase-entitytermsview-heading-description ');
			const starts = claims.P580.filter(filterValues);
			const start = formatDate(starts[0]);
			let entityICal = new iCal(RLCONF.wgPageName);
			entityICal.setStartDate(start);
			let startTimeId = starts[0].qualifiers?.P4241?.[0].datavalue?.value?.id;
			if (startTimeId && indcies.hasOwnProperty(startTimeId)) {
				entityICal.setStartTime(indcies[startTimeId]);
			}
			let startTimeZoneId = starts[0].qualifiers?.P421?.[0].datavalue?.value?.id;
			if (startTimeZoneId && timezones.hasOwnProperty(startTimeZoneId)) {
				entityICal.set('TZOFFSETFROM', timezones[startTimeZoneId]);
			}


			if (claims.hasOwnProperty('P582')) {
				const ends = claims.P582.filter(filterValues);
				const end = formatDate(ends[0]);
				entityICal.setEndDate(end);
				let endTimeId = ends[0].qualifiers?.P4241?.[0].datavalue?.value?.id;
				if (endTimeId && indcies.hasOwnProperty(endTimeId)) {
					entityICal.setEndTime(indcies[endTimeId]);
				}
				let endTimeZoneId = starts[0].qualifiers?.P421[0].datavalue?.value?.id;
				if (endTimeZoneId && timezones.hasOwnProperty(endTimeZoneId)) {
					entityICal.set('TZOFFSETTO', timezones[endTimeZoneId]);
				}
			}
			let website = claims.hasOwnProperty('P856') ? claims.P856.filter(filterValues) : false;
			if (website) {
				entityICal.set('URL', website[0].mainsnak.datavalue.value);
			}
			
			let geoCoordinates = claims.hasOwnProperty('P625') ? claims.P625.filter(filterValues) : false;
			if (geoCoordinates) {
				let geoValue = geoCoordinates[0].mainsnak.datavalue.value;
				entityICal.set('GEO', `${geoValue.latitude};${geoValue.longitude}`);
			}

			console.debug(entityICal.toString());
			entityICal.set('SUMMARY', etitle.innerText);
			const titleLink = generateCalendarLink(etitle.innerText, entityICal.toString());
			etitle.parentElement.insertBefore(titleLink, etitle.parentElement.lastChild);
		}
		for (const prop in claims) {
			for (const statement of claims[prop]) {
				if (statement?.mainsnak?.datatype === 'time' && statement?.mainsnak?.hasOwnProperty('datavalue')) {
					let value = statement.mainsnak.datavalue.value;
					let qualifiers = statement?.qualifiers;
					let vid = statement.id;
					let ptitle = document.querySelector("#".concat(prop, " .wikibase-statementgroupview-property-label"));
					
					if (value.calendarmodel === "http://www.wikidata.org/entity/Q1985727") {
						let wrapper = document.getElementById(vid).querySelector('.wikibase-snakview-value');
						let qalifiers = document.getElementById(vid).querySelector('.wikibase-statementview-qualifiers');
						let time = value.time;
						
						let propICal = new iCal(statement.id);
						propICal.setStartDate(time);
						propICal.set('SUMMARY', `${etitle.innerText}${ptitle.innerText}`);
						propICal.set('URL', `${location.origin}${location.pathname}#${vid}`);
						if (qualifiers) {
							
							let startTimeId = qualifiers?.P4241?.[0].datavalue?.value?.id;
							if (startTimeId && indcies.hasOwnProperty(startTimeId)) {
								propICal.setStartTime(indcies[startTimeId]);
							}
							let startTimeZoneId = qualifiers?.P421?.[0].datavalue?.value?.id;
							if (startTimeZoneId && timezones.hasOwnProperty(startTimeZoneId)) {
								propICal.set('TZOFFSETFROM', timezones[startTimeZoneId]);
							}
						}
						
						wrapper.appendChild(generateCalendarLink(`${etitle.innerText}${ptitle.innerText}`, propICal.toString()));
					}
				}
			}
		}
    }
   
})();