feat: device metrics time breaks (#1456)

* The battery line is only drawn from point to point when we don't have a significant break in time.

* Implemented GraphUtil.plotPoint

* Implemented GraphUtil.createPath

* Added licence to GraphUtil.kt.
pull/1457/head
Robert-0410 2024-12-11 06:48:15 -08:00 zatwierdzone przez GitHub
rodzic 5d0b0e7d72
commit 06bf9e5ecd
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
2 zmienionych plików z 154 dodań i 49 usunięć

Wyświetl plik

@ -46,7 +46,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
@ -66,6 +65,8 @@ import com.geeksville.mesh.ui.BatteryInfo
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.theme.Orange
import com.geeksville.mesh.util.GraphUtil.plotPoint
import com.geeksville.mesh.util.GraphUtil.createPath
private val DEVICE_METRICS_COLORS = listOf(Color.Green, Color.Magenta, Color.Cyan)
private const val MAX_PERCENT_VALUE = 100f
@ -195,61 +196,60 @@ private fun DeviceMetricsChart(
val height = size.height
val width = size.width
val dataPointRadius = 2.dp.toPx()
val strokePath = Path().apply {
for (i in telemetries.indices) {
val telemetry = telemetries[i]
for (i in telemetries.indices) {
val telemetry = telemetries[i]
/* x-value for all three */
val x1Ratio = (telemetry.time - oldest.time).toFloat() / timeDiff
val x1 = x1Ratio * width
/* x-value time */
val xRatio = (telemetry.time - oldest.time).toFloat() / timeDiff
val x = xRatio * width
/* Channel Utilization */
val chUtilRatio =
telemetry.deviceMetrics.channelUtilization / MAX_PERCENT_VALUE
val yChUtil = height - (chUtilRatio * height)
drawCircle(
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
radius = dataPointRadius,
center = Offset(x1, yChUtil)
)
/* Channel Utilization */
plotPoint(
drawContext = drawContext,
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
radius = dataPointRadius,
x = x,
value = telemetry.deviceMetrics.channelUtilization,
divisor = MAX_PERCENT_VALUE
)
/* Air Utilization Transmit */
val airUtilRatio = telemetry.deviceMetrics.airUtilTx / MAX_PERCENT_VALUE
val yAirUtil = height - (airUtilRatio * height)
drawCircle(
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
radius = dataPointRadius,
center = Offset(x1, yAirUtil)
)
/* Battery line */
val nextTelemetry = telemetries.getOrNull(i + 1) ?: telemetries.last()
val y1Ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
val y1 = height - (y1Ratio * height)
val x2Ratio = (nextTelemetry.time - oldest.time).toFloat() / timeDiff
val x2 = x2Ratio * width
val y2Ratio = nextTelemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
val y2 = height - (y2Ratio * height)
if (i == 0) {
moveTo(x1, y1)
}
quadraticTo(x1, y1, (x1 + x2) / 2f, (y1 + y2) / 2f)
}
/* Air Utilization Transmit */
plotPoint(
drawContext = drawContext,
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
radius = dataPointRadius,
x = x,
value = telemetry.deviceMetrics.airUtilTx,
divisor = MAX_PERCENT_VALUE
)
}
/* Battery Line */
drawPath(
path = strokePath,
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
style = Stroke(
width = dataPointRadius,
cap = StrokeCap.Round
var index = 0
while (index < telemetries.size) {
val path = Path()
index = createPath(
telemetries = telemetries,
index = index,
path = path,
oldestTime = oldest.time,
timeRange = timeDiff,
width = width
) { i ->
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE
val y = height - (ratio * height)
return@createPath y
}
drawPath(
path = path,
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
style = Stroke(
width = dataPointRadius,
cap = StrokeCap.Round
)
)
)
}
}
}
YAxisLabels(

Wyświetl plik

@ -0,0 +1,105 @@
/*
* Copyright (c) 2024 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.util
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawContext
import com.geeksville.mesh.TelemetryProtos.Telemetry
import java.util.concurrent.TimeUnit
private const val TIME_SEPARATION_THRESHOLD = 2L
object GraphUtil {
/**
* @param value Must be zero-scaled before passing.
* @param divisor The range for the data set.
*/
fun plotPoint(
drawContext: DrawContext,
color: Color,
radius: Float,
x: Float,
value: Float,
divisor: Float,
) {
val height = drawContext.size.height
val ratio = value / divisor
val y = height - (ratio * height)
drawContext.canvas.drawCircle(
center = Offset(x, y),
radius = radius,
paint = androidx.compose.ui.graphics.Paint().apply { this.color = color }
)
}
/**
* Creates a [Path] that could be used to draw a line from the `index` to the end of `telemetries`
* or the last point before a time separation between [Telemetry]s.
*
* @param telemetries data used to create the [Path]
* @param index current place in the [List]
* @param path [Path] that will be used to draw
* @param timeRange The time range for the data set
* @param width of the [DrawContext]
* @param calculateY (`index`) -> `y` coordinate
*/
fun createPath(
telemetries: List<Telemetry>,
index: Int,
path: Path,
oldestTime: Int,
timeRange: Int,
width: Float,
calculateY: (Int) -> Float
): Int {
var i = index
var isNewLine = true
with(path) {
while (i < telemetries.size) {
val telemetry = telemetries[i]
val nextTelemetry = telemetries.getOrNull(i + 1) ?: telemetries.last()
/* Check to see if we have a significant time break between telemetries. */
if (nextTelemetry.time - telemetry.time > TimeUnit.HOURS.toSeconds(TIME_SEPARATION_THRESHOLD)) {
i++
break
}
val x1Ratio = (telemetry.time - oldestTime).toFloat() / timeRange
val x1 = x1Ratio * width
val y1 = calculateY(i)
val x2Ratio = (nextTelemetry.time - oldestTime).toFloat() / timeRange
val x2 = x2Ratio * width
val y2 = calculateY(i + 1)
if (isNewLine || i == 0) {
isNewLine = false
moveTo(x1, y1)
}
quadraticTo(x1, y1, (x1 + x2) / 2f, (y1 + y2) / 2f)
i++
}
}
return i
}
}