Skip to content

Instantly share code, notes, and snippets.

@TaseerAhmad
Last active May 15, 2020 09:11
Show Gist options
  • Save TaseerAhmad/a0e7e84ae677fed0dd5a7f6f60243420 to your computer and use it in GitHub Desktop.
Save TaseerAhmad/a0e7e84ae677fed0dd5a7f6f60243420 to your computer and use it in GitHub Desktop.
Air quality Index calculator (US EPA). This is an unformatted code, requires to be wrapped in a library. Currently, the AQI calculator assumes the provided unit are based on US EPA for each pollutant.
typealias DoubleRange = ClosedFloatingPointRange<Double>
sealed class Pollutant {
object CO : Pollutant()
object NO2 : Pollutant()
object SO2 : Pollutant()
object PM25 : Pollutant()
object PM10 : Pollutant()
object Ozone : Pollutant()
}
sealed class PollutantCaution {
object Good : PollutantCaution()
object Moderate : PollutantCaution()
object Hazardous : PollutantCaution()
object GeneralUnhealthy : PollutantCaution()
object ExtremelyUnhealthy : PollutantCaution()
object UnhealthyForSensitives : PollutantCaution()
}
data class Result(val aqi: Int, val category: AqiCategoryInfo)
data class AqiCategoryInfo(
val color: String,
val pollutantType: Pollutant,
val pollutantCaution: PollutantCaution
)
class AirQualityIndex private constructor() {
private val aqiIndexRange = getIndexRange()
private val aqiCategory = AqiCategory(aqiIndexRange)
private val pmTwo by lazy(LazyThreadSafetyMode.NONE) { PmTwo(aqiIndexRange) }
private val pmTen by lazy(LazyThreadSafetyMode.NONE) { PmTen(aqiIndexRange) }
private val ozone by lazy(LazyThreadSafetyMode.NONE) { Ozone(aqiIndexRange) }
private val carbonMonoxide by lazy(LazyThreadSafetyMode.NONE) { CarbonMonoxide(aqiIndexRange) }
private val sulphurDioxide by lazy(LazyThreadSafetyMode.NONE) { SulphurDioxide(aqiIndexRange) }
private val nitrogenDioxide by lazy(LazyThreadSafetyMode.NONE) { NitrogenDioxide(aqiIndexRange) }
companion object {
private lateinit var airQualityIndex: AirQualityIndex
fun getInstance(): AirQualityIndex {
if (!::airQualityIndex.isInitialized)
airQualityIndex = AirQualityIndex()
return airQualityIndex
}
}
/**
* Returns a result object consisting of calculated AQI and its category information based on US EPA. Category information
* contains Pollutant type, associated color and the category type of the calculated AQI. The cautionary statements
* associated with each category are not provided for translation purposes. You are required to provide your own
* cautionary statements for each category.
* **Returns null if the provided reading is less than 0**
*/
fun getResult(pollutant: Pollutant, reading: Double, averagingPeriod: Int): Result? {
if (reading < 0)
return null
return getPollutantResult(pollutant, reading, averagingPeriod)
}
private fun getPollutantResult(pollutant: Pollutant, reading: Double, averagingPeriod: Int): Result {
val calculatedAqi = when (pollutant) {
Pollutant.PM25 -> pmTwo.getAqi(reading)
Pollutant.PM10 -> pmTen.getAqi(reading)
Pollutant.CO -> carbonMonoxide.getAqi(reading)
Pollutant.SO2 -> sulphurDioxide.getAqi(reading)
Pollutant.NO2 -> nitrogenDioxide.getAqi(reading)
Pollutant.Ozone -> ozone.getAqi(reading, averagingPeriod)
}
val categoryInfo = aqiCategory.getCategoryInfo(pollutant, calculatedAqi)
return Result(calculatedAqi, categoryInfo)
}
private fun getIndexRange(): List<IntRange> {
return listOf(0..50, 51..100, 101..150, 151..200, 201..300, 301..400, 401..500)
}
}
abstract class PollutantAqi(private val aqiIndexRange: List<IntRange>) {
private val indexRange by lazy(LazyThreadSafetyMode.NONE) { getPmIndexMap() }
protected fun calculateAqi(reading: Double): Int {
val breakPointRange = getBreakPoint(reading)
val indexValueRange = getIndexValue(breakPointRange)
return getAqi(reading, indexValueRange, breakPointRange)
}
private fun getAqi(
reading: Double,
indexValueRange: IntRange,
breakPointRange: ClosedFloatingPointRange<Double>
): Int {
val indexResult = indexValueRange.last - indexValueRange.first
val truncatedBreakPoint = reading - breakPointRange.start
val breakPointResult = breakPointRange.endInclusive - breakPointRange.start
val aqi = (((indexResult * truncatedBreakPoint) / breakPointResult) + indexValueRange.first)
return ceil(aqi).toInt()
}
private fun getBreakPoint(reading: Double): DoubleRange {
return getConcentrationRange().find { reading in it }
?: throw NoSuchElementException("No breakpoint elements available in the list for $reading. The reading may be out of bound.")
}
private fun getIndexValue(breakPointRange: DoubleRange): IntRange {
return indexRange.getValue(breakPointRange.endInclusive)
}
private fun getPmIndexMap(): Map<Double, IntRange> {
val map = mutableMapOf<Double, IntRange>()
val range = getConcentrationRange()
for (index in aqiIndexRange.indices) {
val endInclusiveKey = range[index].endInclusive
map[endInclusiveKey] = aqiIndexRange[index]
}
return Collections.unmodifiableMap(map)
}
protected abstract fun getConcentrationRange(): List<DoubleRange>
}
private class NitrogenDioxide(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val concentrations = generateConcentrations()
private val maxReading = 2049.0
fun getAqi(reading: Double): Int {
if (reading > maxReading)
return 500
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return concentrations
}
private fun generateConcentrations(): List<DoubleRange> {
return listOf(0.0..53.0, 51.0..100.0, 101.0..150.0, 151.0..200.0, 201.0..300.0, 301.0..400.0, 401.0..500.0)
}
}
private class SulphurDioxide(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val concentrations = generateConcentrations()
private val maxReading = 1004.0
fun getAqi(reading: Double): Int {
if (reading > maxReading)
return 500
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return concentrations
}
private fun generateConcentrations(): List<DoubleRange> {
return listOf(0.0..35.0, 36.0..75.0, 76.0..185.0, 186.0..304.0, 305.0..604.0, 605.0..804.0, 805.0..1004.0)
}
}
private class CarbonMonoxide(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val concentrations = generateConcentrations()
private val maxReading = 50.4
fun getAqi(reading: Double): Int {
if (reading > maxReading)
return 500
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return concentrations
}
private fun generateConcentrations(): List<DoubleRange> {
return listOf(0.0..4.4, 4.5..9.4, 9.5..12.4, 12.5..15.4, 15.5..30.4, 30.5..40.4, 40.5..50.4)
}
}
private class PmTen(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val concentrations = generateConcentrations()
private val maxReading = 604.0
fun getAqi(reading: Double): Int {
if (reading > maxReading)
return 500
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return concentrations
}
private fun generateConcentrations(): List<DoubleRange> {
return listOf(0.0..54.0, 55.0..154.0, 155.0..254.0, 255.0..354.0, 355.0..424.0, 425.0..504.0, 505.0..604.0)
}
}
private class Ozone(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val oneHourConcentrations = getOneHourConcentrations()
private val eightHourConcentrations = getEightHourConcentrations()
private lateinit var currentConcentrationList: List<DoubleRange>
private val maxReading = 0.604
fun getAqi(reading: Double, averagingPeriod: Int): Int {
if (reading > maxReading)
return 500
setConcentrationRange(averagingPeriod)
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return currentConcentrationList
}
private fun setConcentrationRange(averagingPeriod: Int) {
currentConcentrationList = when (averagingPeriod) {
1 -> oneHourConcentrations
8 -> eightHourConcentrations
else -> eightHourConcentrations
}
}
private fun getEightHourConcentrations(): List<DoubleRange> {
return listOf(0.000..0.054, 0.055..0.070,
0.071..0.085, 0.086..0.105, 0.106..0.200,
0.405..0.504, 0.505..0.604)
}
private fun getOneHourConcentrations(): List<DoubleRange> {
return listOf(0.000..0.054, 0.055..0.070, 0.165..0.204, 0.205..0.404, 0.405..0.504, 0.505..0.604)
}
}
private class PmTwo(aqiIndexRange: List<IntRange>) : PollutantAqi(aqiIndexRange) {
private val concentrations = generateConcentrations()
private val maxReading = 500.4
fun getAqi(reading: Double): Int {
if (reading > maxReading)
return 500
return super.calculateAqi(reading)
}
override fun getConcentrationRange(): List<DoubleRange> {
return concentrations
}
private fun generateConcentrations(): List<DoubleRange> {
return listOf(0.0..12.0, 12.1..35.4, 35.5..55.4, 55.5..150.4, 150.5..250.4, 250.5..350.4, 350.5..500.4)
}
}
private class AqiColor(aqiIndexRange: List<IntRange>) {
private val red = "#D84315"
private val green = "#00C853"
private val yellow = "#F9A825"
private val orange = "#EF6C00"
private val purple = "#4527A0"
private val deepPurple = "#4A148C"
private val colors = getColorMap(aqiIndexRange)
fun getColor(aqi: IntRange): String {
return colors.getValue(aqi.last)
}
private fun getColorMap(aqiIndexRange: List<IntRange>): Map<Int, String> {
return mapOf(
aqiIndexRange[0].last to green,
aqiIndexRange[1].last to yellow,
aqiIndexRange[2].last to orange,
aqiIndexRange[3].last to red,
aqiIndexRange[4].last to purple,
aqiIndexRange[5].last to deepPurple,
aqiIndexRange[6].last to deepPurple
)
}
}
private class AqiCategory(private val aqiIndexRange: List<IntRange>) {
private val aqiColor = AqiColor(aqiIndexRange)
fun getCategoryInfo(pollutant: Pollutant, aqi: Int): AqiCategoryInfo {
val aqiRange = getAqiRange(aqi, aqiIndexRange)
val color = aqiColor.getColor(aqiRange)
val caution = PollutantDescriptor(aqiRange).getPollutionCaution()
return AqiCategoryInfo(color, pollutant, caution)
}
private fun getAqiRange(aqi: Int, aqiIndexRange: List<IntRange>): IntRange {
return aqiIndexRange.find { aqi in it }!!
}
}
private inline class PollutantDescriptor(private val aqi: IntRange) {
fun getPollutionCaution(): PollutantCaution {
return when (aqi) {
0..50 -> PollutantCaution.Good
51..100 -> PollutantCaution.Moderate
101..150 -> PollutantCaution.UnhealthyForSensitives
151..200 -> PollutantCaution.GeneralUnhealthy
201..300 -> PollutantCaution.ExtremelyUnhealthy
301..500 -> PollutantCaution.Hazardous
else -> PollutantCaution.Hazardous
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment