Skip to content

Instantly share code, notes, and snippets.

@rafaelmaeuer
Last active March 8, 2023 09:15
Show Gist options
  • Save rafaelmaeuer/242e39bc29ec99b4bc42aecb7b60ca5d to your computer and use it in GitHub Desktop.
Save rafaelmaeuer/242e39bc29ec99b4bc42aecb7b60ca5d to your computer and use it in GitHub Desktop.
Kombination der Covid-19 Neuinfektionen und der Inzidenz-Zahl für die aktuelle Location. Inspiriert von https://gist.github.com/kevinkub/46caebfebc7e26be63403a7f0587f664
// COVID-19 iOS-Widget for Incidence and Newcases in Germany and current geo-location
// Modified: https://gist.github.com/oliverandrich/0f34c8d4e6de4b6ff32937c584009a65
// Inspired by: https://gist.github.com/kevinkub/46caebfebc7e26be63403a7f0587f664
// Added Data Source: https://api.corona-zahlen.org/docs/
const GEO = {
latitude: 49.87,
longitude: 8.65
}
class IncidenceNewCasesWidget {
constructor() {
this.coronaApiUrl = `https://api.corona-zahlen.org/germany`;
this.incidenceUrl = (location) =>
`https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=1%3D1&outFields=RS,GEN,cases7_per_100k,BL,last_update&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&returnGeometry=false&outSR=4326&f=json";
try {
this.fileManager = FileManager.iCloud();
} catch (e) {
console.error(e);
this.fileManager = FileManager.local();
}
this.locationPath = this.fileManager.joinPath(
this.fileManager.documentsDirectory(),
"incidenceNewCases"
);
this.locationFileName = this.fileManager.joinPath(
this.locationPath,
"location.json"
);
this.fileManager.createDirectory(this.locationPath, true);
}
async run() {
const widget = await this.createWidget();
if (!config.runsInWidget) {
await widget.presentSmall();
}
Script.setWidget(widget);
Script.complete();
}
async createWidget() {
let header, value, label, trend, data, dataLocal;
// Basic widget setup
const list = new ListWidget();
list.setPadding(15, 15, 15, 15);
// Add background color
list.backgroundColor = new Color("ffffff");
list.backgroundColor = Color.dynamic(list.backgroundColor, new Color("1c1c1c"));
// New Cases
data = await this.getCoronaData();
header = list.addText("🦠 Neu-Infektionen".toUpperCase());
header.font = Font.mediumSystemFont(10);
value = list.addText("+" + parseInt(data.cases).toLocaleString());
value.font = Font.mediumSystemFont(24);
label = list.addText(data.areaName);
label.font = Font.lightSystemFont(12);
label.textColor = Color.gray();
list.addSpacer();
// Incidence
dataLocal = await this.getIncidenceDataLocal();
header = list.addText("📈 7-Tage Inzidenz".toUpperCase());
header.font = Font.mediumSystemFont(10);
const dataValue = data ? parseFloat(data.incidence).toLocaleString() : "NaN";
const localValue = dataLocal ? parseFloat(dataLocal.value).toLocaleString() : "NaN";
const dataString = dataLocal ? `${localValue} (${dataValue})` : dataValue;
const dataFontSize = data?.length > 3 ? 2 : 0;
const localFontSize = (dataLocal && dataLocal.length) > 3 ? 2 : 0;
const fontSize = 22 - dataFontSize - localFontSize;
value = list.addText(dataString);
value.font = Font.boldSystemFont(fontSize);
if (data?.value || dataLocal?.value >= 50) {
value.textColor = Color.red();
} else if (data?.value || dataLocal?.value >= 35) {
value.textColor = Color.orange();
} else if (data?.value || dataLocal?.value >= 25) {
value.textColor = Color.yellow();
}
const dataArea = data ? data.areaName.substring(0, 2).toUpperCase() : 'No Data';
const localArea = dataLocal ? dataLocal.areaName : "";
const areaString = dataLocal ? `${localArea} (${dataArea})` : dataArea;
const date = dataLocal ? dataLocal.date.split(",")[0] : data.update;
label = list.addText(areaString);
label.font = Font.lightSystemFont(12);
label.textColor = Color.gray();
if (date) {
list.addSpacer(4);
// Fetch Time
const updatedLabel = list.addText(`Update: ${date}`);
updatedLabel.font = Font.regularSystemFont(8);
updatedLabel.textOpacity = 0.8;
updatedLabel.centerAlignText();
}
// Enable caching
if (data?.shouldCache) {
list.refreshAfterDate = new Date(Date.now() + 60 * 60 * 1000);
}
return list;
}
getDateString(date) {
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
return date.toLocaleDateString('de-DE', options);
}
async getCoronaData() {
// Load new cases data
let data
try {
data = await new Request(this.coronaApiUrl).loadJSON();
console.log(`getNewCasesData: ${JSON.stringify(data)}`);
} catch {
console.error('getCoronaData: request failed');
return null;
}
// Check data results
if (!data || !data.casesPerWeek || !data.weekIncidence) {
console.error('getCoronaData: no data');
return null;
}
const date = new Date(data.meta.lastUpdate);
// Return data
return {
cases: data.casesPerWeek,
incidence: data.weekIncidence.toFixed(0),
update: this.getDateString(date),
areaName: "Deutschland",
};
}
async getIncidenceData() {
// Load incidence data
let data
try {
data = await new Request(this.incidenceUrlStates).loadJSON();
//console.log(`getIncidenceData: ${JSON.stringify(data)}`);
} catch {
console.error('getIncidenceData: request failed');
return null;
}
// Check data results
if (!data || !data.features || data.features.length == 0) {
console.error('getIncidenceData: no data');
return null;
}
// Map data results
const incidencePerState = data.features.map(
(f) => f.attributes.cases7_bl_per_100k
);
// Calculate incidence average
const averageIncidence =
incidencePerState.reduce((a, b) => a + b) / incidencePerState.length;
const decimal = averageIncidence.toFixed(0).length > 2 ? 0 : 1;
// Return incidence
return {
value: averageIncidence.toFixed(decimal),
length: averageIncidence.toFixed(0).length,
areaName: "Deutschland",
shouldCache: true,
};
}
async getIncidenceDataLocal() {
// Get location for local data
const location = await this.getLocation();
// Load incidence data for location
let data
try {
data = await new Request(this.incidenceUrl(location)).loadJSON();
//console.log(`getIncidenceDataLocal : ${JSON.stringify(data)}`);
} catch {
console.error('getIncidenceDataLocal: request failed');
return null;
}
// check data results
if (!data || !data.features || data.features.length == 0) {
console.error('getIncidenceDataLocal: no data');
return null;
}
// Get incidence from data
const attr = data.features[0].attributes;
const incidence = attr.cases7_per_100k;
const decimal = incidence.toFixed(0).length > 2 ? 0 : 1;
// Return local incidence
return {
value: incidence.toFixed(decimal),
length: incidence.toFixed(0).length,
areaName: attr.GEN,
date: attr.last_update,
shouldCache: true,
};
}
async getLocation() {
try {
// Check if widget has parameters
if (args.widgetParameter) {
const fixedCoordinates = args.widgetParameter
.split(",")
.map(parseFloat);
return {
latitude: fixedCoordinates[0],
longitude: fixedCoordinates[1],
};
}
// Get approximate device location
else {
Location.setAccuracyToThreeKilometers();
let location = await Location.current();
// Fall back to predefined geo data
if (!location || !location.latitude) {
location = GEO;
}
// Store location data in storage
this.storeLocation(location);
return location;
}
} catch (e) {
console.error(e);
// Load location data from storage
return this.readLocation();
}
}
storeLocation(location) {
this.fileManager.writeString(
this.locationFileName,
JSON.stringify(location)
);
}
readLocation() {
if (this.fileManager.fileExists(this.locationFileName)) {
return JSON.parse(this.fileManager.readString(this.locationFileName));
} else {
console.error('readLocation failed');
return null;
}
}
}
await new IncidenceNewCasesWidget().run();
@rafaelmaeuer
Copy link
Author

rafaelmaeuer commented May 9, 2021

Left side: modified version for Gran Canaria. Right side: this widget (modified with double incidence and update time).
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment