Skip to content

Instantly share code, notes, and snippets.

@richterd
Last active January 14, 2021 13:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save richterd/d9a961632dd0a258b13b72eebdd7626b to your computer and use it in GitHub Desktop.
Save richterd/d9a961632dd0a258b13b72eebdd7626b to your computer and use it in GitHub Desktop.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: magic;
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0
// Vaccine API by @_ThisIsBenny_
// Define URLs based on the corona.rki.de webpage
class CoronaWidget {
constructor() {
//API URLs
this.previousDaysToShow = 16;
this.newCasesApiUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?f=json&where=NeuerFall%20IN(1%2C%20-1)&returnGeometry=false&spatialRel=esriSpatialRelIntersects&outFields=*&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22AnzahlFall%22%2C%22outStatisticFieldName%22%3A%22value%22%7D%5D&resultType=standard&cacheHint=true`;
this.incidenceUrl = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,last_update,cases,cases7_bl_per_100k,cases7_per_100k&geometry=${location.longitude.toFixed(3)}%2C${location.latitude.toFixed(3)}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json`;
this.incidenceUrlStates = "https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Coronaf%E4lle_in_den_Bundesl%E4ndern/FeatureServer/0/query?where=1%3D1&outFields=cases7_bl_per_100k,Fallzahl&returnGeometry=false&outSR=4326&f=json";
this.apiUrlDistrictsHistory = (districtId) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27${districtId}%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27${this.getDateString(-this.previousDaysToShow)}%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27${this.getDateString(1)}%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json`
this.vaccineStatusUrl = "https://rki-vaccination-data.vercel.app/api";
}
async createWidget(items) {
const list = new ListWidget();
//const padding = 12;
//list.setPadding(padding, padding, padding, padding);
const location = await this.getLocation();
//Country Information
const newCasesData = await this.getNewCasesData();
const countryHeader = list.addText(`🦠 ${newCasesData.areaName}`);
countryHeader.font = Font.mediumSystemFont(10);
const countryLabel = list.addText(`+${newCasesData.value.toLocaleString()}`);
countryLabel.font = Font.mediumSystemFont(22);
list.addSpacer();
//City Information
const localData = await this.getLocalData(location);
const reproductionData = await this.getReproductionData(localData.cityCode);
const localHeader = list.addText(`🦠 ${localData.cityName}`);
localHeader.font = Font.mediumSystemFont(10);
const row = list.addStack();
row.centerAlignContent();
const incidenceCell = row.addText(`${localData.incidence.toLocaleString()} ${reproductionData.trend}`);
row.addSpacer();
const rString = parseFloat(reproductionData.r7.toFixed(2)).toLocaleString();
const rCell = row.addText(`R: ${rString}`);
incidenceCell.font = Font.mediumSystemFont(22);
rCell.font = Font.mediumSystemFont(10);
rCell.textColor = Color.gray();
if (localData.incidence >= 50) {
incidenceCell.textColor = Color.red();
} else if (localData.incidence >= 25) {
incidenceCell.textColor = Color.orange();
}
list.addSpacer();
//Immunity Information
const sumCases = await this.getInfectedData();
const sumCasesString = parseFloat((sumCases / 1000000).toFixed(2)).toLocaleString();
const sumCasesPercentageString = parseFloat((sumCases / 83020000 * 100).toFixed(2)).toLocaleString();
const casesLabel = list.addText(`😷 ${sumCasesString} Mio (${sumCasesPercentageString}%)`);
casesLabel.font = Font.mediumMonospacedSystemFont(10);
casesLabel.textColor = Color.gray();
//Vaccine Information
const vaccineData = await this.getVaccineData();
const amount = parseFloat((vaccineData.value / 1000000).toFixed(2)).toLocaleString();
const quote = vaccineData.quote.toLocaleString();
const vaccinatedLabel = list.addText(`💉 ${amount} Mio (${quote}%)`);
vaccinatedLabel.font = Font.mediumMonospacedSystemFont(10);
vaccinatedLabel.textColor = Color.gray();
list.addSpacer();
//Updated Time
const timeLabel = list.addText(`Letztes Update: ${localData.lastUpdated.split(",")[0]}`);
timeLabel.font = Font.mediumSystemFont(7);
timeLabel.textColor = Color.lightGray();
return list;
}
async getLocation() {
let location;
if (args.widgetParameter) {
const fixedCoordinates = args.widgetParameter.split(",").map(parseFloat);
location = {
latitude: fixedCoordinates[0],
longitude: fixedCoordinates[1]
}
} else {
Location.setAccuracyToThreeKilometers();
try {
location = await Location.current();
console.log('get current lat/lon');
this.saveIncidenceLatLon(location)
} catch (e) {
console.log('using saved lat/lon');
location = this.getSavedIncidenceLatLon();
}
}
return location;
}
async getInfectedData() {
const casesData = await new Request(this.incidenceUrlStates).loadJSON();
const casesPerState = casesData.features.map(
(f) => f.attributes.Fallzahl
);
const sumCases = casesPerState.reduce((a, b) => a + b);
return sumCases;
}
async getReproductionData(cityCode) {
const historicalData = await new Request(this.apiUrlDistrictsHistory(cityCode)).loadJSON();
const aggregate = historicalData.features.map(f => f.attributes).reduce((dict, feature) => {
dict[feature["Meldedatum"]] = (dict[feature["Meldedatum"]] | 0) + feature["AnzahlFall"];
return dict;
}, {});
const timeline = Object.keys(aggregate).sort().map(k => aggregate[k]);
const casesYesterday7 = timeline.slice(-8, -1).reduce((a, b) => a + b);
const casesToday7 = timeline.slice(-7).reduce((a, b) => a + b);
const casesWeekBefore7 = timeline.slice(-15, -8).reduce((a, b) => a + b);
const trend = (casesToday7 == casesYesterday7) ? '→' : (casesToday7 > casesYesterday7) ? '↑' : '↓';
//From: https://pavelmayer.de/covid/risks/
const rwk = (casesToday7 + 5) / (casesWeekBefore7 + 5);
const r7 = Math.pow(rwk, 4 / 7);
console.log(r7)
console.log(`Today: ${casesToday7}, Before: ${casesWeekBefore7}`);
return {
trend: trend,
r7: r7
};
}
async getLocalData(location) {
const data = await new Request(this.incidenceUrl(location)).loadJSON();
if (!data || !data.features || !data.features.length) {
const errorList = new ListWidget();
errorList.addText("Keine Ergebnisse für den aktuellen Ort gefunden.");
return errorList;
}
const attr = data.features[0].attributes;
const incidence = parseFloat(attr.cases7_per_100k.toFixed(1));
const cityName = attr.GEN;
const cityCode = attr.RS;
const cases = attr.cases;
const lastUpdated = attr.last_update;
return {
incidence: incidence,
cityName: cityName,
cityCode: cityCode,
cases: cases,
lastUpdated: lastUpdated
};
}
// Get vaccine Status
async getVaccineData() {
const data = await new Request(this.vaccineStatusUrl).loadJSON();
const attr = data.vaccinated;
const quote = data.quote;
return {
value: attr,
quote: quote,
};
}
async getNewCasesData() {
const data = await new Request(this.newCasesApiUrl).loadJSON();
const attr = data.features[0].attributes;
return {
value: parseFloat(attr.value),
areaName: "Deutschland",
shouldCache: false,
};
}
getDateString(addDays) {
addDays = addDays || 0;
return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10);
}
saveIncidenceLatLon(location) {
const fm = FileManager.iCloud();
const path = fm.joinPath(fm.documentsDirectory(), "covid19latlon.json");
fm.writeString(path, JSON.stringify(location));
}
getSavedIncidenceLatLon() {
const fm = FileManager.iCloud();
const path = fm.joinPath(fm.documentsDirectory(), "covid19latlon.json");
const data = fm.readString(path);
return JSON.parse(data);
}
}
//Create Widget
const widget = await new CoronaWidget().createWidget();
if (!config.runsInWidget) {
await widget.presentSmall();
}
//Set Widget
Script.setWidget(widget);
Script.complete();
@richterd
Copy link
Author

richterd commented Jan 10, 2021

Hey there 👋
the widget looks the following and imho shows the most relevant information for now. Thanks to all the people who did a great job in creating early versions. Feel free to use or share it.

72BB53F0-A561-459E-B539-917C42EBBD70

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment