From 8cf1412f4c890e15137283ba897cd20a7f6befdc Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Thu, 8 Oct 2026 13:47:20 +0200 Subject: [PATCH 1/3] feat(gauge): readable scales, alert-driven red zones, fitted layout - Scale labels on nice steps (1/2/2.5/4/5 x 10^n) with the range extended to the nearest step; progress, labels and ticks share GaugeScale. - Red marks only the PID's alert ranges instead of every dial's end. - Side labels pulled inward; min/avg/max centred with captions and fitted; long values shrink to the dial. - Phone cards share the free height when the grid does not scroll. - Fix: out-of-range values drew the arc past the dial; angle no longer truncated; scale bitmap cache keyed by range and alerts, old bitmap recycled. - Cache the container gradient and resolved colours per frame. --- CLAUDE.md | 12 + doc/specs/gauge-drawing-improvements.md | 75 +++ doc/specs/img/gauge-before.png | Bin 0 -> 134482 bytes .../obd/graphs/renderer/gauge/GaugeDrawer.kt | 455 ++++++++++-------- .../graphs/renderer/gauge/GaugeGeometry.kt | 205 ++++++++ .../renderer/gauge/GaugeSurfaceRenderer.kt | 23 +- .../renderer/gauge/GaugeGeometryTest.kt | 135 ++++++ 7 files changed, 713 insertions(+), 192 deletions(-) create mode 100644 doc/specs/gauge-drawing-improvements.md create mode 100644 doc/specs/img/gauge-before.png create mode 100644 screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt create mode 100644 screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 7b4d6e7c..4559dbd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -201,6 +201,18 @@ Gradle needs JDK 17+ (Crashlytics plugin); the shell default may be JDK 11 and f screen, which has no setting of its own. Gauges past `PERFORMANCE_MAX_GAUGES` fall back into the grid, not nowhere; the gauge dialog only warns (toast), it does not block. +### Gauge drawer (`renderer/gauge/`) +* `GaugeDrawer` is shared by the Gauge, Performance, Drag Racing and Brake Boosting screens (phone + AA): + any dial change shows on all four. Spec: `doc/specs/gauge-drawing-improvements.md`. +* **The dial maps values through `GaugeScale`, not `pid.min`/`pid.max`.** It extends the PID range to + "nice" steps; progress, labels and ticks must all use `scale.fraction()` or they drift apart. + Pure geometry lives in `GaugeGeometry.kt` (unit-tested); keep Canvas out of it. +* Red on the dial = the PID's `alert` thresholds. The scale bitmap cache is keyed by `GaugeScale` and + `GaugeRedZones` too — a cache keyed by PID id alone kept a stale scale after a PID edit. +* Phone cards may be taller than the dial (`GaugeGeometry.cardHeight`); `borderArea` is the card, `top` + the dial. Captions belong to the card. `contentHeight`'s bottom pad must be excluded from the height + the cards share, or a filled grid scrolls by a few pixels. + ### Connectors (`:datalogger/.../connectors`) * One `AdapterConnection` per transport, chosen by `ConnectionManager.obtain()` on `pref.adapter.connection.type`. Two Bluetooth transports, deliberately separate: diff --git a/doc/specs/gauge-drawing-improvements.md b/doc/specs/gauge-drawing-improvements.md new file mode 100644 index 00000000..5cf6b46b --- /dev/null +++ b/doc/specs/gauge-drawing-improvements.md @@ -0,0 +1,75 @@ +# Spec: Gauge drawing improvements + +2026-10-08 · branch `feat/gauge-drawing-improvements` · **Status: implemented** + +## Summary + +Readability, correctness and per-frame cost fixes for the dial gauge (`GaugeDrawer`), taken from a phone Gauge-screen screenshot. + +![Phone Gauge screen before](img/gauge-before.png) + +**Problem.** Scale labels were thirds of the PID range (`-40, -6, 26, 60 …`, `0.8, 2.7, 4.5 …`). The last part of every dial was painted red whether or not the PID has an alert. End-of-scale labels ran into the ticks and the card border. The min / avg / max row was unlabelled and spaced by fixed offsets. A long value ran past the dial. Four gauges used under half of a portrait screen. A value outside the PID's min..max drew the progress arc past the dial's end (or backwards). + +**Scope.** `:screen_renderer` only: `GaugeDrawer`, `GaugeSurfaceRenderer`, new pure `GaugeGeometry.kt`. `GaugeDrawer` is shared, so items 1–5 and 7–9 also change the gauges of Performance, Drag Racing and Brake Boosting (phone and AA). No preference, query or data changes. + +## Behaviour changes + +| # | Area | Before | After | +| --- | --- | --- | --- | +| 1 | Scale labels | 6 equal parts of `min..max`, rounded (`-40, -6, 26 …`) | "Nice" steps (1, 2, 2.5, 4, 5 × 10ⁿ), 4–7 intervals, the range extended to the nearest step: `-40, 0, 40 … 160`; gear `-2, 0, 2 … 10` | +| 2 | Red zone | Last ~2 labels / ticks red on every dial (fixed divider indexes) | Red only over the PID's alert ranges (`alert.upperThreshold`..max, min..`alert.lowerThreshold`); none without thresholds | +| 3 | Label placement | Centre on a fixed radius; wide labels at the sides overlapped ticks and the border | Outer edge on a fixed radius; side labels move inward by their width | +| 4 | Stats row | Fixed offsets from the centre, no captions | Measured, centred, equal gaps, scaled down to 90 % of the card; `▼` before min, `▲` before max | +| 5 | Value text | Fixed size | Shrunk (never enlarged) to 70 % of the dial width with its unit; label / stats stay where they were | +| 6 | Phone layout | Square cards | When the grid does not scroll, cards share the free height up to 1.5 × width; dial centred, module name / rate stay in the card's top corners | +| 7 | Out-of-range value | Arc drawn past the end, or backwards below min | Clamped to the dial | +| 8 | Arc angle | Truncated to whole degrees | Float | +| 9 | Scale bitmap cache | Keyed by PID id, size, colour | Also by scale and red zones, so editing a PID's range or alerts redraws it; the replaced bitmap is recycled | +| 10 | Per frame | New `RadialGradient` per gauge; colour parsed / resolved per frame | Gradient cached per PID and card rect; colours resolved once | + +## Settings / keys touched + +None. + +## Implementation + +* `GaugeGeometry.kt` (pure, unit-tested): `GaugeScale.of(min, max)` picks the step by least range extension, then interval count nearest 5; `fraction()` clamps. `GaugeRedZones` turns thresholds into scale fractions. `GaugeGeometry.labelCenterRadius`, `fitScale`, `statsRow`, `cardHeight`. +* `GaugeDrawer`: `pidScale(metric)` caches scale + zones per PID, invalidated when min/max/thresholds change. Progress, numbers and ticks all use the scale's fraction. Ticks: majors on the labels, minors halfway; outer gray majors outside zones; in each zone the glowing dense ticks plus a solid band over its outer half. `DrawerSettings.dividersCount`, `dividersStepAngle`, `dividerHighlightStart` removed (no caller set them). +* `GaugeSurfaceRenderer` (phone only, not landscape single-column): `rowHeight = cardHeight + 2 × margin`; `borderRects` take the card height, the dial top is offset by half the slack. The free height excludes `CONTENT_BOTTOM_PADDING`, otherwise a filled grid scrolls by a few pixels. + +## Backward compatibility + +Visual only. Existing profiles keep their PIDs and ranges; the dial's range may extend slightly past a PID's min/max to the nearest step (gear `-1..10` → `-2..10`), so the progress arc's position for the same value can shift a little. Dials with no alert thresholds lose their decorative red end. + +## Tests + +`screen_renderer/src/test/.../gauge/GaugeGeometryTest.kt`: + +| Test | Pins | +| --- | --- | +| temperature scale uses round labels instead of thirds of the range | `-40..160` → `-40, 0, 40 … 160` | +| gear scale has integer labels instead of 0,8 and 2,7 | `-1..10` → `-2 … 10` | +| common ranges get round steps without extending the range | 0..100, 0..8000, 0..300, 0..3 | +| scale always covers the PID range with 4 to 7 intervals | Invariant over assorted ranges | +| no negative zero label / invalid range falls back to a single interval | Edge cases | +| value outside the scale sticks to its ends | Item 7 (clamp) | +| red zones come from the alert thresholds only / thresholds beyond the scale draw no zone | Item 2 | +| label at the top keeps its radius, wide labels at the sides move inward | Item 3 | +| stats row is centred with equal gaps / wider than the card is scaled down | Item 4 | +| value text is only ever shrunk | Item 5 | +| cards fill the free height up to a limit, stay square when scrolling | Item 6 | + +## Risks and verification checklist + +- [ ] Phone Gauge screen, portrait, 4 gauges: cards fill the height, no scrollbar, module name in the card corner. +- [ ] Portrait, many gauges (scrolls): cards square as before. +- [ ] Landscape, 1 and several gauges. +- [ ] A PID with an upper alert threshold shows red from the threshold; one without shows no red. +- [ ] Edit a PID's min/max: the dial redraws. +- [ ] Performance, Drag Racing, Brake Boosting gauges (phone + AA DHU): labels readable, no overlap with ticks. +- [ ] `▼` / `▲` render (font fallback) on the target devices. + +## Out of scope + +* Pre-rendering the progress glow: the arc changes every frame, so it cannot be cached; the `BlurMaskFilter` cost was not measured. +* Captions in words (`min / avg / max`): would need strings in `:app`'s resources for both locales. diff --git a/doc/specs/img/gauge-before.png b/doc/specs/img/gauge-before.png new file mode 100644 index 0000000000000000000000000000000000000000..8a1cfe8be399b148f7ec6bd92f44c8c951226a02 GIT binary patch literal 134482 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

OE@l= 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 z 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 z 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%0DwR$>@A2-u5u&9 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 + private val gradientCache = mutableMapOf() + private val numbersPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = color(R.color.gray) + color = colorGray } private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = color(R.color.gray) + color = colorGray + } + + private val statsCaptionPaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = colorGray } private val histogramPaint = @@ -158,7 +199,7 @@ internal class GaugeDrawer( private val modulePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = color(R.color.gray) + color = colorGray typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC) } @@ -180,9 +221,30 @@ internal class GaugeDrawer( super.recycle() scaleBitmapCache.values.forEach { it.bitmap.recycle() } scaleBitmapCache.clear() + pidScales.clear() + gradientCache.clear() textCache.clear() } + private fun pidScale(metric: Metric): PidScale { + val pid = metric.pid + val min = pid.min.toDouble() + val max = pid.max.toDouble() + val lower = pid.alert?.lowerThreshold?.toDouble() + val upper = pid.alert?.upperThreshold?.toDouble() + + val cached = pidScales[pid.id] + if (cached != null && + cached.scale.sourceMin == min && + cached.scale.sourceMax == max && + cached.redZones.lower == lower && + cached.redZones.upper == upper + ) { + return cached + } + return PidScale(GaugeScale.of(min, max), GaugeRedZones(lower, upper)).also { pidScales[pid.id] = it } + } + fun drawGauge( canvas: Canvas, metric: Metric, @@ -214,27 +276,33 @@ internal class GaugeDrawer( drawingCache.workingRect.bottom + strokeWidth ) + // A card taller than the dial keeps its captions in its own top corners. + val cardTop = borderArea?.top ?: top + if (drawMetricRate) { - drawMetricRate(metric, drawingCache.workingRect, fontSize, width, left, top, canvas) + drawMetricRate(metric, drawingCache.workingRect, fontSize, width, left, cardTop, canvas) } if (drawModule) { - drawModuleName(metric, drawingCache.workingRect, fontSize, width, left, top, canvas) + drawModuleName(metric, drawingCache.workingRect, fontSize, width, left, cardTop, canvas) } if (drawBorder) { drawBorder(canvas, width, left, top, borderArea) } - drawContainerBackground(canvas, width, left, top, borderArea) + drawContainerBackground(canvas, metric.pid.id, width, left, top, borderArea) - drawBackground(canvas, drawingCache.workingRect, drawingCache.arcTopRect, strokeWidth, strokeWidth, metric) + val pidScale = pidScale(metric) + + drawBackground(canvas, drawingCache.workingRect, drawingCache.arcTopRect, strokeWidth, strokeWidth, metric, pidScale.scale) drawScale( canvas, drawingCache.workingRect, drawingCache.arcTopRect, metric, + pidScale, scaleEnabled, radius, dynamicPadding @@ -296,6 +364,7 @@ internal class GaugeDrawer( private fun drawContainerBackground( canvas: Canvas, + cacheKey: Long, width: Float, left: Float, top: Float, @@ -314,20 +383,27 @@ internal class GaugeDrawer( drawingCache.destRectF } - val gradientRadius = min(destRect.width(), destRect.height()) * 0.45f - - drawingCache.gradientColors2[0] = gradientColor - drawingCache.gradientColors2[1] = Color.TRANSPARENT - + // A RadialGradient per gauge per frame was pure allocation churn: the card rarely moves. + val cached = gradientCache[cacheKey] val gradient = - RadialGradient( - destRect.centerX(), - destRect.centerY(), - gradientRadius, - drawingCache.gradientColors2, - drawingCache.backgroundPositions, - Shader.TileMode.CLAMP - ) + if (cached != null && cached.matches(destRect, gradientColor)) { + cached.shader + } else { + drawingCache.gradientColors2[0] = gradientColor + drawingCache.gradientColors2[1] = Color.TRANSPARENT + + RadialGradient( + destRect.centerX(), + destRect.centerY(), + min(destRect.width(), destRect.height()) * 0.45f, + drawingCache.gradientColors2, + drawingCache.backgroundPositions, + Shader.TileMode.CLAMP + ).also { + gradientCache[cacheKey] = + CachedGradient(destRect.left, destRect.top, destRect.right, destRect.bottom, gradientColor, it) + } + } backgroundGradientPaint.shader = gradient val cornerRadius = 8f * context.resources.displayMetrics.density @@ -362,14 +438,15 @@ internal class GaugeDrawer( arcTopRect: RectF, arcTopOffset: Float, strokeWidth: Float, - metric: Metric + metric: Metric, + scale: GaugeScale ) { paint.style = Paint.Style.STROKE - paint.color = "#0D000000".toColorInt() + paint.color = trackShadowColor paint.strokeWidth = strokeWidth canvas.drawArc(rect, drawerSettings.startAngle, drawerSettings.sweepAngle, false, paint) - paint.color = color(R.color.gray_dark) + paint.color = colorGrayDark paint.strokeWidth = 2f canvas.drawArc( arcTopRect, @@ -404,13 +481,14 @@ internal class GaugeDrawer( ) val progressBarHeight = (drawingCache.arcBottomRect.top - arcTopRect.top - 2f) - drawProgressBar(metric, canvas, rect, progressBarHeight) + drawProgressBar(metric, scale, canvas, rect, progressBarHeight) paint.strokeWidth = strokeWidth } private fun drawProgressBar( metric: Metric, + scale: GaugeScale, canvas: Canvas, rect: RectF, strokeWidth: Float @@ -428,11 +506,10 @@ internal class GaugeDrawer( setProgressGradient(rect) } - val value = metric.source.toFloat() - val startValue = metric.pid.min.toFloat() - val endValue = metric.pid.max.toFloat() + // Clamped: a value past the PID's max used to sweep beyond the dial, below min backwards. + val fraction = scale.fraction(metric.source.toDouble()) - if (value == startValue) { + if (fraction == 0f) { canvas.drawArc( drawingCache.progressRect, drawerSettings.startAngle, @@ -441,28 +518,12 @@ internal class GaugeDrawer( progressPaint ) } else { - val pointAngle = abs(drawerSettings.sweepAngle).toDouble() / (endValue - startValue) - val point = (drawerSettings.startAngle + (value - startValue) * pointAngle).toInt() - val currentSweep = - if (drawerSettings.gaugeProgressBarType == GaugeProgressBarType.SHORT) { - drawerSettings.gaugeProgressWidth - } else { - (point - drawerSettings.startAngle) - } - - val startAngle = - if (drawerSettings.gaugeProgressBarType == GaugeProgressBarType.SHORT) { - drawerSettings.startAngle + (point - drawerSettings.startAngle) - } else { - drawerSettings.startAngle - } - - val progressBarWidth = - if (drawerSettings.gaugeProgressBarType == GaugeProgressBarType.SHORT) { - strokeWidth - } else { - strokeWidth / 2f - } + // Not truncated to whole degrees: on a short range that made the bar step visibly. + val point = drawerSettings.startAngle + fraction * abs(drawerSettings.sweepAngle) + val isShort = drawerSettings.gaugeProgressBarType == GaugeProgressBarType.SHORT + val currentSweep = if (isShort) drawerSettings.gaugeProgressWidth else point - drawerSettings.startAngle + val startAngle = if (isShort) point else drawerSettings.startAngle + val progressBarWidth = if (isShort) strokeWidth else strokeWidth / 2f glowPaint.color = settings.getColorTheme().progressColor glowPaint.strokeWidth = progressBarWidth * 2.5f @@ -519,7 +580,23 @@ internal class GaugeDrawer( valuePaint.textSize = calculatedFontSize } - val unitPadding = calculatedFontSize * 0.3f + // Layout height is taken before shrinking, so a long value does not move the label and stats. + val valueLayoutHeight = drawingCache.textRect.height() + + // A long value (e.g. "100.0" plus its unit) ran past the dial; shrink it to the inner width. + val fit = + GaugeGeometry.fitScale( + drawingCache.textRect.width() + if (unitText != null) calculatedFontSize * 0.3f + unitWidth else 0f, + area.width() * VALUE_MAX_WIDTH_RATIO + ) + val valueFontSize = calculatedFontSize * fit + if (fit < 1f) { + valuePaint.textSize = valueFontSize + valuePaint.getTextBounds(value, 0, value.length, drawingCache.textRect) + unitWidth *= fit + } + + val unitPadding = valueFontSize * 0.3f var valueX = area.centerX() - (drawingCache.textRect.width() / 2f) if (value.length >= 4 && unitText != null) { @@ -539,7 +616,7 @@ internal class GaugeDrawer( histogramPaint.textSize = calculatedFontSize * 0.4f val verticalGap = calculatedFontSize * 0.2f - val valueLineH = max(drawingCache.textRect.height(), MIN_TEXT_VALUE_HEIGHT) + settings.getGaugeScreenSettings().topOffset + val valueLineH = max(valueLayoutHeight, MIN_TEXT_VALUE_HEIGHT) + settings.getGaugeScreenSettings().topOffset labelPaint.getTextBounds("Ty", 0, 2, drawingCache.labelRect) val labelLineH = drawingCache.labelRect.height() @@ -560,7 +637,7 @@ internal class GaugeDrawer( } } - val valueHeight = max(drawingCache.textRect.height(), MIN_TEXT_VALUE_HEIGHT) + settings.getGaugeScreenSettings().topOffset + val valueHeight = max(valueLayoutHeight, MIN_TEXT_VALUE_HEIGHT) + settings.getGaugeScreenSettings().topOffset val valueY = centerY - valueHeight valuePaint.setShadowLayer(radius / 4, 0f, 0f, Color.WHITE) @@ -569,8 +646,8 @@ internal class GaugeDrawer( val unitY = centerY - valueHeight if (unitText != null) { - valuePaint.textSize = calculatedFontSize * 0.32f - valuePaint.color = color(R.color.gray) + valuePaint.textSize = valueFontSize * 0.32f + valuePaint.color = colorGray val unitX = valueX + drawingCache.textRect.width() + unitPadding canvas.drawText(unitText, unitX, unitY, valuePaint) } @@ -601,31 +678,75 @@ internal class GaugeDrawer( } if (statsEnabled) { - histogramPaint.textSize = calculatedFontSize * 0.4f - histogramPaint.getTextBounds("0000", 0, "0000".length, drawingCache.histsRect) - var left = area.centerX() - (drawingCache.histsRect.width() * 1.5f) - - val statsY = labelY + drawingCache.histsRect.height() + verticalGap + drawStatsRow(canvas, area, metric, calculatedFontSize, labelY, verticalGap, borderArea) + } + } - if (pid.historgam.isMinEnabled) { - val minStr = textCache.min.get(pid.id, metric.min) { metric.min.format(pid) } - histogramPaint.color = minValueColorScheme(metric) - canvas.drawText(minStr, left, statsY, histogramPaint) - left += (drawingCache.histsRect.width() * 1.2f) + // min / avg / max, centred with equal gaps. Fixed offsets crowded long values to the right, and + // nothing said which number was which: min and max get a small ▼ / ▲ caption. + private fun drawStatsRow( + canvas: Canvas, + area: RectF, + metric: Metric, + calculatedFontSize: Float, + labelY: Float, + verticalGap: Float, + borderArea: RectF? + ) { + val pid = metric.pid + val statsTextSize = calculatedFontSize * 0.4f + histogramPaint.textSize = statsTextSize + histogramPaint.getTextBounds("0000", 0, "0000".length, drawingCache.histsRect) + val statsY = labelY + drawingCache.histsRect.height() + verticalGap + + val minStr = if (pid.historgam.isMinEnabled) textCache.min.get(pid.id, metric.min) { metric.min.format(pid) } else null + val avgStr = if (pid.historgam.isAvgEnabled) textCache.avg.get(pid.id, metric.mean) { metric.mean.format(pid) } else null + val maxStr = if (pid.historgam.isMaxEnabled) textCache.max.get(pid.id, metric.max) { metric.max.format(pid) } else null + + val captionTextSize = statsTextSize * STATS_CAPTION_RATIO + statsCaptionPaint.textSize = captionTextSize + val captionGap = statsTextSize * 0.1f + + fun width( + caption: String?, + text: String? + ): Float = + if (text == null) { + 0f + } else { + histogramPaint.measureText(text) + if (caption != null) statsCaptionPaint.measureText(caption) + captionGap else 0f } - if (pid.historgam.isAvgEnabled) { - val avgStr = textCache.avg.get(pid.id, metric.mean) { metric.mean.format(pid) } - histogramPaint.color = settings.getColorTheme().valueColor - canvas.drawText(avgStr, left, statsY, histogramPaint) - left += (drawingCache.histsRect.width() * 1.5f) - } + val widths = floatArrayOf(width(MIN_CAPTION, minStr), width(null, avgStr), width(MAX_CAPTION, maxStr)) + val present = widths.indices.filter { widths[it] > 0f } + if (present.isEmpty()) return + + val maxWidth = (borderArea?.width() ?: area.width()) * STATS_MAX_WIDTH_RATIO + val row = + GaugeGeometry.statsRow( + present.map { widths[it] }.toFloatArray(), + gap = drawingCache.histsRect.width() * 0.35f, + centerX = area.centerX(), + maxWidth = maxWidth + ) - if (pid.historgam.isMaxEnabled) { - val maxStr = textCache.max.get(pid.id, metric.max) { metric.max.format(pid) } - histogramPaint.color = maxValueColorScheme(metric) - canvas.drawText(maxStr, left, statsY, histogramPaint) + histogramPaint.textSize = statsTextSize * row.scale + statsCaptionPaint.textSize = captionTextSize * row.scale + + present.forEachIndexed { slot, item -> + var x = row.lefts[slot] + val (caption, text, color) = + when (item) { + 0 -> Triple(MIN_CAPTION, minStr!!, minValueColorScheme(metric)) + 1 -> Triple(null, avgStr!!, settings.getColorTheme().valueColor) + else -> Triple(MAX_CAPTION, maxStr!!, maxValueColorScheme(metric)) + } + if (caption != null) { + canvas.drawText(caption, x, statsY, statsCaptionPaint) + x += statsCaptionPaint.measureText(caption) + captionGap * row.scale } + histogramPaint.color = color + canvas.drawText(text, x, statsY, histogramPaint) } } @@ -634,6 +755,7 @@ internal class GaugeDrawer( rect: RectF, arcTopRect: RectF, metric: Metric, + pidScale: PidScale, scaleEnabled: Boolean, radius: Float, bitmapPadding: Float @@ -644,13 +766,15 @@ internal class GaugeDrawer( val pidId = metric.pid.id val currentCache = scaleBitmapCache[pidId] + // The scale and red zones are part of the key: editing a PID's range or alerts must redraw it. val isValid = currentCache != null && currentCache.scaleEnabled == scaleEnabled && currentCache.progressColor == settings.getColorTheme().progressColor && currentCache.width == targetWidth && currentCache.height == targetHeight && - currentCache.dividerCount == drawerSettings.dividersCount + currentCache.scale == pidScale.scale && + currentCache.redZones == pidScale.redZones drawingCache.destRectF.set(rect) drawingCache.destRectF.inset(-bitmapPadding, -bitmapPadding) @@ -672,67 +796,70 @@ internal class GaugeDrawer( cacheCanvas.translate(-rect.left + bitmapPadding, -rect.top + bitmapPadding) if (scaleEnabled && metric.source.isNumber()) { - drawNumbers(cacheCanvas, arcTopRect, metric, radius) + drawNumbers(cacheCanvas, arcTopRect, pidScale, radius) } - drawTicks(cacheCanvas, rect) + drawTicks(cacheCanvas, rect, pidScale) - scaleBitmapCache[pidId] = + scaleBitmapCache.put( + pidId, ScaleBitmapCache( cachedBitmap, targetWidth, targetHeight, - drawerSettings.dividersCount, + pidScale.scale, + pidScale.redZones, settings.getColorTheme().progressColor, scaleEnabled ) + )?.bitmap?.recycle() canvas.drawBitmap(cachedBitmap, null, drawingCache.destRectF, bitmapPaint) } } + private fun angleOf(fraction: Float): Float = drawerSettings.startAngle + fraction * drawerSettings.sweepAngle + private fun drawNumbers( canvas: Canvas, area: RectF, - metric: Metric, + pidScale: PidScale, radius: Float ) { - val pid = metric.pid - val startValue = pid.min.toDouble() - val endValue = pid.max.toDouble() - val numberOfItems = (drawerSettings.dividersCount / drawerSettings.scaleStep) - val stepValue = (endValue - startValue) / numberOfItems - + val scale = pidScale.scale val baseRadius = radius * 0.75f - val start = 0 - val end = drawerSettings.dividersCount + 1 - numbersPaint.textSize = area.width() * 0.055f - for (j in start..end step drawerSettings.scaleStep) { - val angle = (drawerSettings.startAngle + j * drawerSettings.dividersStepAngle) * (Math.PI / 180) - val text = valueAsString(metric, value = (startValue + stepValue * j / drawerSettings.scaleStep).round(1)) + for (i in 0..scale.intervals) { + val angle = angleOf(i.toFloat() / scale.intervals) * (Math.PI / 180) + val text = scale.label(i) numbersPaint.getTextBounds(text, 0, text.length, drawingCache.numberTextRect) + val textWidth = drawingCache.numberTextRect.width().toFloat() + val textHeight = drawingCache.numberTextRect.height().toFloat() - val x = area.left + (area.width() / 2.0f + cos(angle) * baseRadius - drawingCache.numberTextRect.width() / 2).toFloat() - val y = area.top + (area.height() / 2.0f + sin(angle) * baseRadius + drawingCache.numberTextRect.height() / 2).toFloat() + val labelRadius = GaugeGeometry.labelCenterRadius(baseRadius, angle, textWidth, textHeight) + val x = area.left + (area.width() / 2.0f + cos(angle) * labelRadius - textWidth / 2).toFloat() + val y = area.top + (area.height() / 2.0f + sin(angle) * labelRadius + textHeight / 2).toFloat() numbersPaint.color = - if (j == (numberOfItems - 1) * drawerSettings.scaleStep || j == numberOfItems * drawerSettings.scaleStep) { - settings.getColorTheme().progressColor - } else { - color(R.color.gray) - } + if (pidScale.redZones.contains(scale.value(i))) settings.getColorTheme().progressColor else colorGray canvas.drawText(text, x, y, numbersPaint) } } + // Major ticks sit on the labels, minor ones halfway between. Red marks the PID's alert ranges + // only; it used to be painted on the last part of every dial, alert or not. private fun drawTicks( canvas: Canvas, - rect: RectF + rect: RectF, + pidScale: PidScale ) { + val scale = pidScale.scale + val zones = pidScale.redZones.ranges(scale) + val progressColor = settings.getColorTheme().progressColor + drawingCache.scaleRect.set( rect.left + drawerSettings.lineOffset, rect.top + drawerSettings.lineOffset, @@ -740,77 +867,46 @@ internal class GaugeDrawer( rect.bottom - drawerSettings.lineOffset ) - val start = 0 - val end = drawerSettings.dividersCount + 1 - - drawArcTicks(canvas, drawingCache.scaleRect, start, end, paintColor = { - if (it == 10 || it == 12) { - settings.getColorTheme().progressColor - } else { - color(R.color.gray_light) - } - }) { - drawerSettings.startAngle + it * drawerSettings.dividersStepAngle - } - - drawArcTicks(canvas, drawingCache.scaleRect, start, drawerSettings.dividersCount + 2) { - drawerSettings.startAngle + it * drawerSettings.dividersStepAngle * 0.5f + val ticks = scale.intervals * 2 + for (k in 0..ticks) { + val fraction = k.toFloat() / ticks + val major = k % 2 == 0 + paint.color = if (major && zones.any { fraction in it }) progressColor else colorGrayLight + canvas.drawArc(drawingCache.scaleRect, angleOf(fraction), drawerSettings.dividerWidth, false, paint) } drawingCache.alignedOuterRect.set(rect) drawingCache.alignedOuterRect.inset(2f, 2f) - val grayEndIndex = drawerSettings.dividerHighlightStart - drawArcTicks( - canvas, - drawingCache.alignedOuterRect, - start, - grayEndIndex, - paintColor = { getScaleColor(it) } - ) { - drawerSettings.startAngle + it * drawerSettings.dividersStepAngle - } - - val highlightStartDegrees = (drawerSettings.dividersStepAngle * drawerSettings.dividerHighlightStart + 3).toInt() - val highlightEndDegrees = (drawerSettings.dividersStepAngle * (drawerSettings.dividersCount - 1)).toInt() - - drawLineTicks( - canvas, - drawingCache.alignedOuterRect, - highlightStartDegrees, - highlightEndDegrees, - widthInDegrees = drawerSettings.dividerWidth, - paintColor = { settings.getColorTheme().progressColor } - ) { - drawerSettings.startAngle + it + for (i in 0..scale.intervals) { + val fraction = i.toFloat() / scale.intervals + if (zones.none { fraction in it }) { + paint.color = colorGrayLight + canvas.drawArc(drawingCache.alignedOuterRect, angleOf(fraction), drawerSettings.dividerWidth, false, paint) + } } - val widthArc = - (drawerSettings.startAngle + drawerSettings.dividersCount * (drawerSettings.dividersStepAngle - 1)) - - (drawerSettings.startAngle + drawerSettings.dividersCount * (drawerSettings.dividersStepAngle - 3)) - - paint.color = settings.getColorTheme().progressColor - canvas.drawArc( - drawingCache.alignedOuterRect, - drawerSettings.startAngle + drawerSettings.dividersCount * (drawerSettings.dividersStepAngle - 2), - widthArc, - false, - paint - ) - } + paint.color = progressColor + zones.forEach { zone -> + val startDegrees = (zone.start * drawerSettings.sweepAngle).toInt() + val endDegrees = (zone.endInclusive * drawerSettings.sweepAngle).toInt() + + drawLineTicks( + canvas, + drawingCache.alignedOuterRect, + startDegrees, + endDegrees, + widthInDegrees = drawerSettings.dividerWidth, + paintColor = { progressColor } + ) { + drawerSettings.startAngle + it + } - private inline fun drawArcTicks( - canvas: Canvas, - rect: RectF, - start: Int, - end: Int, - width: Float = drawerSettings.dividerWidth, - paintColor: (j: Int) -> Int = { color(R.color.gray_light) }, - angle: (j: Int) -> Float - ) { - for (j in start..end step drawerSettings.scaleStep) { - paint.color = paintColor(j) - canvas.drawArc(rect, angle(j), width, false, paint) + // A solid band on the outer half of the zone, towards the dial's end. + val half = (zone.endInclusive - zone.start) / 2f + val bandStart = if (zone.endInclusive >= 1f) zone.start + half else zone.start + paint.color = progressColor + canvas.drawArc(drawingCache.alignedOuterRect, angleOf(bandStart), half * drawerSettings.sweepAngle, false, paint) } } @@ -845,7 +941,7 @@ internal class GaugeDrawer( val centerAngle = startAngle + (widthInDegrees / 2f) canvas.rotate(centerAngle) - if (color != color(R.color.gray_light)) { + if (color != colorGrayLight) { glowPaint.color = color glowPaint.strokeWidth = paint.strokeWidth * 2.0f canvas.drawLine(radius, -dashLength / 2f, radius, dashLength / 2f, glowPaint) @@ -907,25 +1003,6 @@ internal class GaugeDrawer( padding: Float ): Float = (width - 2 * padding) / 2 - private inline fun getScaleColor(j: Int): Int = - if (j == drawerSettings.dividerHighlightStart || j == drawerSettings.dividersCount) { - settings.getColorTheme().progressColor - } else { - color(R.color.gray_light) - } - - private inline fun valueAsString( - metric: Metric, - value: Double - ): String = - if (metric.source.command.pid.max - .toInt() > 20 - ) { - value.toInt().toString() - } else { - value.toString() - } - private fun getHeightPixels(): Int = context.resources.displayMetrics.heightPixels private fun getWidthPixels(): Int = context.resources.displayMetrics.widthPixels diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt new file mode 100644 index 00000000..4472d7b4 --- /dev/null +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2019-2026, Tomasz Żebrowski + * + *

Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.renderer.gauge + +import java.util.Locale +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.log10 +import kotlin.math.pow +import kotlin.math.roundToInt + +private val NICE_MULTIPLIERS = doubleArrayOf(1.0, 2.0, 2.5, 4.0, 5.0) +private const val MIN_INTERVALS = 4 +private const val MAX_INTERVALS = 7 +private const val PREFERRED_INTERVALS = 5 +private const val MAX_DECIMALS = 4 +private const val EPSILON = 1e-9 + +// A taller card than this only adds empty space around the dial. +internal const val MAX_CARD_HEIGHT_RATIO = 1.5f + +/** + * The dial's scale: [min]..[max] cut into [intervals] steps of [step], all "nice" numbers + * (1, 2, 2.5, 4 or 5 × 10ⁿ). The range covers the PID's own min..max ([sourceMin], [sourceMax]), + * extended to the nearest step, so every label sits on a major tick. + */ +internal data class GaugeScale( + val sourceMin: Double, + val sourceMax: Double, + val min: Double, + val max: Double, + val step: Double, + val intervals: Int, + val decimals: Int +) { + fun value(index: Int): Double = (min + step * index).let { if (abs(it) < step * EPSILON) 0.0 else it } + + fun label(index: Int): String = String.format(Locale.ROOT, "%.${decimals}f", value(index)) + + /** Position of [value] on the dial, 0..1. Values outside the scale stick to its ends. */ + fun fraction(value: Double): Float = + if (value.isNaN()) 0f else ((value - min) / (max - min)).coerceIn(0.0, 1.0).toFloat() + + companion object { + fun of( + sourceMin: Double, + sourceMax: Double + ): GaugeScale { + if (!sourceMin.isFinite() || !sourceMax.isFinite() || sourceMax <= sourceMin) { + val min = if (sourceMin.isFinite()) sourceMin else 0.0 + return GaugeScale(sourceMin, sourceMax, min, min + 1.0, 1.0, 1, decimals(1.0, min)) + } + + val range = sourceMax - sourceMin + val baseMagnitude = 10.0.pow(floor(log10(range / MAX_INTERVALS))) + + var best: GaugeScale? = null + var bestScore = Double.MAX_VALUE + for (magnitude in doubleArrayOf(baseMagnitude, baseMagnitude * 10)) { + for (multiplier in NICE_MULTIPLIERS) { + val step = multiplier * magnitude + val min = floor(sourceMin / step + EPSILON) * step + val max = ceil(sourceMax / step - EPSILON) * step + val intervals = ((max - min) / step).roundToInt() + if (intervals !in MIN_INTERVALS..MAX_INTERVALS) continue + + // Least extension first: it costs dial resolution. Then the label count. + val extension = (max - min - range) / range + val score = extension * 1000 + abs(intervals - PREFERRED_INTERVALS) + if (score < bestScore - EPSILON) { + bestScore = score + best = GaugeScale(sourceMin, sourceMax, min, max, step, intervals, decimals(step, min)) + } + } + } + + return best ?: run { + val step = range / PREFERRED_INTERVALS + GaugeScale(sourceMin, sourceMax, sourceMin, sourceMax, step, PREFERRED_INTERVALS, decimals(step, sourceMin)) + } + } + + private fun decimals( + step: Double, + min: Double + ): Int = + (0..MAX_DECIMALS).firstOrNull { d -> + val factor = 10.0.pow(d) + isWhole(step * factor) && isWhole(min * factor) + } ?: MAX_DECIMALS + + private fun isWhole(value: Double) = abs(value - value.roundToInt()) < 1e-6 + } +} + +/** + * The red parts of the dial, as fractions of the scale: from the PID's upper alert threshold to + * the end, and from the start to its lower one. None when the PID defines no thresholds. + */ +internal data class GaugeRedZones( + val lower: Double?, + val upper: Double? +) { + fun contains(value: Double): Boolean = (upper != null && value >= upper) || (lower != null && value <= lower) + + fun ranges(scale: GaugeScale): List> = + listOfNotNull( + lower?.takeIf { it > scale.min }?.let { 0f..scale.fraction(it) }, + upper?.takeIf { it < scale.max }?.let { scale.fraction(it)..1f } + ) + + fun containsFraction( + scale: GaugeScale, + fraction: Float + ): Boolean = ranges(scale).any { fraction in it } + + companion object { + val NONE = GaugeRedZones(null, null) + } +} + +internal object GaugeGeometry { + /** + * Distance from the dial centre to a scale label's centre. The label's outer edge stays on + * the circle a label at the top would touch, so wide labels at the sides move inward instead + * of running into the ticks and the card border. + * + * @param baseRadius where a label at the top of the dial is centred. + */ + fun labelCenterRadius( + baseRadius: Float, + angleRadians: Double, + width: Float, + height: Float + ): Float { + val outerRadius = baseRadius + height / 2f + val radialHalfExtent = abs(kotlin.math.cos(angleRadians)).toFloat() * width / 2f + + abs(kotlin.math.sin(angleRadians)).toFloat() * height / 2f + return outerRadius - radialHalfExtent + } + + /** Text size factor that makes [width] fit [maxWidth]; never enlarges. */ + fun fitScale( + width: Float, + maxWidth: Float + ): Float = if (width <= maxWidth || width <= 0f) 1f else maxWidth / width + + /** + * Left edges of items laid out in a row centred on [centerX], separated by [gap]. The whole + * row is scaled down by [StatsRow.scale] when wider than [maxWidth]. + */ + fun statsRow( + widths: FloatArray, + gap: Float, + centerX: Float, + maxWidth: Float + ): StatsRow { + if (widths.isEmpty()) return StatsRow(FloatArray(0), 1f) + + val total = widths.sum() + gap * (widths.size - 1) + val scale = fitScale(total, maxWidth) + val lefts = FloatArray(widths.size) + var x = centerX - total * scale / 2f + widths.forEachIndexed { i, w -> + lefts[i] = x + x += (w + gap) * scale + } + return StatsRow(lefts, scale) + } + + /** + * Height of a phone gauge card. Square when the grid scrolls anyway; otherwise the rows share + * [availableHeight], up to [MAX_CARD_HEIGHT_RATIO] × the gauge width. + */ + fun cardHeight( + gaugeWidth: Float, + itemMargin: Float, + rows: Int, + availableHeight: Float + ): Float { + if (rows <= 0) return gaugeWidth + val shared = availableHeight / rows - 2 * itemMargin + return shared.coerceIn(gaugeWidth, gaugeWidth * MAX_CARD_HEIGHT_RATIO) + } +} + +internal class StatsRow( + val lefts: FloatArray, + val scale: Float +) diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt index 708cdd99..bda0d03c 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt @@ -63,6 +63,9 @@ private class GaugeLayoutCache { private const val TAG = "cache" +// Below the last row; the free height the cards share excludes it, or a filled grid would scroll. +private const val CONTENT_BOTTOM_PADDING = 20f + internal class GaugeSurfaceRenderer( context: Context, private val settings: ScreenSettings, @@ -310,7 +313,21 @@ internal class GaugeSurfaceRenderer( layoutCache.startX = startX(area, isAA, count) val totalRows = kotlin.math.ceil(count / layoutCache.columns.toDouble()).toInt() - layoutCache.contentHeight = topOffset + (totalRows * layoutCache.rowHeight) + 20f + + // Phone rows were square, so a few gauges left most of a portrait screen empty. When the grid + // does not scroll, the cards share the free height; the dial stays square and is centred. + val fillsHeight = !isAA && !(isLandscape && layoutCache.columns == 1) + val cardHeight = + if (fillsHeight) { + GaugeGeometry.cardHeight(layoutCache.gaugeWidth, itemMargin, totalRows, area.bottom - topOffset - CONTENT_BOTTOM_PADDING) + } else { + layoutCache.gaugeWidth + } + if (fillsHeight) { + layoutCache.rowHeight = cardHeight + 2 * itemMargin + } + val dialOffset = (cardHeight - layoutCache.gaugeWidth) / 2f + layoutCache.contentHeight = topOffset + (totalRows * layoutCache.rowHeight) + CONTENT_BOTTOM_PADDING layoutCache.maxScroll = max(0f, layoutCache.contentHeight - availableHeight) for (i in 0 until count) { @@ -334,12 +351,12 @@ internal class GaugeSurfaceRenderer( } layoutCache.centeredLefts[i] = centeredLeft - layoutCache.centeredTops[i] = centeredTop + layoutCache.centeredTops[i] = centeredTop + dialOffset layoutCache.borderRects[i].set( centeredLeft, centeredTop, centeredLeft + layoutCache.gaugeWidth, - centeredTop + layoutCache.gaugeWidth + centeredTop + cardHeight ) } } diff --git a/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt new file mode 100644 index 00000000..31e45634 --- /dev/null +++ b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2019-2026, Tomasz Żebrowski + * + *

Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.renderer.gauge + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class GaugeGeometryTest { + private fun labels(scale: GaugeScale) = (0..scale.intervals).map { scale.label(it) } + + @Test + fun `temperature scale uses round labels instead of thirds of the range`() { + // Was -40, -6, 26, 60, 93, 126, 160. + assertEquals(listOf("-40", "0", "40", "80", "120", "160"), labels(GaugeScale.of(-40.0, 160.0))) + } + + @Test + fun `gear scale has integer labels instead of 0,8 and 2,7`() { + // Was -1.0, 0.8, 2.7, 4.5, 6.3, 8.2, 10.0; the start is extended to the nearest step. + assertEquals(listOf("-2", "0", "2", "4", "6", "8", "10"), labels(GaugeScale.of(-1.0, 10.0))) + } + + @Test + fun `common ranges get round steps without extending the range`() { + assertEquals(listOf("0", "20", "40", "60", "80", "100"), labels(GaugeScale.of(0.0, 100.0))) + assertEquals(listOf("0", "2000", "4000", "6000", "8000"), labels(GaugeScale.of(0.0, 8000.0))) + assertEquals(listOf("0", "50", "100", "150", "200", "250", "300"), labels(GaugeScale.of(0.0, 300.0))) + assertEquals(listOf("0.0", "0.5", "1.0", "1.5", "2.0", "2.5", "3.0"), labels(GaugeScale.of(0.0, 3.0))) + } + + @Test + fun `scale always covers the PID range with 4 to 7 intervals`() { + val ranges = listOf(-40.0 to 160.0, -1.0 to 10.0, 0.0 to 255.0, 0.0 to 65535.0, -0.5 to 0.5, 12.0 to 15.0, 0.0 to 1.0) + ranges.forEach { (min, max) -> + val scale = GaugeScale.of(min, max) + assertTrue("$min..$max -> $scale", scale.min <= min && scale.max >= max) + assertTrue("$min..$max -> $scale", scale.intervals in 4..7) + } + } + + @Test + fun `no negative zero label`() { + assertEquals("0", GaugeScale.of(-40.0, 160.0).label(1)) + } + + @Test + fun `invalid range falls back to a single interval`() { + val scale = GaugeScale.of(5.0, 5.0) + + assertEquals(1, scale.intervals) + assertEquals(0f, scale.fraction(5.0)) + } + + @Test + fun `value outside the scale sticks to its ends`() { + // The progress arc used to be drawn past the end of the dial, or backwards below min. + val scale = GaugeScale.of(0.0, 100.0) + + assertEquals(1f, scale.fraction(250.0)) + assertEquals(0f, scale.fraction(-30.0)) + assertEquals(0.5f, scale.fraction(50.0)) + assertEquals(0f, scale.fraction(Double.NaN)) + } + + @Test + fun `red zones come from the alert thresholds only`() { + val scale = GaugeScale.of(0.0, 100.0) + + assertTrue(GaugeRedZones.NONE.ranges(scale).isEmpty()) + assertEquals(listOf(0.8f..1f), GaugeRedZones(null, 80.0).ranges(scale)) + assertEquals(listOf(0f..0.2f, 0.9f..1f), GaugeRedZones(20.0, 90.0).ranges(scale)) + assertTrue(GaugeRedZones(null, 80.0).contains(80.0)) + assertFalse(GaugeRedZones(null, 80.0).contains(79.9)) + } + + @Test + fun `thresholds beyond the scale draw no zone`() { + assertTrue(GaugeRedZones(-10.0, 200.0).ranges(GaugeScale.of(0.0, 100.0)).isEmpty()) + } + + @Test + fun `label at the top keeps its radius, wide labels at the sides move inward`() { + val top = GaugeGeometry.labelCenterRadius(100f, -Math.PI / 2, 40f, 10f) + val side = GaugeGeometry.labelCenterRadius(100f, 0.0, 40f, 10f) + + assertEquals(100f, top, 0.001f) + assertEquals(85f, side, 0.001f) + } + + @Test + fun `stats row is centred with equal gaps`() { + val row = GaugeGeometry.statsRow(floatArrayOf(20f, 40f, 20f), gap = 10f, centerX = 100f, maxWidth = 500f) + + assertEquals(1f, row.scale) + assertEquals(listOf(50f, 80f, 130f), row.lefts.toList()) + } + + @Test + fun `stats row wider than the card is scaled down to fit`() { + val row = GaugeGeometry.statsRow(floatArrayOf(100f, 100f), gap = 0f, centerX = 100f, maxWidth = 100f) + + assertEquals(0.5f, row.scale) + assertEquals(listOf(50f, 100f), row.lefts.toList()) + } + + @Test + fun `value text is only ever shrunk`() { + assertEquals(1f, GaugeGeometry.fitScale(50f, 100f)) + assertEquals(0.5f, GaugeGeometry.fitScale(200f, 100f)) + } + + @Test + fun `cards fill the free height up to a limit, stay square when scrolling`() { + // Four gauges in two columns used under half of a portrait phone screen. + assertEquals(180f, GaugeGeometry.cardHeight(gaugeWidth = 140f, itemMargin = 6f, rows = 2, availableHeight = 384f)) + assertEquals(210f, GaugeGeometry.cardHeight(gaugeWidth = 140f, itemMargin = 6f, rows = 2, availableHeight = 1000f)) + assertEquals(140f, GaugeGeometry.cardHeight(gaugeWidth = 140f, itemMargin = 6f, rows = 5, availableHeight = 500f)) + } +} From fe4650f523d9ceb7eec464f7aff7918577bc7a35 Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Thu, 8 Oct 2026 14:51:47 +0200 Subject: [PATCH 2/3] fix(gauge): keep scale numbers on empty dials, clear end label of stats - Scale bitmap was cached without numbers while the value was still null; numbers are now skipped only for non-numeric values and are part of the cache key. - Draw numbers after the ticks (red glow covered the end labels) and keep them at their radius unless they would reach the ticks. - Limit the min/avg/max row to the space beside the end label when the dial ends below its centre. --- CLAUDE.md | 4 ++ doc/specs/gauge-drawing-improvements.md | 14 ++-- .../obd/graphs/renderer/gauge/GaugeDrawer.kt | 65 +++++++++++++++---- .../graphs/renderer/gauge/GaugeGeometry.kt | 38 ++++++++--- .../renderer/gauge/GaugeGeometryTest.kt | 25 +++++-- 5 files changed, 116 insertions(+), 30 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4559dbd0..587b5778 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -207,6 +207,10 @@ Gradle needs JDK 17+ (Crashlytics plugin); the shell default may be JDK 11 and f * **The dial maps values through `GaugeScale`, not `pid.min`/`pid.max`.** It extends the PID range to "nice" steps; progress, labels and ticks must all use `scale.fraction()` or they drift apart. Pure geometry lives in `GaugeGeometry.kt` (unit-tested); keep Canvas out of it. +* The scale bitmap is cached, so anything that decides *what* goes into it (numbers on/off, scale, + zones) must be in its key. Numbers skipped for a still-`null` value were cached numberless for good. +* End labels of a dial that ends below centre sit beside the stats row; `GaugeGeometry.statsMaxWidth` + keeps the row clear. Fix overlaps with draw order (numbers after ticks) before moving labels. * Red on the dial = the PID's `alert` thresholds. The scale bitmap cache is keyed by `GaugeScale` and `GaugeRedZones` too — a cache keyed by PID id alone kept a stale scale after a PID edit. * Phone cards may be taller than the dial (`GaugeGeometry.cardHeight`); `borderArea` is the card, `top` diff --git a/doc/specs/gauge-drawing-improvements.md b/doc/specs/gauge-drawing-improvements.md index 5cf6b46b..e0afa008 100644 --- a/doc/specs/gauge-drawing-improvements.md +++ b/doc/specs/gauge-drawing-improvements.md @@ -18,13 +18,13 @@ Readability, correctness and per-frame cost fixes for the dial gauge (`GaugeDraw | --- | --- | --- | --- | | 1 | Scale labels | 6 equal parts of `min..max`, rounded (`-40, -6, 26 …`) | "Nice" steps (1, 2, 2.5, 4, 5 × 10ⁿ), 4–7 intervals, the range extended to the nearest step: `-40, 0, 40 … 160`; gear `-2, 0, 2 … 10` | | 2 | Red zone | Last ~2 labels / ticks red on every dial (fixed divider indexes) | Red only over the PID's alert ranges (`alert.upperThreshold`..max, min..`alert.lowerThreshold`); none without thresholds | -| 3 | Label placement | Centre on a fixed radius; wide labels at the sides overlapped ticks and the border | Outer edge on a fixed radius; side labels move inward by their width | -| 4 | Stats row | Fixed offsets from the centre, no captions | Measured, centred, equal gaps, scaled down to 90 % of the card; `▼` before min, `▲` before max | +| 3 | Label placement | Drawn before the ticks, so the red ticks' glow covered the end labels | Drawn after the ticks; centred at 0.75 r as before, moved inward only if the label would reach past 0.85 r | +| 4 | Stats row | Fixed offsets from the centre, no captions | Measured, centred, equal gaps; `▼` before min, `▲` before max. Scaled down to fit 90 % of the card and, when the dial ends below its centre, the space left of the end label | | 5 | Value text | Fixed size | Shrunk (never enlarged) to 70 % of the dial width with its unit; label / stats stay where they were | | 6 | Phone layout | Square cards | When the grid does not scroll, cards share the free height up to 1.5 × width; dial centred, module name / rate stay in the card's top corners | | 7 | Out-of-range value | Arc drawn past the end, or backwards below min | Clamped to the dial | | 8 | Arc angle | Truncated to whole degrees | Float | -| 9 | Scale bitmap cache | Keyed by PID id, size, colour | Also by scale and red zones, so editing a PID's range or alerts redraws it; the replaced bitmap is recycled | +| 9 | Scale bitmap cache | Keyed by PID id, size, colour; numbers skipped while the value was still `null`, and that numberless dial stayed cached | Also keyed by scale, red zones and whether numbers are drawn; numbers skipped only for non-numeric values; the replaced bitmap is recycled | | 10 | Per frame | New `RadialGradient` per gauge; colour parsed / resolved per frame | Gradient cached per PID and card rect; colours resolved once | ## Settings / keys touched @@ -33,7 +33,8 @@ None. ## Implementation -* `GaugeGeometry.kt` (pure, unit-tested): `GaugeScale.of(min, max)` picks the step by least range extension, then interval count nearest 5; `fraction()` clamps. `GaugeRedZones` turns thresholds into scale fractions. `GaugeGeometry.labelCenterRadius`, `fitScale`, `statsRow`, `cardHeight`. +* `GaugeGeometry.kt` (pure, unit-tested): `GaugeScale.of(min, max)` picks the step by least range extension, then interval count nearest 5; `fraction()` clamps. `GaugeRedZones` turns thresholds into scale fractions. `GaugeGeometry.labelCenterRadius`, `statsMaxWidth`, `fitScale`, `statsRow`, `cardHeight`. +* Lesson from the first device run: pulling every side label inward by its width put the end label (40° below centre on a 200° dial) into the stats row, which the `▼`/`▲` captions had made wider. The original overlap was draw order (ticks over numbers), not radius. * `GaugeDrawer`: `pidScale(metric)` caches scale + zones per PID, invalidated when min/max/thresholds change. Progress, numbers and ticks all use the scale's fraction. Ticks: majors on the labels, minors halfway; outer gray majors outside zones; in each zone the glowing dense ticks plus a solid band over its outer half. `DrawerSettings.dividersCount`, `dividersStepAngle`, `dividerHighlightStart` removed (no caller set them). * `GaugeSurfaceRenderer` (phone only, not landscape single-column): `rowHeight = cardHeight + 2 × margin`; `borderRects` take the card height, the dial top is offset by half the slack. The free height excludes `CONTENT_BOTTOM_PADDING`, otherwise a filled grid scrolls by a few pixels. @@ -54,7 +55,8 @@ Visual only. Existing profiles keep their PIDs and ranges; the dial's range may | no negative zero label / invalid range falls back to a single interval | Edge cases | | value outside the scale sticks to its ends | Item 7 (clamp) | | red zones come from the alert thresholds only / thresholds beyond the scale draw no zone | Item 2 | -| label at the top keeps its radius, wide labels at the sides move inward | Item 3 | +| labels keep their radius unless they would reach into the ticks | Item 3 | +| stats row stays clear of the end label when the dial ends below its centre / uses the card when it ends at or above | Item 4 | | stats row is centred with equal gaps / wider than the card is scaled down | Item 4 | | value text is only ever shrunk | Item 5 | | cards fill the free height up to a limit, stay square when scrolling | Item 6 | @@ -66,6 +68,8 @@ Visual only. Existing profiles keep their PIDs and ranges; the dial's range may - [ ] Landscape, 1 and several gauges. - [ ] A PID with an upper alert threshold shows red from the threshold; one without shows no red. - [ ] Edit a PID's min/max: the dial redraws. +- [ ] A gauge showing `--` (no value yet) still shows its scale numbers. +- [ ] End label (e.g. `120`) does not touch the `▲` max value. - [ ] Performance, Drag Racing, Brake Boosting gauges (phone + AA DHU): labels readable, no overlap with ticks. - [ ] `▼` / `▲` render (font fallback) on the target devices. diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt index e822dc6a..8ddb4030 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt @@ -61,6 +61,11 @@ private const val VALUE_MAX_WIDTH_RATIO = 0.7f // Share of the card width the min / avg / max row may take before it is shrunk. private const val STATS_MAX_WIDTH_RATIO = 0.9f + +// Scale labels: centred at this share of the dial radius, never reaching past the outer one. +private const val NUMBERS_RADIUS_RATIO = 0.75f +private const val NUMBERS_MAX_OUTER_RADIUS_RATIO = 0.85f +private const val NUMBERS_TEXT_RATIO = 0.055f private const val STATS_CAPTION_RATIO = 0.6f private const val MIN_CAPTION = "\u25BC" private const val MAX_CAPTION = "\u25B2" @@ -87,7 +92,7 @@ private data class ScaleBitmapCache( val scale: GaugeScale, val redZones: GaugeRedZones, val progressColor: Int, - val scaleEnabled: Boolean + val numbersDrawn: Boolean ) // The scale and red zones of one PID, rebuilt only when its range or thresholds change. @@ -312,6 +317,7 @@ internal class GaugeDrawer( canvas, area = drawingCache.workingRect, metric = metric, + statsMaxWidth = statsMaxWidth(metric, pidScale.scale, scaleEnabled, radius, borderArea), radius = radius, labelCenterYPadding = labelCenterYPadding, fontSize = fontSize, @@ -553,6 +559,7 @@ internal class GaugeDrawer( canvas: Canvas, area: RectF, metric: Metric, + statsMaxWidth: Float, radius: Float, labelCenterYPadding: Float = 0f, fontSize: Int, @@ -678,7 +685,7 @@ internal class GaugeDrawer( } if (statsEnabled) { - drawStatsRow(canvas, area, metric, calculatedFontSize, labelY, verticalGap, borderArea) + drawStatsRow(canvas, area, metric, calculatedFontSize, labelY, verticalGap, statsMaxWidth) } } @@ -691,7 +698,7 @@ internal class GaugeDrawer( calculatedFontSize: Float, labelY: Float, verticalGap: Float, - borderArea: RectF? + maxWidth: Float ) { val pid = metric.pid val statsTextSize = calculatedFontSize * 0.4f @@ -721,7 +728,6 @@ internal class GaugeDrawer( val present = widths.indices.filter { widths[it] > 0f } if (present.isEmpty()) return - val maxWidth = (borderArea?.width() ?: area.width()) * STATS_MAX_WIDTH_RATIO val row = GaugeGeometry.statsRow( present.map { widths[it] }.toFloatArray(), @@ -767,9 +773,13 @@ internal class GaugeDrawer( val currentCache = scaleBitmapCache[pidId] // The scale and red zones are part of the key: editing a PID's range or alerts must redraw it. + // Numbers are skipped only for non-numeric values. Skipping them while there was no value yet + // cached a dial without numbers until the layout changed. + val numbersDrawn = scaleEnabled && (metric.source.value == null || metric.source.isNumber()) + val isValid = currentCache != null && - currentCache.scaleEnabled == scaleEnabled && + currentCache.numbersDrawn == numbersDrawn && currentCache.progressColor == settings.getColorTheme().progressColor && currentCache.width == targetWidth && currentCache.height == targetHeight && @@ -795,10 +805,11 @@ internal class GaugeDrawer( cacheCanvas.scale(CACHE_SCALE, CACHE_SCALE) cacheCanvas.translate(-rect.left + bitmapPadding, -rect.top + bitmapPadding) - if (scaleEnabled && metric.source.isNumber()) { + // Numbers last: the red ticks' glow used to paint over the end labels. + drawTicks(cacheCanvas, rect, pidScale) + if (numbersDrawn) { drawNumbers(cacheCanvas, arcTopRect, pidScale, radius) } - drawTicks(cacheCanvas, rect, pidScale) scaleBitmapCache.put( pidId, @@ -809,7 +820,7 @@ internal class GaugeDrawer( pidScale.scale, pidScale.redZones, settings.getColorTheme().progressColor, - scaleEnabled + numbersDrawn ) )?.bitmap?.recycle() @@ -817,6 +828,32 @@ internal class GaugeDrawer( } } + private fun statsMaxWidth( + metric: Metric, + scale: GaugeScale, + scaleEnabled: Boolean, + radius: Float, + borderArea: RectF? + ): Float { + val cardLimit = (borderArea?.width() ?: drawingCache.workingRect.width()) * STATS_MAX_WIDTH_RATIO + if (!scaleEnabled || (metric.source.value != null && !metric.source.isNumber())) return cardLimit + + val endLabel = scale.label(scale.intervals) + numbersPaint.textSize = drawingCache.arcTopRect.width() * NUMBERS_TEXT_RATIO + numbersPaint.getTextBounds(endLabel, 0, endLabel.length, drawingCache.numberTextRect) + val endWidth = drawingCache.numberTextRect.width().toFloat() + val endAngle = Math.toRadians(angleOf(1f).toDouble()) + val endRadius = + GaugeGeometry.labelCenterRadius( + radius * NUMBERS_RADIUS_RATIO, + radius * NUMBERS_MAX_OUTER_RADIUS_RATIO, + endAngle, + endWidth, + drawingCache.numberTextRect.height().toFloat() + ) + return GaugeGeometry.statsMaxWidth(cardLimit, endAngle, endRadius, endWidth, gap = radius * 0.04f) + } + private fun angleOf(fraction: Float): Float = drawerSettings.startAngle + fraction * drawerSettings.sweepAngle private fun drawNumbers( @@ -826,9 +863,8 @@ internal class GaugeDrawer( radius: Float ) { val scale = pidScale.scale - val baseRadius = radius * 0.75f - numbersPaint.textSize = area.width() * 0.055f + numbersPaint.textSize = area.width() * NUMBERS_TEXT_RATIO for (i in 0..scale.intervals) { val angle = angleOf(i.toFloat() / scale.intervals) * (Math.PI / 180) @@ -838,7 +874,14 @@ internal class GaugeDrawer( val textWidth = drawingCache.numberTextRect.width().toFloat() val textHeight = drawingCache.numberTextRect.height().toFloat() - val labelRadius = GaugeGeometry.labelCenterRadius(baseRadius, angle, textWidth, textHeight) + val labelRadius = + GaugeGeometry.labelCenterRadius( + radius * NUMBERS_RADIUS_RATIO, + radius * NUMBERS_MAX_OUTER_RADIUS_RATIO, + angle, + textWidth, + textHeight + ) val x = area.left + (area.width() / 2.0f + cos(angle) * labelRadius - textWidth / 2).toFloat() val y = area.top + (area.height() / 2.0f + sin(angle) * labelRadius + textHeight / 2).toFloat() diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt index 4472d7b4..a06b0499 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt @@ -19,10 +19,13 @@ package org.obd.graphs.renderer.gauge import java.util.Locale import kotlin.math.abs import kotlin.math.ceil +import kotlin.math.cos import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt +import kotlin.math.sin private val NICE_MULTIPLIERS = doubleArrayOf(1.0, 2.0, 2.5, 4.0, 5.0) private const val MIN_INTERVALS = 4 @@ -136,22 +139,39 @@ internal data class GaugeRedZones( internal object GaugeGeometry { /** - * Distance from the dial centre to a scale label's centre. The label's outer edge stays on - * the circle a label at the top would touch, so wide labels at the sides move inward instead - * of running into the ticks and the card border. - * - * @param baseRadius where a label at the top of the dial is centred. + * Distance from the dial centre to a scale label's centre: [baseRadius], unless the label would + * then reach past [maxOuterRadius] (into the ticks), in which case it moves just inside it. + * Pulling every label inward by its width put the end labels into the value / stats area. */ fun labelCenterRadius( baseRadius: Float, + maxOuterRadius: Float, angleRadians: Double, width: Float, height: Float ): Float { - val outerRadius = baseRadius + height / 2f - val radialHalfExtent = abs(kotlin.math.cos(angleRadians)).toFloat() * width / 2f + - abs(kotlin.math.sin(angleRadians)).toFloat() * height / 2f - return outerRadius - radialHalfExtent + val radialHalfExtent = abs(cos(angleRadians)).toFloat() * width / 2f + + abs(sin(angleRadians)).toFloat() * height / 2f + return min(baseRadius, maxOuterRadius - radialHalfExtent) + } + + /** + * Widest the min / avg / max row may be. When the dial ends below its centre, the last scale + * label sits beside that row, so the row must stay clear of the label's inner edge. + * + * @param endAngleRadians the dial's end angle (canvas convention: positive is below the centre). + * @param endLabelRadius distance from the dial centre to the end label's centre. + */ + fun statsMaxWidth( + cardLimit: Float, + endAngleRadians: Double, + endLabelRadius: Float, + endLabelWidth: Float, + gap: Float + ): Float { + if (sin(endAngleRadians) <= 0.0) return cardLimit + val labelInnerX = abs(cos(endAngleRadians)).toFloat() * endLabelRadius - endLabelWidth / 2f + return min(cardLimit, 2f * (labelInnerX - gap)).coerceAtLeast(0f) } /** Text size factor that makes [width] fit [maxWidth]; never enlarges. */ diff --git a/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt index 31e45634..9a4cce30 100644 --- a/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt +++ b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt @@ -95,12 +95,27 @@ class GaugeGeometryTest { } @Test - fun `label at the top keeps its radius, wide labels at the sides move inward`() { - val top = GaugeGeometry.labelCenterRadius(100f, -Math.PI / 2, 40f, 10f) - val side = GaugeGeometry.labelCenterRadius(100f, 0.0, 40f, 10f) + fun `labels keep their radius unless they would reach into the ticks`() { + val narrowAtSide = GaugeGeometry.labelCenterRadius(75f, 85f, 0.0, 10f, 8f) + val wideAtSide = GaugeGeometry.labelCenterRadius(75f, 85f, 0.0, 40f, 8f) - assertEquals(100f, top, 0.001f) - assertEquals(85f, side, 0.001f) + assertEquals(75f, narrowAtSide, 0.001f) + assertEquals(65f, wideAtSide, 0.001f) + } + + @Test + fun `stats row stays clear of the end label when the dial ends below its centre`() { + // 200 degree dial ends at 40 degrees, beside the stats row: "120" overlapped "max -29". + val end = Math.toRadians(40.0) + val limit = GaugeGeometry.statsMaxWidth(cardLimit = 300f, endAngleRadians = end, endLabelRadius = 100f, endLabelWidth = 30f, gap = 5f) + + assertEquals(2f * (100f * kotlin.math.cos(end).toFloat() - 15f - 5f), limit, 0.01f) + } + + @Test + fun `stats row uses the card when the dial ends at or above its centre`() { + assertEquals(300f, GaugeGeometry.statsMaxWidth(300f, Math.toRadians(0.0), 100f, 30f, 5f)) + assertEquals(300f, GaugeGeometry.statsMaxWidth(300f, Math.toRadians(-20.0), 100f, 30f, 5f)) } @Test From a04367536f59f333eb3cc1997436da8e993039a9 Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Thu, 8 Oct 2026 16:04:04 +0200 Subject: [PATCH 3/3] =?UTF-8?q?feat(gauge):=20centre=20the=20drawn=20arc?= =?UTF-8?q?=20in=20the=20card,=20show=20=C2=B0C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phone dials are centred on what the arc draws, not on their square: a 200-degree dial left the bottom third of every card empty. - displayUnits() adds the degree sign to bare C/F units on every surface-rendered screen; PID data, exports and logs keep "C". --- CLAUDE.md | 4 ++ doc/specs/gauge-drawing-improvements.md | 11 ++++- .../org/obd/graphs/renderer/AbstractDrawer.kt | 4 +- .../java/org/obd/graphs/renderer/Units.kt | 28 ++++++++++++ .../obd/graphs/renderer/gauge/GaugeDrawer.kt | 3 +- .../graphs/renderer/gauge/GaugeGeometry.kt | 44 +++++++++++++++++++ .../renderer/gauge/GaugeSurfaceRenderer.kt | 14 ++++-- .../graphs/renderer/giulia/GiuliaDrawer.kt | 5 ++- .../graphs/renderer/trip/TripInfoDrawer.kt | 3 +- .../java/org/obd/graphs/renderer/UnitsTest.kt | 38 ++++++++++++++++ .../renderer/gauge/GaugeGeometryTest.kt | 20 +++++++++ 11 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 screen_renderer/src/main/java/org/obd/graphs/renderer/Units.kt create mode 100644 screen_renderer/src/test/java/org/obd/graphs/renderer/UnitsTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 587b5778..5302c165 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -211,6 +211,10 @@ Gradle needs JDK 17+ (Crashlytics plugin); the shell default may be JDK 11 and f zones) must be in its key. Numbers skipped for a still-`null` value were cached numberless for good. * End labels of a dial that ends below centre sit beside the stats row; `GaugeGeometry.statsMaxWidth` keeps the row clear. Fix overlaps with draw order (numbers after ticks) before moving labels. +* Phone dials are centred on what they draw (`GaugeGeometry.dialTopOffset` from start/sweep), not on + their square — a 200° arc leaves the lower circle empty. +* Units: draw `displayUnits(pid.units)` (`renderer/Units.kt`), never `pid.units` directly. ObdMetrics' + PID resources spell °C as a bare `C`, and they are not ours to change (shared with exports/logs). * Red on the dial = the PID's `alert` thresholds. The scale bitmap cache is keyed by `GaugeScale` and `GaugeRedZones` too — a cache keyed by PID id alone kept a stale scale after a PID edit. * Phone cards may be taller than the dial (`GaugeGeometry.cardHeight`); `borderArea` is the card, `top` diff --git a/doc/specs/gauge-drawing-improvements.md b/doc/specs/gauge-drawing-improvements.md index e0afa008..38bbf06c 100644 --- a/doc/specs/gauge-drawing-improvements.md +++ b/doc/specs/gauge-drawing-improvements.md @@ -10,7 +10,7 @@ Readability, correctness and per-frame cost fixes for the dial gauge (`GaugeDraw **Problem.** Scale labels were thirds of the PID range (`-40, -6, 26, 60 …`, `0.8, 2.7, 4.5 …`). The last part of every dial was painted red whether or not the PID has an alert. End-of-scale labels ran into the ticks and the card border. The min / avg / max row was unlabelled and spaced by fixed offsets. A long value ran past the dial. Four gauges used under half of a portrait screen. A value outside the PID's min..max drew the progress arc past the dial's end (or backwards). -**Scope.** `:screen_renderer` only: `GaugeDrawer`, `GaugeSurfaceRenderer`, new pure `GaugeGeometry.kt`. `GaugeDrawer` is shared, so items 1–5 and 7–9 also change the gauges of Performance, Drag Racing and Brake Boosting (phone and AA). No preference, query or data changes. +**Scope.** `:screen_renderer` only: `GaugeDrawer`, `GaugeSurfaceRenderer`, new pure `GaugeGeometry.kt`; `renderer/Units.kt` used by every drawer that draws units. `GaugeDrawer` is shared, so items 1–5 and 7–9 also change the gauges of Performance, Drag Racing and Brake Boosting (phone and AA). No preference, query or data changes. ## Behaviour changes @@ -25,6 +25,8 @@ Readability, correctness and per-frame cost fixes for the dial gauge (`GaugeDraw | 7 | Out-of-range value | Arc drawn past the end, or backwards below min | Clamped to the dial | | 8 | Arc angle | Truncated to whole degrees | Float | | 9 | Scale bitmap cache | Keyed by PID id, size, colour; numbers skipped while the value was still `null`, and that numberless dial stayed cached | Also keyed by scale, red zones and whether numbers are drawn; numbers skipped only for non-numeric values; the replaced bitmap is recycled | +| 11 | Dial position (phone) | Dial square centred in the card; a 200° dial draws nothing below 40° under its centre, so the bottom third of each card was empty | The drawn part of the arc (from its start / sweep angles) is centred in the card (`GaugeGeometry.dialTopOffset`) | +| 12 | Units | `C` as defined in the ObdMetrics PID resources | `°C` / `°F` on every surface-rendered screen (gauge, Giulia, Trip Info, Performance, status panel) via `displayUnits()`; the PID data, exports and logs keep `C` | | 10 | Per frame | New `RadialGradient` per gauge; colour parsed / resolved per frame | Gradient cached per PID and card rect; colours resolved once | ## Settings / keys touched @@ -60,6 +62,8 @@ Visual only. Existing profiles keep their PIDs and ranges; the dial's range may | stats row is centred with equal gaps / wider than the card is scaled down | Item 4 | | value text is only ever shrunk | Item 5 | | cards fill the free height up to a limit, stay square when scrolling | Item 6 | +| phone dial is centred on what it draws / upper half dial moves to the middle / full circle dial stays | Item 11 | +| `UnitsTest`: bare temperature units get the degree sign / other units are drawn as defined | Item 12 | ## Risks and verification checklist @@ -71,9 +75,14 @@ Visual only. Existing profiles keep their PIDs and ranges; the dial's range may - [ ] A gauge showing `--` (no value yet) still shows its scale numbers. - [ ] End label (e.g. `120`) does not touch the `▲` max value. - [ ] Performance, Drag Racing, Brake Boosting gauges (phone + AA DHU): labels readable, no overlap with ticks. +- [ ] Phone, square (scrolling) cards: dial and stats sit in the middle of the card, the arc's end stays inside it. +- [ ] Temperatures read `°C` on Gauge, Giulia, Trip Info, Performance and the AA status panel. - [ ] `▼` / `▲` render (font fallback) on the target devices. ## Out of scope +* `°C` in the `:app` views (Dashboard, graph marker, DTC details): they format units themselves; `displayUnits` would have to move to `:common` first. +* Fixing `C` in the ObdMetrics PID resources: a separate repository, and the unit also feeds exports and logs. + * Pre-rendering the progress glow: the arc changes every frame, so it cannot be cached; the `BlurMaskFilter` cost was not measured. * Captions in words (`min / avg / max`): would need strings in `:app`'s resources for both locales. diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/AbstractDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/AbstractDrawer.kt index 7a3eb86b..ea423846 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/AbstractDrawer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/AbstractDrawer.kt @@ -318,7 +318,7 @@ internal abstract class AbstractDrawer( marginLeft += getTextWidth(currentStatusText, statusPaint) + 4F drawText( canvas, - "${it.source.format(castToInt = false)}${it.pid.units ?: ""}", + "${it.source.format(castToInt = false)}${displayUnits(it.pid.units) ?: ""}", marginLeft, top, Color.WHITE, @@ -343,7 +343,7 @@ internal abstract class AbstractDrawer( marginLeft += getTextWidth(currentStatusText, statusPaint) + 4F drawText( canvas, - "${it.source.format(castToInt = false)}${it.pid.units ?: ""}", + "${it.source.format(castToInt = false)}${displayUnits(it.pid.units) ?: ""}", marginLeft, top, Color.WHITE, diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/Units.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/Units.kt new file mode 100644 index 00000000..372e6272 --- /dev/null +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/Units.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2026, Tomasz Żebrowski + * + *

Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.renderer + +// The PID definitions (ObdMetrics resources) spell temperatures as a bare "C"; they are shared with +// exports and logs, so the degree sign is added only where the unit is drawn. +private val DISPLAY_UNITS = + mapOf( + "C" to "°C", + "F" to "°F" + ) + +/** [units] as drawn on screen. */ +internal fun displayUnits(units: String?): String? = units?.let { DISPLAY_UNITS[it.trim()] ?: it } diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt index 8ddb4030..04f70868 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt @@ -40,6 +40,7 @@ import org.obd.graphs.renderer.AbstractDrawer import org.obd.graphs.renderer.api.GaugeProgressBarType import org.obd.graphs.renderer.api.ScreenSettings import org.obd.graphs.renderer.cache.TextCache +import org.obd.graphs.renderer.displayUnits import org.obd.graphs.round import org.obd.graphs.toDouble import org.obd.graphs.toFloat @@ -577,7 +578,7 @@ internal class GaugeDrawer( valuePaint.getTextBounds(value, 0, value.length, drawingCache.textRect) val pid = metric.pid - val unitText = pid.units + val unitText = displayUnits(pid.units) var unitWidth = 0f if (unitText != null) { diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt index a06b0499..c3c1d584 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeGeometry.kt @@ -22,6 +22,7 @@ import kotlin.math.ceil import kotlin.math.cos import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt @@ -203,6 +204,49 @@ internal object GaugeGeometry { return StatsRow(lefts, scale) } + /** + * Offset of the dial's square from the card top that centres what the dial actually draws. + * A 200° dial draws nothing in the lower part of its circle, so a dial centred as a square + * left the bottom third of every card empty. + */ + fun dialTopOffset( + cardHeight: Float, + gaugeWidth: Float, + startAngle: Float, + sweepAngle: Float + ): Float { + val radius = gaugeWidth / 2f + val (minSin, maxSin) = arcSinRange(startAngle, sweepAngle) + val drawnHeight = radius * (maxSin - minSin) + // The drawn part starts radius * minSin below the circle's centre, i.e. this far below its square's top. + val drawnTop = radius + radius * minSin + return ((cardHeight - drawnHeight) / 2f - drawnTop).coerceIn(0f, max(0f, cardHeight - gaugeWidth / 2f - radius * maxSin)) + } + + // Lowest and highest sine over the arc (canvas angles: 90 is the bottom, 270 the top). + private fun arcSinRange( + startAngle: Float, + sweepAngle: Float + ): Pair { + val start = Math.toRadians(startAngle.toDouble()) + val end = Math.toRadians((startAngle + sweepAngle).toDouble()) + var minSin = min(sin(start), sin(end)).toFloat() + var maxSin = max(sin(start), sin(end)).toFloat() + val from = min(startAngle, startAngle + sweepAngle) + val to = max(startAngle, startAngle + sweepAngle) + var a = ceil((from - 90f) / 360f) * 360f + 90f + while (a <= to) { + maxSin = 1f + a += 360f + } + a = ceil((from - 270f) / 360f) * 360f + 270f + while (a <= to) { + minSin = -1f + a += 360f + } + return minSin to maxSin + } + /** * Height of a phone gauge card. Square when the grid scrolls anyway; otherwise the rows share * [availableHeight], up to [MAX_CARD_HEIGHT_RATIO] × the gauge width. diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt index bda0d03c..0ee16626 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeSurfaceRenderer.kt @@ -66,6 +66,9 @@ private const val TAG = "cache" // Below the last row; the free height the cards share excludes it, or a filled grid would scroll. private const val CONTENT_BOTTOM_PADDING = 20f +private const val MOBILE_START_ANGLE = 200f +private const val MOBILE_SWEEP_ANGLE = 200f + internal class GaugeSurfaceRenderer( context: Context, private val settings: ScreenSettings, @@ -87,8 +90,8 @@ internal class GaugeSurfaceRenderer( context = context, drawerSettings = DrawerSettings( - startAngle = 200f, - sweepAngle = 200f, + startAngle = MOBILE_START_ANGLE, + sweepAngle = MOBILE_SWEEP_ANGLE, gaugeProgressBarType = settings.getGaugeScreenSettings().gaugeProgressBarType ) ) @@ -326,7 +329,12 @@ internal class GaugeSurfaceRenderer( if (fillsHeight) { layoutCache.rowHeight = cardHeight + 2 * itemMargin } - val dialOffset = (cardHeight - layoutCache.gaugeWidth) / 2f + val dialOffset = + if (fillsHeight) { + GaugeGeometry.dialTopOffset(cardHeight, layoutCache.gaugeWidth, MOBILE_START_ANGLE, MOBILE_SWEEP_ANGLE) + } else { + 0f + } layoutCache.contentHeight = topOffset + (totalRows * layoutCache.rowHeight) + CONTENT_BOTTOM_PADDING layoutCache.maxScroll = max(0f, layoutCache.contentHeight - availableHeight) diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt index 94f98cc2..8a39a9f4 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt @@ -32,6 +32,7 @@ import org.obd.graphs.mapRange import org.obd.graphs.renderer.AbstractDrawer import org.obd.graphs.renderer.api.ScreenSettings import org.obd.graphs.renderer.cache.TextCache +import org.obd.graphs.renderer.displayUnits import org.obd.graphs.toFloat import org.obd.graphs.toNumber import kotlin.math.max @@ -362,7 +363,7 @@ internal class GiuliaDrawer( castToInt: Boolean = false ): Float { valuePaint.color = valueColorScheme(metric) - val units = metric.source.command.pid.units ?: "" + val units = displayUnits(metric.source.command.pid.units) ?: "" val left1 = left - getTextWidth(units, valuePaint) valuePaint.setShadowLayer(30f * density, 0f, 0f, Color.WHITE) @@ -375,7 +376,7 @@ internal class GiuliaDrawer( canvas.drawText(value, left1, top, valuePaint) - metric.source.command.pid.units?.let { + displayUnits(metric.source.command.pid.units)?.let { valuePaint.color = Color.LTGRAY valuePaint.textAlign = Paint.Align.LEFT valuePaint.textSize = (textSize * 0.4).toFloat() diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/trip/TripInfoDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/trip/TripInfoDrawer.kt index 052f8f16..5cd71ff9 100644 --- a/screen_renderer/src/main/java/org/obd/graphs/renderer/trip/TripInfoDrawer.kt +++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/trip/TripInfoDrawer.kt @@ -29,6 +29,7 @@ import org.obd.graphs.renderer.AbstractDrawer import org.obd.graphs.renderer.MARGIN_END import org.obd.graphs.renderer.api.ScreenSettings import org.obd.graphs.renderer.cache.TextCache +import org.obd.graphs.renderer.displayUnits import org.obd.graphs.renderer.giulia.GiuliaDrawer import org.obd.graphs.toNumber @@ -321,7 +322,7 @@ internal class TripInfoDrawer( var textWidth = getTextWidth(text, valuePaint) + textPadding if (unitEnabled) { - metric.source.command.pid.units?.let { + displayUnits(metric.source.command.pid.units)?.let { valuePaint.color = Color.LTGRAY valuePaint.textSize = (textSize * 0.4).toFloat() canvas.drawText(it, (left + textWidth), top, valuePaint) diff --git a/screen_renderer/src/test/java/org/obd/graphs/renderer/UnitsTest.kt b/screen_renderer/src/test/java/org/obd/graphs/renderer/UnitsTest.kt new file mode 100644 index 00000000..f562b49d --- /dev/null +++ b/screen_renderer/src/test/java/org/obd/graphs/renderer/UnitsTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2026, Tomasz Żebrowski + * + *

Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.renderer + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class UnitsTest { + @Test + fun `bare temperature units get the degree sign`() { + assertEquals("°C", displayUnits("C")) + assertEquals("°F", displayUnits("F")) + assertEquals("°C", displayUnits(" C ")) + } + + @Test + fun `other units are drawn as defined`() { + assertEquals("%", displayUnits("%")) + assertEquals("°C", displayUnits("°C")) + assertEquals("Ctrl", displayUnits("Ctrl")) + assertNull(displayUnits(null)) + } +} diff --git a/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt index 9a4cce30..fd422998 100644 --- a/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt +++ b/screen_renderer/src/test/java/org/obd/graphs/renderer/gauge/GaugeGeometryTest.kt @@ -140,6 +140,26 @@ class GaugeGeometryTest { assertEquals(0.5f, GaugeGeometry.fitScale(200f, 100f)) } + @Test + fun `phone dial is centred on what it draws, not on its square`() { + // 200 degree dial from 200: top of the circle to 40 degrees below the centre. + val drawn = 50f * (1f + kotlin.math.sin(Math.toRadians(40.0)).toFloat()) + val offset = GaugeGeometry.dialTopOffset(cardHeight = 100f, gaugeWidth = 100f, startAngle = 200f, sweepAngle = 200f) + + assertEquals((100f - drawn) / 2f, offset, 0.01f) + } + + @Test + fun `upper half dial moves to the middle of a square card`() { + assertEquals(25f, GaugeGeometry.dialTopOffset(100f, 100f, startAngle = 180f, sweepAngle = 180f), 0.01f) + } + + @Test + fun `full circle dial stays where it was`() { + assertEquals(0f, GaugeGeometry.dialTopOffset(100f, 100f, startAngle = 0f, sweepAngle = 360f), 0.01f) + assertEquals(25f, GaugeGeometry.dialTopOffset(150f, 100f, startAngle = 0f, sweepAngle = 360f), 0.01f) + } + @Test fun `cards fill the free height up to a limit, stay square when scrolling`() { // Four gauges in two columns used under half of a portrait phone screen.