valetudo-map-parser 0.1.9b53__py3-none-any.whl → 0.1.9b55__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- valetudo_map_parser/__init__.py +3 -2
- valetudo_map_parser/config/auto_crop.py +27 -7
- valetudo_map_parser/config/color_utils.py +2 -2
- valetudo_map_parser/config/colors.py +1 -0
- valetudo_map_parser/config/drawable.py +1 -2
- valetudo_map_parser/config/drawable_elements.py +4 -19
- valetudo_map_parser/config/enhanced_drawable.py +1 -1
- valetudo_map_parser/config/shared.py +1 -20
- valetudo_map_parser/config/types.py +1 -5
- valetudo_map_parser/config/utils.py +1 -51
- valetudo_map_parser/hypfer_draw.py +7 -13
- valetudo_map_parser/hypfer_handler.py +28 -88
- valetudo_map_parser/rand25_handler.py +37 -52
- valetudo_map_parser/rooms_handler.py +470 -0
- {valetudo_map_parser-0.1.9b53.dist-info → valetudo_map_parser-0.1.9b55.dist-info}/METADATA +1 -1
- valetudo_map_parser-0.1.9b55.dist-info/RECORD +27 -0
- valetudo_map_parser-0.1.9b53.dist-info/RECORD +0 -26
- {valetudo_map_parser-0.1.9b53.dist-info → valetudo_map_parser-0.1.9b55.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b53.dist-info → valetudo_map_parser-0.1.9b55.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b53.dist-info → valetudo_map_parser-0.1.9b55.dist-info}/WHEEL +0 -0
valetudo_map_parser/__init__.py
CHANGED
@@ -15,13 +15,14 @@ from .config.types import (
|
|
15
15
|
TrimCropData,
|
16
16
|
UserLanguageStore,
|
17
17
|
)
|
18
|
-
from .hypfer_rooms_handler import HypferRoomsHandler
|
19
18
|
from .hypfer_handler import HypferMapImageHandler
|
20
19
|
from .rand25_handler import ReImageHandler
|
20
|
+
from .rooms_handler import RoomsHandler, RandRoomsHandler
|
21
21
|
|
22
22
|
|
23
23
|
__all__ = [
|
24
|
-
"
|
24
|
+
"RoomsHandler",
|
25
|
+
"RandRoomsHandler",
|
25
26
|
"HypferMapImageHandler",
|
26
27
|
"ReImageHandler",
|
27
28
|
"RRMapParser",
|
@@ -125,7 +125,7 @@ class AutoCrop:
|
|
125
125
|
if self.auto_crop:
|
126
126
|
self.auto_crop_offset()
|
127
127
|
else:
|
128
|
-
self.handler.max_frames =
|
128
|
+
self.handler.max_frames = 1205
|
129
129
|
|
130
130
|
# Fallback: Ensure auto_crop is valid
|
131
131
|
if not self.auto_crop or any(v < 0 for v in self.auto_crop):
|
@@ -137,12 +137,32 @@ class AutoCrop:
|
|
137
137
|
async def async_image_margins(
|
138
138
|
self, image_array: NumpyArray, detect_colour: Color
|
139
139
|
) -> tuple[int, int, int, int]:
|
140
|
-
"""Crop the image based on the auto crop area."""
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
140
|
+
"""Crop the image based on the auto crop area using scipy.ndimage for better performance."""
|
141
|
+
# Import scipy.ndimage here to avoid import at module level
|
142
|
+
from scipy import ndimage
|
143
|
+
|
144
|
+
# Create a binary mask where True = non-background pixels
|
145
|
+
# This is much more memory efficient than storing coordinates
|
146
|
+
mask = ~np.all(image_array == list(detect_colour), axis=2)
|
147
|
+
|
148
|
+
# Use scipy.ndimage.find_objects to efficiently find the bounding box
|
149
|
+
# This returns a list of slice objects that define the bounding box
|
150
|
+
# Label the mask with a single label (1) and find its bounding box
|
151
|
+
labeled_mask = mask.astype(np.int8) # Convert to int8 (smallest integer type)
|
152
|
+
objects = ndimage.find_objects(labeled_mask)
|
153
|
+
|
154
|
+
if not objects: # No objects found
|
155
|
+
_LOGGER.warning(
|
156
|
+
"%s: No non-background pixels found in image", self.handler.file_name
|
157
|
+
)
|
158
|
+
# Return full image dimensions as fallback
|
159
|
+
return 0, 0, image_array.shape[1], image_array.shape[0]
|
160
|
+
|
161
|
+
# Extract the bounding box coordinates from the slice objects
|
162
|
+
y_slice, x_slice = objects[0]
|
163
|
+
min_y, max_y = y_slice.start, y_slice.stop - 1
|
164
|
+
min_x, max_x = x_slice.start, x_slice.stop - 1
|
165
|
+
|
146
166
|
_LOGGER.debug(
|
147
167
|
"%s: Found trims max and min values (y,x) (%s, %s) (%s, %s)...",
|
148
168
|
self.handler.file_name,
|
@@ -1,9 +1,9 @@
|
|
1
1
|
"""Utility functions for color operations in the map parser."""
|
2
2
|
|
3
|
-
from typing import
|
3
|
+
from typing import Optional, Tuple
|
4
4
|
|
5
5
|
from .colors import ColorsManagement
|
6
|
-
from .types import
|
6
|
+
from .types import Color, NumpyArray
|
7
7
|
|
8
8
|
|
9
9
|
def get_blended_color(
|
@@ -10,15 +10,14 @@ Optimized with NumPy and SciPy for better performance.
|
|
10
10
|
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
|
-
|
14
13
|
import logging
|
15
14
|
import math
|
16
15
|
|
17
16
|
import numpy as np
|
18
17
|
from PIL import ImageDraw, ImageFont
|
19
18
|
|
20
|
-
from .colors import ColorsManagement
|
21
19
|
from .color_utils import get_blended_color
|
20
|
+
from .colors import ColorsManagement
|
22
21
|
from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
|
23
22
|
|
24
23
|
|
@@ -8,10 +8,12 @@ from __future__ import annotations
|
|
8
8
|
|
9
9
|
from enum import IntEnum
|
10
10
|
from typing import Dict, List, Tuple, Union
|
11
|
+
|
11
12
|
import numpy as np
|
12
|
-
from .types import LOGGER
|
13
13
|
|
14
14
|
from .colors import DefaultColors, SupportedColor
|
15
|
+
from .types import LOGGER
|
16
|
+
|
15
17
|
|
16
18
|
# Type aliases
|
17
19
|
Color = Tuple[int, int, int, int] # RGBA color
|
@@ -170,13 +172,7 @@ class DrawingConfig:
|
|
170
172
|
|
171
173
|
def is_enabled(self, element_code: DrawableElement) -> bool:
|
172
174
|
"""Check if an element is enabled for drawing."""
|
173
|
-
|
174
|
-
LOGGER.debug(
|
175
|
-
"Checking if element %s is enabled: %s",
|
176
|
-
element_code.name if hasattr(element_code, "name") else element_code,
|
177
|
-
enabled,
|
178
|
-
)
|
179
|
-
return enabled
|
175
|
+
return self._enabled_elements.get(element_code, False)
|
180
176
|
|
181
177
|
def set_property(
|
182
178
|
self, element_code: DrawableElement, property_name: str, value
|
@@ -238,10 +234,6 @@ class DrawingConfig:
|
|
238
234
|
self.set_property(room_element, "color", rgba)
|
239
235
|
self.set_property(room_element, "opacity", alpha / 255.0)
|
240
236
|
|
241
|
-
LOGGER.debug(
|
242
|
-
"Updated room %d color to %s with alpha %s", room_id, rgb, alpha
|
243
|
-
)
|
244
|
-
|
245
237
|
# Update other element colors
|
246
238
|
for element, color_key in element_color_mapping.items():
|
247
239
|
if color_key in device_info:
|
@@ -265,13 +257,6 @@ class DrawingConfig:
|
|
265
257
|
self.set_property(element, "color", rgba)
|
266
258
|
self.set_property(element, "opacity", alpha / 255.0)
|
267
259
|
|
268
|
-
LOGGER.debug(
|
269
|
-
"Updated element %s color to %s with alpha %s",
|
270
|
-
element.name,
|
271
|
-
rgb,
|
272
|
-
alpha,
|
273
|
-
)
|
274
|
-
|
275
260
|
# Check for disabled elements using specific boolean flags
|
276
261
|
# Map element disable flags to DrawableElement enum values
|
277
262
|
element_disable_mapping = {
|
@@ -13,12 +13,12 @@ from typing import Optional, Tuple
|
|
13
13
|
|
14
14
|
import numpy as np
|
15
15
|
|
16
|
+
from .colors import ColorsManagement
|
16
17
|
from .drawable import Drawable
|
17
18
|
from .drawable_elements import (
|
18
19
|
DrawableElement,
|
19
20
|
DrawingConfig,
|
20
21
|
)
|
21
|
-
from .colors import ColorsManagement
|
22
22
|
|
23
23
|
|
24
24
|
# Type aliases
|
@@ -88,6 +88,7 @@ class CameraShared:
|
|
88
88
|
self.vac_json_id = None # Vacuum json id
|
89
89
|
self.margins = "100" # Image margins
|
90
90
|
self.obstacles_data = None # Obstacles data
|
91
|
+
self.obstacles_pos = None # Obstacles position
|
91
92
|
self.offset_top = 0 # Image offset top
|
92
93
|
self.offset_down = 0 # Image offset down
|
93
94
|
self.offset_left = 0 # Image offset left
|
@@ -247,26 +248,6 @@ class CameraSharedManager:
|
|
247
248
|
)
|
248
249
|
instance.trims = TrimsData.from_dict(trim_data)
|
249
250
|
|
250
|
-
# Log disable_obstacles and disable_path settings
|
251
|
-
if "disable_obstacles" in device_info:
|
252
|
-
_LOGGER.info(
|
253
|
-
"%s: device_info contains disable_obstacles: %s",
|
254
|
-
instance.file_name,
|
255
|
-
device_info["disable_obstacles"],
|
256
|
-
)
|
257
|
-
if "disable_path" in device_info:
|
258
|
-
_LOGGER.info(
|
259
|
-
"%s: device_info contains disable_path: %s",
|
260
|
-
instance.file_name,
|
261
|
-
device_info["disable_path"],
|
262
|
-
)
|
263
|
-
if "disable_elements" in device_info:
|
264
|
-
_LOGGER.info(
|
265
|
-
"%s: device_info contains disable_elements: %s",
|
266
|
-
instance.file_name,
|
267
|
-
device_info["disable_elements"],
|
268
|
-
)
|
269
|
-
|
270
251
|
except TypeError as ex:
|
271
252
|
_LOGGER.error("Shared data can't be initialized due to a TypeError! %s", ex)
|
272
253
|
except AttributeError as ex:
|
@@ -8,7 +8,7 @@ import json
|
|
8
8
|
import logging
|
9
9
|
import threading
|
10
10
|
from dataclasses import asdict, dataclass
|
11
|
-
from typing import Any, Dict, Optional, Tuple,
|
11
|
+
from typing import Any, Dict, Optional, Tuple, TypedDict, Union
|
12
12
|
|
13
13
|
import numpy as np
|
14
14
|
from PIL import Image
|
@@ -639,7 +639,3 @@ class TrimsData:
|
|
639
639
|
self.trim_down = 0
|
640
640
|
self.trim_right = 0
|
641
641
|
return asdict(self)
|
642
|
-
|
643
|
-
def self_instance(self):
|
644
|
-
"""Return self instance."""
|
645
|
-
return self.self_instance()
|
@@ -550,58 +550,8 @@ def initialize_drawing_config(handler):
|
|
550
550
|
hasattr(handler.shared, "device_info")
|
551
551
|
and handler.shared.device_info is not None
|
552
552
|
):
|
553
|
-
LOGGER.info(
|
554
|
-
"%s: Initializing drawing config from device_info", handler.file_name
|
555
|
-
)
|
556
|
-
LOGGER.info(
|
557
|
-
"%s: device_info contains disable_obstacles: %s",
|
558
|
-
handler.file_name,
|
559
|
-
"disable_obstacles" in handler.shared.device_info,
|
560
|
-
)
|
561
|
-
LOGGER.info(
|
562
|
-
"%s: device_info contains disable_path: %s",
|
563
|
-
handler.file_name,
|
564
|
-
"disable_path" in handler.shared.device_info,
|
565
|
-
)
|
566
|
-
LOGGER.info(
|
567
|
-
"%s: device_info contains disable_elements: %s",
|
568
|
-
handler.file_name,
|
569
|
-
"disable_elements" in handler.shared.device_info,
|
570
|
-
)
|
571
|
-
|
572
|
-
if "disable_obstacles" in handler.shared.device_info:
|
573
|
-
LOGGER.info(
|
574
|
-
"%s: disable_obstacles value: %s",
|
575
|
-
handler.file_name,
|
576
|
-
handler.shared.device_info["disable_obstacles"],
|
577
|
-
)
|
578
|
-
if "disable_path" in handler.shared.device_info:
|
579
|
-
LOGGER.info(
|
580
|
-
"%s: disable_path value: %s",
|
581
|
-
handler.file_name,
|
582
|
-
handler.shared.device_info["disable_path"],
|
583
|
-
)
|
584
|
-
if "disable_elements" in handler.shared.device_info:
|
585
|
-
LOGGER.info(
|
586
|
-
"%s: disable_elements value: %s",
|
587
|
-
handler.file_name,
|
588
|
-
handler.shared.device_info["disable_elements"],
|
589
|
-
)
|
590
|
-
|
591
553
|
drawing_config.update_from_device_info(handler.shared.device_info)
|
592
554
|
|
593
|
-
# Verify elements are disabled
|
594
|
-
LOGGER.info(
|
595
|
-
"%s: After initialization, PATH enabled: %s",
|
596
|
-
handler.file_name,
|
597
|
-
drawing_config.is_enabled(DrawableElement.PATH),
|
598
|
-
)
|
599
|
-
LOGGER.info(
|
600
|
-
"%s: After initialization, OBSTACLE enabled: %s",
|
601
|
-
handler.file_name,
|
602
|
-
drawing_config.is_enabled(DrawableElement.OBSTACLE),
|
603
|
-
)
|
604
|
-
|
605
555
|
# Initialize both drawable systems for backward compatibility
|
606
556
|
draw = Drawable() # Legacy drawing utilities
|
607
557
|
enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
|
@@ -632,7 +582,7 @@ def blend_colors(base_color, overlay_color):
|
|
632
582
|
|
633
583
|
# Avoid division by zero
|
634
584
|
if a_out < 0.0001:
|
635
|
-
return
|
585
|
+
return [0, 0, 0, 0]
|
636
586
|
|
637
587
|
# Calculate blended RGB components
|
638
588
|
r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
|
@@ -142,12 +142,6 @@ class ImageDraw:
|
|
142
142
|
room_element = getattr(DrawableElement, f"ROOM_{current_room_id}", None)
|
143
143
|
if room_element and hasattr(self.img_h.drawing_config, "is_enabled"):
|
144
144
|
draw_room = self.img_h.drawing_config.is_enabled(room_element)
|
145
|
-
_LOGGER.debug(
|
146
|
-
"%s: Room %d is %s",
|
147
|
-
self.file_name,
|
148
|
-
current_room_id,
|
149
|
-
"enabled" if draw_room else "disabled",
|
150
|
-
)
|
151
145
|
|
152
146
|
# Get the room color
|
153
147
|
room_color = self.img_h.shared.rooms_colors[room_id]
|
@@ -170,13 +164,6 @@ class ImageDraw:
|
|
170
164
|
except IndexError as e:
|
171
165
|
_LOGGER.warning("%s: Image Draw Error: %s", self.file_name, str(e))
|
172
166
|
|
173
|
-
_LOGGER.debug(
|
174
|
-
"%s Active Zones: %s and Room ID: %s",
|
175
|
-
self.file_name,
|
176
|
-
str(self.img_h.active_zones),
|
177
|
-
str(room_id),
|
178
|
-
)
|
179
|
-
|
180
167
|
return img_np_array, room_id
|
181
168
|
|
182
169
|
def _get_active_room_color(self, room_id, room_color, color_zone_clean):
|
@@ -316,6 +303,13 @@ class ImageDraw:
|
|
316
303
|
await self.img_h.draw.async_draw_obstacles(
|
317
304
|
np_array, obstacle_positions, color_no_go
|
318
305
|
)
|
306
|
+
|
307
|
+
# Update both obstacles_pos and obstacles_data
|
308
|
+
self.img_h.shared.obstacles_pos = obstacle_positions
|
309
|
+
# Only update obstacles_data if it's None or if the number of obstacles has changed
|
310
|
+
if (self.img_h.shared.obstacles_data is None or
|
311
|
+
len(self.img_h.shared.obstacles_data) != len(obstacle_positions)):
|
312
|
+
self.img_h.shared.obstacles_data = obstacle_positions
|
319
313
|
return np_array
|
320
314
|
|
321
315
|
async def async_draw_charger(
|
@@ -20,6 +20,7 @@ from .config.types import (
|
|
20
20
|
CalibrationPoints,
|
21
21
|
Colors,
|
22
22
|
RoomsProperties,
|
23
|
+
RoomStore,
|
23
24
|
)
|
24
25
|
from .config.utils import (
|
25
26
|
BaseHandler,
|
@@ -28,8 +29,8 @@ from .config.utils import (
|
|
28
29
|
prepare_resize_params,
|
29
30
|
)
|
30
31
|
from .hypfer_draw import ImageDraw as ImDraw
|
31
|
-
from .hypfer_rooms_handler import HypferRoomsHandler
|
32
32
|
from .map_data import ImageData
|
33
|
+
from .rooms_handler import RoomsHandler
|
33
34
|
|
34
35
|
|
35
36
|
class HypferMapImageHandler(BaseHandler, AutoCrop):
|
@@ -57,7 +58,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
57
58
|
self.imd = ImDraw(self) # Image Draw class.
|
58
59
|
self.color_grey = (128, 128, 128, 255)
|
59
60
|
self.file_name = self.shared.file_name # file name of the vacuum.
|
60
|
-
self.rooms_handler =
|
61
|
+
self.rooms_handler = RoomsHandler(
|
61
62
|
self.file_name, self.drawing_config
|
62
63
|
) # Room data handler
|
63
64
|
|
@@ -68,51 +69,24 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
68
69
|
|
69
70
|
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
70
71
|
"""Extract room properties from the JSON data."""
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
# x_max,
|
90
|
-
# y_max,
|
91
|
-
# ) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
|
92
|
-
# corners = self.get_corners(x_max, x_min, y_max, y_min)
|
93
|
-
# room_id = str(segment_id)
|
94
|
-
# self.rooms_pos.append(
|
95
|
-
# {
|
96
|
-
# "name": name,
|
97
|
-
# "corners": corners,
|
98
|
-
# }
|
99
|
-
# )
|
100
|
-
# room_properties[room_id] = {
|
101
|
-
# "number": segment_id,
|
102
|
-
# "outline": corners,
|
103
|
-
# "name": name,
|
104
|
-
# "x": ((x_min + x_max) // 2),
|
105
|
-
# "y": ((y_min + y_max) // 2),
|
106
|
-
# }
|
107
|
-
# if room_properties:
|
108
|
-
# rooms = RoomStore(self.file_name, room_properties)
|
109
|
-
# LOGGER.debug(
|
110
|
-
# "%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
|
111
|
-
# )
|
112
|
-
# else:
|
113
|
-
# LOGGER.debug("%s: Rooms data not available!", self.file_name)
|
114
|
-
# self.rooms_pos = None
|
115
|
-
# return room_properties
|
72
|
+
room_properties = await self.rooms_handler.async_extract_room_properties(
|
73
|
+
json_data
|
74
|
+
)
|
75
|
+
if room_properties:
|
76
|
+
rooms = RoomStore(self.file_name, room_properties)
|
77
|
+
LOGGER.debug(
|
78
|
+
"%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
|
79
|
+
)
|
80
|
+
# Convert room_properties to the format expected by async_get_robot_in_room
|
81
|
+
self.rooms_pos = []
|
82
|
+
for room_id, room_data in room_properties.items():
|
83
|
+
self.rooms_pos.append(
|
84
|
+
{"name": room_data["name"], "outline": room_data["outline"]}
|
85
|
+
)
|
86
|
+
else:
|
87
|
+
LOGGER.debug("%s: Rooms data not available!", self.file_name)
|
88
|
+
self.rooms_pos = None
|
89
|
+
return room_properties
|
116
90
|
|
117
91
|
# noinspection PyUnresolvedReferences,PyUnboundLocalVariable
|
118
92
|
async def async_get_image_from_json(
|
@@ -164,9 +138,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
164
138
|
img_np_array = await self.draw.create_empty_image(
|
165
139
|
size_x, size_y, colors["background"]
|
166
140
|
)
|
167
|
-
|
168
|
-
LOGGER.info("%s: Drawing map with color blending", self.file_name)
|
169
|
-
|
170
141
|
# Draw layers and segments if enabled
|
171
142
|
room_id = 0
|
172
143
|
# Keep track of disabled rooms to skip their walls later
|
@@ -218,25 +189,13 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
218
189
|
room_element = getattr(
|
219
190
|
DrawableElement, f"ROOM_{current_room_id}", None
|
220
191
|
)
|
221
|
-
if room_element:
|
222
|
-
# Log the room check for debugging
|
223
|
-
LOGGER.debug(
|
224
|
-
"%s: Checking if room %d is enabled: %s",
|
225
|
-
self.file_name,
|
226
|
-
current_room_id,
|
227
|
-
self.drawing_config.is_enabled(
|
228
|
-
room_element
|
229
|
-
),
|
230
|
-
)
|
231
192
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
) % 16 # Increment room_id even if we skip
|
239
|
-
continue
|
193
|
+
# Skip this room if it's disabled
|
194
|
+
if not self.drawing_config.is_enabled(room_element):
|
195
|
+
room_id = (
|
196
|
+
room_id + 1
|
197
|
+
) % 16 # Increment room_id even if we skip
|
198
|
+
continue
|
240
199
|
|
241
200
|
# Check if this is a wall layer and if walls are enabled
|
242
201
|
is_wall_layer = layer_type == "wall"
|
@@ -244,22 +203,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
244
203
|
if not self.drawing_config.is_enabled(
|
245
204
|
DrawableElement.WALL
|
246
205
|
):
|
247
|
-
|
248
|
-
"%s: Skipping wall layer because WALL element is disabled",
|
249
|
-
self.file_name,
|
250
|
-
)
|
251
|
-
continue
|
252
|
-
|
253
|
-
# Filter out walls for disabled rooms
|
254
|
-
if disabled_rooms:
|
255
|
-
# Need to modify compressed_pixels_list to exclude walls of disabled rooms
|
256
|
-
# This requires knowledge of which walls belong to which rooms
|
257
|
-
# For now, we'll just log that we're drawing walls for all rooms
|
258
|
-
LOGGER.debug(
|
259
|
-
"%s: Drawing walls for all rooms (including disabled ones)",
|
260
|
-
self.file_name,
|
261
|
-
)
|
262
|
-
# In a real implementation, we would filter the walls here
|
206
|
+
pass
|
263
207
|
|
264
208
|
# Draw the layer
|
265
209
|
(
|
@@ -281,10 +225,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
|
|
281
225
|
room_element = getattr(
|
282
226
|
DrawableElement, f"ROOM_{room_id}", None
|
283
227
|
)
|
284
|
-
if room_element:
|
285
|
-
# This is a simplification - in a real implementation we would
|
286
|
-
# need to identify the exact pixels that belong to this room
|
287
|
-
pass
|
288
228
|
|
289
229
|
# Draw the virtual walls if enabled
|
290
230
|
if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
|
@@ -29,13 +29,14 @@ from .config.types import (
|
|
29
29
|
)
|
30
30
|
from .config.utils import (
|
31
31
|
BaseHandler,
|
32
|
+
# async_extract_room_outline,
|
32
33
|
initialize_drawing_config,
|
33
34
|
manage_drawable_elements,
|
34
35
|
prepare_resize_params,
|
35
|
-
async_extract_room_outline,
|
36
36
|
)
|
37
37
|
from .map_data import RandImageData
|
38
38
|
from .reimg_draw import ImageDraw
|
39
|
+
from .rooms_handler import RandRoomsHandler
|
39
40
|
|
40
41
|
|
41
42
|
_LOGGER = logging.getLogger(__name__)
|
@@ -68,6 +69,7 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
68
69
|
self.active_zones = None # Active zones
|
69
70
|
self.file_name = self.shared.file_name # File name
|
70
71
|
self.imd = ImageDraw(self) # Image Draw
|
72
|
+
self.rooms_handler = RandRoomsHandler(self.file_name, self.drawing_config) # Room data handler
|
71
73
|
|
72
74
|
async def extract_room_outline_from_map(self, room_id_int, pixels):
|
73
75
|
"""Extract the outline of a room using the pixel data and element map.
|
@@ -110,62 +112,45 @@ class ReImageHandler(BaseHandler, AutoCrop):
|
|
110
112
|
) = await RandImageData.async_get_rrm_segments(
|
111
113
|
json_data, size_x, size_y, top, left, True
|
112
114
|
)
|
115
|
+
|
113
116
|
dest_json = destinations
|
114
|
-
room_data = dict(dest_json).get("rooms", [])
|
115
117
|
zones_data = dict(dest_json).get("zones", [])
|
116
118
|
points_data = dict(dest_json).get("spots", [])
|
117
|
-
|
119
|
+
|
120
|
+
# Use the RandRoomsHandler to extract room properties
|
121
|
+
room_properties = await self.rooms_handler.async_extract_room_properties(
|
122
|
+
json_data, dest_json
|
123
|
+
)
|
124
|
+
|
125
|
+
# Update self.rooms_pos from room_properties for compatibility with other methods
|
118
126
|
self.rooms_pos = []
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
"corners": corners,
|
138
|
-
}
|
139
|
-
)
|
140
|
-
room_properties[int(room_id)] = {
|
141
|
-
"number": int(room_id),
|
142
|
-
"outline": corners,
|
143
|
-
"name": name,
|
144
|
-
"x": (x_min + x_max) // 2,
|
145
|
-
"y": (y_min + y_max) // 2,
|
146
|
-
}
|
147
|
-
# get the zones and points data
|
148
|
-
zone_properties = await self.async_zone_propriety(zones_data)
|
149
|
-
# get the points data
|
150
|
-
point_properties = await self.async_points_propriety(points_data)
|
151
|
-
if room_properties or zone_properties:
|
152
|
-
extracted_data = [
|
153
|
-
f"{len(room_properties)} Rooms" if room_properties else None,
|
154
|
-
f"{len(zone_properties)} Zones" if zone_properties else None,
|
155
|
-
]
|
156
|
-
extracted_data = ", ".join(filter(None, extracted_data))
|
157
|
-
_LOGGER.debug("Extracted data: %s", extracted_data)
|
158
|
-
else:
|
159
|
-
self.rooms_pos = None
|
160
|
-
_LOGGER.debug(
|
161
|
-
"%s: Rooms and Zones data not available!", self.file_name
|
162
|
-
)
|
163
|
-
rooms = RoomStore(self.file_name, room_properties)
|
164
|
-
_LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
|
165
|
-
return room_properties, zone_properties, point_properties
|
127
|
+
for room_id, props in room_properties.items():
|
128
|
+
self.rooms_pos.append({
|
129
|
+
"name": props["name"],
|
130
|
+
"corners": props["outline"], # Use the enhanced outline
|
131
|
+
})
|
132
|
+
|
133
|
+
# get the zones and points data
|
134
|
+
zone_properties = await self.async_zone_propriety(zones_data)
|
135
|
+
# get the points data
|
136
|
+
point_properties = await self.async_points_propriety(points_data)
|
137
|
+
|
138
|
+
if room_properties or zone_properties:
|
139
|
+
extracted_data = [
|
140
|
+
f"{len(room_properties)} Rooms" if room_properties else None,
|
141
|
+
f"{len(zone_properties)} Zones" if zone_properties else None,
|
142
|
+
]
|
143
|
+
extracted_data = ", ".join(filter(None, extracted_data))
|
144
|
+
_LOGGER.debug("Extracted data: %s", extracted_data)
|
166
145
|
else:
|
167
|
-
|
168
|
-
|
146
|
+
self.rooms_pos = None
|
147
|
+
_LOGGER.debug(
|
148
|
+
"%s: Rooms and Zones data not available!", self.file_name
|
149
|
+
)
|
150
|
+
|
151
|
+
rooms = RoomStore(self.file_name, room_properties)
|
152
|
+
_LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
|
153
|
+
return room_properties, zone_properties, point_properties
|
169
154
|
except (RuntimeError, ValueError) as e:
|
170
155
|
_LOGGER.debug(
|
171
156
|
"No rooms Data or Error in extract_room_properties: %s",
|
@@ -0,0 +1,470 @@
|
|
1
|
+
"""
|
2
|
+
Hipfer Rooms Handler Module.
|
3
|
+
Handles room data extraction and processing for Valetudo Hipfer vacuum maps.
|
4
|
+
Provides async methods for room outline extraction and properties management.
|
5
|
+
Version: 0.1.9
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import time
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
12
|
+
|
13
|
+
import numpy as np
|
14
|
+
from scipy.ndimage import binary_dilation, binary_erosion
|
15
|
+
from scipy.spatial import ConvexHull
|
16
|
+
|
17
|
+
from .config.drawable_elements import DrawableElement, DrawingConfig
|
18
|
+
from .config.types import LOGGER, RoomsProperties
|
19
|
+
|
20
|
+
from .map_data import RandImageData, ImageData
|
21
|
+
|
22
|
+
class RoomsHandler:
|
23
|
+
"""
|
24
|
+
Handler for extracting and managing room data from Hipfer vacuum maps.
|
25
|
+
|
26
|
+
This class provides methods to:
|
27
|
+
- Extract room outlines using the Ramer-Douglas-Peucker algorithm
|
28
|
+
- Process room properties from JSON data
|
29
|
+
- Generate room masks and extract contours
|
30
|
+
|
31
|
+
All methods are async for better integration with the rest of the codebase.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
|
35
|
+
"""
|
36
|
+
Initialize the HipferRoomsHandler.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
vacuum_id: Identifier for the vacuum
|
40
|
+
drawing_config: Configuration for which elements to draw (optional)
|
41
|
+
"""
|
42
|
+
self.vacuum_id = vacuum_id
|
43
|
+
self.drawing_config = drawing_config
|
44
|
+
self.current_json_data = (
|
45
|
+
None # Will store the current JSON data being processed
|
46
|
+
)
|
47
|
+
|
48
|
+
@staticmethod
|
49
|
+
def sublist(data: list, chunk_size: int) -> list:
|
50
|
+
return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
|
51
|
+
|
52
|
+
@staticmethod
|
53
|
+
def convex_hull_outline(mask: np.ndarray) -> list[tuple[int, int]]:
|
54
|
+
y_indices, x_indices = np.where(mask > 0)
|
55
|
+
if len(x_indices) == 0 or len(y_indices) == 0:
|
56
|
+
return []
|
57
|
+
|
58
|
+
points = np.column_stack((x_indices, y_indices))
|
59
|
+
if len(points) < 3:
|
60
|
+
return [(int(x), int(y)) for x, y in points]
|
61
|
+
|
62
|
+
hull = ConvexHull(points)
|
63
|
+
# Convert numpy.int64 values to regular Python integers
|
64
|
+
hull_points = [
|
65
|
+
(int(points[vertex][0]), int(points[vertex][1])) for vertex in hull.vertices
|
66
|
+
]
|
67
|
+
if hull_points[0] != hull_points[-1]:
|
68
|
+
hull_points.append(hull_points[0])
|
69
|
+
return hull_points
|
70
|
+
|
71
|
+
async def _process_room_layer(
|
72
|
+
self, layer: Dict[str, Any], width: int, height: int, pixel_size: int
|
73
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
|
74
|
+
"""Process a single room layer and extract its outline.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
layer: The layer data from the JSON
|
78
|
+
width: The width of the map
|
79
|
+
height: The height of the map
|
80
|
+
pixel_size: The size of each pixel
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
Tuple of (room_id, room_data) or (None, None) if processing failed
|
84
|
+
"""
|
85
|
+
meta_data = layer.get("metaData", {})
|
86
|
+
segment_id = meta_data.get("segmentId")
|
87
|
+
name = meta_data.get("name", "Room {}".format(segment_id))
|
88
|
+
compressed_pixels = layer.get("compressedPixels", [])
|
89
|
+
pixels = self.sublist(compressed_pixels, 3)
|
90
|
+
|
91
|
+
# Check if this room is enabled in the drawing configuration
|
92
|
+
if self.drawing_config is not None:
|
93
|
+
# Convert segment_id to room element (ROOM_1 to ROOM_15)
|
94
|
+
try:
|
95
|
+
# Segment IDs might not be sequential, so we need to map them to room elements
|
96
|
+
# We'll use a simple approach: if segment_id is an integer, use it directly
|
97
|
+
room_element_id = int(segment_id)
|
98
|
+
if 1 <= room_element_id <= 15:
|
99
|
+
room_element = getattr(
|
100
|
+
DrawableElement, f"ROOM_{room_element_id}", None
|
101
|
+
)
|
102
|
+
if room_element:
|
103
|
+
is_enabled = self.drawing_config.is_enabled(room_element)
|
104
|
+
if not is_enabled:
|
105
|
+
# Skip this room if it's disabled
|
106
|
+
LOGGER.debug("Skipping disabled room %s", segment_id)
|
107
|
+
return None, None
|
108
|
+
except (ValueError, TypeError):
|
109
|
+
# If segment_id is not a valid integer, we can't map it to a room element
|
110
|
+
# In this case, we'll include the room (fail open)
|
111
|
+
LOGGER.debug(
|
112
|
+
"Could not convert segment_id %s to room element", segment_id
|
113
|
+
)
|
114
|
+
|
115
|
+
# Optimization: Create a smaller mask for just the room area
|
116
|
+
if not pixels:
|
117
|
+
# Skip if no pixels
|
118
|
+
return None, None
|
119
|
+
|
120
|
+
# Convert to numpy arrays for vectorized operations
|
121
|
+
pixel_data = np.array(pixels)
|
122
|
+
|
123
|
+
if pixel_data.size == 0:
|
124
|
+
return None, None
|
125
|
+
|
126
|
+
# Find the actual bounds of the room to create a smaller mask
|
127
|
+
# Add padding to ensure we don't lose edge details
|
128
|
+
padding = 10 # Add padding pixels around the room
|
129
|
+
min_x = max(0, int(np.min(pixel_data[:, 0])) - padding)
|
130
|
+
max_x = min(
|
131
|
+
width, int(np.max(pixel_data[:, 0]) + np.max(pixel_data[:, 2])) + padding
|
132
|
+
)
|
133
|
+
min_y = max(0, int(np.min(pixel_data[:, 1])) - padding)
|
134
|
+
max_y = min(height, int(np.max(pixel_data[:, 1]) + 1) + padding)
|
135
|
+
|
136
|
+
# Create a smaller mask for just the room area (much faster)
|
137
|
+
local_width = max_x - min_x
|
138
|
+
local_height = max_y - min_y
|
139
|
+
|
140
|
+
# Skip if dimensions are invalid
|
141
|
+
if local_width <= 0 or local_height <= 0:
|
142
|
+
return None, None
|
143
|
+
|
144
|
+
# Create a smaller mask
|
145
|
+
local_mask = np.zeros((local_height, local_width), dtype=np.uint8)
|
146
|
+
|
147
|
+
# Fill the mask efficiently
|
148
|
+
for x, y, length in pixel_data:
|
149
|
+
x, y, length = int(x), int(y), int(length)
|
150
|
+
# Adjust coordinates to local mask
|
151
|
+
local_x = x - min_x
|
152
|
+
local_y = y - min_y
|
153
|
+
|
154
|
+
# Ensure we're within bounds
|
155
|
+
if 0 <= local_y < local_height and 0 <= local_x < local_width:
|
156
|
+
# Calculate the end point, clamping to mask width
|
157
|
+
end_x = min(local_x + length, local_width)
|
158
|
+
if end_x > local_x: # Only process if there's a valid segment
|
159
|
+
local_mask[local_y, local_x:end_x] = 1
|
160
|
+
|
161
|
+
# Apply morphological operations
|
162
|
+
struct_elem = np.ones((3, 3), dtype=np.uint8)
|
163
|
+
eroded = binary_erosion(local_mask, structure=struct_elem, iterations=1)
|
164
|
+
mask = binary_dilation(eroded, structure=struct_elem, iterations=1).astype(
|
165
|
+
np.uint8
|
166
|
+
)
|
167
|
+
|
168
|
+
# Extract contour from the mask
|
169
|
+
outline = self.convex_hull_outline(mask)
|
170
|
+
if not outline:
|
171
|
+
return None, None
|
172
|
+
|
173
|
+
# Adjust coordinates back to global space
|
174
|
+
outline = [(x + min_x, y + min_y) for (x, y) in outline]
|
175
|
+
|
176
|
+
# Use coordinates as-is without flipping Y coordinates
|
177
|
+
xs, ys = zip(*outline)
|
178
|
+
x_min, x_max = min(xs), max(xs)
|
179
|
+
y_min, y_max = min(ys), max(ys)
|
180
|
+
|
181
|
+
room_id = str(segment_id)
|
182
|
+
|
183
|
+
# Scale coordinates by pixel_size and convert to regular Python integers
|
184
|
+
scaled_outline = [
|
185
|
+
(int(x * pixel_size), int(y * pixel_size)) for x, y in outline
|
186
|
+
]
|
187
|
+
room_data = {
|
188
|
+
"number": segment_id,
|
189
|
+
"outline": scaled_outline,
|
190
|
+
"name": name,
|
191
|
+
"x": int(((x_min + x_max) * pixel_size) // 2),
|
192
|
+
"y": int(((y_min + y_max) * pixel_size) // 2),
|
193
|
+
}
|
194
|
+
|
195
|
+
return room_id, room_data
|
196
|
+
|
197
|
+
async def async_extract_room_properties(self, json_data) -> RoomsProperties:
|
198
|
+
"""Extract room properties from the JSON data.
|
199
|
+
|
200
|
+
This method processes all room layers in the JSON data and extracts their outlines.
|
201
|
+
It respects the drawing configuration, skipping rooms that are disabled.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
json_data: The JSON data from the vacuum
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
Dictionary of room properties
|
208
|
+
"""
|
209
|
+
start_total = time.time()
|
210
|
+
room_properties = {}
|
211
|
+
pixel_size = json_data.get("pixelSize", 5)
|
212
|
+
height = json_data["size"]["y"]
|
213
|
+
width = json_data["size"]["x"]
|
214
|
+
|
215
|
+
for layer in json_data.get("layers", []):
|
216
|
+
if layer.get("__class") == "MapLayer" and layer.get("type") == "segment":
|
217
|
+
room_id, room_data = await self._process_room_layer(
|
218
|
+
layer, width, height, pixel_size
|
219
|
+
)
|
220
|
+
if room_id is not None and room_data is not None:
|
221
|
+
room_properties[room_id] = room_data
|
222
|
+
|
223
|
+
# Log timing information
|
224
|
+
total_time = time.time() - start_total
|
225
|
+
LOGGER.debug("Room extraction Total time: %.3fs", total_time)
|
226
|
+
return room_properties
|
227
|
+
|
228
|
+
class RandRoomsHandler:
|
229
|
+
"""
|
230
|
+
Handler for extracting and managing room data from Rand25 vacuum maps.
|
231
|
+
|
232
|
+
This class provides methods to:
|
233
|
+
- Extract room outlines using the Convex Hull algorithm
|
234
|
+
- Process room properties from JSON data and destinations JSON
|
235
|
+
- Generate room masks and extract contours
|
236
|
+
|
237
|
+
All methods are async for better integration with the rest of the codebase.
|
238
|
+
"""
|
239
|
+
|
240
|
+
def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
|
241
|
+
"""
|
242
|
+
Initialize the RandRoomsHandler.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
vacuum_id: Identifier for the vacuum
|
246
|
+
drawing_config: Configuration for which elements to draw (optional)
|
247
|
+
"""
|
248
|
+
self.vacuum_id = vacuum_id
|
249
|
+
self.drawing_config = drawing_config
|
250
|
+
self.current_json_data = None # Will store the current JSON data being processed
|
251
|
+
self.segment_data = None # Segment data
|
252
|
+
self.outlines = None # Outlines data
|
253
|
+
|
254
|
+
@staticmethod
|
255
|
+
def sublist(data: list, chunk_size: int) -> list:
|
256
|
+
"""Split a list into chunks of specified size."""
|
257
|
+
return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
def convex_hull_outline(points: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
|
261
|
+
"""
|
262
|
+
Generate a convex hull outline from a set of points.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
points: List of (x, y) coordinate tuples
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
List of (x, y) tuples forming the convex hull outline
|
269
|
+
"""
|
270
|
+
if len(points) == 0:
|
271
|
+
return []
|
272
|
+
|
273
|
+
# Convert to numpy array for processing
|
274
|
+
points_array = np.array(points)
|
275
|
+
|
276
|
+
if len(points) < 3:
|
277
|
+
# Not enough points for a convex hull, return the points as is
|
278
|
+
return [(int(x), int(y)) for x, y in points_array]
|
279
|
+
|
280
|
+
try:
|
281
|
+
# Calculate the convex hull
|
282
|
+
hull = ConvexHull(points_array)
|
283
|
+
|
284
|
+
# Extract the vertices in order
|
285
|
+
hull_points = [
|
286
|
+
(int(points_array[vertex][0]), int(points_array[vertex][1]))
|
287
|
+
for vertex in hull.vertices
|
288
|
+
]
|
289
|
+
|
290
|
+
# Close the polygon by adding the first point at the end
|
291
|
+
if hull_points[0] != hull_points[-1]:
|
292
|
+
hull_points.append(hull_points[0])
|
293
|
+
|
294
|
+
return hull_points
|
295
|
+
|
296
|
+
except Exception as e:
|
297
|
+
LOGGER.warning(f"Error calculating convex hull: {e}")
|
298
|
+
|
299
|
+
# Fallback to bounding box if convex hull fails
|
300
|
+
x_min, y_min = np.min(points_array, axis=0)
|
301
|
+
x_max, y_max = np.max(points_array, axis=0)
|
302
|
+
|
303
|
+
return [
|
304
|
+
(int(x_min), int(y_min)), # Top-left
|
305
|
+
(int(x_max), int(y_min)), # Top-right
|
306
|
+
(int(x_max), int(y_max)), # Bottom-right
|
307
|
+
(int(x_min), int(y_max)), # Bottom-left
|
308
|
+
(int(x_min), int(y_min)), # Back to top-left to close the polygon
|
309
|
+
]
|
310
|
+
|
311
|
+
async def _process_segment_data(
|
312
|
+
self, segment_data: List, segment_id: int, pixel_size: int
|
313
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
|
314
|
+
"""
|
315
|
+
Process a single segment and extract its outline.
|
316
|
+
|
317
|
+
Args:
|
318
|
+
segment_data: The segment pixel data
|
319
|
+
segment_id: The ID of the segment
|
320
|
+
pixel_size: The size of each pixel
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
Tuple of (room_id, room_data) or (None, None) if processing failed
|
324
|
+
"""
|
325
|
+
# Check if this room is enabled in the drawing configuration
|
326
|
+
if self.drawing_config is not None:
|
327
|
+
try:
|
328
|
+
# Convert segment_id to room element (ROOM_1 to ROOM_15)
|
329
|
+
room_element_id = int(segment_id)
|
330
|
+
if 1 <= room_element_id <= 15:
|
331
|
+
room_element = getattr(
|
332
|
+
DrawableElement, f"ROOM_{room_element_id}", None
|
333
|
+
)
|
334
|
+
if room_element:
|
335
|
+
is_enabled = self.drawing_config.is_enabled(room_element)
|
336
|
+
if not is_enabled:
|
337
|
+
# Skip this room if it's disabled
|
338
|
+
LOGGER.debug("Skipping disabled room %s", segment_id)
|
339
|
+
return None, None
|
340
|
+
except (ValueError, TypeError):
|
341
|
+
# If segment_id is not a valid integer, we can't map it to a room element
|
342
|
+
# In this case, we'll include the room (fail open)
|
343
|
+
LOGGER.debug(
|
344
|
+
"Could not convert segment_id %s to room element", segment_id
|
345
|
+
)
|
346
|
+
|
347
|
+
# Skip if no pixels
|
348
|
+
if not segment_data:
|
349
|
+
return None, None
|
350
|
+
|
351
|
+
# Extract points from segment data
|
352
|
+
points = []
|
353
|
+
for x, y, _ in segment_data:
|
354
|
+
points.append((int(x), int(y)))
|
355
|
+
|
356
|
+
if not points:
|
357
|
+
return None, None
|
358
|
+
|
359
|
+
# Use convex hull to get the outline
|
360
|
+
outline = self.convex_hull_outline(points)
|
361
|
+
if not outline:
|
362
|
+
return None, None
|
363
|
+
|
364
|
+
# Calculate bounding box for the room
|
365
|
+
xs, ys = zip(*outline)
|
366
|
+
x_min, x_max = min(xs), max(xs)
|
367
|
+
y_min, y_max = min(ys), max(ys)
|
368
|
+
|
369
|
+
# Scale coordinates by pixel_size
|
370
|
+
scaled_outline = [
|
371
|
+
(int(x * pixel_size), int(y * pixel_size)) for x, y in outline
|
372
|
+
]
|
373
|
+
|
374
|
+
room_id = str(segment_id)
|
375
|
+
room_data = {
|
376
|
+
"number": segment_id,
|
377
|
+
"outline": scaled_outline,
|
378
|
+
"name": f"Room {segment_id}", # Default name, will be updated from destinations
|
379
|
+
"x": int(((x_min + x_max) * pixel_size) // 2),
|
380
|
+
"y": int(((y_min + y_max) * pixel_size) // 2),
|
381
|
+
}
|
382
|
+
|
383
|
+
return room_id, room_data
|
384
|
+
|
385
|
+
async def async_extract_room_properties(
|
386
|
+
self, json_data: Dict[str, Any], destinations: Dict[str, Any]
|
387
|
+
) -> RoomsProperties:
|
388
|
+
"""
|
389
|
+
Extract room properties from the JSON data and destinations.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
json_data: The JSON data from the vacuum
|
393
|
+
destinations: The destinations JSON containing room names and IDs
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
Dictionary of room properties
|
397
|
+
"""
|
398
|
+
start_total = time.time()
|
399
|
+
room_properties = {}
|
400
|
+
|
401
|
+
# Get basic map information
|
402
|
+
unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
|
403
|
+
size_x, size_y = RandImageData.get_rrm_image_size(json_data)
|
404
|
+
top, left = RandImageData.get_rrm_image_position(json_data)
|
405
|
+
pixel_size = 50 # Rand25 vacuums use a larger pixel size to match the original implementation
|
406
|
+
|
407
|
+
# Get segment data and outlines if not already available
|
408
|
+
if not self.segment_data or not self.outlines:
|
409
|
+
(
|
410
|
+
self.segment_data,
|
411
|
+
self.outlines,
|
412
|
+
) = await RandImageData.async_get_rrm_segments(
|
413
|
+
json_data, size_x, size_y, top, left, True
|
414
|
+
)
|
415
|
+
|
416
|
+
# Process destinations JSON to get room names
|
417
|
+
dest_json = destinations
|
418
|
+
room_data = dest_json.get("rooms", [])
|
419
|
+
room_id_to_data = {room["id"]: room for room in room_data}
|
420
|
+
|
421
|
+
# Process each segment
|
422
|
+
if unsorted_id and self.segment_data and self.outlines:
|
423
|
+
for idx, segment_id in enumerate(unsorted_id):
|
424
|
+
# Extract points from segment data
|
425
|
+
points = []
|
426
|
+
for x, y, _ in self.segment_data[idx]:
|
427
|
+
points.append((int(x), int(y)))
|
428
|
+
|
429
|
+
if not points:
|
430
|
+
continue
|
431
|
+
|
432
|
+
# Use convex hull to get the outline
|
433
|
+
outline = self.convex_hull_outline(points)
|
434
|
+
if not outline:
|
435
|
+
continue
|
436
|
+
|
437
|
+
# Scale coordinates by pixel_size
|
438
|
+
scaled_outline = [
|
439
|
+
(int(x * pixel_size), int(y * pixel_size)) for x, y in outline
|
440
|
+
]
|
441
|
+
|
442
|
+
# Calculate center point
|
443
|
+
xs, ys = zip(*outline)
|
444
|
+
x_min, x_max = min(xs), max(xs)
|
445
|
+
y_min, y_max = min(ys), max(ys)
|
446
|
+
center_x = int(((x_min + x_max) * pixel_size) // 2)
|
447
|
+
center_y = int(((y_min + y_max) * pixel_size) // 2)
|
448
|
+
|
449
|
+
# Create room data
|
450
|
+
room_id = str(segment_id)
|
451
|
+
room_data = {
|
452
|
+
"number": segment_id,
|
453
|
+
"outline": scaled_outline,
|
454
|
+
"name": f"Room {segment_id}", # Default name, will be updated from destinations
|
455
|
+
"x": center_x,
|
456
|
+
"y": center_y,
|
457
|
+
}
|
458
|
+
|
459
|
+
# Update room name from destinations if available
|
460
|
+
if segment_id in room_id_to_data:
|
461
|
+
room_info = room_id_to_data[segment_id]
|
462
|
+
room_data["name"] = room_info.get("name", room_data["name"])
|
463
|
+
|
464
|
+
room_properties[room_id] = room_data
|
465
|
+
|
466
|
+
# Log timing information
|
467
|
+
total_time = time.time() - start_total
|
468
|
+
LOGGER.debug("Room extraction Total time: %.3fs", total_time)
|
469
|
+
|
470
|
+
return room_properties
|
@@ -0,0 +1,27 @@
|
|
1
|
+
valetudo_map_parser/__init__.py,sha256=Fz-gtKf_OlZcDQqVfGlBwIWi5DJAiRucMbBMdQ2tX_U,1060
|
2
|
+
valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
|
3
|
+
valetudo_map_parser/config/auto_crop.py,sha256=6xt_wJQqphddWhlrr7MNUkodCi8ZYdRk42qvAaxlYCM,13546
|
4
|
+
valetudo_map_parser/config/color_utils.py,sha256=nXD6WeNmdFdoMxPDW-JFpjnxJSaZR1jX-ouNfrx6zvE,4502
|
5
|
+
valetudo_map_parser/config/colors.py,sha256=DG-oPQoN5gsnwDbEsuFr8a0hRCxmbFHObWa4_5pr-70,29910
|
6
|
+
valetudo_map_parser/config/drawable.py,sha256=2MeVHXqZuVuJk3eerMJYGwo25rVetHx3xB_vxecEFOQ,34168
|
7
|
+
valetudo_map_parser/config/drawable_elements.py,sha256=o-5oiXmfqPwNQLzKIhkEcZD_A47rIU9E0CqKgWipxgc,11516
|
8
|
+
valetudo_map_parser/config/enhanced_drawable.py,sha256=QlGxlUMVgECUXPtFwIslyjubWxQuhIixsRymWV3lEvk,12586
|
9
|
+
valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
|
10
|
+
valetudo_map_parser/config/rand25_parser.py,sha256=kIayyqVZBfQfAMkiArzqrrj9vqZB3pkgT0Y5ufrQmGA,16448
|
11
|
+
valetudo_map_parser/config/room_outline.py,sha256=D20D-yeyKnlmVbW9lI7bsPtQGn2XkcWow6YNOEPnWVg,4800
|
12
|
+
valetudo_map_parser/config/shared.py,sha256=Vr4bicL7aJoRQbwbXyjEpiWhfzZ-cakLlfRqL3LBhpM,10475
|
13
|
+
valetudo_map_parser/config/types.py,sha256=TaRKoo7G7WIUw7ljOz2Vn5oYzKaLyQH-7Eb8ZYql8Ls,17464
|
14
|
+
valetudo_map_parser/config/utils.py,sha256=CFuuiS5IufEu9aeaZwi7xa1jEF1z6yDZB0mcyVX79Xo,29261
|
15
|
+
valetudo_map_parser/hypfer_draw.py,sha256=bwNTYopTJFY0nElrHquQrSfGHgN_-6t5E-8xBxDsRXA,26660
|
16
|
+
valetudo_map_parser/hypfer_handler.py,sha256=wvkZt6MsUF0gkHZDAwiUgOOawkdvakRzYBV3NtjxuJQ,19938
|
17
|
+
valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
|
18
|
+
valetudo_map_parser/map_data.py,sha256=zQKE8EzWxR0r0qyfD1QQq51T1wFrpcIeXtnpm92-LXQ,17743
|
19
|
+
valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
valetudo_map_parser/rand25_handler.py,sha256=VOBzx0AY61AggOkiX-P4c73MrPrQBGfsDQTEbVL1wCQ,18855
|
21
|
+
valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
|
22
|
+
valetudo_map_parser/rooms_handler.py,sha256=YP8OLotBH-RXluv398l7TTT2zIBHJp91b8THWxl3NdI,17794
|
23
|
+
valetudo_map_parser-0.1.9b55.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
|
24
|
+
valetudo_map_parser-0.1.9b55.dist-info/METADATA,sha256=Af2KutWU3EHPE8F1HNCWWKlIGKqr9bz1nTbC1-d3ZCo,3321
|
25
|
+
valetudo_map_parser-0.1.9b55.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
|
26
|
+
valetudo_map_parser-0.1.9b55.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
27
|
+
valetudo_map_parser-0.1.9b55.dist-info/RECORD,,
|
@@ -1,26 +0,0 @@
|
|
1
|
-
valetudo_map_parser/__init__.py,sha256=cewtLadNSOg3X2Ts2SuG8mTJqo0ncsFRg_sQ4VkM4ow,1037
|
2
|
-
valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
|
3
|
-
valetudo_map_parser/config/auto_crop.py,sha256=6OvRsWzXMXBaSEvgwpaaisNdozDKiDyTmPjknFxoUMc,12624
|
4
|
-
valetudo_map_parser/config/color_utils.py,sha256=D4NXRhuPdQ7UDKM3vLNYR0HnACl9AB75EnfCp5tGliI,4502
|
5
|
-
valetudo_map_parser/config/colors.py,sha256=LE7sl4Qy0TkxbkjgB3dotYIfXqhc-fllkFQxexVvUvg,29909
|
6
|
-
valetudo_map_parser/config/drawable.py,sha256=qenuxD1-Vvyus9o8alJFYRqL54aO3pakMqPSYNGvpe8,34169
|
7
|
-
valetudo_map_parser/config/drawable_elements.py,sha256=bkEwdbx1upt9vaPaqE_VR1rtwRsaiH-IVKc3mHNa8IY,12065
|
8
|
-
valetudo_map_parser/config/enhanced_drawable.py,sha256=6yGoOq_dLf2VCghO_URSyGfLAshFyzS3iEPeHw1PeDo,12586
|
9
|
-
valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
|
10
|
-
valetudo_map_parser/config/rand25_parser.py,sha256=kIayyqVZBfQfAMkiArzqrrj9vqZB3pkgT0Y5ufrQmGA,16448
|
11
|
-
valetudo_map_parser/config/room_outline.py,sha256=D20D-yeyKnlmVbW9lI7bsPtQGn2XkcWow6YNOEPnWVg,4800
|
12
|
-
valetudo_map_parser/config/shared.py,sha256=GIEMF-M6BVA6SFBrql7chV7TciWNMLJ8geqwHB0NrW8,11253
|
13
|
-
valetudo_map_parser/config/types.py,sha256=e-eZSwbPm3m5JfCDaKhnUFspmcRFSv74huxegkSBDXM,17566
|
14
|
-
valetudo_map_parser/config/utils.py,sha256=RsMjpjVqNbkI502yhLiRaB0GjCADqmRRcz-TkC6zklQ,31073
|
15
|
-
valetudo_map_parser/hypfer_draw.py,sha256=P8CrKysLaBb63ZArfqxN2Og6JCU6sPHPFHOte5noCGg,26654
|
16
|
-
valetudo_map_parser/hypfer_handler.py,sha256=WYFrp-q5wBsy0cTcVQUCXXVGTtW30z2W2dYvjKz2el8,23292
|
17
|
-
valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
|
18
|
-
valetudo_map_parser/map_data.py,sha256=zQKE8EzWxR0r0qyfD1QQq51T1wFrpcIeXtnpm92-LXQ,17743
|
19
|
-
valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
-
valetudo_map_parser/rand25_handler.py,sha256=eLFX_gmGLaWQwvp8hVj8CgcNOfLsYNIdE1OLRcQy_yM,19988
|
21
|
-
valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
|
22
|
-
valetudo_map_parser-0.1.9b53.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
|
23
|
-
valetudo_map_parser-0.1.9b53.dist-info/METADATA,sha256=i8nDujD2s2Qs62WwEv-3AIO23XJTvxvSZXY5QcswEtc,3321
|
24
|
-
valetudo_map_parser-0.1.9b53.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
|
25
|
-
valetudo_map_parser-0.1.9b53.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
26
|
-
valetudo_map_parser-0.1.9b53.dist-info/RECORD,,
|
File without changes
|
{valetudo_map_parser-0.1.9b53.dist-info → valetudo_map_parser-0.1.9b55.dist-info}/NOTICE.txt
RENAMED
File without changes
|
File without changes
|