Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Script for ScriptableApp displaying the 7-Day-Incidence in your county in Germany
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: magic;
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0
// Author: Marcus Raitner (https://fuehrung-erfahren.de)
// Source: https://gist.github.com/marcusraitner/a1b633625d1016498eaaab712461dfc4
const DAY_IN_MICROSECONDS = 86400000;
const widgetHeight = 338;
const widgetWidth = 720;
const graphLow = 0;
const graphHeight = 170;
const spaceBetweenDays = 47;
const bedsLineWidth = 12;
const vertLineWeight = 42;
const tickWidth = 4;
// colors for incidence highlighting
const colorNorm = new Color('#cccccf', 1) // < 35
const colorLow = new Color('#dea657', 1); // < 50
const colorMed = new Color('#c9533c', 1); // < 100
const colorHigh = new Color('#b02c30', 1); // < 200
const colorUltra = new Color('#6d1d21', 1); // >= 200
// other colors
const accentColor2 = Color.lightGray(); // used for weekends
const bedsLineColor = new Color('#939598', 1);
const bedsLineFreeColor = new Color('#4D8802', 1);
// APIs
const apiUrl = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=GEN,EWZ,cases,death_rate,deaths,cases7_per_100k,cases7_bl_per_100k,BL,county&geometry=${ location.longitude.toFixed( 3 ) }%2C${ location.latitude.toFixed( 3 ) }&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json`;
const apiUrlData = (county, minDate) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_RKI_Sums/FeatureServer/0/query?where=Landkreis+LIKE+%27%25${ encodeURIComponent( county ) }%25%27+AND+Meldedatum+%3E+%27${ encodeURIComponent( minDate ) }%27&objectIds=&time=&resultType=none&outFields=*&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields=Meldedatum&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&sqlFormat=none&f=json&token=`;
const diviApiUrl = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/DIVI_Intensivregister_Landkreise/FeatureServer/0/query?where=1%3D1&outFields=*&geometry=${ location.longitude.toFixed( 3 ) }%2C${ location.latitude.toFixed( 3 ) }&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json`;
const vaccinationUrl = (county) => `https://api.vaccination-tracker.app/v1/de-vaccinations-current?key=quote_initial&geo=${ encodeURIComponent( county ) }`;
const vaccinationBoosterUrl = (county) => `https://api.vaccination-tracker.app/v1/de-vaccinations-current?key=quote_booster&geo=${ encodeURIComponent( county ) }`;
const 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'
};
let widget = await createWidget();
await widget.presentMedium();
Script.setWidget(widget);
Script.complete();
async function createWidget(items) {
let location;
const list = new ListWidget();
let gradient = new LinearGradient();
gradient.locations = [0, 1];
gradient.colors = [
new Color("#141E30"),
new Color("#28416F")
];
// list.backgroundColor = new Color('#191a1d', 1);
list.backgroundGradient = gradient;
list.setPadding(0, 0, 0, 0);
// get current location or use given args
if (args.widgetParameter) {
console.log('get fixed lat/lon');
const fixedCoordinates = args.widgetParameter.split(',').map(parseFloat);
location = {
latitude: fixedCoordinates[0],
longitude: fixedCoordinates[1]
};
} else {
Location.setAccuracyToThreeKilometers();
try {
location = await Location.current();
console.log('get current lat/lon');
} catch (e) {
errorText = list.addText('Keine Ortsdaten gefunden');
errorText.textColor = Color.white();
return list;
}
}
// get data for current location
const locationData = await new Request(apiUrl(location)).loadJSON();
if (!locationData || !locationData.features || !locationData.features.length) {
errorText = list.addText('Keine Ergebnisse für den aktuellen Ort gefunden.');
errorText.textColor = Color.white();
return list;
}
const attr = locationData.features[0].attributes;
// get data for ICU beds of current location
const diviLocationData = await new Request(diviApiUrl(location)).loadJSON();
if (!diviLocationData || !diviLocationData.features || !diviLocationData.features.length) {
errorText = list.addText('Keine DIVI-Ergebnisse für den aktuellen Ort gefunden.');
errorText.textColor = Color.white();
return list;
}
const diviAttr = diviLocationData.features[0].attributes;
// extract information needed
const cityName = attr.GEN; // name of 'Landkreis'
const ewz = attr.EWZ / 100000; // number of inhabitants
const county = attr.county; // Landkreis
const bundesLand = stateToAbbr[attr.BL];
const incidenceBl = Math.round(attr.cases7_bl_per_100k);
const freeBeds = diviAttr.betten_frei;
const beds = diviAttr.betten_gesamt;
const usedBeds = diviAttr.betten_belegt;
const cases = diviAttr.faelle_covid_aktuell;
const casesBeatmet = diviAttr.faelle_covid_aktuell_beatmet;
// get data for the last 21 days
const date = new Date();
date.setTime(date.getTime() - 20 * DAY_IN_MICROSECONDS);
const minDate = ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2) + '-' + date.getFullYear();
const countyData = await new Request(apiUrlData(county, minDate)).loadJSON();
if (!countyData || !countyData.features || !countyData.features.length) {
list.addText('Keine Statistik gefunden.');
return list;
}
// get vaccination status
const vaccinationData = await new Request(vaccinationUrl(attr.BL)).loadJSON();
if (!vaccinationData || !vaccinationData.data || !vaccinationData.data.length) {
list.addText('Kein Impfstatus gefunden.');
return list;
}
const quoteInitial = Math.round(vaccinationData.data[0].value * 10) / 10;
const vaccinationBoosterData = await new Request(vaccinationBoosterUrl(attr.BL)).loadJSON();
if (!vaccinationBoosterData || !vaccinationBoosterData.data || !vaccinationBoosterData.data.length) {
list.addText('Kein Impfstatus gefunden.');
return list;
}
const quoteBooster = Math.round(vaccinationBoosterData.data[0].value * 10) / 10;
let stack = list.addStack();
stack.layoutHorizontally();
stack.setPadding(0, 0, 0, 0);
let leftStack = stack.addStack();
leftStack.layoutVertically();
leftStack.setPadding(10, 10, 10, 0);
stack.addSpacer(10);
let rightStack = stack.addStack();
rightStack.setPadding(10, 0, 10, 10);
let incidenceText = leftStack.addText('🦠 7-Tage-Inzidenz'.toUpperCase() + ' – ' + county);
incidenceText.font = Font.semiboldRoundedSystemFont(11);
incidenceText.textColor = Color.white();
leftStack.addSpacer();
// Add vaccination status
const vaccinationHeight = widgetHeight - 20;
const vaccinationBottom = vaccinationHeight - 25;
const vaccinationWidth = 75;
const vaccinationBarWidth = 20;
let drawContext = new DrawContext();
drawContext.size = new Size(vaccinationWidth, vaccinationHeight);
drawContext.opaque = false;
let vaccinationTotalRect = new Rect(0, 0, vaccinationBarWidth, vaccinationBottom);
let path = new Path();
path.addRoundedRect(vaccinationTotalRect, 4, 4);
drawContext.addPath(path);
drawContext.setFillColor(bedsLineColor);
drawContext.fillPath();
let vaccinationTextRect = new Rect(vaccinationBarWidth + 6, 10, vaccinationWidth - 10, 34);
drawTextR(drawContext, '💉', vaccinationTextRect, Color.white(), Font.mediumSystemFont(30));
vaccinationTextRect = new Rect(vaccinationBarWidth + 6, 44, vaccinationWidth - 10, 25);
drawTextR(drawContext, bundesLand, vaccinationTextRect, Color.white(), Font.mediumSystemFont(22));
let vaccinationRect = new Rect(0, (1 - quoteInitial / 100) * vaccinationBottom, vaccinationBarWidth, vaccinationBottom * quoteInitial / 100);
path = new Path();
path.addRoundedRect(vaccinationRect, 4, 4);
drawContext.addPath(path);
drawContext.setFillColor(colorLow);
drawContext.fillPath();
path = new Path();
path.move(new Point(0, (1 - quoteInitial / 100) * vaccinationBottom));
path.addLine(new Point(vaccinationWidth, (1 - quoteInitial / 100) * vaccinationBottom));
drawContext.addPath(path);
drawContext.setLineWidth(2);
drawContext.setStrokeColor(colorLow);
drawContext.strokePath();
vaccinationTextRect = new Rect(vaccinationBarWidth + 6, (1 - quoteInitial / 100) * vaccinationBottom - 26, vaccinationWidth, 22);
drawTextR(drawContext, quoteInitial + '%', vaccinationTextRect, Color.white(), Font.regularSystemFont(22));
let vaccinationBoosterRect = new Rect(0, (1 - quoteBooster / 100) * vaccinationBottom, vaccinationBarWidth, vaccinationBottom * quoteBooster / 100);
path = new Path();
path.addRoundedRect(vaccinationBoosterRect, 4, 4);
drawContext.addPath(path);
drawContext.setFillColor(bedsLineFreeColor);
drawContext.fillPath();
path = new Path();
path.move(new Point(0, (1 - quoteBooster / 100) * vaccinationBottom));
path.addLine(new Point(vaccinationWidth, (1 - quoteBooster / 100) * vaccinationBottom));
drawContext.addPath(path);
drawContext.setLineWidth(2);
drawContext.setStrokeColor(bedsLineFreeColor);
drawContext.strokePath();
vaccinationTextRect = new Rect(vaccinationBarWidth + 6, (1 - quoteBooster / 100) * vaccinationBottom - 2, vaccinationWidth, 22);
drawTextR(drawContext, quoteBooster + '%', vaccinationTextRect, Color.white(), Font.regularSystemFont(22));
path = new Path();
path.move(new Point(0, 0.3 * vaccinationBottom));
path.addLine(new Point(vaccinationBarWidth, 0.3 * vaccinationBottom));
drawContext.addPath(path);
drawContext.setLineWidth(2);
drawContext.setStrokeColor(Color.white());
drawContext.strokePath();
rightStack.addImage(drawContext.getImage());
/*
let bundeslandText = rightStack.addText(bundesLand);
bundeslandText.centerAlignText();
bundeslandText.font = Font.boldRoundedSystemFont(15);*/
let min, max, diff;
// calculate incidence in place.
for (let i = countyData.features.length - 1; i >= 6; i--) {
let sum = 0;
for (let j = 0; j < 7; j++) {
sum += countyData.features[i - j].attributes.AnzahlFall;
}
sum /= ewz;
countyData.features[i].attributes.AnzahlFall = Math.round(sum);
}
countyData.features.splice(0, 6);
for (let i = 0; i < countyData.features.length; i++) {
let aux = countyData.features[i].attributes.AnzahlFall;
// min = (aux < min || min == undefined ? aux : min);
max = (aux > max || max == undefined ? aux : max);
}
if (incidenceBl > max) {
max = incidenceBl;
}
min = 0;
diff = max - min;
let graphDrawContext = new DrawContext();
graphDrawContext.size = new Size(widgetWidth - spaceBetweenDays, graphHeight);
graphDrawContext.opaque = false;
graphDrawContext.setFont(Font.mediumSystemFont(22));
graphDrawContext.setTextAlignedCenter();
const graphTop = 23;
const graphBottom = graphHeight - 23;
const barHeight = graphBottom - graphTop;
for (let i = 0; i < countyData.features.length; i++) {
const day = (new Date(countyData.features[i].attributes.Meldedatum)).getDate();
const dayOfWeek = (new Date(countyData.features[i].attributes.Meldedatum)).getDay();
const cases = countyData.features[i].attributes.AnzahlFall;
const delta = (cases - min) / diff;
let drawColor;
if (cases < 35) {
drawColor = colorNorm;
} else if (cases < 50) {
drawColor = colorLow;
} else if (cases < 100) {
drawColor = colorMed;
} else if (cases < 200) {
drawColor = colorHigh;
} else {
drawColor = colorUltra;
}
path = new Path();
let rect = new Rect(spaceBetweenDays * i, graphBottom - (barHeight * delta), vertLineWeight, barHeight * delta);
path.addRoundedRect(rect, 4, 4);
graphDrawContext.addPath(path);
graphDrawContext.setFillColor(drawColor);
graphDrawContext.fillPath();
let dayColor;
if (dayOfWeek == 0 || dayOfWeek == 6) {
dayColor = accentColor2;
} else {
dayColor = Color.white();
}
const casesRect = new Rect(spaceBetweenDays * i, graphBottom - (barHeight * delta) - 28, vertLineWeight, 23);
const dayRect = new Rect(spaceBetweenDays * i, graphBottom + 3, vertLineWeight, 23);
drawTextR(graphDrawContext, cases, casesRect, dayColor, Font.mediumSystemFont(21));
drawTextR(graphDrawContext, day, dayRect, dayColor, Font.mediumSystemFont(21));
if (i == countyData.features.length - 1) {
const delta = (incidenceBl - min) / diff;
const y = graphBottom - (barHeight * delta);
const x1 = spaceBetweenDays * (i + 1) + spaceBetweenDays / 3;
let x0;
if (y >= casesRect.origin.y - 5 && y <= casesRect.origin.y + casesRect.height + 5) {
x0 = spaceBetweenDays * i + vertLineWeight;
} else {
x0 = spaceBetweenDays * i
}
if (incidenceBl < 50) {
drawColor = colorLow;
} else if (incidenceBl < 100) {
drawColor = colorMed;
} else if (incidenceBl < 200) {
drawColor = colorHigh;
} else {
drawColor = colorUltra;
}
path = new Path();
let rect = new Rect(spaceBetweenDays * (i + 1) + spaceBetweenDays / 3 + 2, graphBottom - (barHeight * delta), vertLineWeight - 2, barHeight * delta - 2);
path.addRoundedRect(rect, 4, 4);
graphDrawContext.addPath(path);
graphDrawContext.setLineWidth(4);
graphDrawContext.setStrokeColor(drawColor);
graphDrawContext.strokePath();
drawLine(graphDrawContext, new Point(x0, y), new Point(widgetWidth, y), 2, Color.white());
const bundesLandRect = new Rect(x1, y + 3, vertLineWeight, 23);
drawTextR(graphDrawContext, bundesLand, bundesLandRect, dayColor, Font.mediumSystemFont(21));
const bundesLandIncidenceRect = new Rect(x1, y - 28, vertLineWeight, 23);
drawTextR(graphDrawContext, incidenceBl, bundesLandIncidenceRect, dayColor, Font.mediumSystemFont(21));
}
}
let graphImage = graphDrawContext.getImage();
leftStack.addImage(graphImage);
leftStack.addSpacer();
drawContext = new DrawContext();
const bedsHeight = 80;
const bedsWidth = graphImage.size.width;
drawContext.size = new Size(bedsWidth, bedsHeight);
drawContext.opaque = false;
let freeBedsWidth = freeBeds / beds * bedsWidth;
let covidBedsWidth = cases / beds * bedsWidth;
let beatmetBedsWidth = casesBeatmet / beds * bedsWidth;
freeBedsWidth = (freeBedsWidth == 0 ? tickWidth / 2 : freeBedsWidth);
covidBedsWidth = (covidBedsWidth == 0 ? tickWidth / 2 : covidBedsWidth);
beatmetBedsWidth = (beatmetBedsWidth == 0 ? tickWidth / 2 : beatmetBedsWidth);
// Line representing all beds
path = new Path();
let bedsLineRect = new Rect(0, bedsHeight / 2 - bedsLineWidth / 2, bedsWidth, bedsLineWidth);
path.addRoundedRect(bedsLineRect, 2, 2);
drawContext.addPath(path);
drawContext.setFillColor(bedsLineColor);
drawContext.fillPath();
let bedsRect = new Rect(0, bedsHeight / 2 - 40, bedsWidth - freeBedsWidth - 10, 26);
drawContext.setFont(Font.mediumSystemFont(22));
drawContext.setTextColor(Color.white());
drawContext.drawTextInRect('🛏' + 'Intensivbetten'.toUpperCase() + ': ' + beds, bedsRect)
// Portion representing free beds
path = new Path();
bedsLineRect = new Rect(bedsWidth - freeBedsWidth, bedsHeight / 2 - bedsLineWidth / 2, freeBedsWidth, bedsLineWidth);
path.addRoundedRect(bedsLineRect, 2, 2);
drawContext.addPath(path);
drawContext.setFillColor(bedsLineFreeColor);
drawContext.fillPath();
drawLine(drawContext, new Point(bedsWidth - freeBedsWidth, bedsHeight / 2 + bedsLineWidth / 2 + 5), new Point(bedsWidth - freeBedsWidth, bedsHeight / 2 - 40), tickWidth, new Color('#4D8802', 1));
drawContext.setFont(Font.mediumSystemFont(22));
let freeRect = new Rect(0, bedsHeight / 2 - 35, bedsWidth - freeBedsWidth - 10, 22);
drawContext.setTextAlignedRight();
drawContext.drawTextInRect('frei'.toUpperCase() + ': ' + freeBeds, freeRect)
// Portion representing covid patients
path = new Path();
bedsLineRect = new Rect(0, bedsHeight / 2 - bedsLineWidth / 2, covidBedsWidth, bedsLineWidth);
path.addRoundedRect(bedsLineRect, 2, 2);
drawContext.addPath(path);
drawContext.setFillColor(colorHigh);
drawContext.fillPath();
drawLine(drawContext, new Point(covidBedsWidth, bedsHeight / 2 - bedsLineWidth / 2 - 5), new Point(covidBedsWidth, bedsHeight / 2 + 38), tickWidth, colorHigh);
// Portion representing cases beatmet
path = new Path();
bedsLineRect = new Rect(0, bedsHeight / 2 - bedsLineWidth / 2, beatmetBedsWidth, bedsLineWidth);
path.addRoundedRect(bedsLineRect, 2, 2);
drawContext.addPath(path);
drawContext.setFillColor(colorUltra);
drawContext.fillPath();
drawLine(drawContext, new Point(beatmetBedsWidth, bedsHeight / 2 - bedsLineWidth / 2 - 5), new Point(beatmetBedsWidth, bedsHeight / 2 + 20), tickWidth, colorUltra);
let covidRect = new Rect(covidBedsWidth + 10, bedsHeight / 2 + 10, bedsWidth - covidBedsWidth, 22);
drawContext.setTextAlignedLeft();
drawContext.drawTextInRect('🦠COVID-19: ' + cases + ' (davon ' + casesBeatmet + ' beatmet)', covidRect);
leftStack.addImage(drawContext.getImage());
return list;
}
function drawTextR(drawContext, text, rect, color, font) {
drawContext.setFont(font);
drawContext.setTextColor(color);
drawContext.drawTextInRect(new String(text).toString(), rect);
}
function drawLine(drawContext, point1, point2, width, color) {
const path = new Path();
path.move(point1);
path.addLine(point2);
drawContext.addPath(path);
drawContext.setStrokeColor(color);
drawContext.setLineWidth(width);
drawContext.strokePath();
}
@ken-sa

This comment has been minimized.

Copy link

@ken-sa ken-sa commented Jan 26, 2021

Hi, finde deinen Sript richtig gut. Wäre es gut bei den Intensivbetten einen Trend mit anzuzeigen?

@marcusraitner

This comment has been minimized.

Copy link
Owner Author

@marcusraitner marcusraitner commented Jan 26, 2021

@ken-sa Danke dir. Das mit den Trend ist eine gute Idee, aber die Daten geben das (anders als bei den Fallzahlen) nicht her. Da müsste ich mir die Werte zwischenspeichern und das klappt auch nur wenn du den Ort nicht wechselst … 

@ken-sa

This comment has been minimized.

Copy link

@ken-sa ken-sa commented Jan 26, 2021

Danke für deine schnelle Rückmeldung. Sprich eine Datei in der Cloud zum Beispiel oder meinst du wirklich irgendwo Zentral für ganz Deutschland (was ja fast unmöglich wäre)? Die Daten wie auf dem
Bild sind meine ich auch auf der Cloud.
49703DC9-F937-457B-A545-B241A25419C3

@marcusraitner

This comment has been minimized.

Copy link
Owner Author

@marcusraitner marcusraitner commented Jan 26, 2021

Ja, ich meinte eine Datei in deiner iCloud, wo ich mir den letzten Wert speichern müsste, um ihn mit dem aktuellen zu vergleichen.

@UnityProductDevelopment

This comment has been minimized.

Copy link

@UnityProductDevelopment UnityProductDevelopment commented Jan 27, 2021

Super Sache, vielen Dank Marcus!

@marcusraitner

This comment has been minimized.

Copy link
Owner Author

@marcusraitner marcusraitner commented Jan 27, 2021

Super Sache, vielen Dank Marcus!

Danke, Daniel!

@dennerforen

This comment has been minimized.

Copy link

@dennerforen dennerforen commented Jan 27, 2021

Nach update Fehler

IMG_1222

@marcusraitner

This comment has been minimized.

Copy link
Owner Author

@marcusraitner marcusraitner commented Jan 27, 2021

Nach update Fehler

IMG_1222

Danke! Mein Fehler. Ist korrigiert.

@dennerforen

This comment has been minimized.

Copy link

@dennerforen dennerforen commented Jan 27, 2021

Danke, funktioniert wieder. Neue Farbe gefällt mir.

@Lutti45

This comment has been minimized.

Copy link

@Lutti45 Lutti45 commented Feb 1, 2021

Hallo Marcus,
ist es auch möglich einen individuellen Namen für den Landkreis einzurichten. Z.B. Zuhause, Büro usw.
Ansonsten gefällt mir dein Script sehr gut.

@marcusraitner

This comment has been minimized.

Copy link
Owner Author

@marcusraitner marcusraitner commented Feb 1, 2021

Hallo Marcus,
ist es auch möglich einen individuellen Namen für den Landkreis einzurichten. Z.B. Zuhause, Büro usw.
Ansonsten gefällt mir dein Script sehr gut.

Du kannst dem Widget auf dem Homescreen Länge und Breite als Parameter mitgeben (kriegst du über Google Maps oder Karten App), z.B. als 48.15,9.05 für Tübingen

@Tvius

This comment has been minimized.

Copy link

@Tvius Tvius commented Feb 20, 2021

Hallo und vielen Dank für dein Script. Hat jemand eine Idee, was damit gemeint ist:
AA469BFC-D54A-4A09-A592-B3A33F7B2A6A
3C7D2641-0E52-461E-9AD0-81EE2D99B97E
1C7BDA64-4005-4572-8B54-580406EC0BE8
Ich konnte das Problem mittlerweile eingrenzen, d.h. es liegt Zuhause am W-Lan.

Lg Tvius

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