Created
May 21, 2021 17:22
-
-
Save MaZderMind/6c59cb2b53288a300a8ec74cb4ca36af to your computer and use it in GitHub Desktop.
Inzidenz + Impfprogress Deutschland – Widget für iOS Scriptable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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