Last active
May 15, 2020 09:11
-
-
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.
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
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