Skip to content

Instantly share code, notes, and snippets.

@aploe
Forked from kevinkub/incidence.js
Last active November 16, 2020 08:15
Show Gist options
  • Select an option

  • Save aploe/6644b61953c3f42e7369c2de21e1b87d to your computer and use it in GitHub Desktop.

Select an option

Save aploe/6644b61953c3f42e7369c2de21e1b87d to your computer and use it in GitHub Desktop.
COVID-19 Inzidenz-Widget für iOS innerhalb Deutschlands - Erweitert um Neuinfektionen 🇩🇪
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0
class IncidenceWidget {
constructor() {
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) {
let data = await this.getData()
// Basic widget setup
let list = new ListWidget()
list.setPadding(0, 0, 0, 0)
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()
if(data.error) {
// Error handling
let loadingIndicator = textStack.addText(data.error.toUpperCase())
textStack.setPadding(14, 14, 14, 14)
loadingIndicator.font = Font.mediumSystemFont(13)
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();
incidenceStack.addText(data.areaName)
// 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', .25)
stateStack.cornerRadius = 4
stateStack.setPadding(2, 4, 2, 4)
stateText.font = Font.mediumSystemFont(9)
stateText.textColor = Color.white()
valueStack.addSpacer()
// Chart
let chart = new LineChart(400, 120, 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 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();
@aploe
Copy link
Author

aploe commented Nov 16, 2020

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.

IMG_7189
IMG_7188

Have fun!

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