Skip to content

Instantly share code, notes, and snippets.

@MatzeKitt
Last active June 6, 2023 18:12
Show Gist options
  • Save MatzeKitt/21a001c542dfa2c0e159f7971d41e7c6 to your computer and use it in GitHub Desktop.
Save MatzeKitt/21a001c542dfa2c0e159f7971d41e7c6 to your computer and use it in GitHub Desktop.
// 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
//
// -------------
// Configuration
// -------------
// whether a second graph with old data (-7 days) should be draw for comparison
const drawOldData = true;
// whether to shorten big numbers, e. g. 10.256 becomes 10,2k
const shortenBigNumbers = true;
// ---------------------------
// do not edit after this line
// ---------------------------
const DAY_IN_MICROSECONDS = 86400000;
const lineWeight = 2;
const vertLineWeight = .5;
const accentColor1 = new Color( '#33cc33', 1 );
const accentColor2 = Color.lightGray();
const apiUrl = ( location ) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=GEN,cases,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 widgetHeight = 338;
const widgetWidth = 720;
const graphLow = 280;
const graphHeight = 160;
const spaceBetweenDays = 44.5;
const saveIncidenceLatLon = ( location ) => {
let fm = FileManager.iCloud();
let path = fm.joinPath( fm.documentsDirectory(), 'covid19latlon.json' );
fm.writeString( path, JSON.stringify( location ) );
};
const getSavedIncidenceLatLon = () => {
let fm = FileManager.iCloud();
let path = fm.joinPath( fm.documentsDirectory(), 'covid19latlon.json' );
let data = fm.readString( path );
return JSON.parse( data );
};
let drawContext = new DrawContext();
drawContext.size = new Size( widgetWidth, widgetHeight );
drawContext.opaque = false;
let widget = await createWidget();
widget.setPadding( 0, 0, 0, 0 );
widget.backgroundImage = ( drawContext.getImage() );
await widget.presentMedium();
Script.setWidget( widget );
Script.complete();
async function createWidget( items ) {
let location;
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' );
saveIncidenceLatLon( location );
}
catch ( e ) {
console.log( 'using saved lat/lon' );
location = getSavedIncidenceLatLon();
}
}
const locationData = await new Request( apiUrl( location ) ).loadJSON();
if ( ! locationData || ! locationData.features || ! locationData.features.length ) {
const errorList = new ListWidget();
errorList.backgroundColor = new Color( '#191a1d', 1 );
errorList.addText( 'Keine Ergebnisse für den aktuellen Ort gefunden.' );
return errorList;
}
const attr = locationData.features[ 0 ].attributes;
const cityName = attr.GEN;
const county = attr.county;
const list = new ListWidget();
const date = new Date();
date.setTime( date.getTime() - 23 * DAY_IN_MICROSECONDS );
const minDate = ( '0' + ( date.getMonth() + 1 ) ).slice( -2 ) + '-' + ( '0' + date.getDate() ).slice( -2 ) + '-' + date.getFullYear();
const apiUrlData = `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=`;
date.setTime( ( date.getTime() + 7 * DAY_IN_MICROSECONDS ) );
const cityData = await new Request( apiUrlData ).loadJSON();
if ( ! cityData || ! cityData.features || ! cityData.features.length ) {
const errorList = new ListWidget();
errorList.backgroundColor = new Color( '#191a1d', 1 );
errorList.addText( 'Keine Statistik gefunden.' );
return errorList;
}
list.backgroundColor = new Color( '#191a1d', 1 );
drawContext.setTextColor( Color.white() );
drawContext.setFont( Font.mediumSystemFont( 26 ) );
drawContext.drawText( '🦠 Statistik'.toUpperCase() + ' ' + cityName, new Point( 25, 25 ) );
drawContext.setTextAlignedCenter();
let data = {};
let oldData = {};
for ( const dataset of cityData.features ) {
if ( drawOldData && dataset.attributes.Meldedatum < date.getTime() ) {
// get old data
if ( typeof oldData[ dataset.attributes.Meldedatum ] === 'undefined' ) {
oldData[ dataset.attributes.Meldedatum ] = {
Meldedatum: dataset.attributes.Meldedatum,
AnzahlFall: 0,
};
}
oldData[ dataset.attributes.Meldedatum ].AnzahlFall += parseInt( dataset.attributes.AnzahlFall );
}
else if ( dataset.attributes.Meldedatum >= date.getTime() ) {
// get old data
if ( drawOldData && dataset.attributes.Meldedatum < date.getTime() + 8 * DAY_IN_MICROSECONDS ) {
if ( typeof oldData[ dataset.attributes.Meldedatum ] === 'undefined' ) {
oldData[ dataset.attributes.Meldedatum ] = {
Meldedatum: dataset.attributes.Meldedatum,
AnzahlFall: 0,
};
}
oldData[ dataset.attributes.Meldedatum ].AnzahlFall += parseInt( dataset.attributes.AnzahlFall );
}
// get current data
if ( typeof data[ dataset.attributes.Meldedatum ] === 'undefined' ) {
data[ dataset.attributes.Meldedatum ] = {
Meldedatum: dataset.attributes.Meldedatum,
AnzahlFall: 0,
};
}
data[ dataset.attributes.Meldedatum ].AnzahlFall += parseInt( dataset.attributes.AnzahlFall );
}
}
// get minimal value of current and old data
const currentDataData = Object.values( data );
const oldDataData = Object.values( oldData );
let currentMin, currentMax, oldMin, oldMax;
for ( let i = 0; i < currentDataData.length; i++ ) {
let aux = currentDataData[ i ].AnzahlFall;
currentMin = ( aux < currentMin || currentMin == undefined ? aux : currentMin );
currentMax = ( aux > currentMax || currentMax == undefined ? aux : currentMax );
}
for ( let i = 0; i < oldDataData.length; i++ ) {
let aux = oldDataData[ i ].AnzahlFall;
oldMin = ( aux < oldMin || oldMin == undefined ? aux : oldMin );
oldMax = ( aux > oldMax || oldMax == undefined ? aux : oldMax );
}
const min = currentMin <= oldMin ? currentMin : oldMin;
const max = currentMax >= oldMax ? currentMax : oldMax;
if ( drawOldData ) {
drawChart( oldDataData, 'old', min, max );
}
drawChart( currentDataData, 'current', min, max );
return list;
}
function drawChart( dataArray, chartType, min, max ) {
let diff = max - min;
const highestIndex = dataArray.length - 1;
for ( let i = 0, j = highestIndex; i < dataArray.length; i++, j-- ) {
const day = ( new Date( dataArray[ i ].Meldedatum ) ).getDate();
const dayOfWeek = ( new Date( dataArray[ i ].Meldedatum ) ).getDay();
const cases = dataArray[ i ].AnzahlFall;
const delta = ( cases - min ) / diff;
if ( i < highestIndex ) {
const nextCases = dataArray[ i + 1 ].AnzahlFall;
const nextDelta = ( nextCases - min ) / diff;
const point1 = new Point( spaceBetweenDays * i + 50, graphLow - ( graphHeight * delta ) );
const point2 = new Point( spaceBetweenDays * ( i + 1 ) + 50, graphLow - ( graphHeight * nextDelta ) );
if ( chartType === 'current' ) {
drawLine( point1, point2, lineWeight, accentColor1 );
}
else {
drawLine( point1, point2, 1, accentColor2 );
}
}
// Vertical Line
if ( chartType === 'current' ) {
const point1 = new Point( spaceBetweenDays * i + 50, graphLow - ( graphHeight * delta ) );
const point2 = new Point( spaceBetweenDays * i + 50, graphLow );
drawLine( point1, point2, vertLineWeight, accentColor2 );
let dayColor;
if ( dayOfWeek == 0 || dayOfWeek == 6 ) {
dayColor = accentColor2;
}
else {
dayColor = Color.white();
}
const casesRect = new Rect( spaceBetweenDays * i, ( graphLow - 40 ) - ( graphHeight * delta ), 100, 23 );
const dayRect = new Rect( spaceBetweenDays * i + 27, graphLow + 10, 50, 23 );
drawTextR( formatNumber( cases ), casesRect, dayColor, Font.systemFont( 22 ) );
drawTextR( day, dayRect, dayColor, Font.systemFont( 22 ) );
}
}
return min;
}
function drawTextR( text, rect, color, font ) {
drawContext.setFont( font );
drawContext.setTextColor( color );
drawContext.drawTextInRect( new String( text ).toString(), rect );
}
function drawLine( 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();
}
function formatNumber( number ) {
let tooBig = false;
if ( shortenBigNumbers && number > 999 ) {
tooBig = true;
}
// replace dot by comma
number = number.toString().replace( '.', ',' );
// add thousands separator
number = number.replace( /\B(?=(\d{3})+(?!\d))/g, '.' );
if ( tooBig ) {
const thousandsSeparatorPosition = number.indexOf( '.' );
number = number.replace( '.', ',' );
number = number.substring( 0, thousandsSeparatorPosition + 2 ) + 'k';
}
return number;
}
@thommesborg
Copy link

Wie sich herausgestellt hat, es gab gestern wohl keine Ergebnisse für meinen genauen Stadort. Nachdem ich die Standorterfassung auf ungefähren Standort geändert hatte, lief auch der Code sauber durch. Heute Morgen habe ich dann den Standort erneut auf genauen Standort umgestellt und bekomme nun auch hier wieder Ergebnisse angezeigt. Enschuldigt bitte die Störung.

@MatzeKitt
Copy link
Author

Ich habe die Berechnung der Minimal- und Maximalwerte korrigiert, sodass nun sowohl die Daten der jeweils vorangegangenen Woche und der aktuellen Woche beachtet werden.

@pftnhr
Copy link

pftnhr commented Apr 8, 2021

7382CF10-14E9-40CD-B45A-3583080B2A25

Hallo, seit gestern sehen die Widgets so aus. Ist das etwas bekanntes, das sich von selbst wieder einkriegt?

@marcusraitner
Copy link

7382CF10-14E9-40CD-B45A-3583080B2A25

Hallo, seit gestern sehen die Widgets so aus. Ist das etwas bekanntes, das sich von selbst wieder einkriegt?

Ja, bei mir auch. Das liegt daran, dass die Schnittstelle, die das RKI für die Daten bereitstellte, jetzt einen Internal Server Error liefert: https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_RKI_Sums/

Gab es immer wieder mal und ich hoffe es gibt sich wieder …

@MatzeKitt
Copy link
Author

Das ist nur immer wieder eine Erinnerung, wie gut die Digitalisierung im öffentlichen Sektor ist. 🙂🙈

@pftnhr
Copy link

pftnhr commented Apr 8, 2021

Das ist nur immer wieder eine Erinnerung, wie gut die Digitalisierung im öffentlichen Sektor ist. 🙂🙈

😂 Ok, da weiß ich Bescheid und ignoriere es.

@marcusraitner
Copy link

😉 Aber irgendwie bin ich mir im Moment nicht sicher, ob diese Schnittstelle nicht komplett weg ist, weil ich sie im Katalog eine Ebene höher gar nicht mehr sehe: https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/

@pftnhr
Copy link

pftnhr commented Apr 8, 2021

Oh, das wär schade.

@marcusraitner
Copy link

Eine gute Alternative könnte sein: https://api.corona-zahlen.org/docs/ (und ich spendiere dem Autor davon gerne einen Kaffee!) Was meinst du @MatzeKitt?

@pftnhr
Copy link

pftnhr commented Apr 8, 2021

Die Charts sind wieder da.

@MatzeKitt
Copy link
Author

Ich sehe auch keinen Grund darin, wegen temporärer Ausfälle in diesem Fall eine andere API in Betracht zu ziehen. Aus mehreren Gründen:

  1. Ich weiß nicht, ob dort ähnliche Probleme auftraten oder auftreten werden.
  2. Ich weiß nicht, wer diesen Service betreibt und was dieser mit meinen personenbezogenen Daten macht, ganz gleich, was er einem verspricht.
  3. Das RKI ist eine öffentliche Stelle, bei der Transparenz zwingend vorhanden ist (siehe auch Punkt 2).

Man könnte sich eher überlegen, ob man die Daten zwischenspeichert, da diese sowieso nur einmal täglich vom RKI aktualisiert werden.

@thommesborg
Copy link

thommesborg commented Apr 8, 2021 via email

@pftnhr
Copy link

pftnhr commented Apr 8, 2021

Ich sehe auch keinen Grund darin, wegen temporärer Ausfälle in diesem Fall eine andere API in Betracht zu ziehen. Aus mehreren Gründen:

  1. Ich weiß nicht, ob dort ähnliche Probleme auftraten oder auftreten werden.
  2. Ich weiß nicht, wer diesen Service betreibt und was dieser mit meinen personenbezogenen Daten macht, ganz gleich, was er einem verspricht.
  3. Das RKI ist eine öffentliche Stelle, bei der Transparenz zwingend vorhanden ist (siehe auch Punkt 2).

Man könnte sich eher überlegen, ob man die Daten zwischenspeichert, da diese sowieso nur einmal täglich vom RKI aktualisiert werden.

Zwischenspeichern halte ich für eine gute Idee. Wenn nicht, kann ich aber auch damit leben. Gute Arbeit!

@marcusraitner
Copy link

Ich sehe auch keinen Grund darin, wegen temporärer Ausfälle in diesem Fall eine andere API in Betracht zu ziehen. Aus mehreren Gründen:

  1. Ich weiß nicht, ob dort ähnliche Probleme auftraten oder auftreten werden.
  2. Ich weiß nicht, wer diesen Service betreibt und was dieser mit meinen personenbezogenen Daten macht, ganz gleich, was er einem verspricht.
  3. Das RKI ist eine öffentliche Stelle, bei der Transparenz zwingend vorhanden ist (siehe auch Punkt 2).

Man könnte sich eher überlegen, ob man die Daten zwischenspeichert, da diese sowieso nur einmal täglich vom RKI aktualisiert werden.

Aus meiner Sicht, speichert diese API die Daten nur in verschiedener Form zwischen; ist eigentlich aber dieselbe Quelle. Ich habe es in meinem Skript mal umgebaut, aber grundsätzlich verstehe ich dein Argument auch sehr gut.

@MatzeKitt
Copy link
Author

ist eigentlich aber dieselbe Quelle.

Das ja, mir geht es eher darum, dass man in Verbindung mit der IP-Adresse sowie den angegebenen Koordinaten (auch wenn diese auf drei Nachkommastellen gekürzt werden) ein ziemlich schönes Bewegungsprofil erstellen könnte.

Ich möchte damit nicht sagen, dass das dort passiert. Ich gehe davon aus, dass das nicht so ist. Auf der sichereren Seite ist man meiner Meinung nach aber mit der Verwendung der API des RKI.

@marcusraitner
Copy link

ist eigentlich aber dieselbe Quelle.

Das ja, mir geht es eher darum, dass man in Verbindung mit der IP-Adresse sowie den angegebenen Koordinaten (auch wenn diese auf drei Nachkommastellen gekürzt werden) ein ziemlich schönes Bewegungsprofil erstellen könnte.

Ich möchte damit nicht sagen, dass das dort passiert. Ich gehe davon aus, dass das nicht so ist. Auf der sichereren Seite ist man meiner Meinung nach aber mit der Verwendung der API des RKI.

Ok, das ist ein Argument. Bin mir aber nur nicht sicher, ob das Argument für oder gegen das RKI spricht. Bei https://github.com/marlon360/rki-covid-api habe ich wenigstens Einsicht in den Source-Code und kann genau das ausschließen, beim RKI nicht. Und wenn diese API dann noch die Zugriffe auf RKI bündelt, bin ich damit vielleicht sogar sicherer …

@MatzeKitt
Copy link
Author

MatzeKitt commented Apr 8, 2021

Was ich meine, kann man vollständig über die Zugriffsprotokolle am Server abbilden, ganz egal, ob die Applikation als solche Open Source ist oder nicht. 🙂

@marcusraitner
Copy link

Ok, da hast du recht. Wobei ich da immer noch nicht sicher bin, wem ich mehr vertraue und wo meine Daten schlechter aufgehoben sind … In meinem Skript hole ich mir aber über diese Schnittstelle nur die Fälle des Landkreises pro Tag und insofern gebe ich nur den Landkreis preis und nicht meine vollen Koordinaten.

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