Skip to content

Instantly share code, notes, and snippets.

@MaZderMind
Created May 21, 2021 17:22
Show Gist options
  • Save MaZderMind/6c59cb2b53288a300a8ec74cb4ca36af to your computer and use it in GitHub Desktop.
Save MaZderMind/6c59cb2b53288a300a8ec74cb4ca36af to your computer and use it in GitHub Desktop.
Inzidenz + Impfprogress Deutschland – Widget für iOS Scriptable
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0
// Thanks to @rphl (https://github.com/rphl) and @tzschies (https://github.com/tzschies) for their inspiring work on this widget. See https://gist.github.com/rphl/0491c5f9cb345bf831248732374c4ef5 and https://gist.github.com/tzschies/563fab70b37609bc8f2f630d566bcbc9.
function round(n, d) {
const m = Math.pow(10, d);
return Math.round((n + Number.EPSILON) * m) / m;
}
function buildQuery(params) {
return Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
}
function getDateString(addDays) {
addDays = addDays || 0;
return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10)
}
class IncidenceWidget {
constructor() {
this.previousDaysToShow = 14;
this.apiUrlDistricts = (location) => 'https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?' + buildQuery({
'where': '1=1',
'outFields': 'RS,GEN,cases7_bl_per_100k,cases7_per_100k,BL',
'geometry': `${location.longitude.toFixed(3)},${location.latitude.toFixed(3)}`,
'geometryType': 'esriGeometryPoint',
'spatialRel': 'esriSpatialRelWithin',
'inSR': 4326,
'outSR': 4326,
'returnGeometry': false,
'f': 'json',
});
this.apiUrlDistrictsHistory = (districtId) => 'https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?' + buildQuery({
'outFields': 'Landkreis,Meldedatum,AnzahlFall',
'outSR': 4326,
'f': 'json',
'where': `IdLandkreis = '${districtId}' AND Meldedatum >= TIMESTAMP '${getDateString(-this.previousDaysToShow)} 00:00:00' AND Meldedatum <= TIMESTAMP '${getDateString(1)} 00:00:00'`,
});
this.incidenceUrlStates = () => 'https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Coronaf%E4lle_in_den_Bundesl%E4ndern/FeatureServer/0/query?'+buildQuery({
'where': '1=1',
'outFields': 'cases7_bl_per_100k',
'returnGeometry': false,
'outSR': 4326,
'f': 'json',
});
this.tsvUrlVaccination = () => 'https://impfdashboard.de/static/data/germany_vaccinations_timeseries_v2.tsv';
this.stateToAbbr = {
'Baden-Württemberg': 'BW',
'Bayern': 'BY',
'Berlin': 'BE',
'Brandenburg': 'BB',
'Bremen': 'HB',
'Hamburg': 'HH',
'Hessen': 'HE',
'Mecklenburg-Vorpommern': 'MV',
'Niedersachsen': 'NI',
'Nordrhein-Westfalen': 'NRW',
'Rheinland-Pfalz': 'RP',
'Saarland': 'SL',
'Sachsen': 'SN',
'Sachsen-Anhalt': 'ST',
'Schleswig-Holstein': 'SH',
'Thüringen': 'TH'
};
}
async run() {
let widget = await this.createWidget()
if (!config.runsInWidget) {
await widget.presentMedium()
}
Script.setWidget(widget)
Script.complete()
}
async createWidget(items) {
let data = await Promise.all([
this.getIncidenceData(),
this.getVaccinationData()
]);
const error = data[0].error || data[1].error;
// Basic widget setup
let list = new ListWidget()
list.setPadding(4, 4, 4, 4)
if(error) {
let textStack = list.addStack()
textStack.setPadding(14, 14, 14, 14)
textStack.layoutVertically()
textStack.topAlignContent()
// Error handling
let errorIndicator = textStack.addText(error.toUpperCase())
textStack.setPadding(14, 14, 14, 14)
errorIndicator.font = Font.mediumSystemFont(13)
errorIndicator.textOpacity = 0.8
errorIndicator.textColor = new Color("aa0000")
} else {
// Enable caching
if(data[0].shouldCache) {
list.refreshAfterDate = new Date(Date.now() + 60*60*1000)
}
this.addIncidenceStack(list, data[0]);
this.addVaccinationStack(list, data[1]);
list.addSpacer()
}
return list
}
addIncidenceStack(list, data) {
let textStack = list.addStack()
textStack.setPadding(14, 14, 0, 14)
textStack.layoutVertically()
textStack.topAlignContent()
// Header
let header = textStack.addText("🦠 Inzidenz".toUpperCase())
header.font = Font.mediumSystemFont(13)
textStack.addSpacer()
// Main stack for value and area name
let incidenceStack = textStack.addStack()
incidenceStack.layoutVertically()
let valueStack = incidenceStack.addStack()
let incidenceValueLabel = valueStack.addText(data.incidence + (data.trendIcon || ''))
incidenceValueLabel.font = Font.boldSystemFont(24)
incidenceValueLabel.textColor = data.incidence >= 100 ? new Color("9e000a") : data.incidence >= 50 ? Color.red() : data.incidence >= 35 ? Color.yellow() : Color.green();
let areaLabel = incidenceStack.addText(data.areaName)
areaLabel.font = Font.lightSystemFont(14)
incidenceStack.addSpacer()
if(data.incidenceBySide) {
// Chip for displaying state data
valueStack.addSpacer(4)
let stateStack = valueStack.addStack()
let stateText = stateStack.addText(data.incidenceBySide + "\n" + data.areaNameBySide)
stateStack.backgroundColor = new Color('888888', .5)
stateStack.cornerRadius = 4
stateStack.setPadding(2, 4, 2, 4)
stateText.font = Font.mediumSystemFont(9)
stateText.textColor = Color.white()
}
valueStack.addSpacer()
}
addVaccinationStack(list, data) {
let textStack = list.addStack()
//textStack.backgroundColor = new Color("300000")
textStack.setPadding(0, 14, 0, 14)
textStack.layoutVertically()
textStack.topAlignContent()
let header = textStack.addText("💉 Impfung".toUpperCase())
header.font = Font.mediumSystemFont(13)
let dataStack = textStack.addStack()
//dataStack.backgroundColor = new Color("003000")
let todayLabel = dataStack.addText(round(data.today * 100, 1) + "%")
todayLabel.font = Font.boldSystemFont(24)
todayLabel.textColor = Color.green();
dataStack.addSpacer(4)
let fmt = new DateFormatter()
fmt.dateFormat = 'E d.'
let yesterdayLabel = dataStack.addText(
"+" + round(data.delta * 100, 1) + "%\n" +
fmt.string(data.date)
)
yesterdayLabel.font = Font.lightSystemFont(14)
yesterdayLabel.textColor = new Color("aaaaaa")
}
async getIncidenceData() {
try {
let location = await this.getLocation()
if(location) {
let currentData = await new Request(this.apiUrlDistricts(location)).loadJSON()
let attr = currentData.features[0].attributes
let historicalData = await new Request(this.apiUrlDistrictsHistory(attr.RS)).loadJSON()
let aggregate = historicalData.features.map(f => f.attributes).reduce((dict, feature) => {
dict[feature["Meldedatum"]] = (dict[feature["Meldedatum"]]|0) + feature["AnzahlFall"];
return dict;
}, {});
let timeline = Object.keys(aggregate).sort().map(k => aggregate[k]);
let casesYesterday7 = timeline.slice(-8, -1).reduce(this.sum);
let casesToday7 = timeline.slice(-7).reduce(this.sum);
let trendIcon = (casesToday7 == casesYesterday7) ? '→' : (casesToday7 > casesYesterday7) ? '↑' : '↓';
return {
incidence: attr.cases7_per_100k.toFixed(0),
areaName: attr.GEN,
trendIcon: trendIcon,
incidenceBySide: attr.cases7_bl_per_100k.toFixed(0),
areaNameBySide: this.stateToAbbr[attr.BL],
shouldCache: true
};
}
else {
let data = await new Request(this.incidenceUrlStates()).loadJSON();
const incidencePerState = data.features.map(
(f) => f.attributes.cases7_bl_per_100k
);
const averageIncidence = incidencePerState.reduce((a, b) => a + b) / incidencePerState.length;
return {
incidence: averageIncidence.toFixed(1),
areaName: 'Deutschland',
shouldCache: false,
};
}
} catch(e) {
return { error: "Fehler beim Datenabruf." };
}
}
async getVaccinationData() {
let tsv = await new Request(this.tsvUrlVaccination()).loadString();
let lines = tsv.split("\n").filter(line => line.length > 0);
let headers = lines[0].split("\t");
let today = lines[lines.length - 1].split("\t");
let yesterday = lines[lines.length - 2].split("\t");
let quote_column = headers.indexOf('impf_quote_erst');
let date_column = headers.indexOf('date');
return {
date: new Date(today[date_column]),
today: today[quote_column],
yesterday: yesterday[quote_column],
delta: (today[quote_column] - yesterday[quote_column]),
};
}
async getLocation() {
try {
if(args.widgetParameter) {
let fixedCoordinates = args.widgetParameter.split(",").map(parseFloat)
return { latitude: fixedCoordinates[0], longitude: fixedCoordinates[1] }
} else {
Location.setAccuracyToThreeKilometers()
return await Location.current()
}
} catch(e) {
return null;
}
}
sum(a, b) {
return a + b;
}
}
await new IncidenceWidget().run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment