Created
June 10, 2021 21:55
-
-
Save nielspetersen/82a3f252150fee2029928c5d0718f4c7 to your computer and use it in GitHub Desktop.
Corona widget
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 | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
* | |
* AUTHOR: https://github.com/rphl - https://github.com/rphl/corona-widget/ | |
* ISSUES: https://github.com/rphl/corona-widget/issues | |
* | |
*/ | |
// ============= ============= ============= ============= ================= | |
// ÄNDERUNGEN HIER, WERDEN BEI AKTIVEN AUTOUPDATE ÜBERSCHRIEBEN | |
// ZUR KONFIGURATION SIEHE README! | |
// https://github.com/rphl/corona-widget#erweiterte-konfiguration | |
// | |
// ============= ============= ============= ============= ================= | |
let CFG = { | |
theme: '', // '' = Automatic Ligh/Darkmode based on iOS. light = only lightmode is used, dark = only lightmode is used | |
showVaccineInMedium: true, // "show vaccine status based on RKI reports. MEDIUMWIDGET IS REQUIRED! | |
openUrl: false, //"https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4", // open RKI dashboard on tap, set false to disable | |
graphShowValues: 'i', // 'i' = incidence OR 'c' = cases | |
graphShowDays: 21, // show days in graph | |
csvRvalueFields: ['Schätzer_7_Tage_R_Wert', 'Punktschätzer des 7-Tage-R Wertes', 'Schไtzer_7_Tage_R_Wert', 'Punktschไtzer des 7-Tage-R Wertes'], // try to find possible field (column) with rvalue, because rki is changing columnsnames and encoding randomly on each update | |
scriptRefreshInterval: 5400, // refresh after 1,5 hours (in seconds) | |
scriptSelfUpdate: false, // script updates itself, | |
disableLiveIncidence: false, // show old, static incidance. update ONLY ONCE A DAY on intial RKI import | |
debugIncidenceCalc: false // show all calculated incidencevalues on console | |
} | |
// ============= ============= ============= ============= ================= | |
// HALT, STOP !!! | |
// NACHFOLGENDE ZEILEN NUR AUF EIGENE GEFAHR ÄNDERN !!! | |
// ============= ============= ============= ============= ================= | |
const ENV = { | |
themes: { | |
light: { | |
mainBackgroundImageURL: '', | |
mainBackgroundColor: '#f0f0f0', | |
stackBackgroundColor: '#99999920', | |
stackBackgroundColorSmall: '#99999915', | |
stackBackgroundColorSmallTop: '#99999900', | |
areaIconBackgroundColor: '#99999930', | |
titleTextColor: '#222222', | |
titleRowTextColor: '#222222', | |
titleRowTextColor2: '#222222', | |
smallNameTextColor: '#777777', | |
dateTextColor: '#777777', | |
dateTextColor2: '#777777', | |
graphTextColor: '#888888', | |
incidenceColorsDarkdarkred: '#941100', | |
incidenceColorsDarkred: '#c01a00', | |
incidenceColorsRed: '#f92206', | |
incidenceColorsOrange: '#faa31b', | |
incidenceColorsYellow: '#ffff64', | |
incidenceColorsGreen: '#00cc00', | |
incidenceColorsGray: '#d0d0d0' | |
}, | |
dark: { | |
mainBackgroundImageURL: '', | |
mainBackgroundColor: '#9999999', | |
stackBackgroundColor: '#99999920', | |
stackBackgroundColorSmall: '#99999910', | |
stackBackgroundColorSmallTop: '#99999900', | |
areaIconBackgroundColor: '#99999930', | |
titleTextColor: '#f0f0f0', | |
titleRowTextColor: '#f0f0f0', | |
titleRowTextColor2: '#f0f0f0', | |
smallNameTextColor: '#888888', | |
dateTextColor: '#777777', | |
dateTextColor2: '#777777', | |
graphTextColor: '#888888', | |
incidenceColorsDarkdarkred: '#941100', | |
incidenceColorsDarkred: '#c01a00', | |
incidenceColorsRed: '#f92206', | |
incidenceColorsOrange: '#faa31b', | |
incidenceColorsYellow: '#ffff64', | |
incidenceColorsGreen: '#00cc00', | |
incidenceColorsGray: '#d0d0d0' | |
} | |
}, | |
incidenceColors: { | |
darkdarkred: { limit: 250, color: 'incidenceColorsDarkdarkred' }, | |
darkred: { limit: 100, color: 'incidenceColorsDarkred' }, | |
red: { limit: 50, color: 'incidenceColorsRed' }, | |
orange: { limit: 35, color: 'incidenceColorsOrange' }, | |
yellow: { limit: 25, color: 'incidenceColorsYellow' }, | |
green: { limit: 1, color: 'incidenceColorsGreen' }, | |
gray: { limit: 0, color: 'incidenceColorsGray' } | |
}, | |
statesAbbr: { | |
'8': 'BW', | |
'9': 'BY', | |
'11': 'BE', | |
'12': 'BB', | |
'4': 'HB', | |
'2': 'HH', | |
'6': 'HE', | |
'13': 'MV', | |
'3': 'NI', | |
'5': 'NRW', | |
'7': 'RP', | |
'10': 'SL', | |
'14': 'SN', | |
'15': 'ST', | |
'1': 'SH', | |
'16': 'TH' | |
}, | |
vaccineSatesAbbr: { | |
'8' : 'Baden-Württemberg', | |
'9' : 'Bayern', | |
'11' : 'Berlin', | |
'12' : 'Brandenburg', | |
'4' : 'Bremen', | |
'2' : 'Hamburg', | |
'6' : 'Hessen', | |
'13' : 'Mecklenburg-Vorpommern', | |
'3' : 'Niedersachsen', | |
'5' : 'Nordrhein-Westfalen', | |
'7' : 'Rheinland-Pfalz', | |
'10' : 'Saarland', | |
'14' : 'Sachsen', | |
'15' : 'Sachsen-Anhalt', | |
'1' : 'Schleswig-Holstein', | |
'16' : 'Thüringen' | |
}, | |
areaIBZ: { | |
'40': 'KS',// Kreisfreie Stadt | |
'41': 'SK', // Stadtkreis | |
'42': 'K', // Kreis | |
'46': 'K', // Sonderverband offiziel Kreis | |
'43': 'LK', // Landkreis | |
'45': 'LK', // Sonderverband offiziel Landkreis | |
null: 'BZ', | |
'': 'BZ' | |
}, | |
fonts: { | |
xlarge: Font.boldSystemFont(26), | |
large: Font.mediumSystemFont(20), | |
medium: Font.mediumSystemFont(14), | |
normal: Font.mediumSystemFont(12), | |
small: Font.boldSystemFont(11), | |
small2: Font.boldSystemFont(10), | |
xsmall: Font.boldSystemFont(9) | |
}, | |
status: { | |
nogps: 555, | |
offline: 418, | |
notfound: 404, | |
error: 500, | |
ok: 200, | |
fromcache: 418 | |
}, | |
isMediumWidget: config.widgetFamily === 'medium', | |
isSameState: false, | |
cache: {}, | |
staticCoordinates: [], | |
script: { | |
selfUpdate: CFG.scriptSelfUpdate, | |
filename: this.module.filename.replace(/^.*[\\\/]/, ''), | |
updateStatus: '' | |
} | |
} | |
class Theme { | |
static getCurrentTheme () { | |
let theme = 'auto'; | |
if (CFG.theme === 'light' || CFG.theme === 'dark') { | |
theme = CFG.theme | |
} | |
return theme | |
} | |
static getColor(colorName, useDefault = false) { | |
let theme = Theme.getCurrentTheme(); | |
if (theme === 'auto' && useDefault) { | |
theme = 'light'; | |
} | |
if (theme === 'light' || theme === 'dark') { | |
return new Color(ENV.themes[theme][colorName]) | |
} | |
return false // no color preferred | |
} | |
static setColor(object, propertyName, colorName, useDefault = false) { | |
if (CFG.theme === 'light' || CFG.theme === 'dark' || useDefault) { | |
let theme = Theme.getCurrentTheme(); | |
object[propertyName] = new Color(ENV.themes[theme][colorName]) | |
} | |
} | |
} | |
class IncidenceWidget { | |
constructor(coordinates = []) { | |
this.loadConfig(); | |
if (args.widgetParameter) ENV.staticCoordinates = Parse.input(args.widgetParameter) | |
ENV.staticCoordinates = [...ENV.staticCoordinates, ...coordinates] | |
if (typeof ENV.staticCoordinates[1] !== 'undefined' && Object.keys(ENV.staticCoordinates[1]).length >= 3) ENV.isMediumWidget = true | |
Helper.log("Current Theme:", Theme.getCurrentTheme()) | |
this.selfUpdate() | |
} | |
async init() { | |
this.widget = await this.createWidget() | |
this.widget.setPadding(0, 0, 0, 0) | |
if (Theme.getCurrentTheme() === 'light' && Theme.getCurrentTheme() === 'dark') { | |
const backgroundImageUrl = ENV.themes[Theme.getCurrentTheme()]['mainBackgroundImageURL'] | |
if (backgroundImageUrl !== '') { | |
const i = await new Request(backgroundImageUrl); | |
const img = await i.loadImage(); | |
this.widget.backgroundImage = img | |
} | |
} | |
Theme.setColor(this.widget, 'backgroundColor', 'mainBackgroundColor') | |
if (!config.runsInWidget) { | |
(ENV.isMediumWidget) ? await this.widget.presentMedium() : await this.widget.presentSmall() | |
} | |
Script.setWidget(this.widget) | |
Script.complete() | |
} | |
async createWidget() { | |
const list = new ListWidget() | |
const statusPos0 = await Data.load(0) | |
const statusPos1 = (ENV.isMediumWidget && typeof ENV.staticCoordinates[1] !== 'undefined') ? await Data.load(1) : false | |
// UI =============== | |
let topBar = new UI(list).stack('h', [4, 8, 4, 4]) | |
topBar.text("🦠", Font.mediumSystemFont(22)) | |
topBar.space(3) | |
if (statusPos0 === ENV.status.error || statusPos1 === ENV.status.error) { | |
topBar.space() | |
list.addSpacer() | |
let statusError = new UI(list).stack('v', [4, 6, 4, 6]) | |
statusError.text('⚡️', ENV.fonts.medium) | |
statusError.text('Standortdaten konnten nicht geladen werden. \nKein Cache verfügbar. \n\nBitte später nochmal versuchen.', ENV.fonts.small, Theme.getColor('titleTextColor')) | |
list.addSpacer(4) | |
list.refreshAfterDate = new Date(Date.now() + ((CFG.scriptRefreshInterval / 2) * 1000)) | |
return list | |
} | |
Helper.calcIncidence('s0') | |
Helper.calcIncidence(ENV.cache['s0'].meta.BL_ID) | |
Helper.calcIncidence('d') | |
ENV.isSameState = false; | |
if (statusPos0 === statusPos1) { | |
ENV.isSameState = (ENV.cache['s0'].meta.BL_ID === ENV.cache['s1'].meta.BL_ID) | |
} | |
if (statusPos1) Helper.calcIncidence('s1') | |
if (statusPos1 && !ENV.isSameState) Helper.calcIncidence(ENV.cache['s1'].meta.BL_ID) | |
let topRStack = new UI(topBar).stack('v', [0,0,0,0]) | |
topRStack.text(Format.number(ENV.cache.d.meta.r, 2, 'n/v') + 'ᴿ', ENV.fonts.medium, Theme.getColor('titleTextColor')) | |
let updatedDate = Format.dateStr(ENV.cache.d.getDay().date); | |
let updatedTime = ('' + new Date().getHours()).padStart(2, '0') + ':' + ('' + new Date().getMinutes()).padStart(2, '0') | |
topRStack.text(updatedDate + ' ' +updatedTime, ENV.fonts.xsmall, Theme.getColor('dateTextColor', true)) | |
topBar.space() | |
UIComp.statusBlock(topBar, statusPos0) | |
topBar.space(4) | |
if (ENV.isMediumWidget && !ENV.isSameState && statusPos1) { | |
topBar.space() | |
UIComp.smallIncidenceRow(topBar, 'd', 'stackBackgroundColorSmallTop') | |
} | |
UIComp.incidenceVaccineRows(list) | |
list.addSpacer(3) | |
let stateBar = new UI(list).stack('h', [0, 0, 0, 0]) | |
stateBar.space(6) | |
let leftCacheID = ENV.cache['s0'].meta.BL_ID | |
if (ENV.isMediumWidget) { UIComp.smallIncidenceRow(stateBar, leftCacheID) } else { UIComp.smallIncidenceBlock(stateBar, leftCacheID) } | |
stateBar.space(4) | |
// DEFAULT IS GER... else STATE | |
let rightCacheID = (ENV.isMediumWidget && !ENV.isSameState && statusPos1) ? ENV.cache['s1'].meta.BL_ID : 'd' | |
if (ENV.isMediumWidget) { UIComp.smallIncidenceRow(stateBar, rightCacheID) } else { UIComp.smallIncidenceBlock(stateBar, rightCacheID) } | |
stateBar.space(6) | |
list.addSpacer(5) | |
// UI =============== | |
if (CFG.openUrl) list.url = CFG.openUrl | |
list.refreshAfterDate = new Date(Date.now() + (CFG.scriptRefreshInterval * 1000)) | |
return list | |
} | |
async selfUpdate() { | |
if (!ENV.script.selfUpdate) return | |
Helper.log('script selfUpdate', 'running') | |
let url = 'https://raw.githubusercontent.com/rphl/corona-widget/master/incidence.js'; | |
let request = new Request(url) | |
let filenameBak = ENV.script.filename.replace('.js', '.bak.js') | |
try { | |
let script = await request.loadString() | |
if (script !== '') { | |
if (cfm.fm.fileExists(filenameBak)) await cfm.fm.remove(filenameBak) | |
cfm.copy(ENV.script.filename, filenameBak) | |
script = script.replace("scriptSelfUpdate: false", "scriptSelfUpdate: true") | |
cfm.save(script, ENV.script.filename) | |
ENV.script.updateStatus = 'updated' | |
Helper.log('script selfUpdate', ENV.script.updateStatus); | |
} | |
} catch (e) { | |
console.warn(e) | |
if (cfm.fm.fileExists(filenameBak)) { | |
// await cfm.fm.copy(filenameBak, ENV.script.filename) | |
// await cfm.fm.remove(filenameBak) | |
ENV.script.updateStatus = 'loading failed, rollback?' | |
Helper.log('script selfUpdate', ENV.script.updateStatus); | |
} | |
} | |
} | |
async loadConfig () { | |
let path = cfm.fm.joinPath(cfm.configPath, 'config.json'); | |
if (cfm.fm.fileExists(path)) { | |
Helper.log('Loading config.json (defaults will be overwritten)') | |
const cfg = await cfm.read('config') | |
if (typeof cfg.data.themes !== 'undefined' && typeof cfg.data.themes.dark !== 'undefined') { | |
ENV.themes.dark = Object.assign(ENV.themes.dark, cfg.data.themes.dark) | |
} | |
Object.keys(ENV.themes).forEach(theme => { | |
if (typeof cfg.data.themes !== 'undefined' && typeof cfg.data.themes[theme] !== 'undefined') { | |
Helper.log('Loading custom theme from config.json: ' + theme) | |
ENV.themes[theme] = Object.assign(ENV.themes[theme], cfg.data.themes[theme]) | |
} | |
}) | |
if (cfg.status === ENV.status.ok) CFG = Object.assign(CFG, cfg.data) | |
} | |
} | |
} | |
class UIComp { | |
static incidenceVaccineRows(view) { | |
let b = new UI(view).stack('v', [4, 6, 4, 6]) | |
let bb = new UI(b).stack('v', false, Theme.getColor('stackBackgroundColor', true), 10) | |
let padding = [4, 6, 4, 4] | |
if (ENV.isMediumWidget) { | |
padding = [2, 8, 2, 8] | |
} | |
let bb2 = new UI(bb).stack('h', padding, Theme.getColor('stackBackgroundColor', true), 10) | |
UIComp.incidenceRow(bb2, 's0') | |
let bb3 = new UI(bb).stack('h', padding) | |
if (ENV.isMediumWidget && CFG.showVaccineInMedium && typeof ENV.cache.s1 === 'undefined' && typeof ENV.cache.vaccine !== 'undefined') { | |
UIComp.vaccineRow(bb3, 's0') | |
} else if (ENV.isMediumWidget && typeof ENV.cache.s1 !== 'undefined') { | |
UIComp.incidenceRow(bb3, 's1') | |
} else if (!ENV.isMediumWidget) { | |
bb3.space() | |
UIComp.areaIcon(bb3, ENV.cache['s0'].meta.IBZ) | |
bb3.space(3) | |
let areaName = ENV.cache['s0'].meta.GEN | |
if (typeof ENV.staticCoordinates[0] !== 'undefined' && ENV.staticCoordinates[0].name !== false) { | |
areaName = ENV.staticCoordinates[0].name | |
} | |
bb3.text(areaName.toUpperCase(), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) | |
bb3.space(8) // center title if small widget | |
bb3.space() | |
} | |
} | |
static incidenceRow(view, cacheID) { | |
let b = new UI(view).stack('h', [2,0,0,0]) | |
let ib = new UI(b).stack('h', [0,0,0,0], false, false, false, [72, 26]) | |
ib.elem.centerAlignContent() | |
let incidence = ENV.cache[cacheID].getDay().incidence | |
let incidenceFormatted = Format.number(incidence, 1, 'n/v', 100) | |
let incidenceParts = incidenceFormatted.split(",") | |
ib.text(incidenceParts[0], Font.boldMonospacedSystemFont(26), UI.getIncidenceColor(incidence), 1, 1) | |
if (typeof incidenceParts[1] !== "undefined") { | |
ib.text(',' + incidenceParts[1], Font.boldMonospacedSystemFont(18), UI.getIncidenceColor(incidence), 1, 1) | |
} | |
let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) | |
let trendColor = (trendArrow === '↑') ? Theme.getColor(ENV.incidenceColors.red.color, true) : (trendArrow === '↓') ? Theme.getColor(ENV.incidenceColors.green.color, true) : Theme.getColor(ENV.incidenceColors.gray.color, true) | |
ib.text(trendArrow, Font.boldRoundedSystemFont(18), trendColor, 1, 0.9) | |
if (ENV.isMediumWidget) { | |
b.space(5) | |
UIComp.areaIcon(b, ENV.cache[cacheID].meta.IBZ) | |
b.space(3) | |
let areaName = ENV.cache[cacheID].meta.GEN | |
let cacheIndex = parseInt(cacheID.replace('s', '')) | |
if (typeof ENV.staticCoordinates[cacheIndex] !== 'undefined' && ENV.staticCoordinates[cacheIndex].name !== false) { | |
areaName = ENV.staticCoordinates[cacheIndex].name | |
} | |
b.text(areaName.toUpperCase(), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 1) | |
} | |
b.space() | |
let b2 = new UI(b).stack('v', [2, 0, 0, 0], false, false, false, [58, 30]) | |
let graphImg | |
if (CFG.graphShowValues === 'i') { | |
graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 16, false).getImage() | |
} else { | |
graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 16, false).getImage() | |
} | |
b2.image(graphImg) | |
let bb2 = new UI(b2).stack('h') | |
bb2.space() | |
bb2.text('+' + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 1) | |
bb2.space(0) | |
} | |
static vaccineRow (view, cacheID) { | |
let vaccineStateName = ENV.vaccineSatesAbbr[ENV.cache[cacheID].meta.BL_ID] | |
let b = new UI(view).stack('h', [4,0,4,0],) | |
b.elem.centerAlignContent() | |
b.space() | |
b.text("🧬 ", ENV.fonts.medium, false, 1, 0.9) | |
let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID | |
let vaccinatedState = ENV.cache.vaccine.data.states[vaccineStateName].vaccinated / 1000000; | |
b.text(name + ": " + Format.number(vaccinatedState, 3) + '', ENV.fonts.medium, Theme.getColor('titleRowTextColor2'), 1, 0.9) | |
b.space(4) | |
let vaccinated = ENV.cache.vaccine.data.vaccinated / 1000000; | |
b.text("/ D: " + Format.number(vaccinated, 3) + '', ENV.fonts.medium, Theme.getColor('titleRowTextColor2'), 1, 0.9) | |
b.space(4) | |
let dateTS = new Date(ENV.cache.vaccine.meta.lastUpdate).getTime() | |
let date = Format.dateStr(dateTS) | |
date = date.replace('.2021', ''); | |
b.text('(in Mio. / '+ date +')', ENV.fonts.xsmall, Theme.getColor('dateTextColor2', true), 1, 0.9) | |
b.space() | |
view.space() | |
} | |
static smallIncidenceBlock(view, cacheID, options = {}) { | |
let b = new UI(view).stack('v', false, Theme.getColor('stackBackgroundColorSmall', true), 12) | |
let b2 = new UI(b).stack('h', [4, 0, 0, 5]) | |
b2.space() | |
let incidence = ENV.cache[cacheID].getDay().incidence | |
b2.text(Format.number(incidence, 1, 'n/v', 100), ENV.fonts.small2, UI.getIncidenceColor(incidence), 1, 1) | |
let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) | |
let trendColor = (trendArrow === '↑') ? Theme.getColor(ENV.incidenceColors.red.color, true) : (trendArrow === '↓') ? Theme.getColor(ENV.incidenceColors.green.color, true) : Theme.getColor(ENV.incidenceColors.gray.color, true) | |
b2.text(trendArrow, ENV.fonts.small2, trendColor, 1, 1) | |
let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID | |
b2.text(name.toUpperCase(), ENV.fonts.small2, Theme.getColor('smallNameTextColor', true), 1, 1) | |
let b3 = new UI(b).stack('h', [0, 0, 0, 5]) | |
b3.space() | |
//let chartdata = [{ incidence: 0, value: 0 }, { incidence: 10, value: 10 }, { incidence: 20, value: 20 }, { incidence: 30, value: 30 }, { incidence: 40, value: 40 }, { incidence: 50, value: 50 }, { incidence: 70, value: 70 }, { incidence: 100, value: 100 }, { incidence: 60, value: 60 }, { incidence: 70, value: 70 }, { incidence: 39, value: 39 }, { incidence: 20, value: 25 }, { incidence: 10, value: 20 }, { incidence: 30, value: 30 }, { incidence: 0, value: 0 }, { incidence: 10, value: 10 }, { incidence: 20, value: 20 }, { incidence: 30, value: 30 }, { incidence: 60, value: 60 }, { incidence: 70, value: 70 }, { incidence: 39, value: 39 }, { incidence: 40, value: 40 }, { incidence: 50, value: 50 }, { incidence: 70, value: 70 }, { incidence: 100, value: 100 }, { incidence: 60, value: 60 }, { incidence: 70, value: 70 }, { incidence: 40, value: 40 }] | |
let graphImg | |
if (CFG.graphShowValues === 'i') { | |
graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 8, false).getImage() | |
} else { | |
graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 8, false).getImage() | |
} | |
b3.image(graphImg, 0.9) | |
let b4 = new UI(b).stack('h', [0, 0, 1, 5]) | |
b4.space() | |
b4.text('+' + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) | |
b.space(2) | |
} | |
static smallIncidenceRow(view, cacheID, bgColor = 'stackBackgroundColorSmall') { | |
let r = new UI(view).stack('h', false, Theme.getColor(bgColor, true), 12) | |
let b = new UI(r).stack('v') | |
let bb2 = new UI(b).stack('h', [2, 0, 0, 6]) | |
bb2.space() | |
let incidence = ENV.cache[cacheID].getDay().incidence | |
bb2.text(Format.number(incidence, 1, 'n/v', 100), ENV.fonts.normal, UI.getIncidenceColor(incidence), 1 ,1) | |
let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) | |
let trendColor = (trendArrow === '↑') ? Theme.getColor(ENV.incidenceColors.red.color, true) : (trendArrow === '↓') ? Theme.getColor(ENV.incidenceColors.green.color, true) : Theme.getColor(ENV.incidenceColors.gray.color, true) | |
bb2.text(trendArrow, ENV.fonts.normal, trendColor) | |
bb2.space(2) | |
let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID | |
bb2.text(name.toUpperCase(), ENV.fonts.normal, Theme.getColor('smallNameTextColor', true)) | |
let b3 = new UI(b).stack('h', [0, 0, 2, 6]) | |
b3.space() | |
let b3Text = ' '; | |
if (CFG.showVaccineInMedium && ENV.cache.vaccine) { | |
let vaccineStateName = ENV.vaccineSatesAbbr[ENV.cache[cacheID].meta.BL_ID] | |
let vaccineQuote | |
if (typeof ENV.cache.vaccine.data.states[vaccineStateName] !== 'undefined') { | |
vaccineQuote = ENV.cache.vaccine.data.states[vaccineStateName].quote | |
} else { | |
vaccineQuote = ENV.cache.vaccine.data.quote | |
} | |
b3Text = '🧬 ' + Format.number(vaccineQuote, 2, 'n/v') +'%' | |
} | |
b3.text(b3Text, ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) | |
let b2 = new UI(r).stack('v', false, false, false, false, [60, 30]) | |
let b2b2 = new UI(b2).stack('h', [0, 0, 0, 6]) | |
b2b2.space() | |
let graphImg | |
if (CFG.graphShowValues == 'i') { | |
graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 10, false).getImage() | |
} else { | |
graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 10, false).getImage() | |
} | |
b2b2.image(graphImg, 0.9) | |
let b2b3 = new UI(b2).stack('h', [0, 0, 0, 0]) | |
b2b3.space() | |
b2b3.text('+' + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) | |
r.space(6) | |
} | |
static areaIcon(view, ibzID) { | |
let b = new UI(view).stack('h', [1, 3, 1, 3], Theme.getColor('areaIconBackgroundColor', true), 2, 2) | |
b.text(ENV.areaIBZ[ibzID], ENV.fonts.xsmall, Theme.getColor('titleRowTextColor'), 1, 1) | |
} | |
static statusBlock(view, status) { | |
let icon | |
let iconText | |
switch (status) { | |
case ENV.status.offline: | |
icon = '⚡️' | |
iconText = 'Offline' | |
break; | |
case ENV.status.nogps: | |
icon = '📡' | |
iconText = 'GPS?' | |
break; | |
} | |
if (icon && iconText) { | |
let topStatusStack = new UI(view).stack('v') | |
topStatusStack.text(icon, ENV.fonts.small) | |
} | |
} | |
} | |
class UI { | |
constructor(view) { | |
if (view instanceof UI) { | |
this.view = this.elem = view.elem | |
} else { | |
this.view = this.elem = view | |
} | |
} | |
static generateGraph(data, width, height, alignLeft = true) { | |
let graphData = data.data.slice(Math.max(data.data.length - CFG.graphShowDays, 1)); | |
let context = new DrawContext() | |
context.size = new Size(width, height) | |
context.opaque = false | |
context.respectScreenScale = true | |
let max = Math.max.apply(Math, graphData.map(function (o) { return o.cases; })) | |
max = (max <= 0) ? 10 : max; | |
let w = Math.max(2, Math.round((width - (graphData.length * 2)) / graphData.length)) | |
let xOffset = (!alignLeft) ? (width - (graphData.length * (w + 1))) : 0 | |
for (let i = 0; i < CFG.graphShowDays; i++) { | |
let item = graphData[i] | |
let value = parseFloat(item.cases) | |
if (value === -1 && i == 0) value = 10; | |
let h = Math.max(2, (Math.abs(value) / max) * height) | |
let x = xOffset + (w + 1) * i | |
let rect = new Rect(x, height - h, w, h) | |
context.setFillColor(UI.getIncidenceColor((item.cases >= 1) ? item.incidence : 0)) | |
context.fillRect(rect) | |
} | |
return context | |
} | |
static generateIcidenceGraph(data, width, height, alignLeft = true) { | |
let graphData = data.data.slice(Math.max(data.data.length - CFG.graphShowDays, 1)); | |
let context = new DrawContext() | |
context.size = new Size(width, height) | |
context.opaque = false | |
context.respectScreenScale = true | |
let max = Math.max.apply(Math, graphData.map(function (o) { return o.incidence; })) | |
let min = Math.min.apply(Math, graphData.map(function (o) { return o.incidence; })) / 1.2 | |
max = (max <= 0) ? 10 : max - min; | |
let w = Math.max(2, Math.round((width - (graphData.length * 2)) / graphData.length)) | |
let xOffset = (!alignLeft) ? (width - (graphData.length * (w + 1))) : 0 | |
for (let i = 0; i < CFG.graphShowDays; i++) { | |
let item = graphData[i] | |
let value = parseFloat(item.incidence) - min | |
if (value === -1 && i == 0) value = 10; | |
let h = Math.max(2,(Math.abs(value) / max) * (height - 1)) | |
let x = xOffset + (w + 1) * i | |
let rect = new Rect(x, height - h - 1, w, h) | |
context.setFillColor(UI.getIncidenceColor((item.cases >= 0) ? item.incidence : 0)) | |
context.fillRect(rect) | |
} | |
return context | |
} | |
stack(type = 'h', padding = false, borderBgColor = false, radius = false, borderWidth = false, size = false) { | |
this.elem = this.view.addStack() | |
if (radius) this.elem.cornerRadius = radius | |
if (borderWidth !== false) { | |
this.elem.borderWidth = borderWidth | |
this.elem.borderColor = borderBgColor | |
} else if (borderBgColor) { | |
this.elem.backgroundColor = borderBgColor | |
} | |
if (padding) this.elem.setPadding(...padding) | |
if (size) this.elem.size = new Size(size[0], size[1]) | |
if (type === 'h') { this.elem.layoutHorizontally() } else { this.elem.layoutVertically() } | |
this.elem.centerAlignContent() | |
return this | |
} | |
text(text, font = false, color = false, maxLines = 0, minScale = 0.9) { | |
let t = this.elem.addText(text) | |
if (color) t.textColor = (typeof color === 'string') ? new Color(color) : color | |
t.font = (font) ? font : ENV.fonts.normal | |
t.lineLimit = (maxLines > 0 && minScale < 1) ? maxLines + 1 : maxLines | |
t.minimumScaleFactor = minScale | |
return this | |
} | |
image(image, imageOpacity = 1.0) { | |
let i = this.elem.addImage(image) | |
i.resizable = false | |
i.imageOpacity = imageOpacity | |
} | |
space(size) { | |
this.elem.addSpacer(size) | |
return this | |
} | |
static getTrendUpArrow(now, prev) { | |
if (now < 0 && prev < 0) { | |
now = Math.abs(now) | |
prev = Math.abs(prev) | |
} | |
return (now < prev) ? '↗' : (now > prev) ? '↑' : '→' | |
} | |
static getTrendArrow(value1, value2) { | |
return (value1 < value2) ? '↓' : (value1 > value2) ? '↑' : '→' | |
} | |
static getTrendColor(value1, value2, altColorUp = null, altColorDown = null) { | |
let colorUp = (altColorUp) ? new Color(altColorUp) : Theme.getColor(ENV.incidenceColors.red.color, true) | |
let colorDown = (altColorDown) ? new Color(altColorDown) : Theme.getColor(ENV.incidenceColors.green.color, true) | |
return (value1 < value2) ? colorDown : (value1 > value2) ? colorUp : Theme.getColor(ENV.incidenceColors.gray.color, true) | |
} | |
static getIncidenceColor(incidence) { | |
let color = Theme.getColor(ENV.incidenceColors.green.color, true) | |
if (incidence > ENV.incidenceColors.darkdarkred.limit) { | |
color = Theme.getColor(ENV.incidenceColors.darkdarkred.color, true) | |
} else if (incidence >= ENV.incidenceColors.darkred.limit) { | |
color = Theme.getColor(ENV.incidenceColors.darkred.color, true) | |
} else if (incidence >= ENV.incidenceColors.red.limit) { | |
color = Theme.getColor(ENV.incidenceColors.red.color, true) | |
} else if (incidence >= ENV.incidenceColors.orange.limit) { | |
color = Theme.getColor(ENV.incidenceColors.orange.color, true) | |
} else if (incidence >= ENV.incidenceColors.yellow.limit) { | |
color = Theme.getColor(ENV.incidenceColors.yellow.color, true) | |
} else if (incidence === 0) { | |
color = Theme.getColor(ENV.incidenceColors.gray.color, true) | |
} | |
return color | |
} | |
} | |
class DataResponse { | |
constructor(data, status = ENV.status.ok) { | |
this.data = data | |
this.status = status | |
} | |
} | |
class CustomFilemanager { | |
constructor() { | |
try { | |
this.fm = FileManager.iCloud() | |
this.fm.documentsDirectory() | |
} catch (e) { | |
this.fm = FileManager.local() | |
} | |
this.configDirectory = 'coronaWidgetNext' | |
this.configPath = this.fm.joinPath(this.fm.documentsDirectory(), '/' + this.configDirectory) | |
if (!this.fm.isDirectory(this.configPath)) this.fm.createDirectory(this.configPath) | |
} | |
async copy(oldFilename, newFilename) { | |
let oldPath = this.fm.joinPath(this.configPath, oldFilename); | |
let newPath = this.fm.joinPath(this.configPath, newFilename); | |
this.fm.copy(oldPath, newPath) | |
} | |
async save(data, filename = '') { | |
let path | |
let dataStr | |
if (filename === '') { | |
path = this.fm.joinPath(this.configPath, 'coronaWidget_' + data.dataId + '.json'); | |
dataStr = JSON.stringify(data); | |
} else { | |
path = this.fm.joinPath(this.fm.documentsDirectory(), filename); | |
dataStr = data; | |
} | |
this.fm.writeString(path, dataStr); | |
} | |
async read(filename) { | |
let path = this.fm.joinPath(this.configPath, filename + '.json'); | |
let type = 'json' | |
if (filename.includes('.')) { | |
path = this.fm.joinPath(this.fm.documentsDirectory(), filename); | |
type = 'string' | |
} | |
if (this.fm.isFileStoredIniCloud(path) && !this.fm.isFileDownloaded(path)) await this.fm.downloadFileFromiCloud(path); | |
if (this.fm.fileExists(path)) { | |
try { | |
let resStr = await this.fm.readString(path) | |
let res = (type === 'json') ? JSON.parse(resStr) : resStr | |
return new DataResponse(res); | |
} catch (e) { | |
console.error(e) | |
return new DataResponse('', ENV.status.error); | |
} | |
} | |
return new DataResponse('', ENV.status.notfound); | |
} | |
} | |
class Data { | |
constructor(dataId, data = {}, meta = {}) { | |
this.dataId = dataId | |
this.data = data | |
this.meta = meta | |
} | |
getDay (dayOffset = 0) { | |
return (typeof this.data[this.data.length - 1 - dayOffset] !== 'undefined') ? this.data[this.data.length - 1 - dayOffset] : false; | |
} | |
getAvg (weekOffset = 0, ignoreToday = false) { | |
let casesData = [...this.data].reverse() | |
let skipToday = (ignoreToday) ? 1 : 0; | |
const offsetDays = 7 | |
const weekData = casesData.slice((offsetDays * weekOffset) + skipToday, (offsetDays * weekOffset) + 7 + skipToday) | |
const avg = weekData.reduce((a, b) => a + b.incidence, 0) / offsetDays | |
// Helper.log(weekOffset, avg) | |
return avg | |
} | |
static completeHistory (data) { | |
const completeDataObj = {} | |
for(let i = 0; i <= CFG.graphShowDays + 8; i++) { | |
let lastDate = new Date() | |
let prevDate = lastDate.setDate(lastDate.getDate() - i); | |
completeDataObj[Format.dateStr(prevDate)] = { cases: 0, date: prevDate } | |
} | |
data.map((value) => { | |
let curDate = Format.dateStr(value.date) | |
completeDataObj[curDate].cases = value.cases | |
}) | |
let completeData = Object.values(completeDataObj) | |
completeData.sort((a, b) => { return a.date - b.date; }) | |
return completeData.reverse(); | |
} | |
static async tryLoadFromCache(cacheID, useStaticCoordsIndex) { | |
const dataResponse = await cfm.read(cfm.configDirectory + '/coronaWidget_config.json') | |
if (dataResponse.status !== ENV.status.ok) return ENV.status.error | |
const cacheIDs = JSON.parse(dataResponse.data) | |
if (typeof cacheIDs[cacheID] === 'undefined') return ENV.status.error | |
const dataIds = cacheIDs[cacheID] | |
if (typeof dataIds['dataIndex' + useStaticCoordsIndex] !== 'undefined') { | |
const areaData = await cfm.read('coronaWidget_' + dataIds['dataIndex' + useStaticCoordsIndex]) | |
if (!areaData.data.data) return ENV.status.error | |
const area = new Data(dataIds['dataIndex' + useStaticCoordsIndex], areaData.data.data, areaData.data.meta) | |
ENV.cache['s' + useStaticCoordsIndex] = area | |
const stateData = await cfm.read('coronaWidget_' + areaData.data.meta.BL_ID) | |
if (!stateData.data.data) return ENV.status.error | |
const state = new Data(areaData.data.meta.BL_ID, stateData.data.data, stateData.data.meta) | |
ENV.cache[areaData.data.meta.BL_ID] = state | |
const dData = await cfm.read('coronaWidget_d') | |
if (!dData.data.data) return ENV.status.error | |
const d = new Data('d', dData.data.data, dData.data.meta) | |
ENV.cache.d = d | |
const vaccineData = await cfm.read('coronaWidget_vaccine') | |
if (!vaccineData.data.data) return ENV.status.error | |
const vaccine = new Data('vaccine', vaccineData.data.data, vaccineData.data.meta) | |
ENV.cache.vaccine = vaccine | |
return ENV.status.ok | |
} | |
return ENV.status.error | |
} | |
static async load(useStaticCoordsIndex = false) { | |
if (typeof ENV.cache['s' + useStaticCoordsIndex] !== 'undefined') return true | |
let configId = btoa('cID' + JSON.stringify(ENV.staticCoordinates).replace(/[^a-zA-Z ]/g, "")) | |
const location = await Helper.getLocation(useStaticCoordsIndex) | |
if (!location) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.nogps : ENV.status.error | |
} | |
const locationData = await rkiRequest.locationData(location) | |
if (!locationData) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error | |
} | |
let areaCases = await rkiRequest.areaCases(locationData.RS) | |
if (!areaCases) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error | |
} | |
await Data.geoCache(configId, useStaticCoordsIndex, locationData.RS) | |
let areaData = new Data(locationData.RS) | |
areaData.data = areaCases | |
areaData.meta = locationData | |
await cfm.save(areaData) | |
ENV.cache['s' + useStaticCoordsIndex] = areaData | |
// STATE DATA | |
if (typeof ENV.cache[locationData.BL_ID] === 'undefined') { | |
let stateCases = await rkiRequest.stateCases(locationData.BL_ID) | |
if (!stateCases) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error | |
} | |
let stateData = new Data(locationData.BL_ID) | |
stateData.data = stateCases | |
stateData.meta = { | |
BL_ID: locationData.BL_ID, | |
BL: locationData.BL, | |
EWZ: locationData.EWZ_BL | |
} | |
await cfm.save(stateData) | |
ENV.cache[locationData.BL_ID] = stateData | |
} | |
// GER DATA | |
if (typeof ENV.cache.d === 'undefined') { | |
let dCases = await rkiRequest.dCases() | |
if (!dCases) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error | |
} | |
let dData = new Data('d') | |
dData.data = dCases | |
dData.meta = { | |
r: await rkiRequest.rvalue(), | |
EWZ: 83.02 * 1000000 // @TODO real number? | |
} | |
await cfm.save(dData) | |
ENV.cache.d = dData | |
} | |
if (typeof ENV.cache.vaccine === 'undefined') { | |
let vaccineValues = await rkiRequest.vaccinevalues() | |
if (!vaccineValues) { | |
const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) | |
return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error | |
} | |
let vaccineData = new Data('vaccine') | |
vaccineData.data = vaccineValues | |
vaccineData.meta.lastUpdate = vaccineValues.lastUpdate | |
await cfm.save(vaccineData) | |
ENV.cache.vaccine = vaccineData | |
} | |
if (typeof ENV.cache['s' + useStaticCoordsIndex] !== 'undefined' && typeof ENV.cache[locationData.BL_ID] !== 'undefined' && typeof ENV.cache.d !== 'undefined') { | |
return ENV.status.ok | |
} | |
return ENV.status.error | |
} | |
static async geoCache(configId, dataIndex, rsid) { | |
let data = {} | |
let dataResponse = await cfm.read(cfm.configDirectory + '/coronaWidget_config.json') | |
if (dataResponse.status === ENV.status.ok) data = JSON.parse(dataResponse.data) | |
if (typeof data[configId] === 'undefined') data[configId] = {} | |
data[configId]['dataIndex' + dataIndex] = rsid | |
await cfm.save(JSON.stringify(data), cfm.configDirectory + '/coronaWidget_config.json') | |
} | |
} | |
class Format { | |
static dateStr(timestamp) { | |
let date = new Date(timestamp) | |
return `${('' + date.getDate()).padStart(2, '0')}.${('' + (date.getMonth() + 1)).padStart(2, '0')}.${date.getFullYear()}` | |
} | |
static number(number, fractionDigits = 0, placeholder = null, limit = false) { | |
if (!!placeholder && number === 0) return placeholder | |
if (limit !== false && Math.round(number) >= limit) fractionDigits = 0 | |
return Number(number).toLocaleString('de-DE', { maximumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits }) | |
} | |
static timestamp(dateStr) { | |
const regex = /([\d]+)\.([\d]+)\.([\d]+),\ ([0-2]?[0-9]):([0-5][0-9])/g; | |
let m = regex.exec(dateStr) | |
return new Date(m[3], m[2] - 1, m[1], m[4], m[5]).getTime() | |
} | |
static rValue(data) { | |
const parsedData = Parse.rCSV(data) | |
let r = 0 | |
if (parsedData.length === 0) return r | |
let availeRvalueField | |
Object.keys(parsedData[0]).forEach(key => { | |
CFG.csvRvalueFields.forEach(possibleRKey => { | |
if (key === possibleRKey) availeRvalueField = possibleRKey; | |
}) | |
}); | |
let firstDatefield = Object.keys(parsedData[0])[0]; | |
if (availeRvalueField) { | |
parsedData.forEach(item => { | |
if (item[firstDatefield].includes('.') && typeof item[availeRvalueField] !== 'undefined' && parseFloat(item[availeRvalueField].replace(',', '.')) > 0) { | |
r = item; | |
} | |
}) | |
} | |
return (r) ? parseFloat(r[availeRvalueField].replace(',', '.')) : r | |
} | |
} | |
class RkiRequest { | |
async locationData(location) { | |
const outputFields = 'GEN,RS,EWZ,EWZ_BL,BL_ID,cases,cases_per_100k,cases7_per_100k,cases7_bl_per_100k,last_update,BL,IBZ'; | |
const url = `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 response = await this.exec(url) | |
return (response.status === ENV.status.ok) ? response.data.features[0].attributes : false | |
} | |
async areaCases(areaID) { | |
const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) | |
const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?f=json&where=NeuerFall%20IN(1,-1)%20AND%20IdLandkreis%3D${areaID}&objectIds&time&resultType=standard&outFields&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields&groupByFieldsForStatistics&outStatistics=%5B%7B%22statisticType%22:%22sum%22,%22onStatisticField%22:%22AnzahlFall%22,%22outStatisticFieldName%22:%22cases%22%7D,%20%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22MeldeDatum%22,%22outStatisticFieldName%22:%22date%22%7D%5D&having&resultOffset&resultRecordCount&sqlFormat=none&token` | |
const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=NeuerFall+IN%281%2C0%29+AND+IdLandkreis=${areaID}+AND+MeldeDatum+%3E%3D+TIMESTAMP+%27${apiStartDate}%27&objectIds=&time=&resultType=standard&outFields=AnzahlFall%2CMeldeDatum&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields=MeldeDatum&groupByFieldsForStatistics=MeldeDatum&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22AnzahlFall%22%2C%22outStatisticFieldName%22%3A%22cases%22%7D%5D%0D%0A&having=&resultOffset=&resultRecordCount=&sqlFormat=none&f=pjson&token=` | |
return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) | |
} | |
async stateCases(blID) { | |
const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) | |
const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?f=json&where=NeuerFall%20IN(1,%20-1)%20AND%20IdBundesland%3D${blID}&objectIds&time&resultType=standard&outFields&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields&groupByFieldsForStatistics&outStatistics=%5B%7B%22statisticType%22:%22sum%22,%22onStatisticField%22:%22AnzahlFall%22,%22outStatisticFieldName%22:%22cases%22%7D,%20%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22MeldeDatum%22,%22outStatisticFieldName%22:%22date%22%7D%5D&having&resultOffset&resultRecordCount&sqlFormat=none&token` | |
const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=NeuerFall+IN%281%2C0%29+AND+IdBundesland=${blID}+AND+MeldeDatum+%3E%3D+TIMESTAMP+%27${apiStartDate}%27&objectIds=&time=&resultType=standard&outFields=AnzahlFall%2CMeldeDatum&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields=MeldeDatum&groupByFieldsForStatistics=MeldeDatum&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22AnzahlFall%22%2C%22outStatisticFieldName%22%3A%22cases%22%7D%5D%0D%0A&having=&resultOffset=&resultRecordCount=&sqlFormat=none&f=pjson&token=` | |
return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) | |
} | |
async dCases() { | |
const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) | |
const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?f=json&where=NeuerFall%20IN(1,%20-1)&returnGeometry=false&geometry=42.000,12.000&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&outFields=*&outStatistics=%5B%7B%22statisticType%22:%22sum%22,%22onStatisticField%22:%22AnzahlFall%22,%22outStatisticFieldName%22:%22cases%22%7D,%20%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22MeldeDatum%22,%22outStatisticFieldName%22:%22date%22%7D%5D&resultType=standard&cacheHint=true` | |
const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=NeuerFall+IN%281%2C0%29+AND+MeldeDatum+%3E%3D+TIMESTAMP+%27${apiStartDate}%27&objectIds=&time=&resultType=standard&outFields=AnzahlFall%2CMeldeDatum&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnDistinctValues=false&cacheHint=false&orderByFields=MeldeDatum&groupByFieldsForStatistics=MeldeDatum&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22AnzahlFall%22%2C%22outStatisticFieldName%22%3A%22cases%22%7D%5D%0D%0A&having=&resultOffset=&resultRecordCount=&sqlFormat=none&f=pjson&token=` | |
return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) | |
} | |
async rvalue() { | |
const url = `https://www.rki.de/DE/Content/InfAZ/N/Neuartiges_Coronavirus/Projekte_RKI/Nowcasting_Zahlen_csv.csv?__blob=publicationFile` | |
const response = await this.exec(url, false) | |
return (response.status === ENV.status.ok) ? Format.rValue(response.data) : false | |
} | |
async vaccinevalues () { | |
const url = `https://rki-vaccination-data.vercel.app/api` | |
const response = await this.exec(url) | |
return (response.status === ENV.status.ok) ? response.data : false | |
} | |
async getCases(urlToday, urlHistory) { | |
const responseToday = await this.exec(urlToday) | |
const responseHistory = await this.exec(urlHistory) | |
if (responseToday.status === ENV.status.ok && responseHistory.status === ENV.status.ok) { | |
let data = responseHistory.data.features.map(day => { return { cases: day.attributes.cases, date: day.attributes.MeldeDatum } }) | |
let todayCases = responseToday.data.features.reduce((a, b) => a + b.attributes.cases, 0) | |
let lastDateHistory = Math.max(...responseHistory.data.features.map(a => a.attributes.MeldeDatum)) | |
let lastDateToday = Math.max(...responseToday.data.features.map(a => a.attributes.date)) | |
let lastDate = lastDateHistory; | |
if (!!lastDateToday || new Date(lastDateToday).setHours(0, 0, 0, 0) <= new Date(lastDateHistory).setHours(0, 0, 0, 0)) { | |
let lastReportDate = new Date(lastDateHistory) | |
lastDate = lastReportDate.setDate(lastReportDate.getDate() + 1); | |
} | |
data.push({ cases: todayCases, date: lastDate }) | |
data = Data.completeHistory(data) | |
return data; | |
} | |
return false; | |
} | |
async exec(url, isJson = true) { | |
try { | |
const resData = new Request(url) | |
resData.timeoutInterval = 60 | |
let data = {} | |
let status = ENV.status.ok | |
if (isJson) { | |
data = await resData.loadJSON() | |
} else { | |
data = await resData.loadString() | |
} | |
status = this.checkStatus(data, isJson) | |
return new DataResponse(data, status) | |
} catch (e) { | |
console.warn(e) | |
return new DataResponse({}, ENV.status.notfound) | |
} | |
} | |
checkStatus (data, isJson) { | |
if (typeof data.length === '') return ENV.status.notfound | |
if (isJson && typeof data.error !== 'undefined') return ENV.status.notfound | |
return ENV.status.ok | |
} | |
} | |
class Parse { | |
static input(input) { | |
const _coords = [] | |
const _staticCoordinates = input.split(";").map(coords => { | |
return coords.split(',') | |
}) | |
_staticCoordinates.forEach(coords => { | |
_coords[parseInt(coords[0])] = { | |
index: parseInt(coords[0]), | |
latitude: parseFloat(coords[1]), | |
longitude: parseFloat(coords[2]), | |
name: (coords[3]) ? coords[3] : false | |
} | |
}) | |
return _coords | |
} | |
static rCSV(rDataStr) { | |
let lines = rDataStr.split(/(?:\r\n|\n)+/).filter(function (el) { return el.length != 0 }) | |
let headers = lines.splice(0, 1)[0].split(";"); | |
let elements = [] | |
for (let i = 0; i < lines.length; i++) { | |
let element = {}; | |
let j = 0; | |
let values = lines[i].split(';') | |
element = values.reduce(function (result, field, index) { | |
result[headers[index]] = field; | |
return result; | |
}, {}) | |
elements.push(element) | |
} | |
return elements | |
} | |
} | |
class Helper { | |
static getIncidenceLimits(incidence) { | |
if (incidence >= ENV.incidenceColors.green.limit && incidence < ENV.incidenceColors.yellow.limit) { | |
return { min: ENV.incidenceColors.green.limit, max: ENV.incidenceColors.yellow.limit } | |
} else if (incidence >= ENV.incidenceColors.yellow.limit && incidence < ENV.incidenceColors.orange.limit) { | |
return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } | |
} else if (incidence >= ENV.incidenceColors.orange.limit && incidence < ENV.incidenceColors.red.limit) { | |
return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } | |
} else if (incidence >= ENV.incidenceColors.red.limit && incidence < ENV.incidenceColors.darkred.limit) { | |
return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } | |
} else if (incidence >= ENV.incidenceColors.darkred.limit && incidence < ENV.incidenceColors.darkdarkred.limit) { | |
return { min: ENV.incidenceColors.darkred.limit, max: ENV.incidenceColors.darkdarkred.limit } | |
} else if (incidence > ENV.incidenceColors.darkdarkred.limit) { | |
return { min: ENV.incidenceColors.darkdarkred.limit, max: 500 } | |
} | |
return { min: 0, max: 0 } | |
} | |
static calcIncidence(cacheID) { | |
const casesData = [...ENV.cache[cacheID].data] | |
if (CFG.debugIncidenceCalc) Helper.log('calcIncidence', cacheID) | |
for(let i = 0; i < CFG.graphShowDays; i++) { | |
let theDays = casesData.slice(i + 1, i + 1 + 7) // without today | |
let sumCasesLast7Days = theDays.reduce((a, b) => a + b.cases, 0) | |
casesData[i].incidence = (sumCasesLast7Days / ENV.cache[cacheID].meta.EWZ) * 100000 | |
if (CFG.debugIncidenceCalc) Helper.log(Format.dateStr(casesData[i].date), casesData[i].cases, casesData[i].incidence) | |
} | |
// @TODO Workaround use incidence from api | |
if (CFG.disableLiveIncidence && typeof ENV.cache[cacheID].meta.cases7_per_100k !== 'undefined') { | |
casesData[0].incidence = ENV.cache[cacheID].meta.cases7_per_100k | |
} | |
ENV.cache[cacheID].data = casesData.reverse() | |
} | |
static getDateBefore(days) { | |
let offsetDate = new Date() | |
offsetDate.setDate(new Date().getDate() - days) | |
return offsetDate.toISOString().split('T').shift() | |
} | |
static async getLocation(staticCoordinateIndex = false) { | |
if (typeof ENV.staticCoordinates[staticCoordinateIndex] !== 'undefined' && Object.keys(ENV.staticCoordinates[staticCoordinateIndex]).length >= 3) { | |
return ENV.staticCoordinates[staticCoordinateIndex] | |
} | |
try { | |
Location.setAccuracyToThreeKilometers() | |
return await Location.current() | |
} catch (e) { | |
console.warn(e) | |
} | |
return null; | |
} | |
static log(...data) { | |
console.log(data.map(JSON.stringify).join(' | ')) | |
} | |
} | |
const cfm = new CustomFilemanager() | |
const rkiRequest = new RkiRequest() | |
await new IncidenceWidget().init() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment