User:Shisma/wikidata2ical.js
Jump to navigation
Jump to search
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()));
}
}
}
}
}
})();