|
// Change the following constants to your country of choice |
|
const population = 5837213; |
|
const herdImmunityPercentage = 70; |
|
|
|
// Go to https://github.com/owid/covid-19-data/tree/master/public/data/vaccinations/country_data and find the raw URL for your country |
|
const dataUrl = "https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/vaccinations/country_data/Denmark.csv"; |
|
|
|
// ------------------------- |
|
// With caching and fallback |
|
const cacheMinutes = 60; // 60 min |
|
const today = new Date(); |
|
let result; |
|
let width = 100; |
|
const h = 5; |
|
const neededTotalVaccinations = population * 2 * (herdImmunityPercentage / 100); |
|
|
|
let widget = new ListWidget(); |
|
widget.setPadding(8, 10, 0, 10); |
|
|
|
await getNumbers(); |
|
await createWidget(); |
|
Script.setWidget(widget); |
|
Script.complete(); |
|
|
|
if (config.runsInApp) { |
|
widget.presentSmall(); |
|
} |
|
|
|
async function createWidget() { |
|
|
|
widget.url = result.report_url; |
|
|
|
const upperStack = widget.addStack(); |
|
upperStack.layoutHorizontally(); |
|
|
|
const upperTextStack = upperStack.addStack(); |
|
upperTextStack.layoutVertically(); |
|
let staticText1 = upperTextStack.addText("Vaccinations"); |
|
staticText1.font = Font.semiboldRoundedSystemFont(11); |
|
let staticText2 = upperTextStack.addText("Given:"); |
|
staticText2.font = Font.semiboldRoundedSystemFont(11); |
|
|
|
upperStack.addSpacer(); |
|
|
|
let logoImage = upperStack.addImage(await getImage("vac-logo.png")); |
|
logoImage.imageSize = new Size(30, 30); |
|
|
|
widget.addSpacer(4); |
|
|
|
const amountPerCent = round( |
|
100 / neededTotalVaccinations * result.cumsum_latest, |
|
1 |
|
); |
|
let amountText = widget.addText( |
|
result.cumsum_latest.toLocaleString() + " (" + amountPerCent.toLocaleString() + "%)" |
|
); |
|
amountText.font = Font.boldSystemFont(13); |
|
amountText.textColor = new Color("#00a86b"); |
|
amountText.minimumScaleFactor = 0.8 |
|
|
|
let description3 = widget.addText("(7D. Ø: " + calculateDailyVac().toLocaleString() + ")"); |
|
description3.font = Font.mediumSystemFont(9); |
|
|
|
widget.addSpacer(4); |
|
|
|
let progressStack = widget.addStack(); |
|
progressStack.layoutVertically(); |
|
let progressNumberStack = widget.addStack(); |
|
progressNumberStack.layoutHorizontally(); |
|
const progressText0 = progressNumberStack.addText("0%"); |
|
progressText0.font = Font.mediumSystemFont(8); |
|
progressNumberStack.addSpacer(); |
|
const progressTextEnd = progressNumberStack.addText(herdImmunityPercentage + "%"); |
|
progressTextEnd.font = Font.mediumSystemFont(8); |
|
progressStack.addImage(createProgress(result.cumsum_latest)); |
|
|
|
widget.addSpacer(7); |
|
|
|
let calendarStack = widget.addStack(); |
|
const calendarImage = calendarStack.addImage(await getImage("calendar-icon.png")); |
|
calendarImage.imageSize = new Size(26, 26); |
|
calendarStack.addSpacer(6); |
|
let calendarTextStack = calendarStack.addStack(); |
|
calendarTextStack.layoutVertically(); |
|
calendarTextStack.addSpacer(0); |
|
// calculate date |
|
var estimatedDate = new Date(); |
|
estimatedDate.setDate(new Date().getDate() + calculateRemainingDays()); |
|
let description = calendarTextStack.addText("Herd Immunity:"); |
|
description.font = Font.mediumSystemFont(10); |
|
const description2 = calendarTextStack.addText( |
|
estimatedDate.toLocaleDateString() |
|
); |
|
description2.font = Font.boldSystemFont(10); |
|
|
|
widget.addSpacer(4) |
|
|
|
const lastUpdateDate = new Date(result.date); |
|
let lastUpdatedText = widget.addText( |
|
"Updated: " + lastUpdateDate.toLocaleDateString() |
|
); |
|
lastUpdatedText.font = Font.mediumMonospacedSystemFont(8); |
|
lastUpdatedText.textOpacity = 0.7; |
|
lastUpdatedText.centerAlignText() |
|
} |
|
|
|
// get images from iCloud or download them once |
|
async function getImage(image) { |
|
let fm = FileManager.local(); |
|
let dir = fm.documentsDirectory(); |
|
let path = fm.joinPath(dir, image); |
|
if (fm.fileExists(path)) { |
|
return fm.readImage(path); |
|
} else { |
|
// download once |
|
let imageUrl; |
|
switch (image) { |
|
case "vac-logo.png": |
|
imageUrl = "https://i.imgur.com/ZsBNT8E.png"; |
|
break; |
|
case "calendar-icon.png": |
|
imageUrl = "https://i.imgur.com/Qp8CEFf.png"; |
|
break; |
|
default: |
|
console.log(`Sorry, couldn't find ${image}.`); |
|
} |
|
let req = new Request(imageUrl); |
|
let loadedImage = await req.loadImage(); |
|
fm.writeImage(path, loadedImage); |
|
return loadedImage; |
|
} |
|
} |
|
|
|
async function getNumbers() { |
|
// Set up the file manager. |
|
const files = FileManager.local(); |
|
|
|
// Set up cache |
|
const cachePath = files.joinPath( |
|
files.cacheDirectory(), |
|
"api-cache-covid-vaccine-numbers-mopo" |
|
); |
|
const cacheExists = files.fileExists(cachePath); |
|
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0; |
|
|
|
// Get Data |
|
try { |
|
// If cache exists and it's been less than 60 minutes since last request, use cached data. |
|
if ( |
|
cacheExists && |
|
today.getTime() - cacheDate.getTime() < cacheMinutes * 60 * 1000 |
|
) { |
|
console.log("Get from Cache"); |
|
result = JSON.parse(files.readString(cachePath)); |
|
} else { |
|
console.log("Get from API"); |
|
const req2 = new Request( |
|
dataUrl |
|
); |
|
const csvString = await req2.loadString(); |
|
|
|
const csvArray = CSVToArray(csvString, ","); |
|
|
|
// Find the last line that has content |
|
var latestLineElements = new Array(); |
|
while (latestLineElements.length <= 1) { |
|
latestLineElements = csvArray.pop() |
|
}; |
|
|
|
// Find older entry |
|
const olderLineElements = csvArray.length >= 7 ? csvArray[csvArray.length - 7] : csvArray[0]; |
|
|
|
result = { |
|
date: latestLineElements[1], |
|
cumsum_latest: parseInt(latestLineElements[4]), |
|
cumsum_7_days_ago: parseInt(olderLineElements[4]), |
|
report_url: latestLineElements[3] |
|
}; |
|
|
|
console.log("Write Data to Cache"); |
|
try { |
|
files.writeString(cachePath, JSON.stringify(result)); |
|
} catch (e) { |
|
console.log("Creating Cache failed!"); |
|
console.log(e); |
|
} |
|
} |
|
} catch (e) { |
|
console.error(e); |
|
if (cacheExists) { |
|
console.log("Get from Cache"); |
|
result = JSON.parse(files.readString(cachePath)); |
|
} else { |
|
console.log("No fallback to cache possible. Due to missing cache."); |
|
} |
|
} |
|
} |
|
|
|
function createProgress(currentVacNo) { |
|
const context = new DrawContext(); |
|
context.size = new Size(width, h); |
|
context.opaque = false; |
|
context.respectScreenScale = true; |
|
context.setFillColor(new Color("#d2d2d7")); |
|
const path = new Path(); |
|
path.addRoundedRect(new Rect(0, 0, width, h), 3, 2); |
|
context.addPath(path); |
|
context.fillPath(); |
|
context.setFillColor(new Color("#00a86b")); |
|
const path1 = new Path(); |
|
const path1width = |
|
(width * currentVacNo) / neededTotalVaccinations > width |
|
? width |
|
: (width * currentVacNo) / neededTotalVaccinations; |
|
path1.addRoundedRect(new Rect(0, 0, path1width, h), 3, 2); |
|
context.addPath(path1); |
|
context.fillPath(); |
|
return context.getImage(); |
|
} |
|
|
|
function calculateDailyVac() { |
|
const latestVacAmount = result.cumsum_latest; |
|
const vacAmount7DaysAgo = result.cumsum_7_days_ago; |
|
const dailyVacAmount = Math.round((latestVacAmount - vacAmount7DaysAgo) / 7); |
|
return dailyVacAmount; |
|
} |
|
|
|
function calculateRemainingDays() { |
|
const daysRemaining = Math.round( |
|
(neededTotalVaccinations - result.cumsum_latest) / calculateDailyVac() |
|
); |
|
return daysRemaining; |
|
} |
|
|
|
function round(value, decimals) { |
|
return Number(Math.round(value + "e" + decimals) + "e-" + decimals); |
|
} |
|
|
|
function CSVToArray(strData, strDelimiter) { |
|
// Check to see if the delimiter is defined. If not, |
|
// then default to comma. |
|
strDelimiter = (strDelimiter || ","); |
|
|
|
// Create a regular expression to parse the CSV values. |
|
var objPattern = new RegExp( |
|
( |
|
// Delimiters. |
|
"(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + |
|
|
|
// Quoted fields. |
|
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + |
|
|
|
// Standard fields. |
|
"([^\"\\" + strDelimiter + "\\r\\n]*))" |
|
), |
|
"gi" |
|
); |
|
|
|
|
|
// Create an array to hold our data. Give the array |
|
// a default empty first row. |
|
var arrData = [[]]; |
|
|
|
// Create an array to hold our individual pattern |
|
// matching groups. |
|
var arrMatches = null; |
|
|
|
|
|
// Keep looping over the regular expression matches |
|
// until we can no longer find a match. |
|
while (arrMatches = objPattern.exec(strData)) { |
|
|
|
// Get the delimiter that was found. |
|
var strMatchedDelimiter = arrMatches[1]; |
|
|
|
// Check to see if the given delimiter has a length |
|
// (is not the start of string) and if it matches |
|
// field delimiter. If id does not, then we know |
|
// that this delimiter is a row delimiter. |
|
if ( |
|
strMatchedDelimiter.length && |
|
strMatchedDelimiter !== strDelimiter |
|
) { |
|
|
|
// Since we have reached a new row of data, |
|
// add an empty row to our data array. |
|
arrData.push([]); |
|
} |
|
|
|
var strMatchedValue; |
|
|
|
// Now that we have our delimiter out of the way, |
|
// let's check to see which kind of value we |
|
// captured (quoted or unquoted). |
|
if (arrMatches[2]) { |
|
|
|
// We found a quoted value. When we capture |
|
// this value, unescape any double quotes. |
|
strMatchedValue = arrMatches[2].replace( |
|
new RegExp("\"\"", "g"), |
|
"\"" |
|
); |
|
|
|
} else { |
|
// We found a non-quoted value. |
|
strMatchedValue = arrMatches[3]; |
|
} |
|
|
|
// Now that we have our value string, let's add |
|
// it to the data array. |
|
arrData[arrData.length - 1].push(strMatchedValue); |
|
} |
|
|
|
// Return the parsed data. |
|
return (arrData); |
|
} |
The data got malformed and there’s a 60- minute cache in the script that held onto the malformed data. Could you try again now?