-
-
Save aploe/6644b61953c3f42e7369c2de21e1b87d to your computer and use it in GitHub Desktop.
COVID-19 Inzidenz-Widget für iOS innerhalb Deutschlands - Erweitert um Neuinfektionen 🇩🇪
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. | |
// aploe: Thanks to Kevin Kub (https://gist.github.com/kevinkub) | |
class IncidenceWidget { | |
constructor() { | |
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.previousDaysToShow = 31; | |
this.apiUrlDistricts = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_bl_per_100k,cases7_per_100k,BL&geometry=${location.longitude.toFixed(3)}%2C${location.latitude.toFixed(3)}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&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.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.presentSmall() | |
} | |
Script.setWidget(widget) | |
Script.complete() | |
} | |
async createWidget(items) { | |
// Inzidenz data | |
let data = await this.getData() | |
// New Cases data | |
let data2 = await this.getNewCasesData() | |
// Basic widget setup | |
let list = new ListWidget() | |
list.setPadding(0, 0, 0, 0) | |
let textStack = list.addStack() | |
textStack.setPadding(8, 14, 0, 14) | |
textStack.layoutVertically() | |
textStack.topAlignContent() | |
// Header "Inzidenz" | |
let header = textStack.addText("🦠 Inzidenz".toUpperCase()) | |
header.font = Font.mediumSystemFont(12) | |
textStack.addSpacer(4) | |
if(data.error || data2.error) { | |
// Error handling | |
let loadingIndicator = textStack.addText(data.error.toUpperCase()) | |
textStack.setPadding(14, 14, 14, 14) | |
loadingIndicator.font = Font.mediumSystemFont(12) | |
loadingIndicator.textOpacity = 0.5 | |
let spacer = textStack.addStack() | |
spacer.addSpacer(); | |
} else { | |
// Enable caching | |
list.refreshAfterDate = new Date(Date.now() + 60*60*1000) | |
// Main stack for value and area name | |
let incidenceStack = textStack.addStack() | |
let valueStack = incidenceStack.addStack() | |
incidenceStack.layoutVertically() | |
let incidenceValueLabel = valueStack.addText(data.incidence + data.trend) | |
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(); | |
// Chip for displaying state data | |
valueStack.addSpacer(5) | |
let stateStack = valueStack.addStack() | |
stateStack.backgroundColor = new Color('888888', .5) | |
stateStack.cornerRadius = 4 | |
stateStack.setPadding(2, 4, 2, 4) | |
let stateStackInner = stateStack.addStack() | |
stateStackInner.layoutVertically() | |
let stateText = stateStackInner.addText(data.incidenceBySide) | |
stateText.font = Font.mediumSystemFont(9) | |
stateText.textColor = Color.white() | |
let stateText2 = stateStackInner.addText(data.areaNameBySide) | |
stateText2.font = Font.mediumSystemFont(9) | |
stateText2.textColor = Color.white() | |
// Label Area | |
let incidenceValueArea = incidenceStack.addText(data.areaName) | |
incidenceValueArea.font = Font.mediumSystemFont(12) | |
incidenceValueArea.textColor = Color.gray() | |
// Header "Neuinfektionen" | |
textStack.addSpacer(8) | |
let header = textStack.addText("🦠 Neuinfektionen".toUpperCase()) | |
header.font = Font.mediumSystemFont(12) | |
textStack.addSpacer(1) | |
// New infections stack | |
let newinfStack = textStack.addStack() | |
newinfStack.bottomAlignContent() | |
newinfStack.addSpacer(5) | |
let newinfValue = newinfStack.addText("+" + parseInt(data2.value).toLocaleString()) | |
newinfValue.font = Font.boldSystemFont(20) | |
let newinfValueStack = newinfStack.addStack() | |
let newinfValueLabel = newinfValueStack.addText(" " + data2.areaName) | |
newinfValueLabel.font = Font.mediumSystemFont(13) | |
newinfValueLabel.textColor = Color.gray() | |
newinfValueStack.addSpacer(5) | |
// Chart | |
let chart = new LineChart(400, 100, data.timeline).configure((ctx, path) => { | |
ctx.opaque = false; | |
ctx.setFillColor(new Color("888888", .25)); | |
ctx.addPath(path); | |
ctx.fillPath(path); | |
}).getImage(); | |
let chartStack = list.addStack() | |
chartStack.setPadding(0, 0, 0, 0) | |
let img = chartStack.addImage(chart) | |
img.applyFittingContentMode() | |
} | |
return list | |
} | |
async getNewCasesData() { | |
const data = await new Request(this.newCasesApiUrl).loadJSON(); | |
const attr = data.features[0].attributes; | |
return { | |
value: attr.value, | |
areaName: "DE", | |
}; | |
} | |
async getData() { | |
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 trend = (casesToday7 == casesYesterday7) ? '→' : (casesToday7 > casesYesterday7) ? '↑' : '↓'; | |
return { | |
incidence: attr.cases7_per_100k.toFixed(0), | |
areaName: attr.GEN, | |
trend: trend, | |
incidenceBySide: | |
attr.cases7_bl_per_100k.toFixed(0), | |
areaNameBySide: | |
this.stateToAbbr[attr.BL], | |
timeline: timeline | |
}; | |
} | |
return { error: "Standort nicht verfügbar." } | |
} catch(e) { | |
return { error: "Fehler bei Datenabruf." }; | |
} | |
} | |
getDateString(addDays) { | |
addDays = addDays || 0; | |
return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10) | |
} | |
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; | |
} | |
} | |
class LineChart { | |
constructor(width, height, values) { | |
this.ctx = new DrawContext() | |
this.ctx.size = new Size(width, height) | |
this.values = values; | |
} | |
_calculatePath() { | |
let maxValue = Math.max(...this.values); | |
let minValue = Math.min(...this.values); | |
let difference = maxValue - minValue; | |
let count = this.values.length; | |
let step = this.ctx.size.width / (count - 1); | |
let points = this.values.map((current, index, all) => { | |
let x = step*index | |
let y = this.ctx.size.height - (current - minValue) / difference * this.ctx.size.height; | |
return new Point(x, y) | |
}); | |
return this._getSmoothPath(points); | |
} | |
_getSmoothPath(points) { | |
let path = new Path() | |
path.move(new Point(0, this.ctx.size.height)); | |
path.addLine(points[0]); | |
for(var i = 0; i < points.length-1; i ++) { | |
let xAvg = (points[i].x + points[i+1].x) / 2; | |
let yAvg = (points[i].y + points[i+1].y) / 2; | |
let avg = new Point(xAvg, yAvg); | |
let cp1 = new Point((xAvg + points[i].x) / 2, points[i].y); | |
let next = new Point(points[i+1].x, points[i+1].y); | |
let cp2 = new Point((xAvg + points[i+1].x) / 2, points[i+1].y); | |
path.addQuadCurve(avg, cp1); | |
path.addQuadCurve(next, cp2); | |
} | |
path.addLine(new Point(this.ctx.size.width, this.ctx.size.height)) | |
path.closeSubpath() | |
return path; | |
} | |
configure(fn) { | |
let path = this._calculatePath() | |
if(fn) { | |
fn(this.ctx, path); | |
} else { | |
this.ctx.addPath(path); | |
this.ctx.fillPath(path); | |
} | |
return this.ctx; | |
} | |
} | |
await new IncidenceWidget().run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Großes Dankeschön an Kevin Kub für sein Widget!
Ich hab lediglich den Neuinfektionswert von DE mit integriert. Ich dachte, dass einige von euch das auch nützlich finden könnten.
Have fun!