Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return calibration points together with map #423

Closed
wants to merge 3 commits into from
Closed
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 64 additions & 10 deletions deebot_client/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ class BackgroundImage:
image: bytes


@dataclasses.dataclass
philek marked this conversation as resolved.
Show resolved Hide resolved
class CalibrationPoint:
"""Calibration point."""

vacuum: Point
map: Point
philek marked this conversation as resolved.
Show resolved Hide resolved


@dataclasses.dataclass
philek marked this conversation as resolved.
Show resolved Hide resolved
class CalibratedMap:
"""Map image with calibration points."""

calibration_points: tuple[CalibrationPoint, CalibrationPoint, CalibrationPoint]
philek marked this conversation as resolved.
Show resolved Hide resolved
image: str


# SVG definitions referred by map elements
_SVG_DEFS = svg.Defs(
elements=[
Expand Down Expand Up @@ -262,18 +278,16 @@ 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

return 0


def _calc_point(
def _calc_unbounded_point(
x: float,
y: float,
map_manipulation: MapManipulation,
Expand All @@ -284,6 +298,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]:
Expand Down Expand Up @@ -390,7 +417,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:
Expand Down Expand Up @@ -564,6 +591,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")
Expand All @@ -582,9 +616,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],
Expand All @@ -597,6 +628,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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is these changed needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the SVG image has no size it makes it difficult (or impossible) for the Vacuum Map card to translate the vacuum coordinates back to the map after the image gets scaled again in the Home Assistant UI.
image

For example in my case the viewport size is 229x237, but Chrome reports the naturalWidth/naturalHeight as 145x150. I believe height is always 150 and width is proportional. Firefox returns 0 for both naturalWidth and naturalHeight. The viewport size is not readily available.
image

However, if we set the width and height explicitly for the SVG then its available as naturalWidth/naturalHeight and the coordinate mapping works correctly.
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not set a width or height, as the front end should decide how big it should be drawn.
The problem with setting a width/height on an SVG is that the frontend will use it for showing. For that reason, we use the viewport.
More info about this topic can be found in home-assistant/core#98036 (comment)

We are calculating the calibration points against the viewport and it's the card's job to apply the difference between the viewport and the actual width/height which the card decided to display.

@PiotrMachowski How much work is it to calculate the position of the object according to the viewport if an SVG only has a viewport and no width/height?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edenhaus I haven't tried using the map card with SVG yet, I'll have to check it out.

svg_map.elements = [_SVG_DEFS]

# Set map viewBox based on background map bounding box.
svg_map.viewBox = svg.ViewBoxSpec(
0,
Expand Down Expand Up @@ -637,7 +672,26 @@ def get_svg_map(self) -> str | None:
_get_svg_positions(self._map_data.positions, manipulation)
)

self._last_image = str(svg_map)
p0 = Point(0, 0)
p1 = Point(0, 100000)
p2 = Point(100000, 0)

points = (
CalibrationPoint(
vacuum=p0,
map=_calc_unbounded_point(p0.x, p0.y, manipulation),
),
CalibrationPoint(
vacuum=p1,
map=_calc_unbounded_point(p1.x, p1.y, manipulation),
),
CalibrationPoint(
vacuum=p2,
map=_calc_unbounded_point(p2.x, p2.y, manipulation),
),
)
philek marked this conversation as resolved.
Show resolved Hide resolved

self._last_image = CalibratedMap(calibration_points=points, image=str(svg_map))
_LOGGER.debug("[get_svg_map] Finish")
return self._last_image

Expand Down