Skip to content

Instantly share code, notes, and snippets.

@TheSpirit
Forked from rphl/incidence.js
Created Oct 27, 2020
Embed
What would you like to do?
COVID-19 Inzidenz-Widget für iOS innerhalb Deutschlands 🇩🇪 (Kreis/Stadt + Bundesland + Trend)
// 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 {};
}
// 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