Skip to content

Instantly share code, notes, and snippets.

@kevinkub
Last active June 27, 2023 12:53
Show Gist options
  • Save kevinkub/46caebfebc7e26be63403a7f0587f664 to your computer and use it in GitHub Desktop.
Save kevinkub/46caebfebc7e26be63403a7f0587f664 to your computer and use it in GitHub Desktop.
COVID-19 Inzidenz-Widget für iOS innerhalb Deutschlands 🇩🇪
// 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/Covid19_hubv/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', .5)
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();
@hxperman
Copy link

Hallo zusammen, gibt es einen Fork, wo ein Vergleich zu dem Wert der Neuinfektionen der Vorwoche dargestellt bzw. berechnet wird?

@kevinkub
Copy link
Author

Meinst du den Wert vor 7 Tagen. So wie in der Tagesschau? Ansonsten zeigt dieses Widget die Entwicklung der letzten drei Wochen in dem Chart an. https://github.com/rphl/corona-widget ist die bekannteste Alternative.

@hxperman
Copy link

Meinst du den Wert vor 7 Tagen. So wie in der Tagesschau? Ansonsten zeigt dieses Widget die Entwicklung der letzten drei Wochen in dem Chart an. https://github.com/rphl/corona-widget ist die bekannteste Alternative.

Ja ich meine einen Vergleich mit dem Wert von vor 7 Tagen (wie in der Tagesschau), weil die Woche ja immer ähnlich verläuft, Am WE weniger und zur Wochenmitte am Meisten.

@olle2424
Copy link

Erstmal danke an alle, die sich hier so viel Mühe gemacht haben.
Ich habe selbst leider keine Ahnung vom Scripting, deswegen die Frage, ob jemand ein einfaches Widget machen kann, dass die Inzidenz von zwei Standorten anzeigt und nicht wie hier schon öfter gesehen von Deutschland und einem Standort?
Vielleicht kann man die Geodaten am Anfang des Widgets in Variablen übergeben, so dass man diese nicht an mehreren Stellen im Widget eingeben muss.

@kevinkub
Copy link
Author

Schau dir mal den Fork von rphl an. Dort sollten zwei Standorte möglich sein: https://github.com/rphl/corona-widget

@olle2424
Copy link

Ok, das geht. Sind für mich zwar viel zu viele Infos und leider keine zwei Städte im kleinen Widget aber erstmal besser als nichts :)
Sollte jemand die Inzidenz von 2 Städten im kleinen Widget ohne anderen Schnickschnack hinbekommen, wäre das super.

@NoTrace-Kai
Copy link

Hi …
ich bin mir nicht sicher wo ich das hier gefunden habe.
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0 // Multiple values: Use with 48.260,11.439,DAH;48.809,11.897,KEH;49.122,12.549,LAas parameter e.g. const apiUrl = (location) =>https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=GEN,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`

function parseLocation (location) {
  const fixedCoordinates = location.split(",")
  return {
    latitude: parseFloat(fixedCoordinates[0]),
    longitude: parseFloat(fixedCoordinates[1]),
    name: fixedCoordinates.length >= 2 ? fixedCoordinates[2] : null,
  }
}

async function dataForLocation(location) {
  const data = await new Request(apiUrl(location)).loadJSON()

  if (!data || !data.features || !data.features.length) {
    return {
      error: "Keine Ergebnisse für den aktuellen Ort gefunden.",
    }
  }

  const attr = data.features[0].attributes
  const incidence = attr.cases7_per_100k.toFixed(1)
  const cityName = location["name"] ? location["name"] : attr.GEN

  return {
    incidence: incidence,
    name: cityName,
  }
}

let widget = await createWidget()
if (!config.runsInWidget) {
  await widget.presentSmall()
}

Script.setWidget(widget)
Script.complete()

async function createWidget(items) {
  let locations
  
  if(args.widgetParameter) {
    locations = args.widgetParameter.split(";").map(parseLocation)
  } else {
    Location.setAccuracyToThreeKilometers()
    locations = [await Location.current()]
  }

  const list = new ListWidget()

//   if (Device.isUsingDarkAppearance()) {
//     const gradient = new LinearGradient()
//     gradient.locations = [0, 1]
//     gradient.colors = [
//      new Color("111111"),
//       new Color("222222")
//     ]
//     list.backgroundGradient = gradient
//   }

  const header = list.addText("🦠 Inzidenz".toUpperCase())
  header.font = Font.mediumSystemFont(13)

  list.addSpacer()

  for (location of locations) {
    const data = await dataForLocation(location)

    if (data["error"]) {
      list.addText(data["error"])
      continue
    }

    const incidence = data["incidence"]
    const cityName = data["name"]

    const line = list.addStack()
    line.layoutHorizontally()
    line.centerAlignContent()
    line.useDefaultPadding()

    const label = line.addText(incidence+"")
    label.font = Font.boldSystemFont(24)
    label.leftAlignText()
    
    if(incidence >= 50) {
      label.textColor = Color.red()
    } else if(incidence >= 35) {
      label.textColor = Color.orange()
    } else {
      label.textColor = Color.green()
    }

    line.addSpacer()

    const name = line.addText(cityName)
    name.rightAlignText()
  }

  return list
} `

Aber dann kannst Du die Koordinaten von 3 Orten eintragen.

52.28,7.4,ST;51.96,7.62,MS;52.27,8.04,OS

CU Kai

@olle2424
Copy link

olle2424 commented May 12, 2021

Super, danke dir. Man muss nur beachten, dass die apiUrl auskommentiert ist und dass vor https ein ` fehlt. Sonst bekommt man eine Fehlermeldung.
Btw man kann sogar die Koordinaten von 4 Städten angeben.

@Baumchen
Copy link

Erstmal danke an alle, die sich hier so viel Mühe gemacht haben.
Ich habe selbst leider keine Ahnung vom Scripting, deswegen die Frage, ob jemand ein einfaches Widget machen kann, dass die Inzidenz von zwei Standorten anzeigt und nicht wie hier schon öfter gesehen von Deutschland und einem Standort?
Vielleicht kann man die Geodaten am Anfang des Widgets in Variablen übergeben, so dass man diese nicht an mehreren Stellen im Widget eingeben muss.

https://gist.github.com/Baumchen/6d91df0a4c76c45b15576db0632e4329

@olle2424
Copy link

Weiß jemand warum auf meinem 11 Pro Max alles richtig dargestellt wird (weißes Widget), auf einem 8 Plus (schwarzes Widget) die Werte und Namen aber abgeschnitten werden? Hat das was mit der Displayauflösung zu tun?
Verwendet wird der Code, den NoTrace-Kai gepostet hat.
BC467BF5-FE13-4F3B-A894-659DBD13244E
3B330A66-918E-49BE-8F99-AC8BA851D183

@Baumchen
Copy link

Weiß jemand warum auf meinem 11 Pro Max alles richtig dargestellt wird (weißes Widget), auf einem 8 Plus (schwarzes Widget) die Werte und Namen aber abgeschnitten werden? Hat das was mit der Displayauflösung zu tun?
Verwendet wird der Code, den NoTrace-Kai gepostet hat.
BC467BF5-FE13-4F3B-A894-659DBD13244E
3B330A66-918E-49BE-8F99-AC8BA851D183

Da du anscheinend eh meinen Fork verwendest nimm doch gleich den Code von dort: https://gist.github.com/Baumchen/6d91df0a4c76c45b15576db0632e4329

Dann kannst du den dritten Parameter pro Region einfach abkürzen (also z.B. 49.122,12.549,LA für Landshut), dann ist mehr Platz für die Zahlen.

@olle2424
Copy link

Ok, das wäre ein Workaround aber keine Erklärung warum dem so ist. Bleibt wohl dem iPhone 8 Plus Besitzer nur der Weg über die Abkürzungen. Bei meinem 11 PM geht’s ja ;)

@Vortilion
Copy link

Funktioniert bei mir nicht: „Fehler beim Datenabruf“. Schnittstelle nicht erreichbar?

@dwd0tcom
Copy link

dwd0tcom commented Aug 3, 2021

Ja, scheinbar ist irgendwas offline. Ich schau mal

@emuuu
Copy link

emuuu commented Feb 17, 2022

Wurde in die API irgend ne auth eingezogen? Krieg seit drei Tagen oder so den hier zurück:

{
    "error": {
        "code": 499,
        "message": "Token Required",
        "messageCode": "GWM_0003",
        "details": [
            "Token Required"
        ]
    }
}

Auch schön, dass so nen Fehler mit ner 200 zurückkommt :)

@emuuu
Copy link

emuuu commented Feb 17, 2022

Top, das war es. Besten Dank!

@Conrad016
Copy link

Hallo, bei mir ist auch der Fehler, dass ein Fehler bei Datenabrufen aufgetreten ist. Könnte man unter diesem Kommentar vllt. noch mal das gesamte und „heile“ Script schicken?! Wäre sehr nett und auch schon mal im Voraus.
LG Conrad

@kevinkub
Copy link
Author

Neue URLs sind im Script eingefügt. Einfach nochmal von oben kopieren und es läuft wieder :-).

@Conrad016
Copy link

Danke, jetzt funktioniert es wieder! :)

@michel0271
Copy link

@icsAT @kevinkub DANKE 👍🏼

@JLuetzen
Copy link

JLuetzen commented Mar 5, 2022

@icsAT @kevinkub auch von mir dickes Danke!!

@Shorsti
Copy link

Shorsti commented Mar 20, 2022

D84580DA-AA6B-4DF7-BF63-8AE9C88C1739
Hallo zusammen, nachdem ich zu diesem Thema und Materie ein absoluter Anfänger bin. Wie verändert man das so, das der Landkreis bzw. Ort in der nächsten Zeile steht. Vielen Dank schon mal im Voraus. Auch für das einfache „laienhafte“ erklären.

@lululasse
Copy link

Irgendwie funktioniert seit 2 Tagen der Datenabruf nicht mehr. Entweder gar nicht oder die Inzidenzen sind deutlich zu hoch.
Jemand ne Idee was man tun kann?
Schönen Sonntag!

@emuuu
Copy link

emuuu commented Jun 21, 2022

Dürfte daran liegen, dass du eine Widget-Version mit Impfrate verwendest. Hier existiert die bisherige Quelle, von der die Impfzahlen kamen, nicht mehr.
Quick an dirty wäre folgende Lösung:
Den Wert für vaccineStatus wie folgt ändern:
const vaccineStatus = "https://api.corona-zahlen.org/vaccinations";

Und in getVaccineData folgende Änderung:
const attr = data.vaccinated;
->
const attr = data.data.vaccinated;

@Jens700
Copy link

Jens700 commented Aug 6, 2022

Plessa kenn ich bin in Elsterwerda geboren.

Vielen Dank für eure Mühe. Ich lerne jeden Tag dazu

@lululasse
Copy link

Datenabruf vom RKI seit 1. März unterbrochen. Ich habe gelesen, dass vielleicht hier Hilfe vorliegt
marlon360/rki-covid-api#504
Kann das jemand reparieren?
Vielen Dank!

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