Last active
January 14, 2021 13:01
-
-
Save richterd/d9a961632dd0a258b13b72eebdd7626b to your computer and use it in GitHub Desktop.
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: deep-gray; icon-glyph: magic; | |
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0 | |
// Vaccine API by @_ThisIsBenny_ | |
// Define URLs based on the corona.rki.de webpage | |
class CoronaWidget { | |
constructor() { | |
//API URLs | |
this.previousDaysToShow = 16; | |
this.newCasesApiUrl = `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?f=json&where=NeuerFall%20IN(1%2C%20-1)&returnGeometry=false&spatialRel=esriSpatialRelIntersects&outFields=*&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22AnzahlFall%22%2C%22outStatisticFieldName%22%3A%22value%22%7D%5D&resultType=standard&cacheHint=true`; | |
this.incidenceUrl = (location) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,last_update,cases,cases7_bl_per_100k,cases7_per_100k&geometry=${location.longitude.toFixed(3)}%2C${location.latitude.toFixed(3)}&geometryType=esriGeometryPoint&inSR=4326&spatialRel=esriSpatialRelWithin&returnGeometry=false&outSR=4326&f=json`; | |
this.incidenceUrlStates = "https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/Coronaf%E4lle_in_den_Bundesl%E4ndern/FeatureServer/0/query?where=1%3D1&outFields=cases7_bl_per_100k,Fallzahl&returnGeometry=false&outSR=4326&f=json"; | |
this.apiUrlDistrictsHistory = (districtId) => `https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=IdLandkreis%20%3D%20%27${districtId}%27%20AND%20Meldedatum%20%3E%3D%20TIMESTAMP%20%27${this.getDateString(-this.previousDaysToShow)}%2000%3A00%3A00%27%20AND%20Meldedatum%20%3C%3D%20TIMESTAMP%20%27${this.getDateString(1)}%2000%3A00%3A00%27&outFields=Landkreis,Meldedatum,AnzahlFall&outSR=4326&f=json` | |
this.vaccineStatusUrl = "https://rki-vaccination-data.vercel.app/api"; | |
} | |
async createWidget(items) { | |
const list = new ListWidget(); | |
//const padding = 12; | |
//list.setPadding(padding, padding, padding, padding); | |
const location = await this.getLocation(); | |
//Country Information | |
const newCasesData = await this.getNewCasesData(); | |
const countryHeader = list.addText(`🦠 ${newCasesData.areaName}`); | |
countryHeader.font = Font.mediumSystemFont(10); | |
const countryLabel = list.addText(`+${newCasesData.value.toLocaleString()}`); | |
countryLabel.font = Font.mediumSystemFont(22); | |
list.addSpacer(); | |
//City Information | |
const localData = await this.getLocalData(location); | |
const reproductionData = await this.getReproductionData(localData.cityCode); | |
const localHeader = list.addText(`🦠 ${localData.cityName}`); | |
localHeader.font = Font.mediumSystemFont(10); | |
const row = list.addStack(); | |
row.centerAlignContent(); | |
const incidenceCell = row.addText(`${localData.incidence.toLocaleString()} ${reproductionData.trend}`); | |
row.addSpacer(); | |
const rString = parseFloat(reproductionData.r7.toFixed(2)).toLocaleString(); | |
const rCell = row.addText(`R: ${rString}`); | |
incidenceCell.font = Font.mediumSystemFont(22); | |
rCell.font = Font.mediumSystemFont(10); | |
rCell.textColor = Color.gray(); | |
if (localData.incidence >= 50) { | |
incidenceCell.textColor = Color.red(); | |
} else if (localData.incidence >= 25) { | |
incidenceCell.textColor = Color.orange(); | |
} | |
list.addSpacer(); | |
//Immunity Information | |
const sumCases = await this.getInfectedData(); | |
const sumCasesString = parseFloat((sumCases / 1000000).toFixed(2)).toLocaleString(); | |
const sumCasesPercentageString = parseFloat((sumCases / 83020000 * 100).toFixed(2)).toLocaleString(); | |
const casesLabel = list.addText(`😷 ${sumCasesString} Mio (${sumCasesPercentageString}%)`); | |
casesLabel.font = Font.mediumMonospacedSystemFont(10); | |
casesLabel.textColor = Color.gray(); | |
//Vaccine Information | |
const vaccineData = await this.getVaccineData(); | |
const amount = parseFloat((vaccineData.value / 1000000).toFixed(2)).toLocaleString(); | |
const quote = vaccineData.quote.toLocaleString(); | |
const vaccinatedLabel = list.addText(`💉 ${amount} Mio (${quote}%)`); | |
vaccinatedLabel.font = Font.mediumMonospacedSystemFont(10); | |
vaccinatedLabel.textColor = Color.gray(); | |
list.addSpacer(); | |
//Updated Time | |
const timeLabel = list.addText(`Letztes Update: ${localData.lastUpdated.split(",")[0]}`); | |
timeLabel.font = Font.mediumSystemFont(7); | |
timeLabel.textColor = Color.lightGray(); | |
return list; | |
} | |
async getLocation() { | |
let location; | |
if (args.widgetParameter) { | |
const fixedCoordinates = args.widgetParameter.split(",").map(parseFloat); | |
location = { | |
latitude: fixedCoordinates[0], | |
longitude: fixedCoordinates[1] | |
} | |
} else { | |
Location.setAccuracyToThreeKilometers(); | |
try { | |
location = await Location.current(); | |
console.log('get current lat/lon'); | |
this.saveIncidenceLatLon(location) | |
} catch (e) { | |
console.log('using saved lat/lon'); | |
location = this.getSavedIncidenceLatLon(); | |
} | |
} | |
return location; | |
} | |
async getInfectedData() { | |
const casesData = await new Request(this.incidenceUrlStates).loadJSON(); | |
const casesPerState = casesData.features.map( | |
(f) => f.attributes.Fallzahl | |
); | |
const sumCases = casesPerState.reduce((a, b) => a + b); | |
return sumCases; | |
} | |
async getReproductionData(cityCode) { | |
const historicalData = await new Request(this.apiUrlDistrictsHistory(cityCode)).loadJSON(); | |
const aggregate = historicalData.features.map(f => f.attributes).reduce((dict, feature) => { | |
dict[feature["Meldedatum"]] = (dict[feature["Meldedatum"]] | 0) + feature["AnzahlFall"]; | |
return dict; | |
}, {}); | |
const timeline = Object.keys(aggregate).sort().map(k => aggregate[k]); | |
const casesYesterday7 = timeline.slice(-8, -1).reduce((a, b) => a + b); | |
const casesToday7 = timeline.slice(-7).reduce((a, b) => a + b); | |
const casesWeekBefore7 = timeline.slice(-15, -8).reduce((a, b) => a + b); | |
const trend = (casesToday7 == casesYesterday7) ? '→' : (casesToday7 > casesYesterday7) ? '↑' : '↓'; | |
//From: https://pavelmayer.de/covid/risks/ | |
const rwk = (casesToday7 + 5) / (casesWeekBefore7 + 5); | |
const r7 = Math.pow(rwk, 4 / 7); | |
console.log(r7) | |
console.log(`Today: ${casesToday7}, Before: ${casesWeekBefore7}`); | |
return { | |
trend: trend, | |
r7: r7 | |
}; | |
} | |
async getLocalData(location) { | |
const data = await new Request(this.incidenceUrl(location)).loadJSON(); | |
if (!data || !data.features || !data.features.length) { | |
const errorList = new ListWidget(); | |
errorList.addText("Keine Ergebnisse für den aktuellen Ort gefunden."); | |
return errorList; | |
} | |
const attr = data.features[0].attributes; | |
const incidence = parseFloat(attr.cases7_per_100k.toFixed(1)); | |
const cityName = attr.GEN; | |
const cityCode = attr.RS; | |
const cases = attr.cases; | |
const lastUpdated = attr.last_update; | |
return { | |
incidence: incidence, | |
cityName: cityName, | |
cityCode: cityCode, | |
cases: cases, | |
lastUpdated: lastUpdated | |
}; | |
} | |
// Get vaccine Status | |
async getVaccineData() { | |
const data = await new Request(this.vaccineStatusUrl).loadJSON(); | |
const attr = data.vaccinated; | |
const quote = data.quote; | |
return { | |
value: attr, | |
quote: quote, | |
}; | |
} | |
async getNewCasesData() { | |
const data = await new Request(this.newCasesApiUrl).loadJSON(); | |
const attr = data.features[0].attributes; | |
return { | |
value: parseFloat(attr.value), | |
areaName: "Deutschland", | |
shouldCache: false, | |
}; | |
} | |
getDateString(addDays) { | |
addDays = addDays || 0; | |
return new Date(Date.now() + addDays * 24 * 60 * 60 * 1000).toISOString().substring(0, 10); | |
} | |
saveIncidenceLatLon(location) { | |
const fm = FileManager.iCloud(); | |
const path = fm.joinPath(fm.documentsDirectory(), "covid19latlon.json"); | |
fm.writeString(path, JSON.stringify(location)); | |
} | |
getSavedIncidenceLatLon() { | |
const fm = FileManager.iCloud(); | |
const path = fm.joinPath(fm.documentsDirectory(), "covid19latlon.json"); | |
const data = fm.readString(path); | |
return JSON.parse(data); | |
} | |
} | |
//Create Widget | |
const widget = await new CoronaWidget().createWidget(); | |
if (!config.runsInWidget) { | |
await widget.presentSmall(); | |
} | |
//Set Widget | |
Script.setWidget(widget); | |
Script.complete(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey there 👋
the widget looks the following and imho shows the most relevant information for now. Thanks to all the people who did a great job in creating early versions. Feel free to use or share it.