hexagram/py/hexagram/cluster.py

198 lines
6.4 KiB
Python

import math
import cairo
from typing import Iterable, List
from hexagram.box import Align
from hexagram.pattern import HexagonPattern
from hexagram.gauge import Gauge, TextGauge
from hexagram.dial import Dial
from hexagram.speedo import Speedo
from hexagram.tacho import Tacho, ShiftIndicator
from hexagram.fuel import FuelGauge
from hexagram.thermo import Thermometer
from hexagram.status import StatusIconBox, ColdStatus, ColdIcon
class Odometer(TextGauge):
def __init__(self, x: float, y: float, align: Align):
super().__init__(x, y, 0, 999999999, align)
def format_text(self):
return "%.1f mi" % self.value
class AmbientTemp(TextGauge):
__slots__ = 'icon',
ICON_WIDTH = 40
ICON_HEIGHT = 40
ICON_PADDING = 16
COLD_THRESHOLD = 4.0 # °C
def __init__(self, x: float, y: float, align: Align):
super().__init__(x, y, -100, 100, align)
self.value = 0
self.icon = ColdIcon(self.ICON_WIDTH, self.ICON_HEIGHT)
def format_text(self):
return "%.1f°C" % self.value
def draw_fg(self, cr: cairo.Context):
cr.select_font_face(self.FONT_FACE,
self.FONT_SLANT,
self.FONT_WEIGHT)
text = self.format_text()
cr.set_font_size(self.FONT_SIZE)
extents = cr.text_extents(text)
width = extents[2] - extents[0]
if self.value <= self.COLD_THRESHOLD:
width += self.icon.width + self.ICON_PADDING
if self.align is Align.LEFT:
text_x_offset = 0.0
elif self.align is Align.MIDDLE:
text_x_offset = self.x - width / 2.0
elif self.align is Align.RIGHT:
text_x_offset = self.x - width
cr.set_source_rgb(1, 1, 1)
cr.move_to(text_x_offset, self.y)
cr.show_text(text)
if self.value <= self.COLD_THRESHOLD:
icon_y_offset = abs(self.icon.height - self.FONT_SIZE) * 2
self.icon.draw(cr,
text_x_offset + width - self.icon.width,
self.y - icon_y_offset,
ColdStatus.ON)
class Clock(TextGauge):
def __init__(self, x: float, y: float, align: Align):
super().__init__(x, y, 0, 9999, align)
def format_text(self):
return "%d:%02d" % (
(self.value - (self.value % 60)) / 60,
self.value % 60
)
class Cluster():
__slots__ = '_redraw', 'bg', 'gauges', 'speedo', 'fuel', 'tacho', 'thermo', \
'odo', 'ambient', 'clock', 'shift_indicator', 'status',
WIDTH = 1280
HEIGHT = 480
SHIFT_INDICATOR_X = 392
SHIFT_INDICATOR_Y = 24
SECTION_HEIGHT_TOP = 64
SECTION_HEIGHT_BOTTOM = 52
def __init__(self, rpm_min: float, rpm_redline: float, rpm_max: float):
self._redraw = True
self.bg = cairo.ImageSurface(cairo.FORMAT_ARGB32,
self.WIDTH,
self.HEIGHT)
self.gauges: List[Gauge] = list()
self.speedo = Speedo(self.HEIGHT / 2,
self.HEIGHT / 2,
self.HEIGHT / 2,
200.0)
self.fuel = FuelGauge(self.HEIGHT / 2,
self.HEIGHT / 2,
self.HEIGHT / 2)
self.tacho = Tacho(self.WIDTH - self.HEIGHT / 2,
self.HEIGHT / 2,
self.HEIGHT / 2,
8000)
self.thermo = Thermometer(self.WIDTH - self.HEIGHT / 2,
self.HEIGHT / 2,
self.HEIGHT / 2)
self.odo = Odometer(0.8 * self.HEIGHT, self.HEIGHT - 16, Align.LEFT)
self.clock = Clock(self.WIDTH / 2, self.HEIGHT - 16, Align.MIDDLE)
self.ambient = AmbientTemp(self.WIDTH - 0.8 * self.HEIGHT, self.HEIGHT - 16, Align.RIGHT)
self.shift_indicator = ShiftIndicator(self.SHIFT_INDICATOR_X,
self.SHIFT_INDICATOR_Y,
rpm_min,
rpm_redline,
rpm_max)
self.status = StatusIconBox(self.WIDTH/ 2 - StatusIconBox.ICON_WIDTH * 3.5,
self.HEIGHT - self.SECTION_HEIGHT_BOTTOM - 16 - StatusIconBox.ICON_HEIGHT * 3,
StatusIconBox.ICON_WIDTH * 7,
StatusIconBox.ICON_HEIGHT * 3)
self.gauges.append(self.speedo)
self.gauges.append(self.fuel)
self.gauges.append(self.tacho)
self.gauges.append(self.thermo)
self.gauges.append(self.odo)
self.gauges.append(self.ambient)
self.gauges.append(self.clock)
self.gauges.append(self.shift_indicator)
self.gauges.append(self.status)
def _update_bg(self):
cr = cairo.Context(self.bg)
cr.set_source_rgb(0.1, 0.1, 0.1)
cr.rectangle(0, 0, self.WIDTH, self.HEIGHT)
cr.fill()
pattern = HexagonPattern()
cr.rectangle(0, 0, self.WIDTH, self.HEIGHT)
pattern.fill(cr)
# Top dark area
cr.set_source_rgba(0, 0, 0, 0.5)
cr.rectangle(0, 0, self.WIDTH, self.SECTION_HEIGHT_TOP)
cr.fill()
cr.set_source_rgba(1, 1, 1, 0.25)
cr.move_to(0, self.SECTION_HEIGHT_TOP)
cr.line_to(self.WIDTH, self.SECTION_HEIGHT_TOP)
cr.stroke()
# Bottom dark area
cr.set_source_rgba(0, 0, 0, 0.5)
cr.rectangle(0, self.HEIGHT - self.SECTION_HEIGHT_BOTTOM,
self.WIDTH,
self.SECTION_HEIGHT_BOTTOM)
cr.fill()
cr.set_source_rgba(1, 1, 1, 0.25)
cr.move_to(0, self.HEIGHT - self.SECTION_HEIGHT_BOTTOM)
cr.line_to(self.WIDTH, self.HEIGHT - self.SECTION_HEIGHT_BOTTOM)
cr.stroke()
for gauge in self.gauges:
gauge.draw_bg(cr)
def draw_bg(self, cr: cairo.Context):
if self._redraw:
self._update_bg()
self._redraw = False
op = cr.get_operator()
cr.set_source_surface(self.bg, 0, 0)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.rectangle(0, 0, self.WIDTH, self.HEIGHT)
cr.fill()
cr.set_operator(op)
def draw_fg(self, cr: cairo.Context):
for gauge in self.gauges:
gauge.draw_fg(cr)