-
-
Save TheSpirit/cb6c55d96846b4e67c9095a05c4fdfe3 to your computer and use it in GitHub Desktop.
COVID-19 Inzidenz-Widget für iOS innerhalb Deutschlands 🇩🇪 (Kreis/Stadt + Bundesland + Trend)
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
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: red; icon-glyph: briefcase-medical; | |
// LICENCE: Robert Koch-Institut (RKI), dl-de/by-2-0 | |
// BASE VERSION FORKED FROM AUTHOR: kevinkub https://gist.github.com/kevinkub/46caebfebc7e26be63403a7f0587f664 | |
// UPDATED VERSION BY AUTHOR: rphl https://gist.github.com/rphl/0491c5f9cb345bf831248732374c4ef5 | |
const outputFields = 'GEN,cases,cases_per_100k,cases7_per_100k,cases7_bl_per_100k,last_update,BL'; | |
const apiUrl = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=${outputFields}&geometry=${location.longitude.toFixed(3)}%2C${location.latitude.toFixed(3)}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json` | |
const outputFieldsStates = 'Fallzahl,LAN_ew_GEN,cases7_bl_per_100k'; | |
const apiUrlStates = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Coronaf%E4lle_in_den_Bundesl%E4ndern/FeatureServer/0/query?where=1%3D1&outFields=${outputFieldsStates}&returnGeometry=false&outSR=4326&f=json` | |
const apiUrlNewCases = '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' | |
/** | |
* Fix Coordinates/MediumWidget | |
* Set Widgetparameter for each column, seperated by ";" Format: POSITION,LAT,LONG;POSITION,LAT,LONG | |
* | |
* Examples: | |
* | |
* First fix column (No second column): 0,51.1244,6.7353 | |
* Second fix column (Second column is visble, MediumWidget): 1,51.1244,6.7353 | |
* Both Fix columns (both are visble, MediumWidget): 0,51.1244,6.7353;1,51.1244,6.7353 | |
* Only Second Fix (both are visble, MediumWidget): 1,51.1244,6.7353 | |
*/ | |
const LIMIT_DARKRED = 100 | |
const LIMIT_RED = 50 | |
const LIMIT_ORANGE = 35 | |
const LIMIT_YELLOW = 25 | |
const LIMIT_DARKRED_COLOR = new Color('f6000f') // DARERED: 9e000a | |
const LIMIT_RED_COLOR = new Color('f6000f') | |
const LIMIT_ORANGE_COLOR = new Color('#ff7927') | |
const LIMIT_YELLOW_COLOR = new Color('F5D800') | |
const LIMIT_GREEN_COLOR = new Color('1CC747') | |
const BUNDESLAENDER_SHORT = { | |
'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 MEDIUMWIDGET = (config.widgetFamily === 'medium') ? true : false | |
let fixedCoordinates = [] | |
if (args.widgetParameter) { | |
fixedCoordinates = parseInput(args.widgetParameter) | |
if (typeof fixedCoordinates[1] !== 'undefined' && Object.keys(fixedCoordinates[1]).length === 3) { | |
MEDIUMWIDGET = true | |
} | |
} else { // DEBUG MEDIUM WIDGET | |
// fixedCoordinates[0] = { index: 1, latitude: 51.1077, longitude: 6.8331 } | |
//fixedCoordinates[1] = { index: 1, latitude: 51.2277, longitude: 6.7731 } | |
//MEDIUMWIDGET = true | |
} | |
let data = {} | |
let weekData = {} | |
const widget = await createWidget() | |
if (!config.runsInWidget) { | |
if (MEDIUMWIDGET) { | |
await widget.presentMedium() | |
} else { | |
await widget.presentSmall() | |
} | |
} | |
Script.setWidget(widget) | |
Script.complete() | |
async function createWidget() { | |
const _data = await getData(0) | |
let areaName; | |
if (_data && typeof _data.areaName !== 'undefined') { | |
areaName = _data.areaName; | |
data[areaName] = _data | |
} | |
const list = new ListWidget() | |
const headerLabel = list.addStack() | |
headerLabel.useDefaultPadding() | |
headerLabel.centerAlignContent() | |
if (MEDIUMWIDGET) { | |
headerLabel.layoutHorizontally() | |
} else { | |
list.setPadding(10,15,10,10) | |
headerLabel.layoutVertically() | |
} | |
const header = headerLabel.addText("🦠 Inzidenz".toUpperCase()) | |
header.font = Font.mediumSystemFont(13) | |
if (data && typeof data[areaName] !== 'undefined') { | |
weekData[areaName] = saveLoadData(data[areaName], areaName) | |
if (!data[areaName].shouldCache) { | |
list.addSpacer(6) | |
const loadingIndicator = list.addText("Ort wird ermittelt...".toUpperCase()) | |
loadingIndicator.font = Font.mediumSystemFont(13) | |
loadingIndicator.textOpacity = 0.5 | |
} | |
if (MEDIUMWIDGET && typeof data[areaName] !== 'undefined') { | |
headerLabel.addSpacer() | |
createGerTopDailyCasesLabel(headerLabel, data[areaName], weekData[areaName]) | |
} | |
list.addSpacer(16) | |
// INCIDENCE | |
const incidenceLabel = list.addStack() | |
if (MEDIUMWIDGET) { | |
incidenceLabel.size = new Size(300, 90) | |
} | |
incidenceLabel.layoutHorizontally() | |
incidenceLabel.useDefaultPadding() | |
incidenceLabel.topAlignContent() | |
createIncidenceLabelBlock(incidenceLabel, data[areaName], weekData[areaName]) | |
const _dataF = await getData(1) | |
let areaNameF | |
if (_dataF && typeof _dataF.areaName !== 'undefined') { | |
areaNameF = _dataF.areaName; | |
data[areaNameF] = _dataF | |
} | |
if (MEDIUMWIDGET && typeof data[areaNameF] !== 'undefined') { | |
weekData[areaNameF] = saveLoadData(data[areaNameF], areaNameF) | |
incidenceLabel.addSpacer(10) | |
createIncidenceLabelBlock(incidenceLabel, data[areaNameF], weekData[areaNameF]) | |
} | |
if (data[areaName].shouldCache) { | |
list.refreshAfterDate = new Date(Date.now() + 60 * 60 * 1000) | |
} | |
} else { | |
list.addSpacer() | |
const errorLabel = list.addText("Daten nicht verfügbar. \nWidget öffnen für reload...") | |
errorLabel.font = Font.mediumSystemFont(12) | |
errorLabel.textColor = Color.gray() | |
} | |
return list | |
} | |
async function getData(useFixedCoordsIndex = false) { | |
try { | |
let dataCases = await new Request(apiUrlNewCases).loadJSON() | |
const cases = dataCases.features[0].attributes.value | |
let dataStates = await new Request(apiUrlStates).loadJSON() | |
const incidencePerState = dataStates.features.map((f) => { return { | |
BL: BUNDESLAENDER_SHORT[f.attributes.LAN_ew_GEN], | |
incidence: f.attributes.cases7_bl_per_100k, | |
cases: f.attributes.Fallzahl // ??? | |
}}) | |
const averageIncidence = incidencePerState.reduce((a, b) => a + b.incidence, 0) / incidencePerState.length | |
const location = await getLocation(useFixedCoordsIndex) | |
let data = await new Request(apiUrl(location)).loadJSON() | |
const attr = data.features[0].attributes | |
const res = { | |
incidence: parseFloat(attr.cases7_per_100k.toFixed(1)), | |
incidenceBL: parseFloat(attr.cases7_bl_per_100k.toFixed(1)), | |
areaName: attr.GEN, | |
areaCases: parseFloat(attr.cases.toFixed(1)), | |
nameBL: BUNDESLAENDER_SHORT[attr.BL], | |
shouldCache: true, | |
updated: attr.last_update, | |
incidencePerState: incidencePerState, | |
averageIncidence: parseFloat(averageIncidence.toFixed(1)), | |
cases: cases | |
} | |
return res | |
} catch (e) { | |
return null | |
} | |
} | |
function parseInput (input) { | |
const _coords = [] | |
const _fixedCoordinates = input.split(";").map(coords => { | |
return coords.split(',') | |
}) | |
_fixedCoordinates.forEach(coords => { | |
_coords[parseInt(coords[0])] = { | |
index: parseInt(coords[0]), | |
latitude: parseFloat(coords[1]), | |
longitude: parseFloat(coords[2]) | |
} | |
}) | |
return _coords | |
} | |
async function getLocation(fixedCoordinateIndex = false) { | |
try { | |
if (fixedCoordinates && typeof fixedCoordinates[fixedCoordinateIndex] !== 'undefined' && Object.keys(fixedCoordinates[fixedCoordinateIndex]).length == 3) { | |
return fixedCoordinates[fixedCoordinateIndex] | |
} else { | |
Location.setAccuracyToThreeKilometers() | |
return await Location.current() | |
} | |
} catch (e) { | |
return null; | |
} | |
} | |
function createGerTopDailyCasesLabel(label, data, weekdata) { | |
let casesStack = label.addStack() | |
casesStack.layoutHorizontally() | |
casesStack.centerAlignContent() | |
casesStack.setPadding(4,4,4,4) | |
casesStack.cornerRadius = 6 | |
let formatedCases = formatCases(data.cases) | |
const prevData = getDataForDate(weekdata); | |
if (prevData) { | |
formatedCases += getTrendArrow(prevData.cases, data.cases) | |
} | |
createUpdatedLabel(casesStack, data) | |
let labelCases = casesStack.addText(`(+${formatedCases})`) | |
labelCases.rightAlignText() | |
labelCases.font = Font.systemFont(10) | |
} | |
function createGerDailyCasesLabel(label, data, weekdata) { | |
let bgColor = new Color('f0f0f0') | |
let textColor = new Color('444444') | |
if(Device.isUsingDarkAppearance()) { | |
bgColor = new Color('202020') | |
textColor = new Color('f0f0f0') | |
} | |
let fontsize = MEDIUMWIDGET ? 10 : 9 | |
let formatedCasesArea = '' | |
let formatedCasesBL = '' | |
let formatedCases = formatCases(data.cases) | |
const prevData = getDataForDate(weekdata); | |
if (prevData) { | |
formatedCases += getTrendArrow(prevData.cases, data.cases) | |
formatedCasesArea = getNewAreaCasesAndTrend(data, weekdata) | |
formatedCasesBL = getNewBLCasesAndTrend(data, weekdata) | |
} | |
let casesStack = label.addStack() | |
casesStack.layoutHorizontally() | |
casesStack.centerAlignContent() | |
casesStack.setPadding(4,4,4,4) | |
casesStack.cornerRadius = 6 | |
casesStack.backgroundColor = bgColor | |
casesStack.size = (MEDIUMWIDGET) ? new Size(140, 15) : new Size(130, 15) | |
let labelCases = casesStack.addText(`${formatedCasesArea}`) | |
labelCases.font = Font.systemFont(fontsize) | |
labelCases.textColor = textColor | |
casesStack.addSpacer() | |
let labelCases2 = casesStack.addText(`${formatedCasesBL}`) | |
labelCases2.centerAlignText() | |
labelCases2.font = Font.systemFont(fontsize) | |
labelCases2.textColor = textColor | |
// GER CASES | |
if (!MEDIUMWIDGET) { | |
casesStack.addSpacer() | |
let labelCases3 = casesStack.addText(`+${formatedCases}`) | |
labelCases3.rightAlignText() | |
labelCases3.font = Font.systemFont(fontsize) | |
labelCases3.textColor = textColor | |
} | |
} | |
function formatCases(cases) { | |
return formatedCases = new Number(cases).toLocaleString('de-DE') | |
} | |
function getTrendArrow (preValue, currentValue) { | |
return (currentValue < preValue) ? '↓' : '↑' | |
} | |
function createUpdatedLabel(label, data, align = 1) { | |
const areaCasesLabel = label.addText(`${data.updated.substr(0, 10)} `) | |
areaCasesLabel.font = Font.systemFont(10) | |
if (align === -1) { areaCasesLabel.rightAlignText() } else { areaCasesLabel.leftAlignText() } | |
} | |
function createIncidenceLabelBlock(labelBlock, data, weekData) { | |
const stack = labelBlock.addStack() | |
stack.layoutVertically() | |
stack.useDefaultPadding() | |
stack.topAlignContent() | |
// DATE | |
if (!MEDIUMWIDGET) { | |
createUpdatedLabel(stack, data) | |
} | |
// MAIN ROW WITH INCIDENCE | |
const stackMainRow = stack.addStack() | |
stackMainRow.useDefaultPadding() | |
stackMainRow.centerAlignContent() | |
stackMainRow.size = (MEDIUMWIDGET) ? new Size(145, 30) : new Size(135, 30) | |
// MAIN INCIDENCE | |
let incidence = data.incidence >= 100 ? Math.floor(data.incidence) : data.incidence; | |
const incidenceLabel = stackMainRow.addText('' + incidence) | |
incidenceLabel.font = Font.boldSystemFont(27) | |
incidenceLabel.leftAlignText(); | |
incidenceLabel.textColor = getIncidenceColor(data.incidence) | |
const incidenceTrend = getIncidenceTrend(data, weekData) | |
const incidenceLabelTrend = stackMainRow.addText(incidenceTrend) | |
incidenceLabelTrend.font = Font.boldSystemFont(27) | |
incidenceLabelTrend.leftAlignText(); | |
incidenceLabelTrend.textColor = (incidenceTrend === '↑') ? LIMIT_RED_COLOR : LIMIT_GREEN_COLOR | |
stackMainRow.addSpacer(5) | |
// BL INCIDENCE | |
const incidenceBLStack = stackMainRow.addStack(); | |
incidenceBLStack.backgroundColor = new Color('f0f0f0') | |
incidenceBLStack.cornerRadius = 4 | |
incidenceBLStack.setPadding(2,3,2,3) | |
const incidenceBL = (data.incidenceBL >= 100) ? Math.floor(data.incidenceBL) : data.incidenceBL | |
const incidenceBLLabel = incidenceBLStack.addText(incidenceBL + getIncidenceBLTrend(data, weekData) + '\n' + data.nameBL) | |
incidenceBLLabel.font = Font.mediumSystemFont(9) | |
incidenceBLLabel.textColor = new Color('444444') | |
stackMainRow.addSpacer() | |
const areanameLabel = stack.addText(data.areaName.toUpperCase()) | |
areanameLabel.font = Font.mediumSystemFont(14) | |
areanameLabel.lineLimit = 2 | |
stack.addSpacer() | |
createGerDailyCasesLabel(stack, data, weekData) | |
} | |
function getIncidenceColor(incidence) { | |
let color = LIMIT_GREEN_COLOR | |
if (incidence >= LIMIT_DARKRED) { | |
color = LIMIT_DARKRED_COLOR | |
} else if (incidence >= LIMIT_RED) { | |
color = LIMIT_RED_COLOR | |
} else if (incidence >= LIMIT_ORANGE) { | |
color = LIMIT_ORANGE_COLOR | |
} else if (incidence >= LIMIT_YELLOW) { | |
color = LIMIT_YELLOW_COLOR | |
} | |
return color | |
} | |
function getIncidenceTrend(data, weekdata) { | |
let incidenceTrend = ' '; | |
if (typeof weekdata !== 'undefined' && Object.keys(weekdata).length > 0) { | |
const prevData = getDataForDate(weekdata); | |
if (prevData) { | |
incidenceTrend = (data.incidence < prevData.incidence) ? '↓' : '↑' | |
} | |
} | |
return incidenceTrend | |
} | |
function getNewAreaCasesAndTrend(data, weekdata) { | |
let newAreaCases = ''; | |
if (typeof weekdata !== 'undefined' && Object.keys(weekdata).length > 0) { | |
const prevData = getDataForDate(weekdata); | |
if (prevData && typeof prevData.areaCases !== 'undefined') { | |
newAreaCases += (data.areaCases < prevData.areaCases) ?'-' : '+' | |
newAreaCases += formatCases(Math.abs(data.areaCases - prevData.areaCases)) | |
newAreaCases += (data.areaCases > prevData.areaCases) ?'↑' : '↓' | |
} | |
} | |
return newAreaCases | |
} | |
function getNewBLCasesAndTrend(data, weekdata) { | |
let newBLCases = '' | |
let d = data.incidencePerState.filter((item) => { | |
return item.BL === data.nameBL | |
}) | |
let currentBLData = (typeof d[0] !== 'undefined') ? d[0] : null | |
const prevData = getDataForDate(weekdata); | |
let dp = prevData.incidencePerState.filter((item) => { | |
return item.BL === data.nameBL | |
}) | |
let prevBLData = (typeof dp[0] !== 'undefined') ? dp[0] : null | |
console.log(d) | |
console.log(dp) | |
if(currentBLData && prevBLData) { | |
newBLCases += (currentBLData.cases < prevBLData.cases) ?'-' : '+' | |
newBLCases += formatCases(Math.abs(currentBLData.cases - prevBLData.cases)) | |
newBLCases += (currentBLData.cases > prevBLData.cases) ?'↑' : '↓' | |
} | |
return newBLCases | |
} | |
function getIncidenceBLTrend(data, weekdata) { | |
let incidenceBLTrend = ' '; | |
if (typeof weekdata !== 'undefined' && Object.keys(weekdata).length > 0) { | |
const prevData = getDataForDate(weekdata); | |
if (prevData) { | |
incidenceBLTrend = (data.incidenceBL < prevData.incidenceBL) ? '↓' : '↑' | |
} | |
} | |
return incidenceBLTrend | |
} | |
function getDataForDate(weekdata, yesterday = true, datestr = '') { | |
let dateKey; | |
let dayOffset = 1 | |
const today = new Date(); | |
const todayDateKey = `${today.getDate()}.${today.getMonth() + 1}.${today.getFullYear()}` | |
if (typeof weekdata[todayDateKey] === 'undefined') { | |
dayOffset = 2 | |
} | |
if (yesterday) { | |
today.setDate(today.getDate() - dayOffset); | |
dateKey = `${today.getDate()}.${today.getMonth() + 1}.${today.getFullYear()}` | |
} else { | |
dateKey = datestr; | |
} | |
if (typeof weekdata[dateKey] !== 'undefined') { | |
return weekdata[dateKey] | |
} | |
return false | |
} | |
// LIMIT TO 7 DAYS | |
function saveLoadData (newData, suffix = '') { | |
const updated = newData.updated.substr(0, 10); | |
const loadedData = loadData(suffix) | |
if (loadedData) { | |
loadedData[updated] = newData | |
const loadedDataKeys = Object.keys(loadedData); | |
const lastDaysKeys = loadedDataKeys.slice(Math.max(Object.keys(loadedData).length - 7, 0)) | |
let loadedDataLimited = {} | |
lastDaysKeys.forEach(key => { | |
loadedDataLimited[key] = loadedData[key] | |
}) | |
try { | |
let fm = FileManager.iCloud() | |
let path = fm.joinPath(fm.documentsDirectory(), 'covid19' + suffix + '.json') | |
fm.writeString(path, JSON.stringify(loadedDataLimited)) | |
console.log('iCloud: save') | |
} catch (e) { | |
let fm = FileManager.local() | |
let path = fm.joinPath(fm.documentsDirectory(), 'covid19' + suffix + '.json') | |
fm.writeString(path, JSON.stringify(loadedDataLimited)) | |
console.log('Local: save') | |
} | |
return loadedData | |
} | |
return {} | |
} | |
function loadData(suffix) { | |
try { | |
let fm = FileManager.iCloud() | |
let path = fm.joinPath(fm.documentsDirectory(), 'covid19' + suffix + '.json') | |
if (fm.fileExists(path)) { | |
let data = fm.readString(path) | |
console.log('iCloud: read') | |
return JSON.parse(data) | |
} | |
} catch (e) { | |
let fm = FileManager.local() | |
let path = fm.joinPath(fm.documentsDirectory(), 'covid19' + suffix + '.json') | |
if (fm.fileExists(path)) { | |
let data = fm.readString(path) | |
console.log('Local: read') | |
return JSON.parse(data) | |
} | |
} | |
return {}; | |
} |
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
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: red; icon-glyph: briefcase-medical; | |
// LICENCE: Robert Koch-Institut (RKI), dl-de/by-2-0 | |
// AUTHOR: rphl https://gist.github.com/rphl/0491c5f9cb345bf831248732374c4ef5 | |
// TEST PROTOTYPE FOR R-VALUE !!! | |
// TEST PROTOTYPE FOR R-VALUE !!! | |
// TEST PROTOTYPE FOR R-VALUE !!! | |
const apiRUrl = `https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Projekte_RKI/Nowcasting_Zahlen_csv.csv?__blob=publicationFile` | |
function parseRCSV(rDataStr) { | |
let lines = rDataStr.split(/(?:\r\n|\n)+/).filter(function(el) {return el.length != 0}); | |
let headers = lines.splice(0, 1)[0].split(";"); | |
let valuesRegExp = /(?:\"([^\"]*(?:\"\"[^\"]*)*)\")|([^\";]+)/g; | |
let elements = []; | |
for (let i = 0; i < lines.length; i++) { | |
let element = {}; | |
let j = 0; | |
while (matches = valuesRegExp.exec(lines[i])) { | |
var value = matches[1] || matches[2]; | |
value = value.replace(/\"\"/g, "\""); | |
element[headers[j]] = value; | |
j++; | |
} | |
elements.push(element); | |
} | |
return elements | |
} | |
const widget = await createWidget() | |
if (!config.runsInWidget) { | |
await widget.presentSmall() | |
} | |
Script.setWidget(widget) | |
Script.complete() | |
async function createWidget() { | |
const list = new ListWidget() | |
const rDataStr = await new Request(apiRUrl).loadString() | |
const rData = parseRCSV(rDataStr) | |
let lastR = {} | |
rData.forEach(item => { | |
if (parseFloat(item['Schätzer_7_Tage_R_Wert']) > 0) { | |
lastR = item; | |
} | |
}) | |
const d = list.addText('Date: ' + lastR['Datum']) | |
d.font = Font.mediumSystemFont(13) | |
const r = list.addText('R: ' + lastR['Schätzer_7_Tage_R_Wert']) | |
r.font = Font.mediumSystemFont(20) | |
return list | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment