├── .github └── stale.yml ├── LICENSE ├── README.md ├── incidence.js └── screenshots ├── info.jpg ├── screenshot.jpg ├── screenshot_hospitalization.jpg ├── screenshot_vaccine.jpg └── widgetparameter.jpg /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - planned 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | Hi 👋 This issue has been automatically marked as stale 📌 because it has not had recent activity. It will be closed 🔒 if no further activity occurs. Thank you for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 4 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 5 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 6 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 7 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 8 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 9 | SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Corona Inzidenz, Impfquoten, Hospitalisierungen Widget für iOS (Scriptable) 2 | 3 | Widget zeigt die Inzidenz, tägl. neue Fälle, den Verlauf für 21 Tage (Inzidenz / neue Fälle) sowie Infos zu den Impfungen/Hospitalisierungen. 4 | 5 | ```diff 6 | + SIEHE "FEATURES" und KONFIGURATIONS ABSCHNITT FÜR AKTUELLE FUNKTIONSWEISE-/UMFANG! 7 | ``` 8 | 9 | **Inzidenz** 10 | ![IMG_5438](https://raw.githubusercontent.com/rphl/corona-widget/master/screenshots/screenshot.jpg) 11 | 12 | **+ Impfquoten** 13 | ![IMG_5438](https://raw.githubusercontent.com/rphl/corona-widget/master/screenshots/screenshot_vaccine.jpg) 14 | 15 | **+ Hospitalisierungen (Ampel)** 16 | ![IMG_5438](https://raw.githubusercontent.com/rphl/corona-widget/master/screenshots/screenshot_hospitalization.jpg) 17 | 18 | _Dank der positiven Resonanz, jetzt im Repo zur einfacheren Wartung/Erweiterung ( [Mein original GIST](https://gist.github.com/rphl/0491c5f9cb345bf831248732374c4ef5) ) Feedback, PRs, etc. sind Willkommen._ 19 | 20 | # ✨ Features 21 | 22 | * **Live Inzidenz** + **Wochentrend!** für Stadt/Kreis, Bundesland, Bund 23 | * **Neue tägl. Fälle** für Stadt/Kreis, Bundesland, Bund 24 | * 21 Tage Diagram für **Inzidenz** oder **Neue tägl. Fälle** je Stadt/Kreis, Bundesland, Bund 25 | * 7 Tage Schätzwert für **Reproduktionszahl (R)** 26 | * tägl. **Impfquoten-/zahlen** _(Siehe Konfiguration!)_ 27 | * tägl. **Hospitalisierungen/-Incidence** _(Siehe Konfiguration!)_ 28 | * iCloud Sync (Optional) 29 | * Automatischer Offlinemodus (📡 = Kein GPS ⚡️ = Kein Internet/Keine aktuellen RKI Daten) 30 | * Dark/Lighmode unterstützung (_Siehe Konfiguration_) 31 | * Autoupdate (Siehe Installation/Update) 32 | * Eine Art **"Themes"**: Farben/Hintergrundbild. (_Siehe Konfiguration_) 33 | * ... 34 | 35 | ![IMG_5438](https://raw.githubusercontent.com/rphl/corona-widget/master/screenshots/info.jpg) 36 | 37 | 38 | # 📚 Quelle/Datenbasis 39 | 40 | * Das Widget basiert auf der offziellen Api des RKI. https://npgeo-corona-npgeo-de.hub.arcgis.com/ 41 | * Die bereitgestellten Daten können in bestimmten Regionen auf Grund von Meldeverzögerungen durch Ämter an das RKI (Api) erst Verzögert (Stunden-Tage) im Widget angezeigt werden. 42 | * Für die Historie werden ausschliesslich Daten aus der Api verwendet. Somit können sich auf Grund von Verzögerunen/Aktualisierungen Werte wie Inzidenzen, neuen Fälle, etc. immer ändern. 43 | * "Live-Inzidenz" basiert immer auf den gemeldeten neuen Fälle aus der Api! Und _kann_ sich von dem statischen Wert aus der (RKI) Api unterscheiden. Siehe auch _Erweiterte Konfiguration: Optionen_. 44 | 45 | 46 | # 📲 Installation/Update 47 | 48 | **Manuell** 49 | * Safari öffnen: https://raw.githubusercontent.com/rphl/corona-widget/master/incidence.js 50 | * Skripttext kopieren 51 | * Scriptable öffnen, kopierten Skripttext als neues Scriptablescript einfügen oder altes erstzen. 52 | 53 | **Update** 54 | * Wenn `CFG.scriptSelfUpdate: true ` aktualisiert sich das Skript im Intervall selbst (Kann via `CFG.scriptSelfUpdate: false` abgestellt werden) 55 | * ...andere Option: https://scriptdu.de/ 56 | 57 | 58 | # ⚙️ Konfiguration 59 | 60 | * Daten werden unter **Dateien (App)** > **iCloud** > **Scriptable** > **coronaWidgetNext** > *.json zwischengespeichert. 61 | * Die allgemeine Konfiguration erfolgt mittels **WidgetParameter**: 62 | 63 | ![IMG_5438](https://raw.githubusercontent.com/rphl/corona-widget/master/screenshots/widgetparameter.jpg) 64 | 65 | 66 | ## Statische Standort Koordinaten 67 | 68 | Das Widget erkennt automatisch den Standort. Es ist jedoch möglich den Standort fest zu setzten. Die Koordinaten können z.B. über die Karten App ermittelt werden. Format: `{POSITION},{LAT},{LON};{POSITION},{LAT},{LON}` 69 | 70 | * `{POSITION}` = Position im Widget. z.B: 0=ErsterStandrt, 1=ZweiterStandort (Zweispaltes MediumWidget) 71 | * `{LAT}` = Breitengrad. z.B: 51.1234 _(NICHT 51,1234 - Kein Komma!)_ 72 | * `{LON}` = Längengrad. z.B: 11.1234 _(NICHT 11,1234 - Kein Komma!)_ 73 | 74 | **Beispiele** 75 | 76 | * Erster Standort statisch (SmallWidget): `0,51.1244,6.7353` 77 | * Zweiter Standort ist statisch (MediumWidget): `1,51.1244,6.7353` 78 | * Beide Standorte sind statisch (MediumWidget): `0,51.1244,6.7353;1,51.1244,6.7353` 79 | * Nur zweiter Standort ist statisch (MediumWidget): `1,51.1244,6.7353` 80 | 81 | 82 | ## Eigene Standortnamen 83 | 84 | Standorte selbst bennenen. Format: `{POSITION},{LAT},{LON},{NAME};{POSITION},{LAT},{LON},{NAME}` 85 | 86 | * `{NAME}` = Name der anstalle der offizielen Bezeichnung aus der API verwendet wird. 87 | 88 | **Beispiele** 89 | 90 | * Eigener Name z.B "Home" für den ersten Standort: `0,51.1244,6.7353,Home` 91 | * Eigener Name z.B "Work" für den zweiten Standort: `1,51.1244,6.7353,Work` 92 | 93 | ## Erweiterte Konfiguration 94 | 95 | Das Skript kann auch über bestimmte Optionen konfiguriert werden. (Änderungen direkt in der incidence.js werden bei `scriptSelfUpdate=true` überschrieben) 96 | 97 | * Die dauerhafte Konfiguration wird in einer externen Datei gespeichert. 98 | * Die Konfigurationsdatei muss selbst angelegt werden: `coronaWidgetNext/config.json`. Diese ist nicht in Scriptable sichtbar! 99 | * Zum anlegen und bearbeiten kann z.B Kodex https://apps.apple.com/de/app/kodex/id1038574481 für iPhone/iPad verwendet werden. 100 | 101 | **Optionen:** 102 | * `theme: ''` Automatic Light/Darkmode switch = `''` OR lightmode only = `light` OR darkmode only = `dark` 103 | * `showDataInRow 'hospitalization'` // show "vaccine", "hospitalization", or false (statictics) based on RKI reports. MEDIUMWIDGET IS REQUIRED! 104 | `showDataInBlocks: 'vaccine'` // show "vaccine", "hospitalization", or false (disabled) based on RKI reports (State/Country). MEDIUMWIDGET IS REQUIRED! 105 | 106 | * `openUrl: false` "https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4", open RKI URL on tap, set false to disable 107 | * `graphShowValues: 'i'` 'i' = incidence OR 'c' = cases 108 | * `graphShowDays: 21` show days in graph 109 | * `csvRvalueFields: ['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 110 | * `scriptRefreshInterval: 5400` refresh after 1,5 hours (in seconds) 111 | * `scriptSelfUpdate: false` script updates itself, 112 | * `disableLiveIncidence: false` show old, static incidance. update ONLY ONCE A DAY on intial RKI import 113 | * `debugIncidenceCalc: false` show all calculated incidencevalues on console 114 | 115 | 116 | **BEISPIELE** config.json = 117 | 118 | **RKI Dashboard beim antippen öffnen** 119 | ``` 120 | { 121 | "openUrl": "https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4", 122 | } 123 | ``` 124 | 125 | **Dark-/Light anpassen. Nur Lightmode nutzen:** 126 | ``` 127 | { 128 | "theme": "light" 129 | .... 130 | "themes": { 131 | "light": { 132 | ... 133 | }, 134 | "dark": { 135 | ... 136 | } 137 | } 138 | ``` 139 | 140 | **Farben anpassen. --- Es müssen immer alle Werte eine Themes überschrieben werden ---** 141 | 142 | Optional kann je Theme mit `mainBackgroundImageURL` eine URL zum Hintergrundbild gesetzt werden (Siehe Themewerte) 143 | 144 | **Standard Light Farben:** 145 | 146 | ``` 147 | { 148 | "themes": { 149 | "light": { 150 | "mainBackgroundImageURL": "", 151 | "mainBackgroundColor": "#f0f0f0", 152 | "stackBackgroundColor": "#99999920", 153 | "stackBackgroundColorSmall": "#99999915", 154 | "stackBackgroundColorSmallTop": "#99999900", 155 | "areaIconBackgroundColor": "#99999930", 156 | "titleTextColor": "#222222", 157 | "titleRowTextColor": "#222222", 158 | "titleRowTextColor2": "#222222", 159 | "smallNameTextColor": "#777777", 160 | "dateTextColor": "#777777", 161 | "dateTextColor2": "#777777", 162 | "graphTextColor": "#888888", 163 | "incidenceColorsDarkdarkred": "#941100", 164 | "incidenceColorsDarkred": "#c01a00", 165 | "incidenceColorsRed": "#f92206", 166 | "incidenceColorsOrange": "#faa31b", 167 | "incidenceColorsYellow": "#ffff64", 168 | "incidenceColorsGreen": "#00cc00", 169 | "incidenceColorsGray": "#d0d0d0" 170 | } 171 | } 172 | } 173 | ``` 174 | **Standard Dark Farben:** 175 | 176 | ``` 177 | { 178 | "themes": { 179 | "dark": { 180 | "mainBackgroundImageURL": "", 181 | "mainBackgroundColor": "#9999999", 182 | "stackBackgroundColor": "#99999920", 183 | "stackBackgroundColorSmall": "#99999915", 184 | "stackBackgroundColorSmallTop": "#99999900", 185 | "areaIconBackgroundColor": "#99999930", 186 | "titleTextColor": "#f0f0f0", 187 | "titleRowTextColor": "#f0f0f0", 188 | "titleRowTextColor2": "#f0f0f0", 189 | "smallNameTextColor": "#888888", 190 | "dateTextColor": "#777777", 191 | "dateTextColor2": "#777777", 192 | "graphTextColor": "#888888", 193 | "incidenceColorsDarkdarkred": "#941100", 194 | "incidenceColorsDarkred": "#c01a00", 195 | "incidenceColorsRed": "#f92206", 196 | "incidenceColorsOrange": "#faa31b", 197 | "incidenceColorsYellow": "#ffff64", 198 | "incidenceColorsGreen": "#00cc00", 199 | "incidenceColorsGray": "#d0d0d0" 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | **Nur Impfquoten anzeigen** 206 | ``` 207 | { 208 | "showDataInBlocks": 'vaccine' 209 | } 210 | ``` 211 | 212 | **... oder** 213 | ``` 214 | { 215 | "openUrl": "https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4", 216 | "showDataInBlocks": 'vaccine' 217 | } 218 | ``` 219 | -------------------------------------------------------------------------------- /incidence.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: red; icon-glyph: briefcase-medical; 4 | 5 | /** 6 | * Licence: Robert Koch-Institut (RKI), dl-de/by-2-0 7 | * 8 | * 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. 9 | * 10 | * AUTHOR: https://github.com/rphl - https://github.com/rphl/corona-widget/ 11 | * ISSUES: https://github.com/rphl/corona-widget/issues 12 | * 13 | */ 14 | 15 | // ============= ============= ============= ============= ================= 16 | // ÄNDERUNGEN HIER, WERDEN BEI AKTIVEN AUTOUPDATE ÜBERSCHRIEBEN 17 | // ZUR KONFIGURATION SIEHE README! 18 | // https://github.com/rphl/corona-widget#erweiterte-konfiguration 19 | // ============= ============= ============= ============= ================= 20 | 21 | let CFG = { 22 | theme: '', // '' = Automatic Ligh/Darkmode based on iOS. light = only lightmode is used, dark = only darkmode is used 23 | showDataInRow: false, // show "hospitalization", or false (statictics) values based on RKI reports. MEDIUMWIDGET IS REQUIRED! 24 | showDataInBlocks: 'hospitalization', // show "hospitalization", or false disabled based on RKI reports (State/Country). Vaccine only in MEDIUMWIDGET. Hospitalization only in SMALLWIDGET. 25 | openUrl: false, //"https://experience.arcgis.com/experience/478220a4c454480e823b17327b2bf1d4", // open RKI dashboard on tap, set false to disable 26 | graphShowValues: 'i', // 'i' = incidence OR 'c' = cases 27 | graphShowDays: 21, // show days in graph 28 | 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', 'PS_7_Tage_R_Wert'], // try to find possible field (column) with rvalue, because rki is changing columnsnames and encoding randomly on each update 29 | scriptRefreshInterval: 5400, // refresh after 1,5 hours (in seconds) 30 | scriptSelfUpdate: false, // script updates itself, 31 | disableLiveIncidence: false, // show old, static incidance. update ONLY ONCE A DAY on intial RKI import 32 | debugIncidenceCalc: false // show all calculated incidencevalues on console 33 | } 34 | 35 | // ============= ============= ============= ============= ================= 36 | // HALT, STOP !!! 37 | // NACHFOLGENDE ZEILEN NUR AUF EIGENE GEFAHR ÄNDERN !!! 38 | // ============= ============= ============= ============= ================= 39 | 40 | const ENV = { 41 | themes: { 42 | light: { 43 | mainBackgroundImageURL: '', 44 | mainBackgroundColor: '#f0f0f0', 45 | stackBackgroundColor: '#99999920', 46 | stackBackgroundColorSmall: '#99999915', 47 | stackBackgroundColorSmallTop: '#99999900', 48 | areaIconBackgroundColor: '#99999930', 49 | titleTextColor: '#222222', 50 | titleRowTextColor: '#222222', 51 | titleRowTextColor2: '#222222', 52 | smallNameTextColor: '#777777', 53 | dateTextColor: '#777777', 54 | dateTextColor2: '#777777', 55 | graphTextColor: '#888888', 56 | incidenceColorsLila: '#93079f', 57 | incidenceColorsPink: '#D90183', 58 | incidenceColorsDarkdarkred: '#941100', 59 | incidenceColorsDarkred: '#c01a00', 60 | incidenceColorsRed: '#f92206', 61 | incidenceColorsOrange: '#faa31b', 62 | incidenceColorsYellow: '#ffd800', 63 | incidenceColorsGreen: '#00cc00', 64 | incidenceColorsGray: '#d0d0d0' 65 | }, 66 | dark: { 67 | mainBackgroundImageURL: '', 68 | mainBackgroundColor: '#9999999', 69 | stackBackgroundColor: '#99999920', 70 | stackBackgroundColorSmall: '#99999910', 71 | stackBackgroundColorSmallTop: '#99999900', 72 | areaIconBackgroundColor: '#99999930', 73 | titleTextColor: '#f0f0f0', 74 | titleRowTextColor: '#f0f0f0', 75 | titleRowTextColor2: '#f0f0f0', 76 | smallNameTextColor: '#888888', 77 | dateTextColor: '#777777', 78 | dateTextColor2: '#777777', 79 | graphTextColor: '#888888', 80 | incidenceColorsLila: '#93079f', 81 | incidenceColorsPink: '#D90183', 82 | incidenceColorsDarkdarkred: '#941100', 83 | incidenceColorsDarkred: '#c01a00', 84 | incidenceColorsRed: '#f92206', 85 | incidenceColorsOrange: '#faa31b', 86 | incidenceColorsYellow: '#ffd800', 87 | incidenceColorsGreen: '#00cc00', 88 | incidenceColorsGray: '#d0d0d0' 89 | } 90 | }, 91 | incidenceColors: { 92 | lila: { limit: 1000, color: 'incidenceColorsLila' }, 93 | pink: { limit: 500, color: 'incidenceColorsPink' }, 94 | darkdarkred: { limit: 250, color: 'incidenceColorsDarkdarkred' }, 95 | darkred: { limit: 100, color: 'incidenceColorsDarkred' }, 96 | red: { limit: 50, color: 'incidenceColorsRed' }, 97 | orange: { limit: 35, color: 'incidenceColorsOrange' }, 98 | yellow: { limit: 25, color: 'incidenceColorsYellow' }, 99 | green: { limit: 1, color: 'incidenceColorsGreen' }, 100 | gray: { limit: 0, color: 'incidenceColorsGray' } 101 | }, 102 | hospitalizedIncidenceLimits: { 103 | green: { limit: 0, color: 'incidenceColorsGreen' }, 104 | yellow: { limit: 3, color: 'incidenceColorsYellow' }, 105 | orange: { limit: 6, color: 'incidenceColorsOrange' }, 106 | red: { limit: 9, color: 'incidenceColorsDarkdarkred' }, 107 | }, 108 | statesAbbr: { 109 | '8': 'BW', 110 | '9': 'BY', 111 | '11': 'BE', 112 | '12': 'BB', 113 | '4': 'HB', 114 | '2': 'HH', 115 | '6': 'HE', 116 | '13': 'MV', 117 | '3': 'NI', 118 | '5': 'NW', 119 | '7': 'RP', 120 | '10': 'SL', 121 | '14': 'SN', 122 | '15': 'ST', 123 | '1': 'SH', 124 | '16': 'TH' 125 | }, 126 | vaccineSatesAbbr: { 127 | '8' : 'Baden-Württemberg', 128 | '9' : 'Bayern', 129 | '11' : 'Berlin', 130 | '12' : 'Brandenburg', 131 | '4' : 'Bremen', 132 | '2' : 'Hamburg', 133 | '6' : 'Hessen', 134 | '13' : 'Mecklenburg-Vorpommern', 135 | '3' : 'Niedersachsen', 136 | '5' : 'Nordrhein-Westfalen', 137 | '7' : 'Rheinland-Pfalz', 138 | '10' : 'Saarland', 139 | '14' : 'Sachsen', 140 | '15' : 'Sachsen-Anhalt', 141 | '1' : 'Schleswig-Holstein', 142 | '16' : 'Thüringen' 143 | }, 144 | areaIBZ: { 145 | '40': 'KS',// Kreisfreie Stadt 146 | '41': 'SK', // Stadtkreis 147 | '42': 'K', // Kreis 148 | '46': 'K', // Sonderverband offiziel Kreis 149 | '43': 'LK', // Landkreis 150 | '45': 'LK', // Sonderverband offiziel Landkreis 151 | null: 'BZ', 152 | '': 'BZ' 153 | }, 154 | fonts: { 155 | xlarge: Font.boldSystemFont(26), 156 | large: Font.mediumSystemFont(20), 157 | medium: Font.mediumSystemFont(14), 158 | normal: Font.mediumSystemFont(12), 159 | small: Font.boldSystemFont(11), 160 | small2: Font.boldSystemFont(10), 161 | xsmall: Font.boldSystemFont(9) 162 | }, 163 | status: { 164 | nogps: 555, 165 | offline: 418, 166 | notfound: 404, 167 | error: 500, 168 | ok: 200, 169 | fromcache: 418 170 | }, 171 | isMediumWidget: config.widgetFamily === 'medium', 172 | isSameState: false, 173 | cache: {}, 174 | staticCoordinates: [], 175 | script: { 176 | selfUpdate: CFG.scriptSelfUpdate, 177 | filename: this.module.filename.replace(/^.*[\\\/]/, ''), 178 | updateStatus: '' 179 | } 180 | } 181 | 182 | class Theme { 183 | static getCurrentTheme () { 184 | let theme = 'auto'; 185 | if (CFG.theme === 'light' || CFG.theme === 'dark') { 186 | theme = CFG.theme 187 | } 188 | return theme 189 | } 190 | static getColor(colorName, useDefault = false) { 191 | let theme = Theme.getCurrentTheme(); 192 | if (theme === 'auto' && useDefault) { 193 | theme = 'light'; 194 | } 195 | if (theme === 'light' || theme === 'dark') { 196 | return new Color(ENV.themes[theme][colorName]) 197 | } 198 | return false // no color preferred 199 | } 200 | static setColor(object, propertyName, colorName, useDefault = false) { 201 | if (CFG.theme === 'light' || CFG.theme === 'dark' || useDefault) { 202 | let theme = Theme.getCurrentTheme(); 203 | object[propertyName] = new Color(ENV.themes[theme][colorName]) 204 | } 205 | } 206 | } 207 | 208 | class IncidenceWidget { 209 | constructor(coordinates = []) { 210 | this.loadConfig(); 211 | if (args.widgetParameter) ENV.staticCoordinates = Parse.input(args.widgetParameter) 212 | ENV.staticCoordinates = [...ENV.staticCoordinates, ...coordinates] 213 | if (typeof ENV.staticCoordinates[1] !== 'undefined' && Object.keys(ENV.staticCoordinates[1]).length >= 3) ENV.isMediumWidget = true 214 | Helper.log("Current Theme:", Theme.getCurrentTheme()) 215 | this.selfUpdate() 216 | } 217 | async init() { 218 | this.widget = await this.createWidget() 219 | this.widget.setPadding(0, 0, 0, 0) 220 | 221 | if (Theme.getCurrentTheme() === 'light' || Theme.getCurrentTheme() === 'dark') { 222 | const backgroundImageUrl = ENV.themes[Theme.getCurrentTheme()]['mainBackgroundImageURL'] 223 | if (backgroundImageUrl !== '') { 224 | const i = await new Request(backgroundImageUrl); 225 | const img = await i.loadImage(); 226 | this.widget.backgroundImage = img 227 | } 228 | } 229 | 230 | Theme.setColor(this.widget, 'backgroundColor', 'mainBackgroundColor') 231 | 232 | if (!config.runsInWidget) { 233 | (ENV.isMediumWidget) ? await this.widget.presentMedium() : await this.widget.presentSmall() 234 | } 235 | Script.setWidget(this.widget) 236 | Script.complete() 237 | } 238 | async createWidget() { 239 | const list = new ListWidget() 240 | const statusPos0 = await Data.load(0) 241 | const statusPos1 = (ENV.isMediumWidget && typeof ENV.staticCoordinates[1] !== 'undefined') ? await Data.load(1) : false 242 | 243 | // UI =============== 244 | let topBar = new UI(list).stack('h', [4, 8, 4, 4]) 245 | topBar.text("🦠", Font.mediumSystemFont(22)) 246 | topBar.space(3) 247 | 248 | if (statusPos0 === ENV.status.error || statusPos1 === ENV.status.error) { 249 | topBar.space() 250 | list.addSpacer() 251 | let statusError = new UI(list).stack('v', [4, 6, 4, 6]) 252 | statusError.text('⚡️', ENV.fonts.medium) 253 | statusError.text('Standortdaten konnten nicht geladen werden. \nKein Cache verfügbar. \n\nBitte später nochmal versuchen.', ENV.fonts.small, Theme.getColor('titleTextColor')) 254 | list.addSpacer(4) 255 | list.refreshAfterDate = new Date(Date.now() + ((CFG.scriptRefreshInterval / 2) * 1000)) 256 | return list 257 | } 258 | 259 | Helper.calcIncidence('s0') 260 | Helper.calcIncidence(ENV.cache['s0'].meta.BL_ID) 261 | Helper.calcIncidence('d') 262 | 263 | ENV.isSameState = false; 264 | if (statusPos0 === statusPos1) { 265 | ENV.isSameState = (ENV.cache['s0'].meta.BL_ID === ENV.cache['s1'].meta.BL_ID) 266 | } 267 | 268 | if (statusPos1) Helper.calcIncidence('s1') 269 | if (statusPos1 && !ENV.isSameState) Helper.calcIncidence(ENV.cache['s1'].meta.BL_ID) 270 | 271 | let topRStack = new UI(topBar).stack('v', [0,0,0,0]) 272 | topRStack.text(Format.number(ENV.cache.d.meta.r, 2, 'n/v') + 'ᴿ', ENV.fonts.medium, Theme.getColor('titleTextColor')) 273 | let updatedDate = Format.dateStr(ENV.cache.d.getDay().date); 274 | let updatedTime = ('' + new Date().getHours()).padStart(2, '0') + ':' + ('' + new Date().getMinutes()).padStart(2, '0') 275 | topRStack.text(updatedDate + ' ' +updatedTime, ENV.fonts.xsmall, Theme.getColor('dateTextColor', true)) 276 | 277 | 278 | topBar.space() 279 | UIComp.statusBlock(topBar, statusPos0) 280 | topBar.space(4) 281 | 282 | if (ENV.isMediumWidget && !ENV.isSameState && statusPos1) { 283 | topBar.space() 284 | UIComp.smallIncidenceRow(topBar, 'd', 'stackBackgroundColorSmallTop') 285 | } 286 | 287 | UIComp.incidenceVaccineRows(list) 288 | list.addSpacer(3) 289 | 290 | let stateBar = new UI(list).stack('h', [0, 0, 0, 0]) 291 | stateBar.space(6) 292 | let leftCacheID = ENV.cache['s0'].meta.BL_ID 293 | if (ENV.isMediumWidget) { UIComp.smallIncidenceRow(stateBar, leftCacheID) } else { UIComp.smallIncidenceBlock(stateBar, leftCacheID) } 294 | stateBar.space(4) 295 | 296 | // DEFAULT IS GER... else STATE 297 | let rightCacheID = (ENV.isMediumWidget && !ENV.isSameState && statusPos1) ? ENV.cache['s1'].meta.BL_ID : 'd' 298 | if (ENV.isMediumWidget) { UIComp.smallIncidenceRow(stateBar, rightCacheID) } else { UIComp.smallIncidenceBlock(stateBar, rightCacheID) } 299 | stateBar.space(6) 300 | list.addSpacer(5) 301 | 302 | // UI =============== 303 | if (CFG.openUrl) list.url = CFG.openUrl 304 | list.refreshAfterDate = new Date(Date.now() + (CFG.scriptRefreshInterval * 1000)) 305 | return list 306 | } 307 | async selfUpdate() { 308 | if (!ENV.script.selfUpdate) return 309 | Helper.log('script selfUpdate', 'running') 310 | let url = 'https://raw.githubusercontent.com/rphl/corona-widget/master/incidence.js'; 311 | let request = new Request(url) 312 | let filenameBak = ENV.script.filename.replace('.js', '.bak.js') 313 | try { 314 | let script = await request.loadString() 315 | if (script !== '') { 316 | if (cfm.fm.fileExists(filenameBak)) await cfm.fm.remove(filenameBak) 317 | cfm.copy(ENV.script.filename, filenameBak) 318 | script = script.replace("scriptSelfUpdate: false", "scriptSelfUpdate: true") 319 | cfm.save(script, ENV.script.filename) 320 | ENV.script.updateStatus = 'updated' 321 | Helper.log('script selfUpdate', ENV.script.updateStatus); 322 | } 323 | } catch (e) { 324 | console.warn(e) 325 | if (cfm.fm.fileExists(filenameBak)) { 326 | // await cfm.fm.copy(filenameBak, ENV.script.filename) 327 | // await cfm.fm.remove(filenameBak) 328 | ENV.script.updateStatus = 'loading failed, rollback?' 329 | Helper.log('script selfUpdate', ENV.script.updateStatus); 330 | } 331 | } 332 | } 333 | async loadConfig () { 334 | let path = cfm.fm.joinPath(cfm.configPath, 'config.json'); 335 | if (cfm.fm.fileExists(path)) { 336 | Helper.log('Loading config.json (defaults will be overwritten)') 337 | const cfg = await cfm.read('config') 338 | if (typeof cfg.data.themes !== 'undefined' && typeof cfg.data.themes.dark !== 'undefined') { 339 | ENV.themes.dark = Object.assign(ENV.themes.dark, cfg.data.themes.dark) 340 | } 341 | Object.keys(ENV.themes).forEach(theme => { 342 | if (typeof cfg.data.themes !== 'undefined' && typeof cfg.data.themes[theme] !== 'undefined') { 343 | Helper.log('Loading custom theme from config.json: ' + theme) 344 | ENV.themes[theme] = Object.assign(ENV.themes[theme], cfg.data.themes[theme]) 345 | } 346 | }) 347 | if (cfg.status === ENV.status.ok) CFG = Object.assign(CFG, cfg.data) 348 | } 349 | } 350 | } 351 | 352 | class UIComp { 353 | static incidenceVaccineRows(view) { 354 | let b = new UI(view).stack('v', [4, 6, 4, 6]) 355 | let bb = new UI(b).stack('v', false, Theme.getColor('stackBackgroundColor', true), 10) 356 | let padding = [4, 6, 4, 4] 357 | if (ENV.isMediumWidget) { 358 | padding = [2, 8, 2, 8] 359 | } 360 | let bb2 = new UI(bb).stack('h', padding, Theme.getColor('stackBackgroundColor', true), 10) 361 | UIComp.incidenceRow(bb2, 's0') 362 | 363 | let bb3 = new UI(bb).stack('h', padding) 364 | if (ENV.isMediumWidget && CFG.showDataInRow === false && typeof ENV.cache.s1 === 'undefined') { 365 | UIComp.statisticsRow(bb3, 's0') 366 | } else if (ENV.isMediumWidget && CFG.showDataInRow === 'hospitalization' && typeof ENV.cache.s1 === 'undefined' && typeof ENV.cache.hospitalization !== 'undefined') { 367 | UIComp.hospitalizationRow(bb3, 's0') 368 | } else if (ENV.isMediumWidget && typeof ENV.cache.s1 !== 'undefined') { 369 | UIComp.incidenceRow(bb3, 's1') 370 | } else if (!ENV.isMediumWidget) { 371 | bb3.space() 372 | UIComp.areaIcon(bb3, ENV.cache['s0'].meta.IBZ) 373 | bb3.space(3) 374 | let areaName = ENV.cache['s0'].meta.GEN 375 | if (typeof ENV.staticCoordinates[0] !== 'undefined' && ENV.staticCoordinates[0].name !== false) { 376 | areaName = ENV.staticCoordinates[0].name 377 | } 378 | bb3.text(areaName.toUpperCase(), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 379 | bb3.space(8) // center title if small widget 380 | bb3.space() 381 | } 382 | } 383 | static incidenceRow(view, cacheID) { 384 | let b = new UI(view).stack('h', [2,0,0,0]) 385 | let ib = new UI(b).stack('h', [0,0,0,0], false, false, false, [72, 26]) 386 | ib.elem.centerAlignContent() 387 | 388 | let incidence = ENV.cache[cacheID].getDay().incidence 389 | let incidenceFormatted = Format.number(incidence, 1, 'n/v', 100) 390 | let incidenceParts = incidenceFormatted.split(",") 391 | let incidenceFontsize = (incidence >= 1000) ? 19 : 26; 392 | ib.text(incidenceParts[0], Font.boldMonospacedSystemFont(incidenceFontsize), UI.getIncidenceColor(incidence), 1, 1) 393 | if (typeof incidenceParts[1] !== "undefined") { 394 | ib.text(',' + incidenceParts[1], Font.boldMonospacedSystemFont(18), UI.getIncidenceColor(incidence), 1, 1) 395 | } 396 | let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) 397 | 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) 398 | ib.text(trendArrow, Font.boldRoundedSystemFont(18), trendColor, 1, 0.9) 399 | 400 | if (ENV.isMediumWidget) { 401 | b.space(5) 402 | UIComp.areaIcon(b, ENV.cache[cacheID].meta.IBZ) 403 | b.space(3) 404 | let areaName = ENV.cache[cacheID].meta.GEN 405 | let cacheIndex = parseInt(cacheID.replace('s', '')) 406 | if (typeof ENV.staticCoordinates[cacheIndex] !== 'undefined' && ENV.staticCoordinates[cacheIndex].name !== false) { 407 | areaName = ENV.staticCoordinates[cacheIndex].name 408 | } 409 | b.text(areaName.toUpperCase(), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 1) 410 | } 411 | b.space() 412 | 413 | let b2 = new UI(b).stack('v', [2, 0, 0, 0], false, false, false, [58, 30]) 414 | let graphImg 415 | if (CFG.graphShowValues === 'i') { 416 | graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 16, false).getImage() 417 | } else { 418 | graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 16, false).getImage() 419 | } 420 | b2.image(graphImg) 421 | 422 | let bb2 = new UI(b2).stack('h') 423 | bb2.space() 424 | let prefix = (ENV.cache[cacheID].getDay().cases > 199999) ? '' : '+' 425 | bb2.text(prefix + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 1) 426 | bb2.space(0) 427 | } 428 | static statisticsRow(view, cacheID) { 429 | const data = ENV.cache[cacheID]; 430 | 431 | let b = new UI(view).stack('h', [4,0,4,0]) 432 | b.elem.centerAlignContent() 433 | b.space() 434 | b.text("Diff. ", ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 435 | const dayLastWeek = Format.dateStr(data.getDay(7).date, false); 436 | b.text(` ${dayLastWeek} `, ENV.fonts.xsmall, Theme.getColor('dateTextColor2', true), 1, 0.9) 437 | 438 | let diffDay = 0 439 | if (data.getDay(0).cases > 0) { 440 | diffDay = (data.getDay(0).incidence / data.getDay(7).incidence * 100) - 100; 441 | if (diffDay > 0) { 442 | b.text("+", ENV.fonts.medium, Theme.getColor(ENV.incidenceColors.red.color, true), 1, 0.9) 443 | } else if (diffDay < 0) { 444 | b.text("-", ENV.fonts.medium, Theme.getColor(ENV.incidenceColors.green.color, true), 1, 0.9) 445 | } 446 | } 447 | 448 | b.text( Format.number(Math.abs(diffDay), 2), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 449 | b.text( '% ', ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 450 | 451 | b.text(` WOCHE Ø `, ENV.fonts.xsmall, Theme.getColor('dateTextColor2', true), 1, 0.9) 452 | const diffWeek = (data.getAvg(0) / data.getAvg(1) * 100) - 100; 453 | if (diffWeek > 0) { 454 | b.text("+", ENV.fonts.medium, Theme.getColor(ENV.incidenceColors.red.color, true), 1, 0.9) 455 | } else if (diffWeek < 0) { 456 | b.text("-", ENV.fonts.medium, Theme.getColor(ENV.incidenceColors.green.color, true), 1, 0.9) 457 | } 458 | 459 | b.text( Format.number(Math.abs(diffWeek), 2) + '%', ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 460 | b.space() 461 | view.space() 462 | } 463 | static vaccineRow (view, cacheID) { 464 | 465 | let b = new UI(view).stack('h', [4,0,4,0]) 466 | b.elem.centerAlignContent() 467 | b.space() 468 | b.text("💉", ENV.fonts.normal, false, 1, 0.9) 469 | 470 | if (ENV.cache.vaccine.meta.status !== ENV.status.error) { 471 | // state data 472 | const blId = ENV.cache[cacheID].meta.BL_ID.toString().padStart(2, '0'); 473 | let stateData = ENV.cache.vaccine.data.data.filter(state => { 474 | return state.rs === blId 475 | }); 476 | stateData = stateData.pop(); 477 | 478 | // let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID 479 | // b.text(name + "", ENV.fonts.medium, Theme.getColor('titleRowTextColor2'), 1, 0.9) 480 | b.text(" ② ", ENV.fonts.normal, Theme.getColor('dateTextColor2', true), 1, 0.9) 481 | b.text(Format.number(stateData.fullyVaccinated.quote, 1) + '%', ENV.fonts.normal, Theme.getColor('titleRowTextColor2'), 1, 1) 482 | b.text(' ③ ', ENV.fonts.normal, Theme.getColor('dateTextColor2', true), 1, 0.9) 483 | b.text(Format.number(stateData.boosterVaccinated.quote, 1) + '%', ENV.fonts.normal, Theme.getColor('titleRowTextColor2'), 1, 1) 484 | b.space(4) 485 | 486 | // country data 487 | let countryData = ENV.cache.vaccine.data.data.filter(state => { 488 | return state.name === "Deutschland" 489 | }); 490 | countryData = countryData.pop(); 491 | 492 | b.text("[ D:", ENV.fonts.normal, Theme.getColor('dateTextColor2'), 1, 0.9) 493 | b.text(" ② ", ENV.fonts.normal, Theme.getColor('dateTextColor2', true), 1, 0.9) 494 | b.text(Format.number(countryData.fullyVaccinated.quote, 1) + '%', ENV.fonts.normal, Theme.getColor('dateTextColor2'), 1, 0.9) 495 | b.text(" ]", ENV.fonts.normal, Theme.getColor('dateTextColor2'), 1, 0.9) 496 | b.space(4) 497 | let dateTS = new Date(ENV.cache.vaccine.data.lastUpdate).getTime() 498 | let date = Format.dateStr(dateTS) 499 | date = date.replace('.2021', ''); 500 | b.text('('+ date +')', ENV.fonts.xsmall, Theme.getColor('dateTextColor2', true), 1, 1) 501 | } else { 502 | b.text('Impfquoten aktuell nicht verfügbar.', ENV.fonts.normal, Theme.getColor('titleRowTextColor2'), 1, 1); 503 | } 504 | b.space() 505 | view.space() 506 | } 507 | static hospitalizationRow (view, cacheID) { 508 | const stateId = ENV.cache[cacheID].meta.BL_ID; 509 | const stateName = ENV.statesAbbr[stateId] 510 | 511 | let b = new UI(view).stack('h', [4,0,4,0]) 512 | b.elem.centerAlignContent() 513 | b.space() 514 | 515 | const stateHospitalizationData = ENV.cache.hospitalization.data[parseInt(stateId)]; 516 | const stateHospitalizedIncidence = stateHospitalizationData.hospitalization['7daysIncidence']; 517 | const stateHospitalized = Format.number(stateHospitalizationData.hospitalization['7daysCases'], 0); 518 | const stateHospitalizedStatus = UI.getHospitalizationStatus(stateHospitalizedIncidence); 519 | b.text('🏥 ', ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 520 | b.image(stateHospitalizedStatus, 0.9) 521 | b.text(' '+Format.number(stateHospitalizedIncidence, 2), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 522 | b.text(' (' + stateHospitalized + ')', ENV.fonts.small, Theme.getColor('dateTextColor2', true), 1, 0.9) 523 | b.space(4) 524 | 525 | const hospitalizationData = ENV.cache.hospitalization.data[0]; 526 | const hospitalizedIncidence = hospitalizationData.hospitalization['7daysIncidence']; 527 | const hospitalized = Format.number(hospitalizationData.hospitalization['7daysCases'], 0); 528 | const hospitalizedStatus = UI.getHospitalizationStatus(hospitalizedIncidence); 529 | b.text("[ D: ", ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 530 | b.image(hospitalizedStatus, 0.9) 531 | b.text(' '+Format.number(hospitalizedIncidence, 2), ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 532 | b.text(' (' + hospitalized + ')', ENV.fonts.small, Theme.getColor('dateTextColor2', true), 1, 0.9) 533 | b.text(" ] ", ENV.fonts.medium, Theme.getColor('titleRowTextColor'), 1, 0.9) 534 | b.space() 535 | view.space() 536 | } 537 | static smallIncidenceBlock(view, cacheID, options = {}) { 538 | let b = new UI(view).stack('v', false, Theme.getColor('stackBackgroundColorSmall', true), 12) 539 | let b2 = new UI(b).stack('h', [4, 0, 0, 5]) 540 | b2.space() 541 | let incidence = ENV.cache[cacheID].getDay().incidence 542 | b2.text(Format.number(incidence, 1, 'n/v', 100), ENV.fonts.small2, UI.getIncidenceColor(incidence), 1, 1) 543 | let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) 544 | 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) 545 | b2.text(trendArrow, ENV.fonts.small2, trendColor, 1, 1) 546 | let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID 547 | b2.text(name.toUpperCase(), ENV.fonts.small2, Theme.getColor('smallNameTextColor', true), 1, 1) 548 | 549 | let b3 = new UI(b).stack('h', [0, 0, 0, 5]) 550 | b3.space() 551 | //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 }] 552 | let graphImg 553 | if (CFG.graphShowValues === 'i') { 554 | graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 8, false).getImage() 555 | } else { 556 | graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 8, false).getImage() 557 | } 558 | b3.image(graphImg, 0.9) 559 | 560 | let b4 = new UI(b).stack('h', [0, 0, 1, 5]) 561 | b4.space() 562 | if (CFG.showDataInBlocks === 'hospitalization' && ENV.cache.hospitalization) { 563 | UIComp.hospitalizationIcon(b4, cacheID, 8, 8); 564 | } 565 | let prefix = (ENV.cache[cacheID].getDay().cases > 199999) ? '' : '+' 566 | b4.text(' ' + prefix + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) 567 | b.space(2) 568 | } 569 | 570 | static smallIncidenceRow(view, cacheID, bgColor = 'stackBackgroundColorSmall') { 571 | let r = new UI(view).stack('h', false, Theme.getColor(bgColor, true), 12) 572 | let b = new UI(r).stack('v') 573 | 574 | let bb2 = new UI(b).stack('h', [2, 0, 0, 6]) 575 | bb2.space() 576 | let incidence = ENV.cache[cacheID].getDay().incidence 577 | bb2.text(Format.number(incidence, 1, 'n/v', 100), ENV.fonts.normal, UI.getIncidenceColor(incidence), 1 ,1) 578 | let trendArrow = UI.getTrendArrow(ENV.cache[cacheID].getAvg(0), ENV.cache[cacheID].getAvg(1)) 579 | 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) 580 | bb2.text(trendArrow, ENV.fonts.normal, trendColor) 581 | bb2.space(2) 582 | let name = (typeof ENV.cache[cacheID].meta.BL_ID !== 'undefined') ? ENV.statesAbbr[ENV.cache[cacheID].meta.BL_ID] : cacheID 583 | bb2.text(name.toUpperCase(), ENV.fonts.normal, Theme.getColor('smallNameTextColor', true)) 584 | 585 | let b3 = new UI(b).stack('h', [0, 0, 2, 6]) 586 | b3.space() 587 | if (CFG.showDataInBlocks === 'hospitalization' && ENV.cache.hospitalization) { 588 | UIComp.hospitalizationInfo(b3, cacheID); 589 | } 590 | 591 | let b2 = new UI(r).stack('v', false, false, false, false, [60, 30]) 592 | let b2b2 = new UI(b2).stack('h', [0, 0, 0, 6]) 593 | b2b2.space() 594 | let graphImg 595 | if (CFG.graphShowValues == 'i') { 596 | graphImg = UI.generateIcidenceGraph(ENV.cache[cacheID], 58, 10, false).getImage() 597 | } else { 598 | graphImg = UI.generateGraph(ENV.cache[cacheID], 58, 10, false).getImage() 599 | } 600 | b2b2.image(graphImg, 0.9) 601 | 602 | let b2b3 = new UI(b2).stack('h', [0, 0, 0, 0]) 603 | b2b3.space() 604 | let prefix = (ENV.cache[cacheID].getDay().cases > 199999) ? '' : '+' 605 | b2b3.text(prefix + Format.number(ENV.cache[cacheID].getDay().cases), ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) 606 | 607 | r.space(6) 608 | } 609 | static vaccineInfo(view, cacheID) { 610 | // state data 611 | let b3Text = ' '; 612 | let quote = 'n/v' 613 | if (ENV.cache.vaccine.meta.status !== ENV.status.error) { 614 | let vaccineData = ENV.cache.vaccine.data.data.filter(state => { 615 | if (cacheID !== 'd') { 616 | const blId = ENV.cache[cacheID].meta.BL_ID.toString().padStart(2, '0'); 617 | return state.rs === blId 618 | } else { 619 | return state.name === "Deutschland" 620 | } 621 | }); 622 | vaccineData = vaccineData.pop(); 623 | quote = vaccineData.fullyVaccinated.quote; 624 | } 625 | b3Text = '💉² ' + Format.number(quote, 1, 'n/v ') 626 | view.text(b3Text, ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) 627 | } 628 | static hospitalizationInfo(view, cacheID) { 629 | let b3Text = ' '; 630 | let stateId = 0; 631 | if (cacheID !== 'd') { 632 | stateId = ENV.cache[cacheID].meta.BL_ID; 633 | } 634 | const stateHospitalizationData = ENV.cache.hospitalization.data[parseInt(stateId)]; 635 | const stateHospitalizedIncidence = stateHospitalizationData.hospitalization['7daysIncidence']; 636 | const stateHospitalizedStatus = UI.getHospitalizationStatus(stateHospitalizedIncidence); 637 | 638 | b3Text += Format.number(stateHospitalizedIncidence, 2) + ' '; 639 | view.text(b3Text, ENV.fonts.xsmall, Theme.getColor('graphTextColor', true), 1, 0.9) 640 | view.image(stateHospitalizedStatus, 0.9) 641 | } 642 | static hospitalizationIcon(view, cacheID, width, height) { 643 | let stateId = 0; 644 | if (cacheID !== 'd') { 645 | stateId = ENV.cache[cacheID].meta.BL_ID; 646 | } 647 | 648 | const stateHospitalizationData = ENV.cache.hospitalization.data[parseInt(stateId)]; 649 | const stateHospitalizedIncidence = stateHospitalizationData.hospitalization['7daysIncidence']; 650 | const stateHospitalizedStatus = UI.getHospitalizationStatus(stateHospitalizedIncidence, width, height); 651 | view.image(stateHospitalizedStatus, 0.9) 652 | } 653 | static areaIcon(view, ibzID) { 654 | let b = new UI(view).stack('h', [1, 3, 1, 3], Theme.getColor('areaIconBackgroundColor', true), 2, 2) 655 | b.text(ENV.areaIBZ[ibzID], ENV.fonts.xsmall, Theme.getColor('titleRowTextColor'), 1, 1) 656 | } 657 | static statusBlock(view, status) { 658 | let icon 659 | let iconText 660 | switch (status) { 661 | case ENV.status.offline: 662 | icon = '⚡️' 663 | iconText = 'Offline' 664 | break; 665 | case ENV.status.nogps: 666 | icon = '📡' 667 | iconText = 'GPS?' 668 | break; 669 | } 670 | if (icon && iconText) { 671 | let topStatusStack = new UI(view).stack('v') 672 | topStatusStack.text(icon, ENV.fonts.small) 673 | } 674 | } 675 | } 676 | class UI { 677 | constructor(view) { 678 | if (view instanceof UI) { 679 | this.view = this.elem = view.elem 680 | } else { 681 | this.view = this.elem = view 682 | } 683 | } 684 | static generateGraph(data, width, height, alignLeft = true) { 685 | let graphData = data.data.slice(Math.max(data.data.length - CFG.graphShowDays, 1)); 686 | let context = new DrawContext() 687 | context.size = new Size(width, height) 688 | context.opaque = false 689 | context.respectScreenScale = true 690 | let max = Math.max.apply(Math, graphData.map(function (o) { return o.cases; })) 691 | max = (max <= 0) ? 10 : max; 692 | let w = Math.max(2, Math.round((width - (graphData.length * 2)) / graphData.length)) 693 | let xOffset = (!alignLeft) ? (width - (graphData.length * (w + 1))) : 0 694 | for (let i = 0; i < CFG.graphShowDays; i++) { 695 | let item = graphData[i] 696 | let value = parseFloat(item.cases) 697 | if (value === -1 && i == 0) value = 10; 698 | let h = Math.max(2, (Math.abs(value) / max) * height) 699 | let x = xOffset + (w + 1) * i 700 | let rect = new Rect(x, height - h, w, h) 701 | context.setFillColor(UI.getIncidenceColor((item.cases >= 1) ? item.incidence : 0)) 702 | context.fillRect(rect) 703 | } 704 | return context 705 | } 706 | static generateIcidenceGraph(data, width, height, alignLeft = true) { 707 | let graphData = data.data.slice(Math.max(data.data.length - CFG.graphShowDays, 1)); 708 | let context = new DrawContext() 709 | context.size = new Size(width, height) 710 | context.opaque = false 711 | context.respectScreenScale = true 712 | let max = Math.max.apply(Math, graphData.map(function (o) { return o.incidence; })) 713 | let min = Math.min.apply(Math, graphData.map(function (o) { return o.incidence; })) / 1.2 714 | max = (max <= 0) ? 10 : max - min; 715 | let w = Math.max(2, Math.round((width - (graphData.length * 2)) / graphData.length)) 716 | let xOffset = (!alignLeft) ? (width - (graphData.length * (w + 1))) : 0 717 | for (let i = 0; i < CFG.graphShowDays; i++) { 718 | let item = graphData[i] 719 | let value = parseFloat(item.incidence) - min 720 | if (value === -1 && i == 0) value = 10; 721 | let h = Math.max(2,(Math.abs(value) / max) * (height - 1)) 722 | let x = xOffset + (w + 1) * i 723 | let rect = new Rect(x, height - h - 1, w, h) 724 | context.setFillColor(UI.getIncidenceColor((item.cases >= 0) ? item.incidence : 0)) 725 | context.fillRect(rect) 726 | } 727 | return context 728 | } 729 | stack(type = 'h', padding = false, borderBgColor = false, radius = false, borderWidth = false, size = false) { 730 | this.elem = this.view.addStack() 731 | if (radius) this.elem.cornerRadius = radius 732 | if (borderWidth !== false) { 733 | this.elem.borderWidth = borderWidth 734 | this.elem.borderColor = borderBgColor 735 | } else if (borderBgColor) { 736 | this.elem.backgroundColor = borderBgColor 737 | } 738 | if (padding) this.elem.setPadding(...padding) 739 | if (size) this.elem.size = new Size(size[0], size[1]) 740 | if (type === 'h') { this.elem.layoutHorizontally() } else { this.elem.layoutVertically() } 741 | this.elem.centerAlignContent() 742 | return this 743 | } 744 | text(text, font = false, color = false, maxLines = 0, minScale = 0.9) { 745 | let t = this.elem.addText(text) 746 | if (color) t.textColor = (typeof color === 'string') ? new Color(color) : color 747 | t.font = (font) ? font : ENV.fonts.normal 748 | t.lineLimit = (maxLines > 0 && minScale < 1) ? maxLines + 1 : maxLines 749 | t.minimumScaleFactor = minScale 750 | return this 751 | } 752 | image(image, imageOpacity = 1.0) { 753 | let i = this.elem.addImage(image) 754 | i.resizable = false 755 | i.imageOpacity = imageOpacity 756 | } 757 | space(size) { 758 | this.elem.addSpacer(size) 759 | return this 760 | } 761 | static getTrendUpArrow(now, prev) { 762 | if (now < 0 && prev < 0) { 763 | now = Math.abs(now) 764 | prev = Math.abs(prev) 765 | } 766 | return (now < prev) ? '↗' : (now > prev) ? '↑' : '→' 767 | } 768 | static getTrendArrow(value1, value2) { 769 | return (value1 < value2) ? '↓' : (value1 > value2) ? '↑' : '→' 770 | } 771 | static getTrendColor(value1, value2, altColorUp = null, altColorDown = null) { 772 | let colorUp = (altColorUp) ? new Color(altColorUp) : Theme.getColor(ENV.incidenceColors.red.color, true) 773 | let colorDown = (altColorDown) ? new Color(altColorDown) : Theme.getColor(ENV.incidenceColors.green.color, true) 774 | return (value1 < value2) ? colorDown : (value1 > value2) ? colorUp : Theme.getColor(ENV.incidenceColors.gray.color, true) 775 | } 776 | static getIncidenceColor(incidence) { 777 | let color = Theme.getColor(ENV.incidenceColors.green.color, true) 778 | if (incidence >= ENV.incidenceColors.lila.limit) { 779 | color = Theme.getColor(ENV.incidenceColors.lila.color, true) 780 | } else if (incidence >= ENV.incidenceColors.pink.limit) { 781 | color = Theme.getColor(ENV.incidenceColors.pink.color, true) 782 | } else if (incidence >= ENV.incidenceColors.darkdarkred.limit) { 783 | color = Theme.getColor(ENV.incidenceColors.darkdarkred.color, true) 784 | } else if (incidence >= ENV.incidenceColors.darkred.limit) { 785 | color = Theme.getColor(ENV.incidenceColors.darkred.color, true) 786 | } else if (incidence >= ENV.incidenceColors.red.limit) { 787 | color = Theme.getColor(ENV.incidenceColors.red.color, true) 788 | } else if (incidence >= ENV.incidenceColors.orange.limit) { 789 | color = Theme.getColor(ENV.incidenceColors.orange.color, true) 790 | } else if (incidence >= ENV.incidenceColors.yellow.limit) { 791 | color = Theme.getColor(ENV.incidenceColors.yellow.color, true) 792 | } 793 | return color 794 | } 795 | static getHospitalizationStatus(hospitalizedIncidence, width = 10, height = 10) { 796 | let context = new DrawContext() 797 | context.size = new Size(width, height) 798 | context.opaque = false 799 | context.respectScreenScale = true 800 | 801 | if (hospitalizedIncidence >= ENV.hospitalizedIncidenceLimits.red.limit) { 802 | context.setFillColor(Theme.getColor(ENV.hospitalizedIncidenceLimits.red.color, true)) 803 | } else if (hospitalizedIncidence >= ENV.hospitalizedIncidenceLimits.orange.limit) { 804 | context.setFillColor(Theme.getColor(ENV.hospitalizedIncidenceLimits.orange.color, true)) 805 | } else if (hospitalizedIncidence >= ENV.hospitalizedIncidenceLimits.yellow.limit) { 806 | context.setFillColor(Theme.getColor(ENV.hospitalizedIncidenceLimits.yellow.color, true)) 807 | } else { 808 | context.setFillColor(Theme.getColor(ENV.hospitalizedIncidenceLimits.green.color, true)) 809 | } 810 | let rect = new Rect(0, 0, width, height) 811 | context.fillEllipse(rect) 812 | 813 | return context.getImage() 814 | } 815 | } 816 | 817 | class DataResponse { 818 | constructor(data, status = ENV.status.ok) { 819 | this.data = data 820 | this.status = status 821 | } 822 | } 823 | 824 | class CustomFilemanager { 825 | constructor() { 826 | try { 827 | this.fm = FileManager.iCloud() 828 | this.fm.documentsDirectory() 829 | } catch (e) { 830 | this.fm = FileManager.local() 831 | } 832 | this.configDirectory = 'coronaWidgetNext' 833 | this.configPath = this.fm.joinPath(this.fm.documentsDirectory(), '/' + this.configDirectory) 834 | if (!this.fm.isDirectory(this.configPath)) this.fm.createDirectory(this.configPath) 835 | } 836 | async copy(oldFilename, newFilename) { 837 | let oldPath = this.fm.joinPath(this.configPath, oldFilename); 838 | let newPath = this.fm.joinPath(this.configPath, newFilename); 839 | this.fm.copy(oldPath, newPath) 840 | } 841 | async save(data, filename = '') { 842 | let path 843 | let dataStr 844 | if (filename === '') { 845 | path = this.fm.joinPath(this.configPath, 'coronaWidget_' + data.dataId + '.json'); 846 | dataStr = JSON.stringify(data); 847 | } else { 848 | path = this.fm.joinPath(this.fm.documentsDirectory(), filename); 849 | dataStr = data; 850 | } 851 | this.fm.writeString(path, dataStr); 852 | } 853 | async read(filename) { 854 | let path = this.fm.joinPath(this.configPath, filename + '.json'); 855 | let type = 'json' 856 | if (filename.includes('.')) { 857 | path = this.fm.joinPath(this.fm.documentsDirectory(), filename); 858 | type = 'string' 859 | } 860 | if (this.fm.isFileStoredIniCloud(path) && !this.fm.isFileDownloaded(path)) await this.fm.downloadFileFromiCloud(path); 861 | if (this.fm.fileExists(path)) { 862 | try { 863 | let resStr = await this.fm.readString(path) 864 | let res = (type === 'json') ? JSON.parse(resStr) : resStr 865 | return new DataResponse(res); 866 | } catch (e) { 867 | console.error(e) 868 | return new DataResponse('', ENV.status.error); 869 | } 870 | } 871 | return new DataResponse('', ENV.status.notfound); 872 | } 873 | } 874 | 875 | class Data { 876 | constructor(dataId, data = {}, meta = {}) { 877 | this.dataId = dataId 878 | this.data = data 879 | this.meta = meta 880 | } 881 | getDay (dayOffset = 0) { 882 | return (typeof this.data[this.data.length - 1 - dayOffset] !== 'undefined') ? this.data[this.data.length - 1 - dayOffset] : false; 883 | } 884 | getAvg (weekOffset = 0, ignoreToday = false) { 885 | let casesData = [...this.data].reverse() 886 | let skipToday = (ignoreToday) ? 1 : 0; 887 | const offsetDays = 7 888 | const weekData = casesData.slice((offsetDays * weekOffset) + skipToday, (offsetDays * weekOffset) + 7 + skipToday) 889 | const avg = weekData.reduce((a, b) => a + b.incidence, 0) / offsetDays 890 | return avg 891 | } 892 | static completeHistory (data) { 893 | const completeDataObj = {} 894 | for(let i = 0; i <= CFG.graphShowDays + 8; i++) { 895 | let lastDate = new Date() 896 | let prevDate = lastDate.setDate(lastDate.getDate() - i); 897 | completeDataObj[Format.dateStr(prevDate)] = { cases: 0, date: prevDate } 898 | } 899 | data.map((value) => { 900 | let curDate = Format.dateStr(value.date) 901 | completeDataObj[curDate].cases = value.cases 902 | }) 903 | let completeData = Object.values(completeDataObj) 904 | completeData.sort((a, b) => { return a.date - b.date; }) 905 | return completeData.reverse(); 906 | } 907 | static async tryLoadFromCache(cacheID, useStaticCoordsIndex) { 908 | const dataResponse = await cfm.read(cfm.configDirectory + '/coronaWidget_config.json') 909 | if (dataResponse.status !== ENV.status.ok) return ENV.status.error 910 | const cacheIDs = JSON.parse(dataResponse.data) 911 | if (typeof cacheIDs[cacheID] === 'undefined') return ENV.status.error 912 | const dataIds = cacheIDs[cacheID] 913 | if (typeof dataIds['dataIndex' + useStaticCoordsIndex] !== 'undefined') { 914 | const areaData = await cfm.read('coronaWidget_' + dataIds['dataIndex' + useStaticCoordsIndex]) 915 | if (!areaData.data.data) return ENV.status.error 916 | const area = new Data(dataIds['dataIndex' + useStaticCoordsIndex], areaData.data.data, areaData.data.meta) 917 | ENV.cache['s' + useStaticCoordsIndex] = area 918 | 919 | const stateData = await cfm.read('coronaWidget_' + areaData.data.meta.BL_ID) 920 | if (!stateData.data.data) return ENV.status.error 921 | const state = new Data(areaData.data.meta.BL_ID, stateData.data.data, stateData.data.meta) 922 | ENV.cache[areaData.data.meta.BL_ID] = state 923 | 924 | const dData = await cfm.read('coronaWidget_d') 925 | if (!dData.data.data) return ENV.status.error 926 | const d = new Data('d', dData.data.data, dData.data.meta) 927 | ENV.cache.d = d 928 | 929 | const vaccineData = await cfm.read('coronaWidget_vaccine') 930 | if (!vaccineData.data.data) return ENV.status.error 931 | const vaccine = new Data('vaccine', vaccineData.data.data, vaccineData.data.meta) 932 | ENV.cache.vaccine = vaccine 933 | 934 | const hospitalizationData = await cfm.read('coronaWidget_hospitalization') 935 | if (!hospitalizationData.data.data) return ENV.status.error 936 | const hospitalization = new Data('hospitalization', hospitalizationData.data.data, hospitalizationData.data.meta) 937 | ENV.cache.hospitalization = hospitalization 938 | 939 | return ENV.status.ok 940 | } 941 | return ENV.status.error 942 | } 943 | static async load(useStaticCoordsIndex = false) { 944 | if (typeof ENV.cache['s' + useStaticCoordsIndex] !== 'undefined') return true 945 | 946 | let configId = btoa('cID' + JSON.stringify(ENV.staticCoordinates).replace(/[^a-zA-Z ]/g, "")) 947 | const location = await Helper.getLocation(useStaticCoordsIndex) 948 | if (!location) { 949 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 950 | return (status === ENV.status.ok) ? ENV.status.nogps : ENV.status.error 951 | } 952 | const locationData = await rkiRequest.locationData(location) 953 | if (!locationData) { 954 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 955 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 956 | } 957 | 958 | let areaCases = await rkiRequest.areaCases(locationData.RS) 959 | if (!areaCases) { 960 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 961 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 962 | } 963 | await Data.geoCache(configId, useStaticCoordsIndex, locationData.RS) 964 | 965 | let areaData = new Data(locationData.RS) 966 | areaData.data = areaCases 967 | areaData.meta = locationData 968 | await cfm.save(areaData) 969 | ENV.cache['s' + useStaticCoordsIndex] = areaData 970 | 971 | // STATE DATA 972 | if (typeof ENV.cache[locationData.BL_ID] === 'undefined') { 973 | let stateCases = await rkiRequest.stateCases(locationData.BL_ID) 974 | if (!stateCases) { 975 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 976 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 977 | } 978 | let stateData = new Data(locationData.BL_ID) 979 | stateData.data = stateCases 980 | stateData.meta = { 981 | BL_ID: locationData.BL_ID, 982 | BL: locationData.BL, 983 | EWZ: locationData.EWZ_BL 984 | } 985 | await cfm.save(stateData) 986 | ENV.cache[locationData.BL_ID] = stateData 987 | } 988 | 989 | // GER DATA 990 | if (typeof ENV.cache.d === 'undefined') { 991 | let dCases = await rkiRequest.dCases() 992 | if (!dCases) { 993 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 994 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 995 | } 996 | let dData = new Data('d') 997 | dData.data = dCases 998 | dData.meta = { 999 | r: await rkiRequest.rvalue(), 1000 | EWZ: 83.02 * 1000000 // @TODO real number? 1001 | } 1002 | await cfm.save(dData) 1003 | ENV.cache.d = dData 1004 | } 1005 | 1006 | // VACCINE DATA 1007 | if (typeof ENV.cache.vaccine === 'undefined' && (CFG.showDataInBlocks === 'vaccine' || CFG.showDataInRow === 'vaccine')) { 1008 | let vaccineValues = await rkiRequest.vaccinevalues() 1009 | if (!vaccineValues) { 1010 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 1011 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 1012 | } 1013 | let vaccineData = new Data('vaccine') 1014 | if (typeof vaccineValues.lastUpdate === 'undefined') { 1015 | vaccineData.meta.status = ENV.status.error; 1016 | } 1017 | vaccineData.data = vaccineValues 1018 | vaccineData.meta.lastUpdate = vaccineValues.lastUpdate 1019 | await cfm.save(vaccineData) 1020 | ENV.cache.vaccine = vaccineData 1021 | } 1022 | 1023 | // HOSPITALIZATION DATA 1024 | if (typeof ENV.cache.hospitalization === 'undefined') { 1025 | let hospitalizationValues = await rkiRequest.hospitalizationvalues() 1026 | if (!hospitalizationValues) { 1027 | const status = await Data.tryLoadFromCache(configId, useStaticCoordsIndex) 1028 | return (status === ENV.status.ok) ? ENV.status.fromcache : ENV.status.error 1029 | } 1030 | let hospitalizationData = new Data('hospitalization') 1031 | hospitalizationData.data = hospitalizationValues 1032 | hospitalizationData.meta.lastUpdate = hospitalizationValues.lastUpdate 1033 | await cfm.save(hospitalizationData) 1034 | ENV.cache.hospitalization = hospitalizationData 1035 | } 1036 | 1037 | if (typeof ENV.cache['s' + useStaticCoordsIndex] !== 'undefined' && typeof ENV.cache[locationData.BL_ID] !== 'undefined' && typeof ENV.cache.d !== 'undefined') { 1038 | return ENV.status.ok 1039 | } 1040 | return ENV.status.error 1041 | } 1042 | static async geoCache(configId, dataIndex, rsid) { 1043 | let data = {} 1044 | let dataResponse = await cfm.read(cfm.configDirectory + '/coronaWidget_config.json') 1045 | if (dataResponse.status === ENV.status.ok) data = JSON.parse(dataResponse.data) 1046 | if (typeof data[configId] === 'undefined') data[configId] = {} 1047 | data[configId]['dataIndex' + dataIndex] = rsid 1048 | await cfm.save(JSON.stringify(data), cfm.configDirectory + '/coronaWidget_config.json') 1049 | } 1050 | } 1051 | 1052 | class Format { 1053 | static dateStr(timestamp, showYear = true) { 1054 | let date = new Date(timestamp) 1055 | let dateStr = `${('' + date.getDate()).padStart(2, '0')}.${('' + (date.getMonth() + 1)).padStart(2, '0')}` 1056 | if (showYear) dateStr += `.${date.getFullYear()}` 1057 | return dateStr 1058 | } 1059 | static number(number, fractionDigits = 0, placeholder = null, limit = false) { 1060 | number = Number(number) 1061 | if (!!placeholder && isNaN(number)) return placeholder 1062 | if (limit !== false && Math.round(number) >= limit) fractionDigits = 0 1063 | return Number(number).toLocaleString('de-DE', { maximumFractionDigits: fractionDigits, minimumFractionDigits: fractionDigits }) 1064 | } 1065 | static timestamp(dateStr) { 1066 | const regex = /([\d]+)\.([\d]+)\.([\d]+),\ ([0-2]?[0-9]):([0-5][0-9])/g; 1067 | let m = regex.exec(dateStr) 1068 | return new Date(m[3], m[2] - 1, m[1], m[4], m[5]).getTime() 1069 | } 1070 | static rValue(data) { 1071 | const parsedData = Parse.rCSV(data) 1072 | let r = 0 1073 | if (parsedData.length === 0) return r 1074 | let availeRvalueField 1075 | Object.keys(parsedData[0]).forEach(key => { 1076 | CFG.csvRvalueFields.forEach(possibleRKey => { 1077 | if (key === possibleRKey) availeRvalueField = possibleRKey; 1078 | }) 1079 | }); 1080 | let firstDatefield = Object.keys(parsedData[0])[0]; 1081 | if (availeRvalueField) { 1082 | parsedData.forEach(item => { 1083 | if (item[firstDatefield].includes('-') && typeof item[availeRvalueField] !== 'undefined' && parseFloat(item[availeRvalueField].replace(',', '.')) > 0) { 1084 | r = item; 1085 | } 1086 | }) 1087 | } 1088 | return (r) ? parseFloat(r[availeRvalueField].replace(',', '.')) : r 1089 | } 1090 | } 1091 | 1092 | class RkiRequest { 1093 | async locationData(location) { 1094 | const outputFields = 'GEN,RS,EWZ,EWZ_BL,BL_ID,cases,cases_per_100k,cases7_per_100k,cases7_bl_per_100k,last_update,BL,IBZ'; 1095 | 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` 1096 | 1097 | const response = await this.exec(url) 1098 | return (response.status === ENV.status.ok) ? response.data.features[0].attributes : false 1099 | } 1100 | async areaCases(areaID) { 1101 | const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) 1102 | const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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` 1103 | const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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=` 1104 | 1105 | return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) 1106 | } 1107 | async stateCases(blID) { 1108 | const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) 1109 | const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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` 1110 | const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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=` 1111 | 1112 | return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) 1113 | } 1114 | async dCases() { 1115 | const apiStartDate = Helper.getDateBefore(CFG.graphShowDays + 7) 1116 | const newCasesTodayUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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` 1117 | const newCasesHistoryUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Covid19_hubv/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=` 1118 | 1119 | return await this.getCases(newCasesTodayUrl, newCasesHistoryUrl) 1120 | } 1121 | async rvalue() { 1122 | const url = `https://raw.githubusercontent.com/robert-koch-institut/SARS-CoV-2-Nowcasting_und_-R-Schaetzung/main/Nowcast_R_aktuell.csv` 1123 | const response = await this.exec(url, false) 1124 | return (response.status === ENV.status.ok) ? Format.rValue(response.data) : false 1125 | } 1126 | async vaccinevalues () { 1127 | const url = `https://rki-vaccination-data.vercel.app/api/v2` 1128 | const response = await this.exec(url) 1129 | return (response.status === ENV.status.ok) ? response.data : false 1130 | } 1131 | async hospitalizationvalues () { 1132 | const url = `https://corona-widget-api.vercel.app/api/hospitalization` 1133 | const response = await this.exec(url) 1134 | return (response.status === ENV.status.ok) ? response.data : false 1135 | } 1136 | async getCases(urlToday, urlHistory) { 1137 | const responseToday = await this.exec(urlToday) 1138 | const responseHistory = await this.exec(urlHistory) 1139 | if (responseToday.status === ENV.status.ok && responseHistory.status === ENV.status.ok) { 1140 | let data = responseHistory.data.features.map(day => { return { cases: day.attributes.cases, date: day.attributes.MeldeDatum } }) 1141 | if (data.length === 0) return false; 1142 | let todayCases = responseToday.data.features.reduce((a, b) => a + b.attributes.cases, 0) 1143 | let lastDateHistory = Math.max(...responseHistory.data.features.map(a => a.attributes.MeldeDatum)) 1144 | let lastDateToday = Math.max(...responseToday.data.features.map(a => a.attributes.date)) 1145 | let lastDate = lastDateHistory; 1146 | if (!!lastDateToday || new Date(lastDateToday).setHours(0, 0, 0, 0) <= new Date(lastDateHistory).setHours(0, 0, 0, 0)) { 1147 | let lastReportDate = new Date(lastDateHistory) 1148 | lastDate = lastReportDate.setDate(lastReportDate.getDate() + 1); 1149 | } 1150 | data.push({ cases: todayCases, date: lastDate }) 1151 | data = Data.completeHistory(data) 1152 | return data; 1153 | } 1154 | return false; 1155 | } 1156 | async exec(url, isJson = true) { 1157 | try { 1158 | const resData = new Request(url) 1159 | resData.timeoutInterval = 60 1160 | let data = {} 1161 | let status = ENV.status.ok 1162 | if (isJson) { 1163 | data = await resData.loadJSON() 1164 | } else { 1165 | data = await resData.loadString() 1166 | } 1167 | status = this.checkStatus(data, isJson) 1168 | return new DataResponse(data, status) 1169 | } catch (e) { 1170 | console.warn(e) 1171 | return new DataResponse({}, ENV.status.notfound) 1172 | } 1173 | } 1174 | checkStatus (data, isJson) { 1175 | if (typeof data.length === '') return ENV.status.notfound 1176 | if (isJson && typeof data.error !== 'undefined') return ENV.status.notfound 1177 | return ENV.status.ok 1178 | } 1179 | } 1180 | 1181 | class Parse { 1182 | static input(input) { 1183 | const _coords = [] 1184 | const _staticCoordinates = input.split(";").map(coords => { 1185 | return coords.split(',') 1186 | }) 1187 | _staticCoordinates.forEach(coords => { 1188 | _coords[parseInt(coords[0])] = { 1189 | index: parseInt(coords[0]), 1190 | latitude: parseFloat(coords[1]), 1191 | longitude: parseFloat(coords[2]), 1192 | name: (coords[3]) ? coords[3] : false 1193 | } 1194 | }) 1195 | return _coords 1196 | } 1197 | static rCSV(rDataStr) { 1198 | let lines = rDataStr.split(/(?:\r\n|\n)+/).filter(function (el) { return el.length != 0 }) 1199 | let headers = lines[0].split(","); 1200 | let elements = [] 1201 | for (let i = 1; i < lines.length; i++) { 1202 | let element = {}; 1203 | let values = lines[i].split(',') 1204 | element = values.reduce(function (result, field, index) { 1205 | result[headers[index]] = field; 1206 | return result; 1207 | }, {}) 1208 | elements.push(element) 1209 | } 1210 | return elements 1211 | } 1212 | } 1213 | 1214 | class Helper { 1215 | static getIncidenceLimits(incidence) { 1216 | if (incidence >= ENV.incidenceColors.green.limit && incidence < ENV.incidenceColors.yellow.limit) { 1217 | return { min: ENV.incidenceColors.green.limit, max: ENV.incidenceColors.yellow.limit } 1218 | } else if (incidence >= ENV.incidenceColors.yellow.limit && incidence < ENV.incidenceColors.orange.limit) { 1219 | return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } 1220 | } else if (incidence >= ENV.incidenceColors.orange.limit && incidence < ENV.incidenceColors.red.limit) { 1221 | return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } 1222 | } else if (incidence >= ENV.incidenceColors.red.limit && incidence < ENV.incidenceColors.darkred.limit) { 1223 | return { min: ENV.incidenceColors.red.limit, max: ENV.incidenceColors.darkred.limit } 1224 | } else if (incidence >= ENV.incidenceColors.darkred.limit && incidence < ENV.incidenceColors.darkdarkred.limit) { 1225 | return { min: ENV.incidenceColors.darkred.limit, max: ENV.incidenceColors.darkdarkred.limit } 1226 | } else if (incidence > ENV.incidenceColors.darkdarkred.limit) { 1227 | return { min: ENV.incidenceColors.darkdarkred.limit, max: 500 } 1228 | } 1229 | return { min: 0, max: 0 } 1230 | } 1231 | static calcIncidence(cacheID) { 1232 | const casesData = [...ENV.cache[cacheID].data] 1233 | if (CFG.debugIncidenceCalc) Helper.log('calcIncidence', cacheID) 1234 | for(let i = 0; i < CFG.graphShowDays; i++) { 1235 | let theDays = casesData.slice(i + 1, i + 1 + 7) // without today 1236 | let sumCasesLast7Days = theDays.reduce((a, b) => a + b.cases, 0) 1237 | casesData[i].incidence = (sumCasesLast7Days / ENV.cache[cacheID].meta.EWZ) * 100000 1238 | if (CFG.debugIncidenceCalc) Helper.log(Format.dateStr(casesData[i].date), casesData[i].cases, casesData[i].incidence) 1239 | } 1240 | // @TODO Workaround use incidence from api 1241 | if (CFG.disableLiveIncidence && typeof ENV.cache[cacheID].meta.cases7_per_100k !== 'undefined') { 1242 | casesData[0].incidence = ENV.cache[cacheID].meta.cases7_per_100k 1243 | } 1244 | ENV.cache[cacheID].data = casesData.reverse() 1245 | } 1246 | static getDateBefore(days) { 1247 | let offsetDate = new Date() 1248 | offsetDate.setDate(new Date().getDate() - days) 1249 | return offsetDate.toISOString().split('T').shift() 1250 | } 1251 | static async getLocation(staticCoordinateIndex = false) { 1252 | if (typeof ENV.staticCoordinates[staticCoordinateIndex] !== 'undefined' && Object.keys(ENV.staticCoordinates[staticCoordinateIndex]).length >= 3) { 1253 | return ENV.staticCoordinates[staticCoordinateIndex] 1254 | } 1255 | try { 1256 | Location.setAccuracyToThreeKilometers() 1257 | return await Location.current() 1258 | } catch (e) { 1259 | console.warn(e) 1260 | } 1261 | return null; 1262 | } 1263 | static log(...data) { 1264 | console.log(data.map(JSON.stringify).join(' | ')) 1265 | } 1266 | static getWeek(timestamp) { 1267 | const date = new Date(timestamp); 1268 | date.setHours(0, 0, 0, 0); 1269 | date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); 1270 | var week1 = new Date(date.getFullYear(), 0, 4); 1271 | return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); 1272 | } 1273 | } 1274 | 1275 | const cfm = new CustomFilemanager() 1276 | const rkiRequest = new RkiRequest() 1277 | await new IncidenceWidget().init() 1278 | -------------------------------------------------------------------------------- /screenshots/info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rphl/corona-widget/d6ca4f8929185923a827623312b4c590705c1efd/screenshots/info.jpg -------------------------------------------------------------------------------- /screenshots/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rphl/corona-widget/d6ca4f8929185923a827623312b4c590705c1efd/screenshots/screenshot.jpg -------------------------------------------------------------------------------- /screenshots/screenshot_hospitalization.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rphl/corona-widget/d6ca4f8929185923a827623312b4c590705c1efd/screenshots/screenshot_hospitalization.jpg -------------------------------------------------------------------------------- /screenshots/screenshot_vaccine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rphl/corona-widget/d6ca4f8929185923a827623312b4c590705c1efd/screenshots/screenshot_vaccine.jpg -------------------------------------------------------------------------------- /screenshots/widgetparameter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rphl/corona-widget/d6ca4f8929185923a827623312b4c590705c1efd/screenshots/widgetparameter.jpg --------------------------------------------------------------------------------