diff --git a/deebot_client/map.py b/deebot_client/map.py index 47371b09..6e00f214 100644 --- a/deebot_client/map.py +++ b/deebot_client/map.py @@ -185,6 +185,30 @@ class BackgroundImage: image: bytes +@dataclasses.dataclass(frozen=True) +class CalibrationPoint: + """Calibration point.""" + + vacuum: Point + map: Point = dataclasses.field(init=False) + manipulation: dataclasses.InitVar[MapManipulation] + + def __post_init__(self, manipulation: MapManipulation) -> None: + object.__setattr__( + self, + "map", + _calc_unbounded_point(self.vacuum.x, self.vacuum.y, manipulation), + ) + + +@dataclasses.dataclass(frozen=True) +class CalibratedMap: + """Map image with calibration points.""" + + calibration_points: tuple[CalibrationPoint, ...] + image: str + + # SVG definitions referred by map elements _SVG_DEFS = svg.Defs( elements=[ @@ -262,10 +286,8 @@ def _calc_value(value: float, axis_manipulation: AxisManipulation) -> float: (float(value) / _PIXEL_WIDTH) + _OFFSET - axis_manipulation.map_shift ) new_value = axis_manipulation.transform(new_value) - # return value inside min and max - return round( - min(axis_manipulation.svg_max, max(0, new_value)), _ROUND_TO_DIGITS - ) + + return round(new_value, _ROUND_TO_DIGITS) except (ZeroDivisionError, ValueError): pass @@ -273,7 +295,7 @@ def _calc_value(value: float, axis_manipulation: AxisManipulation) -> float: return 0 -def _calc_point( +def _calc_unbounded_point( x: float, y: float, map_manipulation: MapManipulation, @@ -284,6 +306,19 @@ def _calc_point( ) +def _calc_point( + x: float, + y: float, + map_manipulation: MapManipulation, +) -> Point: + unbounded = _calc_unbounded_point(x, y, map_manipulation) + + return Point( + min(map_manipulation.x.svg_max, max(0, unbounded.x)), + min(map_manipulation.y.svg_max, max(0, unbounded.y)), + ) + + def _points_to_svg_path( points: Sequence[Point | TracePoint], ) -> list[svg.PathData]: @@ -390,7 +425,7 @@ def __init__( self._map_data: Final[MapData] = MapData(event_bus) self._amount_rooms: int = 0 - self._last_image: str | None = None + self._last_image: CalibratedMap | None = None self._unsubscribers: list[Callable[[], None]] = [] async def on_map_set(event: MapSetEvent) -> None: @@ -564,6 +599,13 @@ def _get_background_image(self) -> BackgroundImage | None: ) def get_svg_map(self) -> str | None: + """Return map as SVG string and set of calibration points.""" + map = self.get_calibrated_map() + if map is None: + return None + return map.image + + def get_calibrated_map(self) -> CalibratedMap | None: """Return map as SVG string.""" if not self._unsubscribers: raise MapError("Please enable the map first") @@ -582,9 +624,6 @@ def get_svg_map(self) -> str | None: self._last_image = None return None - # Build the SVG elements - svg_map = svg.SVG() - svg_map.elements = [_SVG_DEFS] manipulation = MapManipulation( AxisManipulation( map_shift=background.bounding_box[0], @@ -597,6 +636,10 @@ def get_svg_map(self) -> str | None: ), ) + # Build the SVG elements + svg_map = svg.SVG(width=manipulation.x.svg_max, height=manipulation.y.svg_max) + svg_map.elements = [_SVG_DEFS] + # Set map viewBox based on background map bounding box. svg_map.viewBox = svg.ViewBoxSpec( 0, @@ -637,7 +680,12 @@ def get_svg_map(self) -> str | None: _get_svg_positions(self._map_data.positions, manipulation) ) - self._last_image = str(svg_map) + points = tuple( + CalibrationPoint(point, manipulation) + for point in (Point(0, 0), Point(0, 100000), Point(100000, 0)) + ) + + self._last_image = CalibratedMap(calibration_points=points, image=str(svg_map)) _LOGGER.debug("[get_svg_map] Finish") return self._last_image