kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: Add IAQ to environment metrics (#1258)
Adds Indoor Air Quality (IAQ) to the environment metrics chart and card. The IAQ is displayed as a green line on the chart and as a dot with a numerical value on the card. The IAQ scale is available as a legend in the IAQ component.pull/1259/head
rodzic
843e423648
commit
9b90c128fb
|
@ -57,24 +57,25 @@ fun EnvironmentMetricsScreen(telemetries: List<Telemetry>) {
|
|||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(telemetries) { telemetry -> EnvironmentMetricsCard(telemetry)}
|
||||
items(telemetries) { telemetry -> EnvironmentMetricsCard(telemetry) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun EnvironmentMetricsChart(modifier: Modifier = Modifier, telemetries: List<Telemetry>) {
|
||||
|
||||
ChartHeader(amount = telemetries.size)
|
||||
if (telemetries.isEmpty())
|
||||
if (telemetries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val graphColor = MaterialTheme.colors.onSurface
|
||||
val transparentTemperatureColor = remember { ENVIRONMENT_METRICS_COLORS[0].copy(alpha = 0.5f) }
|
||||
val transparentHumidityColor = remember { ENVIRONMENT_METRICS_COLORS[1].copy(alpha = 0.5f) }
|
||||
val transparentIAQColor = remember { ENVIRONMENT_METRICS_COLORS[2].copy(alpha = 0.5f) }
|
||||
val spacing = LEFT_CHART_SPACING
|
||||
|
||||
/* Since both temperature and humidity are being plotted we need a combined min and max. */
|
||||
|
@ -90,8 +91,22 @@ private fun EnvironmentMetricsChart(modifier: Modifier = Modifier, telemetries:
|
|||
telemetries.maxBy { it.environmentMetrics.relativeHumidity }
|
||||
)
|
||||
}
|
||||
val min = minOf(minTemp.environmentMetrics.temperature, minHumidity.environmentMetrics.relativeHumidity)
|
||||
val max = maxOf(maxTemp.environmentMetrics.temperature, maxHumidity.environmentMetrics.relativeHumidity)
|
||||
val (minIAQ, maxIAQ) = remember(key1 = telemetries) {
|
||||
Pair(
|
||||
telemetries.minBy { it.environmentMetrics.iaq },
|
||||
telemetries.maxBy { it.environmentMetrics.iaq }
|
||||
)
|
||||
}
|
||||
val min = minOf(
|
||||
minTemp.environmentMetrics.temperature,
|
||||
minHumidity.environmentMetrics.relativeHumidity,
|
||||
minIAQ.environmentMetrics.iaq.toFloat()
|
||||
)
|
||||
val max = maxOf(
|
||||
maxTemp.environmentMetrics.temperature,
|
||||
maxHumidity.environmentMetrics.relativeHumidity,
|
||||
maxIAQ.environmentMetrics.iaq.toFloat()
|
||||
)
|
||||
val diff = max - min
|
||||
|
||||
Box(contentAlignment = Alignment.TopStart) {
|
||||
|
@ -106,7 +121,6 @@ private fun EnvironmentMetricsChart(modifier: Modifier = Modifier, telemetries:
|
|||
|
||||
/* Plot Temperature and Relative Humidity */
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val height = size.height
|
||||
val width = size.width - 28.dp.toPx()
|
||||
val spacePerEntry = (width - spacing) / telemetries.size
|
||||
|
@ -216,6 +230,62 @@ private fun EnvironmentMetricsChart(modifier: Modifier = Modifier, telemetries:
|
|||
cap = StrokeCap.Round
|
||||
)
|
||||
)
|
||||
|
||||
/* Air Quality */
|
||||
var lastIaqX = 0f
|
||||
val iaqPath = Path().apply {
|
||||
for (i in telemetries.indices) {
|
||||
val envMetrics = telemetries[i].environmentMetrics
|
||||
val nextEnvMetrics =
|
||||
(telemetries.getOrNull(i + 1) ?: telemetries.last()).environmentMetrics
|
||||
val leftRatio = (envMetrics.iaq - min) / diff
|
||||
val rightRatio = (nextEnvMetrics.iaq - min) / diff
|
||||
|
||||
val x1 = spacing + i * spacePerEntry
|
||||
val y1 = height - spacing - (leftRatio * height)
|
||||
|
||||
val x2 = spacing + (i + 1) * spacePerEntry
|
||||
|
||||
val y2 = height - spacing - (rightRatio * height)
|
||||
if (i == 0) {
|
||||
moveTo(x1, y1)
|
||||
}
|
||||
lastIaqX = (x1 + x2) / 2f
|
||||
quadraticBezierTo(
|
||||
x1,
|
||||
y1,
|
||||
lastIaqX,
|
||||
(y1 + y2) / 2f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val fillIaqPath = android.graphics.Path(iaqPath.asAndroidPath())
|
||||
.asComposePath()
|
||||
.apply {
|
||||
lineTo(lastIaqX, height - spacing)
|
||||
lineTo(spacing, height - spacing)
|
||||
close()
|
||||
}
|
||||
drawPath(
|
||||
path = fillIaqPath,
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
transparentIAQColor,
|
||||
Color.Transparent
|
||||
),
|
||||
endY = height - spacing
|
||||
),
|
||||
)
|
||||
|
||||
drawPath(
|
||||
path = iaqPath,
|
||||
color = ENVIRONMENT_METRICS_COLORS[2],
|
||||
style = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
)
|
||||
}
|
||||
TimeLabels(
|
||||
modifier = modifier,
|
||||
|
@ -294,6 +364,26 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry) {
|
|||
)
|
||||
}
|
||||
}
|
||||
if (telemetry.environmentMetrics.hasIaq()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
/* Air Quality */
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.iaq),
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = MaterialTheme.typography.button.fontSize
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
IndoorAirQuality(
|
||||
iaq = telemetry.environmentMetrics.iaq,
|
||||
displayMode = IaqDisplayMode.Dot
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,10 +401,18 @@ private fun EnvironmentLegend() {
|
|||
|
||||
LegendLabel(text = stringResource(R.string.temperature), color = ENVIRONMENT_METRICS_COLORS[0], isLine = true)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
LegendLabel(text = stringResource(R.string.humidity), color = ENVIRONMENT_METRICS_COLORS[1], isLine = true)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
LegendLabel(
|
||||
text = stringResource(R.string.iaq),
|
||||
color = ENVIRONMENT_METRICS_COLORS[2],
|
||||
isLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
package com.geeksville.mesh.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ThumbUp
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
enum class Iaq(val color: Color, val description: String) {
|
||||
Excellent(Color.Green, "Excellent"),
|
||||
Good(Color.Green, "Good"),
|
||||
LightlyPolluted(Color.Yellow, "Lightly Polluted"),
|
||||
ModeratelyPolluted(Color.Orange, "Moderately Polluted"),
|
||||
HeavilyPolluted(Color.Red, "Heavily Polluted"),
|
||||
SeverelyPolluted(Color.Purple, "Severely Polluted"),
|
||||
ExtremelyPolluted(Color.Purple, "Extremely Polluted"),
|
||||
DangerouslyPolluted(Color.Brown, "Dangerously Polluted")
|
||||
}
|
||||
|
||||
val Color.Companion.Mint: Color
|
||||
get() = Color(0xFF98FB98)
|
||||
val Color.Companion.Purple: Color
|
||||
get() = Color(0xFF800080)
|
||||
val Color.Companion.Brown: Color
|
||||
get() = Color(0xFFA52A2A)
|
||||
val Color.Companion.Orange: Color
|
||||
get() = Color(0xFFFFA500)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getIaq(iaq: Int): Iaq {
|
||||
return when {
|
||||
iaq <= 50 -> Iaq.Excellent
|
||||
iaq <= 100 -> Iaq.Good
|
||||
iaq <= 150 -> Iaq.LightlyPolluted
|
||||
iaq <= 200 -> Iaq.ModeratelyPolluted
|
||||
iaq <= 300 -> Iaq.HeavilyPolluted
|
||||
iaq <= 400 -> Iaq.SeverelyPolluted
|
||||
iaq <= 500 -> Iaq.ExtremelyPolluted
|
||||
else -> Iaq.DangerouslyPolluted
|
||||
}
|
||||
}
|
||||
|
||||
enum class IaqDisplayMode {
|
||||
Pill, Dot, Text, Gauge, Gradient
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "UnusedPrivateProperty")
|
||||
@Composable
|
||||
fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill) {
|
||||
var isLegendOpen by remember { mutableStateOf(false) }
|
||||
val iaqEnum = getIaq(iaq)
|
||||
val gradient = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.Green, Color.Mint, Color.Yellow, Color.Orange, Color.Red,
|
||||
Color.Purple, Color.Purple, Color.Brown, Color.Brown, Color.Brown, Color.Brown
|
||||
)
|
||||
)
|
||||
|
||||
Column {
|
||||
when (displayMode) {
|
||||
IaqDisplayMode.Pill -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(iaqEnum.color)
|
||||
.width(125.dp)
|
||||
.height(30.dp)
|
||||
.clickable { isLegendOpen = true }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "IAQ $iaq",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (iaq < 100) Icons.Default.ThumbUp else Icons.Filled.Warning,
|
||||
contentDescription = "AQI Icon",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IaqDisplayMode.Dot -> {
|
||||
Column(modifier = Modifier.clickable { isLegendOpen = true }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = "$iaq")
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.background(iaqEnum.color, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IaqDisplayMode.Text -> {
|
||||
Text(
|
||||
text = iaqEnum.description,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.clickable { isLegendOpen = true }
|
||||
)
|
||||
}
|
||||
|
||||
IaqDisplayMode.Gauge -> {
|
||||
CircularProgressIndicator(
|
||||
progress = iaq / 500f,
|
||||
modifier = Modifier
|
||||
.size(60.dp)
|
||||
.clickable { isLegendOpen = true },
|
||||
strokeWidth = 8.dp,
|
||||
color = iaqEnum.color
|
||||
)
|
||||
Text(text = "$iaq")
|
||||
}
|
||||
|
||||
IaqDisplayMode.Gradient -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.clickable { isLegendOpen = true }
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = iaq / 500f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp),
|
||||
color = iaqEnum.color,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = iaqEnum.description, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isLegendOpen) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { isLegendOpen = false },
|
||||
title = { Text("IAQ Scale") },
|
||||
text = {
|
||||
IAQScale()
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = { isLegendOpen = false }) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assuming Iaq is an enum class with color and description properties
|
||||
// and that it conforms to CaseIterable.
|
||||
// Replace with your actual implementation
|
||||
|
||||
@Composable
|
||||
fun IAQScale(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text("Indoor Air Quality (IAQ)", style = MaterialTheme.typography.h6)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
for (iaq in Iaq.entries) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(20.dp, 15.dp)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.background(iaq.color)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(iaq.description, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -230,4 +230,5 @@
|
|||
<string name="info">Information</string>
|
||||
<string name="ch_util_definition">Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).</string>
|
||||
<string name="air_util_definition">Percent of airtime for transmission used within the last hour.</string>
|
||||
<string name="iaq">IAQ</string>
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue