mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
797 lines
42 KiB
Python
797 lines
42 KiB
Python
"""Validated onroad colors and independent logical-pixel layouts."""
|
|
|
|
import copy
|
|
import json
|
|
import math
|
|
import re
|
|
from functools import lru_cache
|
|
from typing import Any, NotRequired, TypedDict
|
|
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
|
|
from openpilot.starpilot.ui.large_speed_geometry import confirmation_bounds, confirmation_footprint
|
|
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
|
|
from openpilot.starpilot.ui.large_speed_geometry import (WIDTH, CARD_HEIGHT, ACTION_WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y,
|
|
GAP, SOURCE_ROW_HEIGHT, sources_extent)
|
|
|
|
PARAM_KEY = "OnroadCustomizations"
|
|
MAX_BYTES = 16384
|
|
# Legacy saved layouts retain this field even though the paired speed widget uses fixed alignment.
|
|
SPEED_ALIGNMENTS = ("left", "center", "right")
|
|
PALETTE = {"cardFill": "#000000A6", "cardBorder": "#C4CDD0B4", "text": "#FFFFFFFF"}
|
|
ROAD_COLORS = {"path": "#30FF9CFF", "pathEdge": "#00FF40FF", "laneLines": "#FFFFFFFF"}
|
|
PATH_MODES = ("default", "color", "rainbow", "acceleration")
|
|
WHEEL_SIZES = {"large": (144, 192, 240), "compact": (40, 50, 70)}
|
|
DM_SIZES = {"large": (64, 128, 192), "compact": (40, 60, 90)}
|
|
WIDGET_SIZES = {"steering_wheel": WHEEL_SIZES, "driver_monitor": DM_SIZES}
|
|
|
|
|
|
class Bounds(TypedDict):
|
|
x: float
|
|
y: float
|
|
width: float
|
|
height: float
|
|
|
|
|
|
class WidgetPlacement(TypedDict):
|
|
x: float
|
|
y: float
|
|
enabled: bool
|
|
size: NotRequired[int]
|
|
width: NotRequired[int]
|
|
height: NotRequired[int]
|
|
opacity: NotRequired[int]
|
|
|
|
|
|
class Widget(TypedDict):
|
|
label: str
|
|
kind: str
|
|
width: float
|
|
height: float
|
|
default: WidgetPlacement
|
|
bounds: NotRequired[Bounds]
|
|
visualInsetTop: NotRequired[float]
|
|
visualInsetLeft: NotRequired[float]
|
|
layer: NotRequired[str]
|
|
note: NotRequired[str]
|
|
defaultAnchor: NotRequired[str]
|
|
colors: NotRequired[dict[str, str | None]]
|
|
resizable: NotRequired[dict[str, int]]
|
|
sourcesExtent: NotRequired[dict[str, float]]
|
|
confirmation: NotRequired[Bounds]
|
|
confirmationObstacle: NotRequired[Bounds]
|
|
box: NotRequired[dict[str, int]]
|
|
opacity: NotRequired[dict[str, int]]
|
|
anchorShift: NotRequired[float]
|
|
expandable: NotRequired[bool]
|
|
|
|
|
|
class LayoutProfile(TypedDict):
|
|
label: str
|
|
width: float
|
|
height: float
|
|
bounds: Bounds
|
|
widgets: dict[str, Widget]
|
|
reservedZones: list[Bounds]
|
|
protectedWidget: NotRequired[str]
|
|
inputZones: NotRequired[list[dict[str, Any]]]
|
|
inputZonePriority: NotRequired[str]
|
|
widgetOrder: NotRequired[list[str]]
|
|
runtimePreviewWidgets: NotRequired[list[str]]
|
|
|
|
|
|
def _widget(label: str, kind: str, width: float, height: float, x: float, y: float) -> Widget:
|
|
return {"label": label, "kind": kind, "width": width, "height": height,
|
|
"default": {"x": x, "y": y, "enabled": True}}
|
|
|
|
|
|
PROFILES: dict[str, LayoutProfile] = {
|
|
"large": {"label": "Large UI", "width": 2160, "height": 1080,
|
|
"bounds": {"x": 30, "y": 30, "width": 1800, "height": 1020},
|
|
"reservedZones": [],
|
|
"widgets": {
|
|
"current_speed": _widget("Current speed", "current_speed", 580, 300, 640, 30),
|
|
"cruise_limits": _widget("Cruise and speed limit", "cruise_limits", WIDTH, CARD_HEIGHT, 88, 75),
|
|
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", ACTION_WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y),
|
|
"steering_wheel": _widget("Steering wheel", "steering_wheel", 192, 192, 1588, 75),
|
|
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 128, 128, 120, 840)}},
|
|
"compact": {"label": "Small UI", "width": 536, "height": 240,
|
|
"bounds": {"x": 0, "y": 0, "width": 476, "height": 240},
|
|
"protectedWidget": "speed_limit_actions",
|
|
"reservedZones": [],
|
|
"widgets": {
|
|
"max_speed": _widget("Set speed", "max_speed", 146, 162, 0, 0),
|
|
"speed_limit": _widget("Speed limit", "speed_limit", 118, 132, 330, 20),
|
|
"speed_limit_actions": _widget("Speed limit actions", "speed_limit_actions", 184, 120, 146, 44),
|
|
"steering_wheel": _widget("Steering wheel", "steering_wheel", 50, 50, 21, 176),
|
|
"driver_monitor": _widget("Driver monitoring", "driver_monitor", 60, 60, 16, 10)}}}
|
|
|
|
for _profile in PROFILES:
|
|
PROFILES[_profile]['widgets']['speed_limit_actions']['note'] = (
|
|
'Accept / Reject appear inside a pending limit after tapping it. This placement controls Toyota guidance and re-adoption.')
|
|
_owner = 'cruise_limits' if _profile == 'large' else 'speed_limit'
|
|
PROFILES[_profile]['widgets'][_owner]['confirmation'] = confirmation_footprint(_profile)
|
|
|
|
SPEED_LAYOUT_V7 = {
|
|
'large': {'cruise_limits': (320, 210, 88, 75), 'speed_limit_actions': (320, 108, 88, 307)},
|
|
'compact': {'max_speed': (162, 162, 0, 0), 'speed_limit': (118, 132, 174, 12),
|
|
'speed_limit_actions': (282, 54, 174, 180)},
|
|
}
|
|
COMPACT_SPEED_LAYOUT_V8 = {
|
|
'max_speed': (146, 114, 0, 0), 'speed_limit': (264, 82, 158, 18),
|
|
'speed_limit_actions': (440, 52, 16, 122),
|
|
}
|
|
|
|
for profile, sizes in DM_SIZES.items():
|
|
PROFILES[profile]['widgets']['driver_monitor']['default']['size'] = sizes[1]
|
|
|
|
LEGACY_WIDGETS = {"large": {"current_speed", "cruise_limits", "steering_wheel"},
|
|
"compact": {"max_speed", "speed_limit", "steering_wheel"}}
|
|
DM_WIDGETS = {profile: keys | {"driver_monitor"} for profile, keys in LEGACY_WIDGETS.items()}
|
|
for _profile, _data in PROFILES.items():
|
|
_bounds = _data["bounds"]
|
|
_x, _y, _width, _height = maximum_footprint(_bounds["x"], _bounds["y"], _bounds["width"], _bounds["height"], _data["width"])
|
|
_data["widgets"]["torque_bar"] = {
|
|
**_widget("Torque bar", "torque_bar", _width, _height, _x, _y), "layer": "underlay",
|
|
"note": "Torque output, or steering-effort estimate on angle-controlled vehicles"}
|
|
ACTIONLESS_WIDGETS = {profile: set(data["widgets"]) - {"speed_limit_actions"} for profile, data in PROFILES.items()}
|
|
|
|
RAILLESS_WIDGETS = {profile: set(data["widgets"]) for profile, data in PROFILES.items()}
|
|
RAIL_WIDGETS = ("model_confidence", "conditional_mode", "following_distance")
|
|
for _index, (_key, _label) in enumerate(zip(RAIL_WIDGETS, ("Model confidence", "CEM and driving mode", "Following distance"), strict=True)):
|
|
PROFILES["compact"]["widgets"][_key] = {
|
|
**_widget(_label, _key, 60, 80, 476, _index * 80),
|
|
"bounds": {"x": 0, "y": 0, "width": 536, "height": 240},
|
|
}
|
|
|
|
# Keep layouts saved before camera widgets valid when the registry grows.
|
|
CAMERALESS_WIDGETS = {profile: set(data["widgets"]) for profile, data in PROFILES.items()}
|
|
CAMERA_WIDGETS = ("pip_left", "pip_right")
|
|
for _profile, (_size, _margin) in {"large": (600, 24), "compact": (80, 16)}.items():
|
|
_bounds = PROFILES[_profile]["bounds"]
|
|
for _side, _key in zip(("left", "right"), CAMERA_WIDGETS, strict=True):
|
|
_x = _bounds["x"] + (_margin if _side == "left" else _bounds["width"] - _margin - _size)
|
|
_y = _bounds["y"] + _bounds["height"] - _margin - _size
|
|
PROFILES[_profile]["widgets"][_key] = {
|
|
**_widget(f"PiP {_side} side camera", "pip_camera", _size, _size, _x, _y), "layer": "underlay"}
|
|
|
|
MODE_WIDGET = "driving_mode_descriptions"
|
|
for _profile, _geometry in {"large": (580, 68, 640, 344), "compact": (168, 44, 154, 0)}.items():
|
|
_mode_widget = _widget("Driving Mode Descriptions", MODE_WIDGET, *_geometry)
|
|
_mode_widget["default"]["enabled"] = False
|
|
PROFILES[_profile]["widgets"][MODE_WIDGET] = _mode_widget
|
|
|
|
# It starts disabled so an upgrade never adds a new onroad element without the
|
|
# driver's choice; validation below adds the placement to older documents.
|
|
CLOCK_WIDGET = "clock"
|
|
for _profile, _geometry in {"large": (300, 72, 780, 430), "compact": (118, 20, 330, 3)}.items():
|
|
_clock_widget = _widget("Clock", CLOCK_WIDGET, *_geometry)
|
|
_clock_widget["default"]["enabled"] = False
|
|
PROFILES[_profile]["widgets"][CLOCK_WIDGET] = _clock_widget
|
|
|
|
STOPPED_TIMER_WIDGET = "stopped_timer"
|
|
for _profile, (_x, _y) in {"compact": (18, 48), "large": (600, 520)}.items():
|
|
_timer_widget = _widget("Stopped timer", STOPPED_TIMER_WIDGET, *timer_size(_profile), _x, _y)
|
|
_timer_widget["default"]["enabled"] = False
|
|
_timer_widget["note"] = "Counts how long you have been stopped, after the first minute of the drive"
|
|
PROFILES[_profile]["widgets"][STOPPED_TIMER_WIDGET] = _timer_widget
|
|
|
|
NAV_CARD_WIDGET, NAV_MAP_WIDGET = "nav_card", "nav_map"
|
|
NAV_CARD_SIZE = (560, 195)
|
|
MAP_MIN_SIZE = (280, 200)
|
|
MAP_OPACITY = (15, 100, 50) # percent: min, max, default
|
|
_nav_card = _widget("Turn-by-turn", NAV_CARD_WIDGET, *NAV_CARD_SIZE, 1230, 415)
|
|
_nav_card["note"] = "The next maneuver while a route is active"
|
|
PROFILES["large"]["widgets"][NAV_CARD_WIDGET] = _nav_card
|
|
_nav_map = _widget("Map overlay", NAV_MAP_WIDGET, 560, 420, 1230, 625)
|
|
_nav_map["default"].update(enabled=False, width=560, height=420, opacity=MAP_OPACITY[2])
|
|
_nav_map["box"] = {"minWidth": MAP_MIN_SIZE[0], "maxWidth": 1800, "minHeight": MAP_MIN_SIZE[1], "maxHeight": 1020}
|
|
_nav_map["opacity"] = dict(zip(("min", "max", "default"), MAP_OPACITY, strict=True))
|
|
_nav_map["note"] = ("Road lines and your route over the camera. Drag the corner to resize; opacity sets how much shows. "
|
|
+ "Turning it on here turns it off in the other screen's layout.")
|
|
PROFILES["large"]["widgets"][NAV_MAP_WIDGET] = _nav_map
|
|
|
|
JETLINK_WIDGET = "jetlink_status"
|
|
JETLINK_SIZE = (420, 84)
|
|
JETLINK_OPACITY = (0, 100, 85) # percent: min, max, default
|
|
_jetlink = _widget("Jetlink status", JETLINK_WIDGET, *JETLINK_SIZE, 30, 650)
|
|
_jetlink["default"].update(enabled=False, opacity=JETLINK_OPACITY[2])
|
|
_jetlink["opacity"] = dict(zip(("min", "max", "default"), JETLINK_OPACITY, strict=True))
|
|
_jetlink["note"] = "Shown while Jetlink is on: connection, device and host processing time. Opacity sets the card background."
|
|
PROFILES["large"]["widgets"][JETLINK_WIDGET] = _jetlink
|
|
# Restore the Dom road graphic in the gap between the speed card and main speed.
|
|
# Center the road gauge above the driver monitor, below the speed-limit controls.
|
|
# It has no background and appears only with a current selected source.
|
|
CEM_GAUGE_WIDGET = 'cem_gauge'
|
|
# The gauge draws centered in a 344 x 300 frame; its box is the part it can actually fill, at this offset inside
|
|
# that frame. Version 6 and older layouts saved the frame's corner and move by this offset when they load.
|
|
CEM_GAUGE_FRAME = (344, 300)
|
|
CEM_GAUGE_INSET = (44, 24)
|
|
CEM_GAUGE_SIZE = (256, 276)
|
|
PROFILES['large']['widgets'][CEM_GAUGE_WIDGET] = _widget('CEM road gauge', CEM_GAUGE_WIDGET, *CEM_GAUGE_SIZE,
|
|
56, 536)
|
|
ROAD_BASE_WIDTH = 1860
|
|
SIDEBAR_EXTRA = 300
|
|
ANCHOR_SHIFT = {"steering_wheel": 1.0, NAV_CARD_WIDGET: 1.0, NAV_MAP_WIDGET: 1.0,
|
|
"current_speed": .5, "torque_bar": .5, MODE_WIDGET: .5, CLOCK_WIDGET: .5}
|
|
for _key, _widget_data in PROFILES["large"]["widgets"].items():
|
|
if _key in ANCHOR_SHIFT:
|
|
_widget_data["anchorShift"] = ANCHOR_SHIFT[_key]
|
|
_base = PROFILES["large"]["bounds"]
|
|
_widget_data["bounds"] = {**_base, "x": _base["x"] - ANCHOR_SHIFT.get(_key, 0) * SIDEBAR_EXTRA, "width": _base["width"] + SIDEBAR_EXTRA}
|
|
_widget_data["expandable"] = True
|
|
# Older documents retain every saved placement and gain this independent widget.
|
|
OPTIONAL_WIDGETS = frozenset({MODE_WIDGET, CLOCK_WIDGET, STOPPED_TIMER_WIDGET, CEM_GAUGE_WIDGET, NAV_CARD_WIDGET, NAV_MAP_WIDGET, JETLINK_WIDGET})
|
|
|
|
# The torque arc grows when the developer sidebar closes; protect both road widths.
|
|
_torque = PROFILES['large']['widgets']['torque_bar']
|
|
_bounds = PROFILES['large']['bounds']
|
|
_x, _y, _w, _h = maximum_footprint(_bounds['x'], _bounds['y'], _bounds['width'] + SIDEBAR_EXTRA, _bounds['height'], 2160)
|
|
_left, _top = min(_x, _torque['default']['x']), min(_y, _torque['default']['y'])
|
|
_torque['confirmationObstacle'] = {
|
|
'x': _left - _torque['default']['x'], 'y': _top - _torque['default']['y'],
|
|
'width': max(_x + _w, _torque['default']['x'] + _torque['width']) - _left,
|
|
'height': max(_y + _h, _torque['default']['y'] + _torque['height']) - _top}
|
|
|
|
# These editor sprites use the runtime components and their canonical assets/fonts.
|
|
RUNTIME_PREVIEW_WIDGETS = {
|
|
"large": ("current_speed", "cruise_limits", CEM_GAUGE_WIDGET, "steering_wheel", "driver_monitor", "speed_limit_actions"),
|
|
"compact": ("max_speed", "speed_limit", "steering_wheel", "conditional_mode", "following_distance",
|
|
"model_confidence", "driver_monitor", "speed_limit_actions"),
|
|
}
|
|
|
|
_FRAME_COLORS = {"cardFill": "#00000000", "cardBorder": "#00000000"}
|
|
_ACTION_COLORS = {"cardFill": "#0C1820EB", "cardBorder": "#A6DFBEFF", "text": "#FFFFFFFF"}
|
|
WIDGET_COLORS = {
|
|
"large": {
|
|
MODE_WIDGET: {"text": "#FFFFFFFF"},
|
|
CLOCK_WIDGET: {"text": "#FFFFFFFF"},
|
|
CEM_GAUGE_WIDGET: {},
|
|
"current_speed": {"text": None},
|
|
"cruise_limits": {"cardFill": "#10191FFF", "cardBorder": "#35434DFF", "text": None},
|
|
"speed_limit_actions": {"cardFill": "#10191FFF", "cardBorder": "#35434DFF", "text": "#F1F6F3FF"},
|
|
"steering_wheel": {"cardFill": None, "cardBorder": "#00000000"},
|
|
"driver_monitor": _FRAME_COLORS,
|
|
},
|
|
"compact": {
|
|
MODE_WIDGET: {"text": "#FFFFFFFF"},
|
|
CLOCK_WIDGET: {"text": "#FFFFFFFF"},
|
|
"max_speed": {"text": None},
|
|
"speed_limit_actions": {**_ACTION_COLORS, "text": "#F1F6F3FF"},
|
|
"driver_monitor": _FRAME_COLORS,
|
|
"model_confidence": _FRAME_COLORS,
|
|
"conditional_mode": _FRAME_COLORS,
|
|
"following_distance": {**_FRAME_COLORS, "text": "#FFFFFFFF"},
|
|
},
|
|
}
|
|
|
|
|
|
# Back-to-front order used when a layout first opts into custom layers.
|
|
DEFAULT_WIDGET_ORDER = {
|
|
"large": (NAV_MAP_WIDGET, "pip_left", "pip_right", "cruise_limits", "speed_limit_actions", CEM_GAUGE_WIDGET, "current_speed",
|
|
"steering_wheel", "torque_bar", "driver_monitor", MODE_WIDGET, CLOCK_WIDGET, STOPPED_TIMER_WIDGET, NAV_CARD_WIDGET, JETLINK_WIDGET),
|
|
"compact": ("pip_left", "pip_right", "speed_limit", "max_speed", "steering_wheel", "torque_bar",
|
|
"speed_limit_actions", "driver_monitor", *RAIL_WIDGETS, MODE_WIDGET, CLOCK_WIDGET, STOPPED_TIMER_WIDGET),
|
|
}
|
|
|
|
|
|
def validate_widget_order(order, widgets):
|
|
if (type(order) is not list or any(type(key) is not str for key in order) or
|
|
len(order) != len(widgets) or set(order) != set(widgets)):
|
|
raise ValueError("Invalid widget layer order")
|
|
return list(order)
|
|
|
|
|
|
def widget_order(document, profile):
|
|
return document.get("widgetOrder", {}).get(str(profile), DEFAULT_WIDGET_ORDER[str(profile)])
|
|
|
|
|
|
def widget_bounds(profile, widget):
|
|
return PROFILES[profile]["widgets"][widget].get("bounds", PROFILES[profile]["bounds"])
|
|
|
|
|
|
def customization_metadata():
|
|
profiles = copy.deepcopy(PROFILES)
|
|
profiles["large"]["widgets"]["driver_monitor"]["defaultAnchor"] = "driver_side"
|
|
# The editor grows the cruise box by these while speed-limit sources are shown.
|
|
profiles["large"]["widgets"]["cruise_limits"]["sourcesExtent"] = {
|
|
"inline": sources_extent({'speedSources': True}), "card": sources_extent({'speedSources': True, 'speedSourcesCard': True}), 'actionGap': GAP}
|
|
profiles["large"]["inputZones"] = [{"id": "favorites_drawer", "label": "Quick Select drawer",
|
|
"x": 30, "y": 900, "width": 150, "height": 150}]
|
|
profiles["compact"]["inputZones"] = [
|
|
{"id": f"favorite_{index + 1}", "label": f"Quick Select {index + 1}",
|
|
"x": index * 476 / 3, "y": 0, "width": 476 / 3, "height": 240} for index in range(3)]
|
|
for profile in profiles.values():
|
|
profile["inputZonePriority"] = "Enabled Quick Select slots; speed-limit controls and steering wheel take priority"
|
|
for name, profile in profiles.items():
|
|
for key, widget in profile["widgets"].items():
|
|
widget["colors"] = dict(WIDGET_COLORS[name].get(key, {}))
|
|
for key, sizes in WIDGET_SIZES.items():
|
|
minimum, default, maximum = sizes[name]
|
|
profile["widgets"][key]["resizable"] = {"min": minimum, "default": default, "max": maximum}
|
|
profile["widgets"][key]["default"]["size"] = default
|
|
for profile, data in profiles.items():
|
|
data["widgetOrder"] = list(DEFAULT_WIDGET_ORDER[profile])
|
|
data["runtimePreviewWidgets"] = list(RUNTIME_PREVIEW_WIDGETS[profile])
|
|
return {"profiles": profiles, "roadColorFields": [
|
|
{"id": key, "label": label, "default": ROAD_COLORS[key]}
|
|
for key, label in (("path", "Path"), ("pathEdge", "Path edges"), ("laneLines", "Outer lane lines"))], "paletteFields": [
|
|
{"id": key, "label": label, "default": PALETTE[key]}
|
|
for key, label in (("cardFill", "Card fill"), ("cardBorder", "Card border"), ("text", "Text"))]}
|
|
|
|
|
|
def default_document():
|
|
return {"version": 12, "clock24Hour": False, "palette": dict(PALETTE), "roadColors": {profile: {} for profile in PROFILES},
|
|
"widgetColors": {profile: {} for profile in PROFILES}, "layouts": {
|
|
profile: {key: {**widget["default"], **({"size": WIDGET_SIZES[key][profile][1]} if key in WIDGET_SIZES else {})}
|
|
for key, widget in data["widgets"].items()}
|
|
for profile, data in PROFILES.items()}}
|
|
|
|
|
|
def validate_map_box(placement, widget):
|
|
box, opacity = widget["box"], widget["opacity"]
|
|
if (type(placement["width"]) is not int or not box["minWidth"] <= placement["width"] <= box["maxWidth"] or
|
|
type(placement["height"]) is not int or not box["minHeight"] <= placement["height"] <= box["maxHeight"] or
|
|
type(placement["opacity"]) is not int or not opacity["min"] <= placement["opacity"] <= opacity["max"]):
|
|
raise ValueError("Invalid map overlay size or opacity")
|
|
|
|
|
|
def migrate_speed_layout(layout, profile, bounds, old_gap=232, *, previous=None, widgets=None):
|
|
widgets = PROFILES[profile]['widgets'] if widgets is None else widgets
|
|
if profile == 'large':
|
|
card, actions = layout['cruise_limits'], layout['speed_limit_actions']
|
|
if actions['x'] == card['x'] and actions['y'] - card['y'] == old_gap:
|
|
actions.update(x=card['x'] + ACTION_X - 88, y=card['y'] + ACTION_Y - 75)
|
|
gauge = layout[CEM_GAUGE_WIDGET]
|
|
if (gauge['x'], gauge['y']) == (480, 99):
|
|
default = widgets[CEM_GAUGE_WIDGET]['default']
|
|
gauge.update(x=default['x'], y=default['y'])
|
|
for key, (_, _, old_x, old_y) in (previous or SPEED_LAYOUT_V7[profile]).items():
|
|
saved, widget = layout[key], widgets[key]
|
|
area = widget.get('bounds', bounds)
|
|
if (saved['x'], saved['y']) == (old_x, old_y):
|
|
saved.update(x=widget['default']['x'], y=widget['default']['y'])
|
|
for axis, extent in (('x', 'width'), ('y', 'height')):
|
|
saved[axis] = max(area[axis], min(saved[axis], area[axis] + area[extent] - widget[extent]))
|
|
|
|
|
|
def _migrate_action_collisions(layout):
|
|
actions = layout['speed_limit_actions']
|
|
area = PROFILES['compact']['widgets']['speed_limit_actions']
|
|
# Move only placements made invalid by the new protected action area.
|
|
for key, height in _compact_action_collisions(layout):
|
|
saved, widget = layout[key], PROFILES['compact']['widgets'][key]
|
|
width = saved['size'] if key in WIDGET_SIZES else widget['width']
|
|
candidates = ((actions['x'] - width, saved['y']), (actions['x'] + area['width'], saved['y']),
|
|
(saved['x'], actions['y'] - height), (saved['x'], actions['y'] + area['height']))
|
|
valid = [(x, y) for x, y in candidates if 0 <= x <= 476 - width and 0 <= y <= 240 - height]
|
|
if valid:
|
|
x, y = min(valid, key=lambda xy: (xy[0] - saved['x']) ** 2 + (xy[1] - saved['y']) ** 2)
|
|
saved.update(x=x, y=y)
|
|
|
|
|
|
def _compact_action_collisions(layout):
|
|
widgets = PROFILES['compact']['widgets']
|
|
actions, area = layout['speed_limit_actions'], widgets['speed_limit_actions']
|
|
for key, widget in widgets.items():
|
|
saved = layout[key]
|
|
if (not saved['enabled'] or key in ('speed_limit_actions', 'speed_limit', 'stopped_timer') or widget.get('layer') == 'underlay'):
|
|
continue
|
|
width, height = (saved['size'],) * 2 if key in WIDGET_SIZES else (widget['width'], widget['height'])
|
|
if (saved['x'] < actions['x'] + area['width'] and saved['x'] + width > actions['x'] and
|
|
saved['y'] < actions['y'] + area['height'] and saved['y'] + height > actions['y']):
|
|
yield key, height
|
|
|
|
|
|
def migrate_large_sign_layout(layout, bounds, *, widgets=None, extra_height=0):
|
|
widgets = PROFILES['large']['widgets'] if widgets is None else widgets
|
|
card, actions = layout['cruise_limits'], layout['speed_limit_actions']
|
|
# Only the old attached arrangement follows the sign; independently placed actions stay put.
|
|
paired = actions['x'] == card['x'] + 12 and actions['y'] == card['y'] + 173
|
|
card['y'] = min(card['y'], bounds['y'] + bounds['height'] - widgets['cruise_limits']['height'] - extra_height)
|
|
if paired:
|
|
actions['x'] = card['x'] + ACTION_X - 88
|
|
actions['y'] = min(card['y'] + ACTION_Y - 75,
|
|
bounds['y'] + bounds['height'] - widgets['speed_limit_actions']['height'])
|
|
|
|
|
|
def place_attached_slc_actions(layout, bounds, extra_height):
|
|
card, actions = layout['cruise_limits'], layout['speed_limit_actions']
|
|
if (actions['x'], actions['y']) == (card['x'] + ACTION_X - 88, card['y'] + ACTION_Y - 75):
|
|
# Save the actual position so drawing, editor crops and input share coordinates.
|
|
dy = CARD_HEIGHT + extra_height + GAP if extra_height else ACTION_Y - 75
|
|
card['y'] = min(card['y'], bounds['y'] + bounds['height'] - ACTION_HEIGHT - dy)
|
|
actions['y'] = card['y'] + dy
|
|
|
|
|
|
def confirmation_layout_valid(layout, profile, widgets, bounds, *, extra_height=0):
|
|
owner = 'cruise_limits' if profile == 'large' else 'speed_limit'
|
|
bounds = widgets[owner].get('bounds', bounds)
|
|
at = layout[owner]
|
|
if not at['enabled'] or not layout['speed_limit_actions']['enabled']:
|
|
return True
|
|
left, top, right, bottom = confirmation_bounds(profile, at['x'], at['y'], opened=True)
|
|
if extra_height:
|
|
left, right = min(left, at['x']), max(right, at['x'] + widgets[owner]['width'])
|
|
bottom = max(bottom, at['y'] + widgets[owner]['height'] + extra_height)
|
|
if left < bounds['x'] or top < bounds['y'] or right > bounds['x'] + bounds['width'] or bottom > bounds['y'] + bounds['height']:
|
|
return False
|
|
for key in (('torque_bar',) if profile == 'large' else ('max_speed', 'torque_bar')):
|
|
other, area = layout[key], widgets[key]
|
|
area = area.get('confirmationObstacle', {'x': 0, 'y': 0, 'width': area['width'], 'height': area['height']})
|
|
ox, oy = other['x'] + area['x'], other['y'] + area['y']
|
|
if (other['enabled'] and left < ox + area['width'] and ox < right and top < oy + area['height'] and oy < bottom):
|
|
return False
|
|
return True
|
|
|
|
|
|
def migrate_confirmation_layout(layout, profile, widgets, bounds, *, extra_height=0):
|
|
owner = 'cruise_limits' if profile == 'large' else 'speed_limit'
|
|
bounds = widgets[owner].get('bounds', bounds)
|
|
area, at = confirmation_footprint(profile), layout[owner]
|
|
if not at['enabled'] or not layout['speed_limit_actions']['enabled']:
|
|
return
|
|
actions = layout['speed_limit_actions']
|
|
attached = profile == 'large' and actions['x'] == at['x'] + ACTION_X - 88 and actions['y'] == at['y'] + ACTION_Y - 75
|
|
for axis, extent in (('x', 'width'), ('y', 'height')):
|
|
low = max(bounds[axis], bounds[axis] - area[axis])
|
|
high = min(bounds[axis] + bounds[extent] - widgets[owner][extent],
|
|
bounds[axis] + bounds[extent] - area[axis] - area[extent])
|
|
if axis == 'y':
|
|
high = min(high, bounds['y'] + bounds['height'] - widgets[owner]['height'] - extra_height)
|
|
at[axis] = max(math.ceil(low), min(at[axis], math.floor(high)))
|
|
if not confirmation_layout_valid(layout, profile, widgets, bounds, extra_height=extra_height):
|
|
# Preserve independently placed guidance; only the speed cluster needs room for the chooser.
|
|
at.update(x=widgets[owner]['default']['x'], y=widgets[owner]['default']['y'])
|
|
if not confirmation_layout_valid(layout, profile, widgets, bounds, extra_height=extra_height):
|
|
for key in (('torque_bar',) if profile == 'large' else ('max_speed', 'torque_bar')):
|
|
layout[key].update(x=widgets[key]['default']['x'], y=widgets[key]['default']['y'])
|
|
if attached:
|
|
actions.update(x=at['x'] + ACTION_X - 88, y=at['y'] + ACTION_Y - 75)
|
|
|
|
|
|
def validate_document(value):
|
|
if type(value) is not dict or type(value.get("version")) is not int or value["version"] not in range(1, 13):
|
|
raise ValueError("Invalid customization version")
|
|
fields = {"version", "palette", "layouts"} | ({"widgetColors"} if value["version"] >= 2 else set())
|
|
if value["version"] >= 3:
|
|
fields.add("roadColors")
|
|
if value['version'] >= 4 and 'speedSources' in value:
|
|
fields.add('speedSources')
|
|
if type(value['speedSources']) is not bool:
|
|
raise ValueError('Invalid speed source drawer preference')
|
|
if value['version'] >= 4 and 'speedSourcesCard' in value:
|
|
fields.add('speedSourcesCard')
|
|
if type(value['speedSourcesCard']) is not bool:
|
|
raise ValueError('Invalid speed source card preference')
|
|
if value['version'] >= 4 and 'speedAlign' in value:
|
|
fields.add('speedAlign')
|
|
if value['speedAlign'] not in SPEED_ALIGNMENTS:
|
|
raise ValueError('Invalid speed card alignment')
|
|
if value['version'] >= 4 and 'widgetOrder' in value:
|
|
fields.add('widgetOrder')
|
|
if 'clock24Hour' in value:
|
|
fields.add('clock24Hour')
|
|
if type(value['clock24Hour']) is not bool:
|
|
raise ValueError('Invalid clock format preference')
|
|
if 'showMaxSpeed' in value:
|
|
fields.add('showMaxSpeed')
|
|
if type(value['showMaxSpeed']) is not bool:
|
|
raise ValueError('Invalid MAX speed preference')
|
|
if set(value) != fields:
|
|
raise ValueError("Invalid customization fields")
|
|
palette, layouts = value["palette"], value["layouts"]
|
|
if type(palette) is not dict or set(palette) != set(PALETTE) or type(layouts) is not dict or set(layouts) != set(PROFILES):
|
|
raise ValueError("Invalid customization fields")
|
|
if any(all(type(layouts[profile]) is dict and
|
|
(set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS, *CAMERA_WIDGETS}) == keys
|
|
for profile, keys in shape.items())
|
|
for shape in (LEGACY_WIDGETS, DM_WIDGETS, ACTIONLESS_WIDGETS)):
|
|
layouts = copy.deepcopy(layouts)
|
|
cruise = layouts["large"]["cruise_limits"]
|
|
if (type(cruise) is not dict or set(cruise) != {"x", "y", "enabled"} or type(cruise["enabled"]) is not bool or
|
|
any(type(cruise[axis]) not in (int, float) for axis in ("x", "y"))):
|
|
raise ValueError("Invalid widget placement")
|
|
try:
|
|
if not all(math.isfinite(cruise[axis]) for axis in ("x", "y")):
|
|
raise ValueError("Invalid widget placement")
|
|
except OverflowError as exc:
|
|
raise ValueError("Invalid widget placement") from exc
|
|
cruise_default = PROFILES["large"]["widgets"]["cruise_limits"]["default"]
|
|
actions_default = ({'x': 88, 'y': 500 if value['version'] < 5 else 447 if value['version'] < 6 else 307} if value['version'] < 8 else
|
|
PROFILES["large"]["widgets"]["speed_limit_actions"]["default"])
|
|
layouts["large"]["speed_limit_actions"] = {
|
|
"x": actions_default["x"] + cruise["x"] - cruise_default["x"],
|
|
"y": actions_default["y"] + cruise["y"] - cruise_default["y"],
|
|
"enabled": cruise["enabled"],
|
|
}
|
|
if value['version'] == 8 and 'speed_limit_actions' not in layouts['compact']:
|
|
_, _, x, y = COMPACT_SPEED_LAYOUT_V8['speed_limit_actions']
|
|
layouts['compact']['speed_limit_actions'] = {'x': x, 'y': y, 'enabled': True}
|
|
for profile in PROFILES:
|
|
for key in ("driver_monitor", "torque_bar", "speed_limit_actions"):
|
|
layouts[profile].setdefault(key, dict(PROFILES[profile]["widgets"][key]["default"]))
|
|
if all(type(layouts[profile]) is dict and (set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS, *CAMERA_WIDGETS}) == keys
|
|
for profile, keys in RAILLESS_WIDGETS.items()):
|
|
layouts = copy.deepcopy(layouts)
|
|
for key in RAIL_WIDGETS:
|
|
layouts["compact"][key] = dict(PROFILES["compact"]["widgets"][key]["default"])
|
|
if all(type(layouts[profile]) is dict and (set(layouts[profile]) - {"vasm", *OPTIONAL_WIDGETS}) == keys
|
|
for profile, keys in CAMERALESS_WIDGETS.items()):
|
|
layouts = copy.deepcopy(layouts)
|
|
for profile in PROFILES:
|
|
has_legacy = "vasm" in layouts[profile]
|
|
legacy = layouts[profile].pop("vasm", None)
|
|
if has_legacy and (type(legacy) is not dict or set(legacy) != {"x", "y", "enabled"} or
|
|
type(legacy["enabled"]) is not bool or
|
|
any(type(legacy[axis]) not in (int, float) or not math.isfinite(legacy[axis]) for axis in ("x", "y"))):
|
|
raise ValueError("Invalid legacy camera placement")
|
|
for side, key in enumerate(CAMERA_WIDGETS):
|
|
widget = PROFILES[profile]["widgets"][key]
|
|
position = dict(widget["default"])
|
|
if legacy is not None:
|
|
bounds = widget_bounds(profile, key)
|
|
position.update(enabled=legacy["enabled"],
|
|
x=max(bounds["x"], min(legacy["x"], bounds["x"] + bounds["width"] - 2 * widget["width"])) + side * widget["width"],
|
|
y=max(bounds["y"], min(legacy["y"], bounds["y"] + bounds["height"] - widget["height"])))
|
|
layouts[profile][key] = position
|
|
layouts = copy.deepcopy(layouts)
|
|
for profile, data in PROFILES.items():
|
|
if type(layouts[profile]) is dict:
|
|
monitor = layouts[profile].get('driver_monitor')
|
|
if type(monitor) is dict and 'size' not in monitor:
|
|
# Tighten the old 192px box around its 128px icon without moving the icon.
|
|
if profile == 'large':
|
|
for axis in ('x', 'y'):
|
|
if type(monitor.get(axis)) in (int, float):
|
|
monitor[axis] += 32
|
|
monitor['size'] = DM_SIZES[profile][1]
|
|
missing = set(data["widgets"]) - set(layouts[profile])
|
|
if missing and missing <= OPTIONAL_WIDGETS:
|
|
for optional in missing:
|
|
layouts[profile][optional] = dict(data["widgets"][optional]["default"])
|
|
if value['version'] < 11:
|
|
old_defaults = {'large': {'speed_limit_actions': (100, 296), CEM_GAUGE_WIDGET: (56, 452)},
|
|
'compact': {'speed_limit_actions': (128, 186), 'clock': (168, 92), MODE_WIDGET: (162, 22)}}
|
|
result = default_document()
|
|
for key, color in palette.items():
|
|
if type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None:
|
|
raise ValueError("Colors must be #RRGGBBAA")
|
|
result["palette"][key] = color.upper()
|
|
if value["version"] >= 2:
|
|
colors = value["widgetColors"]
|
|
if type(colors) is not dict or set(colors) != set(PROFILES):
|
|
raise ValueError("Invalid widget color profiles")
|
|
for profile, widgets in colors.items():
|
|
if type(widgets) is not dict or not widgets.keys() <= WIDGET_COLORS[profile].keys():
|
|
raise ValueError("Invalid widget color IDs")
|
|
for key, overrides in widgets.items():
|
|
if type(overrides) is not dict or not overrides.keys() <= WIDGET_COLORS[profile][key].keys():
|
|
raise ValueError("Invalid widget color fields")
|
|
if any(type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None for color in overrides.values()):
|
|
raise ValueError("Colors must be #RRGGBBAA")
|
|
if overrides:
|
|
result["widgetColors"][profile][key] = {field: color.upper() for field, color in overrides.items()}
|
|
if value["version"] >= 3:
|
|
colors = value["roadColors"]
|
|
if type(colors) is not dict or set(colors) != set(PROFILES):
|
|
raise ValueError("Invalid road color profiles")
|
|
for profile, overrides in colors.items():
|
|
if type(overrides) is not dict or not overrides.keys() <= ROAD_COLORS.keys() | {"pathMode"}:
|
|
raise ValueError("Invalid road color fields")
|
|
for key, color in overrides.items():
|
|
if key == "pathMode":
|
|
if type(color) is not str or color not in PATH_MODES:
|
|
raise ValueError("Invalid path color mode")
|
|
elif type(color) is not str or re.fullmatch(r"#[0-9a-fA-F]{8}", color) is None:
|
|
raise ValueError("Colors must be #RRGGBBAA")
|
|
result["roadColors"][profile][key] = color if key == "pathMode" else color.upper()
|
|
for profile, data in PROFILES.items():
|
|
layout = layouts[profile]
|
|
if type(layout) is not dict or set(layout) != set(data["widgets"]):
|
|
raise ValueError("Invalid widget IDs")
|
|
for key, widget in data["widgets"].items():
|
|
placement = layout[key]
|
|
bounds = widget_bounds(profile, key)
|
|
if value['version'] < 8 and key in SPEED_LAYOUT_V7[profile]:
|
|
bounds = data['bounds']
|
|
scalable = key == 'driver_monitor' or key == 'steering_wheel' and value['version'] >= 4
|
|
expected = {"x", "y", "enabled"} | ({"size"} if scalable else set())
|
|
if widget.get("box"):
|
|
expected |= {"width", "height", "opacity"}
|
|
elif widget.get("opacity"):
|
|
expected.add("opacity")
|
|
if type(placement) is not dict or set(placement) != expected or type(placement["enabled"]) is not bool:
|
|
raise ValueError("Invalid widget placement")
|
|
if widget.get("box"):
|
|
validate_map_box(placement, widget)
|
|
elif widget.get("opacity"):
|
|
low, high = widget["opacity"]["min"], widget["opacity"]["max"]
|
|
if type(placement["opacity"]) is not int or not low <= placement["opacity"] <= high:
|
|
raise ValueError("Invalid widget opacity")
|
|
size = WHEEL_SIZES[profile][1]
|
|
if scalable:
|
|
size = placement["size"]
|
|
minimum, _, maximum = WIDGET_SIZES[key][profile]
|
|
if type(size) is not int or not minimum <= size <= maximum:
|
|
raise ValueError("Invalid widget size")
|
|
# Only a gauge the saved layout actually placed moves; one filled in from defaults is already in the new box.
|
|
saved_layout = value.get("layouts", {}).get(profile) if type(value.get("layouts")) is dict else None
|
|
old_gauge = key == CEM_GAUGE_WIDGET and value["version"] < 7 and type(saved_layout) is dict and key in saved_layout
|
|
for axis, extent in (("x", "width"), ("y", "height")):
|
|
number = placement[axis]
|
|
legacy = ({'cruise_limits': (176, 483), 'speed_limit_actions': (176, 58)} if value['version'] < 5 else
|
|
{'cruise_limits': (280, 412), 'speed_limit_actions': (280, 96)})
|
|
dimensions = legacy.get(key) if value['version'] < 6 and profile == 'large' else None
|
|
if dimensions is None and value['version'] < 8:
|
|
dimensions = SPEED_LAYOUT_V7[profile].get(key)
|
|
if value['version'] == 8 and profile == 'compact':
|
|
dimensions = COMPACT_SPEED_LAYOUT_V8.get(key)
|
|
if dimensions is None and value['version'] < 10 and profile == 'large' and key == 'cruise_limits':
|
|
dimensions = (WIDTH, 132)
|
|
if dimensions is None and value['version'] < 11 and key == 'speed_limit_actions':
|
|
default = widget['default']
|
|
current_default = (placement['x'], placement['y']) == (default['x'], default['y'])
|
|
if not current_default:
|
|
dimensions = (576, 148) if profile == 'large' else (348, 54)
|
|
dimension = (dimensions[0 if axis == 'x' else 1] if dimensions else
|
|
size if key in WIDGET_SIZES else placement[extent] if widget.get("box") else widget[extent])
|
|
if old_gauge:
|
|
dimension = CEM_GAUGE_FRAME[0 if axis == 'x' else 1]
|
|
if type(number) not in (int, float) or not math.isfinite(number) or not bounds[axis] <= number <= bounds[axis] + bounds[extent] - dimension:
|
|
raise ValueError("Widget outside profile bounds")
|
|
result["layouts"][profile][key] = {**placement, **({"size": size} if key in WIDGET_SIZES else {})}
|
|
if old_gauge:
|
|
result["layouts"][profile][key].update(x=placement["x"] + CEM_GAUGE_INSET[0], y=placement["y"] + CEM_GAUGE_INSET[1])
|
|
if value['version'] < 11:
|
|
for key, old in old_defaults[profile].items():
|
|
saved = result['layouts'][profile][key]
|
|
if (saved['x'], saved['y']) == old:
|
|
default = data['widgets'][key]['default']
|
|
saved.update(x=default['x'], y=default['y'])
|
|
if profile == "compact" and value['version'] >= 11 and any(_compact_action_collisions(result['layouts'][profile])):
|
|
raise ValueError("Widget overlaps protected speed limit actions")
|
|
if 'widgetOrder' in value:
|
|
orders = value['widgetOrder']
|
|
if type(orders) is not dict or not orders.keys() <= PROFILES.keys():
|
|
raise ValueError("Invalid widget layer profiles")
|
|
result['widgetOrder'] = {}
|
|
for profile, order in orders.items():
|
|
if type(order) is list:
|
|
order = [*order, *(key for key in PROFILES[profile]['widgets'] if key in OPTIONAL_WIDGETS and key not in order)]
|
|
result['widgetOrder'][profile] = validate_widget_order(order, PROFILES[profile]['widgets'])
|
|
if 'speedSources' in value:
|
|
result['speedSources'] = value['speedSources']
|
|
if 'speedSourcesCard' in value:
|
|
result['speedSourcesCard'] = value['speedSourcesCard']
|
|
if 'speedAlign' in value:
|
|
result['speedAlign'] = value['speedAlign']
|
|
if 'clock24Hour' in value:
|
|
result['clock24Hour'] = value['clock24Hour']
|
|
if 'showMaxSpeed' in value:
|
|
result['showMaxSpeed'] = value['showMaxSpeed']
|
|
if value['version'] < 10:
|
|
migrate_large_sign_layout(result['layouts']['large'], PROFILES['large']['bounds'], extra_height=sources_extent(result))
|
|
if value['version'] < 8:
|
|
if value['version'] < 5 and value['palette'] != PALETTE:
|
|
colors = result['widgetColors']['large'].setdefault('cruise_limits', {})
|
|
for key in ('cardFill', 'cardBorder'):
|
|
colors.setdefault(key, value['palette'][key])
|
|
old_gap = 425 if value['version'] < 5 else 372 if value['version'] < 6 else 232
|
|
for profile in PROFILES:
|
|
migrate_speed_layout(result['layouts'][profile], profile, PROFILES[profile]['bounds'], old_gap)
|
|
elif value['version'] == 8:
|
|
migrate_speed_layout(result['layouts']['compact'], 'compact', PROFILES['compact']['bounds'], previous=COMPACT_SPEED_LAYOUT_V8)
|
|
if value['version'] < 11:
|
|
for profile, data in PROFILES.items():
|
|
actions = result['layouts'][profile]['speed_limit_actions']
|
|
widget, bounds = data['widgets']['speed_limit_actions'], widget_bounds(profile, 'speed_limit_actions')
|
|
for axis, extent in (('x', 'width'), ('y', 'height')):
|
|
actions[axis] = min(actions[axis], bounds[axis] + bounds[extent] - widget[extent])
|
|
_migrate_action_collisions(result['layouts']['compact'])
|
|
for profile, data in PROFILES.items():
|
|
migrate_confirmation_layout(result['layouts'][profile], profile, data['widgets'], data['bounds'],
|
|
extra_height=sources_extent(result) if profile == 'large' else 0)
|
|
return validate_document(result)
|
|
for profile, data in PROFILES.items():
|
|
if value['version'] < 12:
|
|
if profile == 'large' and result.get('speedSources') is True:
|
|
card, actions = result['layouts'][profile]['cruise_limits'], result['layouts'][profile]['speed_limit_actions']
|
|
old_extra = SOURCE_ROW_HEIGHT + (GAP if result.get('speedSourcesCard') is True else 0)
|
|
if actions['x'] == card['x'] + ACTION_X - 88 and actions['y'] == card['y'] + ACTION_Y - 75 + old_extra:
|
|
actions['y'] = card['y'] + ACTION_Y - 75
|
|
migrate_confirmation_layout(result['layouts'][profile], profile, data['widgets'], data['bounds'],
|
|
extra_height=sources_extent(result) if profile == 'large' else 0)
|
|
elif not confirmation_layout_valid(result['layouts'][profile], profile, data['widgets'], data['bounds'],
|
|
extra_height=sources_extent(result) if profile == 'large' else 0):
|
|
raise ValueError('Speed-limit confirmation overlaps set speed, torque, or screen bounds')
|
|
place_attached_slc_actions(result['layouts']['large'], PROFILES['large']['bounds'], sources_extent(result))
|
|
if len(json.dumps(result, separators=(",", ":")).encode()) > MAX_BYTES:
|
|
raise ValueError("Customization too large")
|
|
return result
|
|
|
|
|
|
def _unique_object(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ValueError("Duplicate customization field")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def decode_document(raw):
|
|
return validate_document(json.loads(raw, object_pairs_hook=_unique_object))
|
|
|
|
|
|
def read_customization(params):
|
|
try:
|
|
raw, readable = read_saved(params, PARAM_KEY, MAX_BYTES)
|
|
return decode_document(raw) if readable and raw is not None else default_document()
|
|
except (AttributeError, OSError, TypeError, ValueError, UnicodeError, RecursionError):
|
|
return default_document()
|
|
|
|
|
|
def placement(document, profile, widget):
|
|
return document['layouts'][str(profile)][widget]
|
|
|
|
|
|
def fit_to_road(document, viewport_width):
|
|
"""The document with expandable Big-screen widgets kept inside the road that is showing."""
|
|
layout = document["layouts"].get("large")
|
|
if layout is None:
|
|
return document
|
|
extra, moved = viewport_width - ROAD_BASE_WIDTH, {}
|
|
for key, widget in PROFILES["large"]["widgets"].items():
|
|
placed = layout.get(key)
|
|
if placed is None or not widget.get("expandable"):
|
|
continue
|
|
shift = widget.get("anchorShift", 0) * extra
|
|
width = widget_size(document, "large", key)[0]
|
|
if (area := widget.get('confirmation')) and placed['enabled'] and layout['speed_limit_actions']['enabled']:
|
|
width = max(width, area['x'] + area['width'])
|
|
x = max(30 - shift, min(placed["x"], viewport_width - 30 - width - shift))
|
|
if x != placed["x"]:
|
|
moved[key] = {**placed, "x": x}
|
|
if not moved:
|
|
return document
|
|
return {**document, "layouts": {**document["layouts"], "large": {**layout, **moved}}}
|
|
|
|
|
|
def widget_size(document, profile, widget):
|
|
default = PROFILES[str(profile)]["widgets"][widget]
|
|
position = placement(document, profile, widget)
|
|
if widget == "cruise_limits":
|
|
return default["width"], default["height"] + sources_extent(document)
|
|
if default.get("box"):
|
|
return position["width"], position["height"]
|
|
return (position.get("size", WIDGET_SIZES[widget][str(profile)][1]),) * 2 if widget in WIDGET_SIZES else (default["width"], default["height"])
|
|
|
|
|
|
def offset(document, profile, widget):
|
|
position = placement(document, profile, widget)
|
|
default = PROFILES[str(profile)]["widgets"][widget]["default"]
|
|
return position["x"] - default["x"], position["y"] - default["y"]
|
|
|
|
|
|
def widget_palette(document, profile, widget):
|
|
defaults = WIDGET_COLORS[str(profile)].get(widget, {})
|
|
colors = {key: document["palette"][key] if value is None else value for key, value in defaults.items()}
|
|
colors.update(document.get("widgetColors", {}).get(str(profile), {}).get(widget, {}))
|
|
return colors
|
|
|
|
|
|
def rgba(document, key, profile=None, widget=None):
|
|
if profile is None:
|
|
color = document["palette"][key]
|
|
else:
|
|
assert widget is not None
|
|
default = WIDGET_COLORS[str(profile)][widget][key]
|
|
color = document.get("widgetColors", {}).get(str(profile), {}).get(widget, {}).get(key, default or document["palette"][key])
|
|
return _rgba_value(color)
|
|
|
|
|
|
@lru_cache(maxsize=256)
|
|
def _rgba_value(color):
|
|
return tuple(int(color[index:index + 2], 16) for index in (1, 3, 5, 7))
|