<|start_filename|>number-of-covild-19-vaccinations/number-of-covild-19-vaccinations.js<|end_filename|>
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: syringe;
/**************

Version 2.0.1

Changelog:  
  v2.0.1
          - Use different colors for the circules
  v2.0.0
          - Show "at least one" and "fully" vaccination in Medium and small widget
          - Upgrade to v2 of the API
          - Switch to "Mio." numbers instead of "Tsd.
  v1.2.0
          - Large Widget: write percentage to the bar and show total numbers
          - Allow to change sorting my add a field namen into sortBy variable
  v1.1.1
          - Cache path changed
          - Allow force Update of the data
  v1.1.0
          - Cache TTL changed to 4 hours
          - Show total numbers in large widget
  v1.0.2
          - prevent error if user deletes Widget-"Parameter"
          - optionally format numbers with more readable units and their correct abbreviation
            add ",1" to the widget "Parameter" to enable.
          
  v1.0.1:
          - fix sorting issue
          
/**************/

////////////////////////////////////////////////////////////////////////////////
//////////////////////////         User-Config         /////////////////////////
////////////////////////////////////////////////////////////////////////////////

// How many minutes should the cache be valid
let cacheMinutes = 4 * 60

// enter the path of the field which should be used for sorting in the large widget list.
// e.g. 'vaccinatedAtLeastOnce.quote' or 'vaccinatedAtLeastOnce.doses'. Default: State name
const sortBy = ''
const sortDirection = '' // asc or desc. Default: asc

////////////////////////////////////////////////////////////////////////////////
//////////////////////////         Dev Settings         ////////////////////////
////////////////////////////////////////////////////////////////////////////////

const debug = false
config.widgetFamily = config.widgetFamily || 'large'

////////////////////////////////////////////////////////////////////////////////
//////////////////////////         System Settings         /////////////////////
////////////////////////////////////////////////////////////////////////////////

let widgetInputRAW = args.widgetParameter
let widgetInput, selectedState

if (widgetInputRAW !== null && widgetInputRAW !== "") {
  selectedState = widgetInputRAW.toString()
  if (/^(Baden-Württemberg|Bayern|Berlin|Brandenburg|Bremen|Hamburg|Hessen|Mecklenburg-Vorpommern|Niedersachsen|Nordrhein-Westfalen|Rheinland-Pfalz|Saarland|Sachsen|Sachsen-Anhalt|Schleswig-Holstein|Thüringen)$/.test(selectedState) === false && selectedState !== '' && selectedState !== undefined) {
    throw new Error('Kein gültiges Bundesland. Bitte prüfen Sie die Eingabe.') 
  }
}
const altUnits = true
const maximumFractionDigits = 1

const fontSize = 9
const fontSize2 = 12
const fontSize3 = 7
const spacing = 5

const width = 100
const h = 9

const thresholds = {  
  amber: 59,
  green: 79
}

if (args.queryParameters.forceUpdate) {    
  cacheMinutes = 0
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

function fetchFromObject(obj, prop) {
    if(typeof obj === 'undefined') {
        return false;
    }
    let _index = prop.indexOf('.')
    if(_index > -1) {
        return fetchFromObject(obj[prop.substring(0, _index)], prop.substr(_index + 1));
    }
    return obj[prop];
}

function creatProgress(percentage) {
  const context = new DrawContext()
  context.size = new Size(width, h)
  context.opaque = false
  context.respectScreenScale = true
  
  // Background Path
  context.setFillColor(Color.gray())
  const path = new Path()
  const backgroundReact = new Rect(0, 0, width, h)
  path.addRect(backgroundReact)
  context.addPath(path)
  context.fillPath()
  
  // Progress Path
  let color
  if (percentage > thresholds.green) {
    color = Color.green()
  } else if (percentage > thresholds.amber) {
    color = Color.orange()
  } else {
    color = Color.red()
  }
  
  context.setFillColor(color)  
  const path1 = new Path()
  const path1width = (width * (percentage / 100) > width) ? width : width * (percentage / 100)
  path1.addRect(new Rect(0, 0, path1width, h))
  context.addPath(path1)
  context.fillPath()

  context.setTextAlignedCenter()
  context.setTextColor(Color.white())
  context.setFont(Font.systemFont(fontSize - 1))
  context.drawTextInRect(`${percentage.toLocaleString(Device.language())}`, backgroundReact)
  
  return context.getImage()
}

function getDiagram(percentage, percentage2) {
  function drawArc(ctr, rad, w, deg, color) {
    bgx = ctr.x - rad
    bgy = ctr.y - rad
    bgd = 2 * rad
    bgr = new Rect(bgx, bgy, bgd, bgd)
  
    canvas.setFillColor(color)
    canvas.setStrokeColor(Color.gray())
    canvas.setLineWidth(w)
    canvas.strokeEllipse(bgr)
  
    for (t = 0; t < deg; t++) {
      rect_x = ctr.x + rad * sinDeg(t) - w / 2
      rect_y = ctr.y - rad * cosDeg(t) - w / 2
      rect_r = new Rect(rect_x, rect_y, w, w)
      canvas.fillEllipse(rect_r)
    }
  }
  function sinDeg(deg) {
    return Math.sin((deg * Math.PI) / 180)
  }
  
  function cosDeg(deg) {
    return Math.cos((deg * Math.PI) / 180)
  }
  const canvas = new DrawContext()
  const canvSize = 200
  const canvTextSize = 20
  
  const canvWidth = 10
  const canvRadius = 85
  
  canvas.opaque = false  
  canvas.size = new Size(canvSize, canvSize)
  canvas.respectScreenScale = true
  
  let color, color2
  if (percentage > thresholds.green) {
    color = Color.green()
  } else if (percentage > thresholds.amber) {
    color = Color.orange()
  } else {
    color = Color.red()
  }
  if (percentage2 > thresholds.green) {
    color2 = Color.green()
  } else if (percentage2 > thresholds.amber) {
    color2 = Color.orange()
  } else {
    color2 = Color.red()
  }
  drawArc(
    new Point(canvSize / 2, canvSize / 2),
    canvRadius,
    canvWidth,
    Math.floor(percentage * 3.6),
    color
  )
  drawArc(
    new Point(canvSize / 2, canvSize / 2),
    canvRadius - 15,
    canvWidth,
    Math.floor(percentage2 * 3.6),
    color2
  )

  const canvTextRect = new Rect(
    0,
    100 - canvTextSize / 2,
    canvSize,
    canvTextSize * 1.4 // X-height "* 1.4" so e.g. commas aren't cut off
  )
  canvas.setTextAlignedCenter()
  canvas.setTextColor(Color.gray())
  canvas.setFont(Font.boldSystemFont(canvTextSize))
  canvas.drawTextInRect(`${Math.round(percentage).toLocaleString(Device.language())}% / ${Math.round(percentage2).toLocaleString(Device.language())}%`, canvTextRect)

  return canvas.getImage()
}

var today = new Date()

// Set up the file manager.
const files = FileManager.local()

// Set up cache
const cachePath = files.joinPath(files.cacheDirectory(), "widget-vaccination-v2")
const cacheExists = files.fileExists(cachePath)
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0

// Get Data
let result
let lastUpdate
try {
  // If cache exists and it's been less than 30 minutes since last request, use cached data.
  if (cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheMinutes * 60 * 1000)) {
    console.log("Get from Cache")
    result = JSON.parse(files.readString(cachePath))
    lastUpdate = cacheDate
  } else {
    console.log("Get from API") 
    const req = new Request('https://rki-vaccination-data.vercel.app/api/v2')
    result = await req.loadJSON()
    lastUpdate = today
    console.log("Write Data to Cache")
    try {
      files.writeString(cachePath, JSON.stringify(result))
    } catch (e) {
      console.log("Creating Cache failed!")
      console.log(e)
    }
  }
} catch (e) {
  console.error(e)
  if (cacheExists) {
    console.log("Get from Cache")
    result = JSON.parse(files.readString(cachePath))
    lastUpdate = cacheDate
  } else {
    console.log("No fallback to cache possible. Due to missing cache.")
  }
}
const germany = result.data.find((e) => e.name === "Deutschland")
const states = result.data.filter((e) => e.isState)

if (debug) {  
  console.log(JSON.stringify(result, null, 2))
}

const widget = new ListWidget()
widget.setPadding(10, 10, 10, 10)
widget.addSpacer(0)

let firstLineStack = widget.addStack()
  
let title = firstLineStack.addText("🦠 COVID-19 Impfungen")
title.font = Font.boldSystemFont(12)
title.minimumScaleFactor = 0.7
title.lineLimit = 1
// Last Update
firstLineStack.addSpacer()

if (config.widgetFamily !== 'small') {
  lastUpdateStack = firstLineStack.addStack()
  lastUpdateStack.layoutVertically()
  lastUpdateStack.addSpacer(3)
  let lastUpdateText = lastUpdateStack.addDate(new Date(result.lastUpdate))
  lastUpdateText.font = Font.systemFont(8)
  lastUpdateText.rightAlignText()
  lastUpdateText.applyDateStyle()
}

widget.addSpacer()

if (config.widgetFamily === 'large') {    
  const stack = widget.addStack()
  stack.layoutVertically()
  stack.spacing = spacing
  
  const list = states.sort((a, b) => {
    const aValue = fetchFromObject(a, sortBy)
    const bValue = fetchFromObject(b, sortBy)
    if(sortBy && aValue !== undefined && bValue !== undefined) {
      if (sortDirection === "" || sortDirection === "asc") {
        return aValue > bValue
      } else {
        return aValue < bValue
      }
    } else {
      return a.name.localeCompare(b.name)
    }
  })
  
  for (const value of list) {
    const row = stack.addStack()
    row.layoutHorizontally()
    const stateText = row.addText(value.name)
    stateText.font = Font.mediumSystemFont(fontSize)
    stateText.lineLimit = 1
    
    row.addSpacer()
    const quoteText = row.addText(`${parseInt(value.vaccinatedAtLeastOnce.doses).toLocaleString(Device.language())}`)
    quoteText.font = Font.systemFont(fontSize)
    
    row.addSpacer(4)
    const progressBar = row.addImage(creatProgress(value.vaccinatedAtLeastOnce.quote))
    progressBar.imageSize = new Size(width, h)
  }
  
  stack.addSpacer(2)
  
  const row = stack.addStack()  
  row.layoutHorizontally()
  const stateText = row.addText('Gesamt')
  stateText.font = Font.boldSystemFont(fontSize + 1)
    
  row.addSpacer()
  const quoteText = row.addText(`${parseInt(germany.vaccinatedAtLeastOnce.doses).toLocaleString(Device.language())}`)
  quoteText.font = Font.boldSystemFont(fontSize + 1)
    
  row.addSpacer(4)
  const progressBar = row.addImage(creatProgress(germany.vaccinatedAtLeastOnce.quote))
  progressBar.imageSize = new Size(width, h)
  
  widget.addSpacer(0)
} else {
  const row = widget.addStack()
  row.layoutHorizontally()
  
  if (selectedState) {
    const state = states.find((e) => e.name === selectedState)
    const column = row.addStack()
    column.layoutVertically()
    //column.addSpacer(2)
    column.centerAlignContent()
    
    const imageStack1 = column.addStack()
    imageStack1.layoutHorizontally()
    imageStack1.addSpacer()
    imageStack1.addImage(getDiagram(state.vaccinatedAtLeastOnce.quote, state.fullyVaccinated.quote));
    imageStack1.addSpacer()
    column.addSpacer(5)
    
    // Total Numbers    
    let total1 = state.inhabitants / 1000
    let total1unit = " Tsd."
    // if total is a million or more, format as millions and not thousands
    if ( altUnits && state.inhabitants > 999999 ){
      total1 =  state.inhabitants / 1000000
      total1unit = " Mio."
    }
    ///////////////////////////////////////////////////////////////////
    
    // vaccinated nunbers
    let vaccinated1
    let vaccinated1unit = " Tsd."
    
    if (altUnits && state.vaccinatedAtLeastOnce.doses > 999999){
      vaccinated1 =  state.vaccinatedAtLeastOnce.doses / 1000000
      vaccinated1unit = " Mio."
    }
    else if (state.vaccinatedAtLeastOnce.doses > 999 ) {
      vaccinated1 =  state.vaccinatedAtLeastOnce.doses / 1000
    } else {
      vaccinated1 = state.vaccinatedAtLeastOnce.doses
      vaccinated1unit = ''
    }
    let vaccinated1b
    let vaccinated1bunit = " Tsd."
    
    if (altUnits && state.fullyVaccinated.doses > 999999){
      vaccinated1b =  state.fullyVaccinated.doses / 1000000
      vaccinated1bunit = " Mio."
    }
    else if ( state.fullyVaccinated.doses > 999 ) {
      vaccinated1b =  state.fullyVaccinated.doses / 1000
    } else {
      vaccinated1b = state.fullyVaccinated.doses
      vaccinated1bunit = ''
    }
    ///////////////////////////////////////////////////////////////////
    if (maximumFractionDigits === 0) {
      total1 = parseInt(total1)
      vaccinated1 = parseInt(vaccinated1)
      vaccinated1b = parseInt(vaccinated1b)
    }
    
    const numbersText1Stack = column.addStack()
    numbersText1Stack.layoutHorizontally()
    numbersText1Stack.addSpacer()
    
    const textString1 = `${
      parseFloat(vaccinated1)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${vaccinated1unit} / ${
      parseFloat(vaccinated1b)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${vaccinated1bunit} von ${
      parseFloat(total1)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${total1unit}`
    const numbersText1 = numbersText1Stack.addText(textString1)  
    numbersText1.font = Font.systemFont(fontSize3)
    numbersText1.centerAlignText()
    numbersText1Stack.addSpacer()
    
    const stateText1Stack = column.addStack()
    stateText1Stack.layoutHorizontally()
    stateText1Stack.addSpacer()
    
    const stateText1 = stateText1Stack.addText(selectedState)
    stateText1.font = Font.boldSystemFont(fontSize2)
    stateText1.minimumScaleFactor = 0.7
    stateText1.lineLimit = 1
    stateText1Stack.addSpacer()
  }
  if (!selectedState || config.widgetFamily == 'medium') {
    const column2 = row.addStack()
    column2.layoutVertically()
    //column2.addSpacer(2)
    column2.centerAlignContent()
        
    const imageStack2 = column2.addStack()
    imageStack2.layoutHorizontally()
    imageStack2.addSpacer()
    imageStack2.addImage(getDiagram(germany.vaccinatedAtLeastOnce.quote, germany.fullyVaccinated.quote));
    imageStack2.addSpacer()
    column2.addSpacer(5)
    
    
    // Total numbers
    let total2 = (germany.inhabitants / 1000).toFixed(0)
    let total2unit = " Tsd."
    // if total is a million or more, format as millions and not thousands
    if (altUnits && germany.inhabitants > 999999 ){
    	total2 = germany.inhabitants / 1000000
    	total2unit = " Mio."
    }
    ///////////////////////////////////////////////////////////////////
    
    // vaccinated numbers
    let vaccinated2 = germany.vaccinatedAtLeastOnce.doses / 1000
    let vaccinated2unit = " Tsd."
    if ( altUnits && germany.vaccinatedAtLeastOnce.doses > 999999 ){
    	vaccinated2 = germany.vaccinatedAtLeastOnce.doses / 1000000
    	vaccinated2unit = " Mio."
    }
    let vaccinated2b = germany.fullyVaccinated.doses / 1000
    let vaccinated2bunit = " Tsd."
    if (altUnits && germany.fullyVaccinated.doses > 999999 ){
    	vaccinated2b = germany.fullyVaccinated.doses / 1000000
    	vaccinated2bunit = " Mio."
    }
    ///////////////////////////////////////////////////////////////////
    if (maximumFractionDigits === 0) {
      total2 = parseInt(total2)
      vaccinated2 = parseInt(vaccinated2)
      vaccinated2b = parseInt(vaccinated2b)
    }
    
    const numbersText2Stack = column2.addStack()
    numbersText2Stack.layoutHorizontally()
    numbersText2Stack.addSpacer()
    
    
    const textString2 = `${
      parseFloat(vaccinated2)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${vaccinated2unit} / ${
      parseFloat(vaccinated2b)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${vaccinated2bunit} von ${
      parseFloat(total2)
      .toLocaleString(Device.language(), {maximumFractionDigits: maximumFractionDigits})
     }${total2unit}`
    
    const numbersText2 = numbersText2Stack.addText(textString2)  
    numbersText2.font = Font.systemFont(fontSize3)
    numbersText2Stack.addSpacer()
    
    const stateText2Stack = column2.addStack()
    stateText2Stack.layoutHorizontally()
    stateText2Stack.addSpacer()
    
    const stateText2 = stateText2Stack.addText('Deutschland')
    stateText2.font = Font.boldSystemFont(fontSize2)
    stateText2Stack.addSpacer()
  }
  if (config.widgetFamily === 'small') {
    const lastUpdateStack = widget.addStack()
    lastUpdateStack.layoutHorizontally()
    lastUpdateStack.addSpacer()
    const lastUpdateText = lastUpdateStack.addDate(new Date(result.lastUpdate))
    lastUpdateText.font = Font.systemFont(6)
    lastUpdateText.rightAlignText()
    lastUpdateText.applyDateStyle()
    lastUpdateStack.addSpacer()
  }
}

if (!config.runsInWidget) {
  switch (config.widgetFamily) {
    case 'small': await widget.presentSmall(); break;
    case 'medium': await widget.presentMedium(); break;
    case 'large': await widget.presentLarge(); break;
  }
} else {
  Script.setWidget(widget)
}
Script.complete()
