Skip to content
Merged
19 changes: 19 additions & 0 deletions OTAnalytics/application/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class CanvasKeys(ResourceKey):
LABEL_ADD_SECTION = "label-add-section"
LABEL_EDIT_SECTION = "label-edit-section"
IMAGE_DEFAULT = "image-default"
MESSAGE_NOT_ENOUGH_POINTS = "message-not-enough-points"


class ConfigurationBarKeys(ResourceKey):
Expand Down Expand Up @@ -144,6 +145,11 @@ class RemarkKeys(ResourceKey):
class HotKeys(ResourceKey):
SAVE_SECTION_HOTKEY = "save-section-hotkey"
CANCEL_SECTION_GEOMETRY_HOTKEY = "cancel-section-geometry-hotkey"
ADD_KNOB_HOTKEY = "add-knob-hotkey"
DELETE_KNOB_HOTKEY = "delete-knob-hotkey"
DELETE_KNOB_ALTERNATIVE_HOTKEY = "delete-knob-alternative-hotkey"
SHIFT_KNOB_BACKWARD_HOTKEY = "shift-knob-backward-hotkey"
SHIFT_KNOB_FORWARD_HOTKEY = "shift-knob-forward-hotkey"


class VisualizationFiltersKeys(ResourceKey):
Expand Down Expand Up @@ -212,6 +218,9 @@ class WorkspaceKeys(ResourceKey):
AnalysisKeys.BUTTON_TEXT_EXPORT_TRACK_STATISTICS: "Export track statistics",
CanvasKeys.LABEL_ADD_SECTION: "Add section",
CanvasKeys.LABEL_EDIT_SECTION: "Edit section",
CanvasKeys.MESSAGE_NOT_ENOUGH_POINTS: (
"Please add at least {count} points to finish the section."
),
ConfigurationBarKeys.LABEL_CONFIGURATION_BAR_FORM_HEADER: "Configuration Bar",
FlowAndSectionKeys.TAB_SECTION: "Section",
FlowAndSectionKeys.TAB_FLOW: "Flow",
Expand Down Expand Up @@ -324,10 +333,20 @@ class WorkspaceKeys(ResourceKey):
# Hotkey values as constants for reuse
SAVE_SECTION_HOTKEY_VALUE = "Enter"
CANCEL_SECTION_GEOMETRY_HOTKEY_VALUE = "Escape"
ADD_KNOB_HOTKEY_VALUE = "+"
DELETE_KNOB_HOTKEY_VALUE = "Delete"
DELETE_KNOB_ALTERNATIVE_HOTKEY_VALUE = "Backspace"
SHIFT_KNOB_BACKWARD_HOTKEY_VALUE = "ArrowLeft"
SHIFT_KNOB_FORWARD_HOTKEY_VALUE = "ArrowRight"

DEFAULT_HOTKEYS: dict[HotKeys, str] = {
HotKeys.SAVE_SECTION_HOTKEY: SAVE_SECTION_HOTKEY_VALUE,
HotKeys.CANCEL_SECTION_GEOMETRY_HOTKEY: CANCEL_SECTION_GEOMETRY_HOTKEY_VALUE,
HotKeys.ADD_KNOB_HOTKEY: ADD_KNOB_HOTKEY_VALUE,
HotKeys.DELETE_KNOB_HOTKEY: DELETE_KNOB_HOTKEY_VALUE,
HotKeys.DELETE_KNOB_ALTERNATIVE_HOTKEY: DELETE_KNOB_ALTERNATIVE_HOTKEY_VALUE,
HotKeys.SHIFT_KNOB_BACKWARD_HOTKEY: SHIFT_KNOB_BACKWARD_HOTKEY_VALUE,
HotKeys.SHIFT_KNOB_FORWARD_HOTKEY: SHIFT_KNOB_FORWARD_HOTKEY_VALUE,
}


Expand Down
54 changes: 54 additions & 0 deletions OTAnalytics/plugin_ui/nicegui_gui/nicegui/svg/arrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import math
from dataclasses import dataclass

DEFAULT_ARROW_WIDTH = 2
DEFAULT_HEAD_LENGTH = 20
DEFAULT_HEAD_HALF_WIDTH = 8


@dataclass
class Arrow:
"""Line from start to end with an arrow head at the end, e.g. a flow."""

id: str
x1: int
y1: int
x2: int
y2: int
color: str
width: int = DEFAULT_ARROW_WIDTH
dash: tuple[int, ...] | None = None
head_length: int = DEFAULT_HEAD_LENGTH
head_half_width: int = DEFAULT_HEAD_HALF_WIDTH

def to_svg(self) -> str:
dash = (
f' stroke-dasharray="{" ".join(str(d) for d in self.dash)}"'
if self.dash
else ""
)
svg = f"""<g id="{self.id}">
<line x1="{self.x1}" y1="{self.y1}" x2="{self.x2}" y2="{self.y2}"
stroke="{self.color}" stroke-width="{self.width}"{dash} />"""
if head := self.head_points():
points = " ".join(f"{x},{y}" for x, y in head)
svg += f"""
<polygon points="{points}" fill="{self.color}" />"""
return svg + "</g>"

def head_points(self) -> list[tuple[float, float]]:
"""Corners of the arrow head: tip and the two trailing points."""
dx = self.x2 - self.x1
dy = self.y2 - self.y1
length = math.hypot(dx, dy)
if length == 0:
return []
ux, uy = dx / length, dy / length
base_x = self.x2 - ux * self.head_length
base_y = self.y2 - uy * self.head_length
nx, ny = -uy * self.head_half_width, ux * self.head_half_width
return [
(float(self.x2), float(self.y2)),
(base_x + nx, base_y + ny),
(base_x - nx, base_y - ny),
]
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from dataclasses import dataclass

from OTAnalytics.plugin_ui.nicegui_gui.nicegui.svg.arrow import Arrow
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.svg.line import Line


@dataclass
class LineResources:
lines: dict[str, Line]
lines: dict[str, Line | Arrow]

def to_svg(self) -> str:
return "\n".join([line.to_svg() for line in self.lines.values()])

def add(self, line: Line) -> None:
def add(self, line: Line | Arrow) -> None:
self.lines[line.id] = line

def clear(self) -> None:
Expand Down
23 changes: 23 additions & 0 deletions OTAnalytics/plugin_ui/nicegui_gui/nicegui/svg/polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass

AREA_FILL_OPACITY = 0.3


@dataclass
class Polygon:
"""Closed shape that is filled semi-transparently, e.g. an area section."""

id: str
points: list[tuple[int, int]]
color: str
fill_opacity: float = AREA_FILL_OPACITY

def to_svg(self) -> str:
return f"""<polygon id="{self.id}"
points="{self.__serialized_points()}"
stroke="{self.color}"
fill="{self.color}"
fill-opacity="{self.fill_opacity}" />"""

def __serialized_points(self) -> str:
return " ".join(f"{x},{y}" for x, y in self.points)
17 changes: 13 additions & 4 deletions OTAnalytics/plugin_ui/nicegui_gui/nicegui/svg/section_resources.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
from dataclasses import dataclass
from dataclasses import dataclass, field

from OTAnalytics.plugin_ui.nicegui_gui.nicegui.svg.polygon import Polygon
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.svg.polyline import Polyline
from OTAnalytics.plugin_ui.nicegui_gui.nicegui.svg.text import Text


@dataclass
class SectionResource:
lines: dict[str, Polyline]
lines: dict[str, Polyline | Polygon]
texts: dict[str, Text] = field(default_factory=dict)

def to_svg(self) -> str:
return "\n".join(line.to_svg() for line in self.lines.values())
shapes = [line.to_svg() for line in self.lines.values()]
texts = [text.to_svg() for text in self.texts.values()]
return "\n".join(shapes + texts)

def clear(self) -> None:
self.lines.clear()
self.texts.clear()

def add(self, section_id: str, line: Polyline) -> None:
def add(self, section_id: str, line: Polyline | Polygon) -> None:
self.lines[section_id] = line

def add_text(self, section_id: str, text: Text) -> None:
self.texts[section_id] = text
22 changes: 22 additions & 0 deletions OTAnalytics/plugin_ui/nicegui_gui/nicegui/svg/text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from dataclasses import dataclass
from html import escape

DEFAULT_FONT_SIZE = 14


@dataclass
class Text:
id: str
x: int
y: int
text: str
color: str
font_size: int = DEFAULT_FONT_SIZE

def to_svg(self) -> str:
return f"""<text id="{escape(self.id)}"
x="{self.x}"
y="{self.y}"
fill="{self.color}"
font-size="{self.font_size}"
pointer-events="none">{escape(self.text)}</text>"""
Loading
Loading