diff --git a/py/hexagram/cluster.py b/py/hexagram/cluster.py index ad4318e..6a64cad 100644 --- a/py/hexagram/cluster.py +++ b/py/hexagram/cluster.py @@ -11,7 +11,7 @@ from hexagram.speedo import Speedo from hexagram.tacho import Tacho from hexagram.fuel import FuelGauge from hexagram.thermo import Thermometer -from hexagram.icons import StatusIconBox +from hexagram.status import StatusIconBox class ShiftIndicator(Gauge): __slots__ = 'x', 'y', 'rpm_min', 'rpm_redline', 'rpm_max', diff --git a/py/hexagram/icons.py b/py/hexagram/icons.py deleted file mode 100644 index bc6ea1f..0000000 --- a/py/hexagram/icons.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -import cairo - -from typing import Optional - -from hexagram.gauge import Gauge -from hexagram.status import VehicleStatus -from hexagram.svg import render_to_image - -class ISO7000Icon(): - __slots__ = 'status', 'name', 'width', 'height', 'color', 'surface', - - __dirs__ = ('./icons', - '/usr/local/share/hexagram/icons' - '/usr/share/hexagram/icons') - - @staticmethod - def _locate(name: str): - for dir in ISO7000Icon.__dirs__: - path = "%s/%s.svg" % (dir, name) - - if os.path.exists(path): - return path - - raise FileNotFoundError(name + '.svg') - - def __init__(self, status: VehicleStatus, name: str, width: float, height: float, color: str, style: Optional[str]): - self.status = status - self.name = name - self.width = width - self.height = height - self.color = color - - if style is None: - style = 'rect,path,circle{stroke:%(s)s;}' - - path = ISO7000Icon._locate(name) - - self.surface = render_to_image(path, width, height, style % {'s': color}) - - def __del__(self): - if self.surface is not None: - self.surface.finish() - - def draw(self, cr: cairo.Context, x: float, y: float): - cr.set_source_surface(self.surface, x, y) - cr.rectangle(x, y, self.width, self.height) - cr.fill() - -class ISO7000(): - __slots__ = 'icons', - - __icons__ = ( - (VehicleStatus.BATTERY_FAULT, 'battery', '#f00', '#rect5, #rect6 {fill: %(s)s} #rect4, path {stroke: %(s)s}'), - (VehicleStatus.BEAMS_FOG, 'beams-fog', '#0f0', '#g9 path {fill: %(s)s} #g5 path {stroke: %(s)s}'), - (VehicleStatus.BEAMS_HIGH, 'beams-high', '#00f', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.BEAMS_LOW, 'beams-low', '#0f0', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.BEAMS_PARKING, 'beams-parking', '#ff0'), - (VehicleStatus.BELT, 'belt', '#f00', 'path {fill: %(s)s}'), - (VehicleStatus.COLD, 'cold', '#eef'), - (VehicleStatus.COLLISION, 'collision', '#f00', 'path {stroke: %(s)s}'), - (VehicleStatus.COOLANT_LOW, 'coolant', '#fa0', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), - (VehicleStatus.COOLANT_OVERHEAT, 'coolant', '#f00', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), - (VehicleStatus.CRUISE, 'cruise', '#0f0', 'path, circle {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.FUEL_LOW, 'fuel', '#fa0', '#g9 * {stroke: %(s)s} #path9 {fill: %(s)s}'), - (VehicleStatus.LANEKEEP_OFF, 'lane-departure', '#fa0'), - (VehicleStatus.OIL_LOW, 'oil', '#fa0', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.OIL_OVERHEAT, 'oil', '#f00', 'path {fill: %(s)s; stroke: %(s)s}'), - (VehicleStatus.PARKING_BRAKE_ON, 'parking', '#fa0', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), - (VehicleStatus.PARKING_BRAKE_FAULT, 'parking', '#f00', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), - (VehicleStatus.STABILITY_OFF, 'stability', '#fa0', '#path9, #path8, #path7, #path6, #path5 {fill: %(s)s} #path10 {stroke: %(s)s}'), - (VehicleStatus.TPMS_WARNING, 'tpms', '#fa0', 'path, circle {fill: %(s)s}'), - (VehicleStatus.WIPER_WASHER_LOW, 'wiper-washer', '#fa0'), - ) - - def __init__(self, width: float, height: float): - self.icons = dict() - - for item in self.__icons__: - status, name, color = item[0:3] - style = 'rect,path,circle{stroke:%(s)s;}' if len(item) < 4 else item[3] - - self.icons[status.value] = ISO7000Icon(status, - name, - width, - height, - color, - style) - -class StatusIconBox(Gauge): - __slots__ = 'x', 'y', 'width', 'height', 'iso7000', 'value', - - ICON_WIDTH = 48 - ICON_HEIGHT = 48 - - def __init__(self, x: float, y: float, width: float, height: float): - self.x = x - self.y = y - self.width = width - self.height = height - self.iso7000 = ISO7000(self.ICON_WIDTH, self.ICON_HEIGHT) - self.value = VehicleStatus.OK.value - - def reset(self): - self.value = VehicleStatus.OK.value - - def set_status(self, status: VehicleStatus): - self.value |= status.value - - def clear_status(self, status: VehicleStatus): - self.value &= ~status.value - - def draw_bg(self, cr: cairo.Context): - pass - - def draw_fg(self, cr: cairo.Context): - x, y = 0, 0 - - for status_value in self.iso7000.icons: - icon = self.iso7000.icons[status_value] - - if self.value & status_value: - if icon is not None: - icon.draw(cr, self.x + x, self.y + y) - - x += self.ICON_WIDTH - - if x >= self.width: - x = 0 - y += self.ICON_HEIGHT diff --git a/py/hexagram/status.py b/py/hexagram/status.py index 7327163..eb4ca28 100644 --- a/py/hexagram/status.py +++ b/py/hexagram/status.py @@ -1,4 +1,11 @@ +import os import enum +import cairo + +from typing import Optional + +from hexagram.svg import render_to_image +from hexagram.gauge import Gauge class VehicleStatus(enum.Enum): OK = 0 @@ -50,3 +57,117 @@ class VehicleStatus(enum.Enum): def __str__(self): return self.__strings__.get(self.value, "unknown") + +class StatusIcon(): + __slots__ = 'status', 'name', 'width', 'height', 'color', 'surface', + + __dirs__ = ('./icons', + '/usr/local/share/hexagram/icons' + '/usr/share/hexagram/icons') + + @staticmethod + def _locate(name: str): + for dir in StatusIcon.__dirs__: + path = "%s/%s.svg" % (dir, name) + + if os.path.exists(path): + return path + + raise FileNotFoundError(name + '.svg') + + def __init__(self, status: VehicleStatus, name: str, width: float, height: float, color: str, style: Optional[str]): + self.status = status + self.name = name + self.width = width + self.height = height + self.color = color + + if style is None: + style = 'rect,path,circle{stroke:%(s)s;}' + + path = StatusIcon._locate(name) + + self.surface = render_to_image(path, width, height, style % {'s': color}) + + def __del__(self): + if self.surface is not None: + self.surface.finish() + + def draw(self, cr: cairo.Context, x: float, y: float): + cr.set_source_surface(self.surface, x, y) + cr.rectangle(x, y, self.width, self.height) + cr.fill() + +class StatusIconBox(Gauge): + __slots__ = 'x', 'y', 'width', 'height', 'icons', 'value', + + __icons__ = ( + (VehicleStatus.BATTERY_FAULT, 'battery', '#f00', '#rect5, #rect6 {fill: %(s)s} #rect4, path {stroke: %(s)s}'), + (VehicleStatus.BEAMS_FOG, 'beams-fog', '#0f0', '#g9 path {fill: %(s)s} #g5 path {stroke: %(s)s}'), + (VehicleStatus.BEAMS_HIGH, 'beams-high', '#00f', 'path {fill: %(s)s; stroke: %(s)s}'), + (VehicleStatus.BEAMS_LOW, 'beams-low', '#0f0', 'path {fill: %(s)s; stroke: %(s)s}'), + (VehicleStatus.BEAMS_PARKING, 'beams-parking', '#ff0'), + (VehicleStatus.BELT, 'belt', '#f00', 'path {fill: %(s)s}'), + (VehicleStatus.COLD, 'cold', '#eef'), + (VehicleStatus.COLLISION, 'collision', '#f00', 'path {stroke: %(s)s}'), + (VehicleStatus.COOLANT_LOW, 'coolant', '#fa0', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), + (VehicleStatus.COOLANT_OVERHEAT, 'coolant', '#f00', 'path, circle, rect {fill: %(s)s} #g9 path {stroke: %(s)s}'), + (VehicleStatus.CRUISE, 'cruise', '#0f0', 'path, circle {fill: %(s)s; stroke: %(s)s}'), + (VehicleStatus.FUEL_LOW, 'fuel', '#fa0', '#g9 * {stroke: %(s)s} #path9 {fill: %(s)s}'), + (VehicleStatus.LANEKEEP_OFF, 'lane-departure', '#fa0'), + (VehicleStatus.OIL_LOW, 'oil', '#fa0', 'path {fill: %(s)s; stroke: %(s)s}'), + (VehicleStatus.OIL_OVERHEAT, 'oil', '#f00', 'path {fill: %(s)s; stroke: %(s)s}'), + (VehicleStatus.PARKING_BRAKE_ON, 'parking', '#fa0', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), + (VehicleStatus.PARKING_BRAKE_FAULT, 'parking', '#f00', '#path6, #path5 {stroke: %(s)s} circle {stroke: %(s)s} #path7 {fill: %(s)s}'), + (VehicleStatus.STABILITY_OFF, 'stability', '#fa0', '#path9, #path8, #path7, #path6, #path5 {fill: %(s)s} #path10 {stroke: %(s)s}'), + (VehicleStatus.TPMS_WARNING, 'tpms', '#fa0', 'path, circle {fill: %(s)s}'), + (VehicleStatus.WIPER_WASHER_LOW, 'wiper-washer', '#fa0'), + ) + + ICON_WIDTH = 48 + ICON_HEIGHT = 48 + + def __init__(self, x: float, y: float, width: float, height: float): + self.x = x + self.y = y + self.width = width + self.height = height + self.value = VehicleStatus.OK.value + self.icons: dict[int, StatusIcon] = dict() + + for item in self.__icons__: + status, name, color = item[0:3] + style = 'rect,path,circle{stroke:%(s)s;}' if len(item) < 4 else item[3] + + self.icons[status.value] = StatusIcon(status, name, + self.ICON_WIDTH, + self.ICON_HEIGHT, + color, style) + + def reset(self): + self.value = VehicleStatus.OK.value + + def set_status(self, status: VehicleStatus): + self.value |= status.value + + def clear_status(self, status: VehicleStatus): + self.value &= ~status.value + + def draw_bg(self, cr: cairo.Context): + pass + + def draw_fg(self, cr: cairo.Context): + x, y = 0, 0 + + for status_value in self.icons: + icon = self.icons[status_value] + + if self.value & status_value: + if icon is not None: + icon.draw(cr, self.x + x, self.y + y) + + x += self.ICON_WIDTH + + if x >= self.width: + x = 0 + y += self.ICON_HEIGHT