valetudo-map-parser 0.1.7__py3-none-any.whl → 0.1.9__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.
Files changed (30) hide show
  1. valetudo_map_parser/__init__.py +28 -13
  2. valetudo_map_parser/config/async_utils.py +93 -0
  3. valetudo_map_parser/config/auto_crop.py +312 -123
  4. valetudo_map_parser/config/color_utils.py +105 -0
  5. valetudo_map_parser/config/colors.py +662 -13
  6. valetudo_map_parser/config/drawable.py +613 -268
  7. valetudo_map_parser/config/drawable_elements.py +292 -0
  8. valetudo_map_parser/config/enhanced_drawable.py +324 -0
  9. valetudo_map_parser/config/optimized_element_map.py +406 -0
  10. valetudo_map_parser/config/rand256_parser.py +395 -0
  11. valetudo_map_parser/config/shared.py +94 -11
  12. valetudo_map_parser/config/types.py +105 -52
  13. valetudo_map_parser/config/utils.py +1025 -0
  14. valetudo_map_parser/hypfer_draw.py +464 -148
  15. valetudo_map_parser/hypfer_handler.py +366 -259
  16. valetudo_map_parser/hypfer_rooms_handler.py +599 -0
  17. valetudo_map_parser/map_data.py +56 -66
  18. valetudo_map_parser/rand256_handler.py +674 -0
  19. valetudo_map_parser/reimg_draw.py +68 -84
  20. valetudo_map_parser/rooms_handler.py +474 -0
  21. valetudo_map_parser-0.1.9.dist-info/METADATA +93 -0
  22. valetudo_map_parser-0.1.9.dist-info/RECORD +27 -0
  23. {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9.dist-info}/WHEEL +1 -1
  24. valetudo_map_parser/config/rand25_parser.py +0 -398
  25. valetudo_map_parser/images_utils.py +0 -398
  26. valetudo_map_parser/rand25_handler.py +0 -455
  27. valetudo_map_parser-0.1.7.dist-info/METADATA +0 -23
  28. valetudo_map_parser-0.1.7.dist-info/RECORD +0 -20
  29. {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9.dist-info}/LICENSE +0 -0
  30. {valetudo_map_parser-0.1.7.dist-info → valetudo_map_parser-0.1.9.dist-info}/NOTICE.txt +0 -0
@@ -2,170 +2,126 @@
2
2
  Hypfer Image Handler Class.
3
3
  It returns the PIL PNG image frame relative to the Map Data extrapolated from the vacuum json.
4
4
  It also returns calibration, rooms data to the card and other images information to the camera.
5
- Version: 2024.08.0
5
+ Version: 0.1.9
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import json
11
- import logging
10
+ import asyncio
11
+ import numpy as np
12
12
 
13
13
  from PIL import Image
14
14
 
15
+ from .config.async_utils import AsyncNumPy, AsyncPIL
16
+ from .config.auto_crop import AutoCrop
17
+ from .config.drawable_elements import DrawableElement
18
+ from .config.shared import CameraShared
19
+ from .config.utils import pil_to_webp_bytes
15
20
  from .config.types import (
21
+ COLORS,
22
+ LOGGER,
16
23
  CalibrationPoints,
17
- ChargerPosition,
18
- ImageSize,
19
- RobotPosition,
24
+ Colors,
20
25
  RoomsProperties,
26
+ RoomStore,
27
+ WebPBytes,
28
+ JsonType,
21
29
  )
22
- from .config.auto_crop import AutoCrop
23
- from .config.drawable import Drawable
24
- from .config.shared import CameraShared
25
- from .map_data import ImageData
26
- from .images_utils import (
27
- ImageUtils as ImUtils,
28
- resize_to_aspect_ratio,
29
- )
30
- from .hypfer_draw import (
31
- ImageDraw as ImDraw,
30
+ from .config.utils import (
31
+ BaseHandler,
32
+ initialize_drawing_config,
33
+ manage_drawable_elements,
34
+ numpy_to_webp_bytes,
35
+ prepare_resize_params,
32
36
  )
33
- from .config.colors import ColorsManagment, SupportedColor
34
-
35
- _LOGGER = logging.getLogger(__name__)
37
+ from .hypfer_draw import ImageDraw as ImDraw
38
+ from .map_data import ImageData
39
+ from .rooms_handler import RoomsHandler
36
40
 
37
41
 
38
- class HypferMapImageHandler:
42
+ class HypferMapImageHandler(BaseHandler, AutoCrop):
39
43
  """Map Image Handler Class.
40
44
  This class is used to handle the image data and the drawing of the map."""
41
45
 
42
46
  def __init__(self, shared_data: CameraShared):
43
47
  """Initialize the Map Image Handler."""
48
+ BaseHandler.__init__(self)
44
49
  self.shared = shared_data # camera shared data
45
- self.file_name = self.shared.file_name # file name of the vacuum.
46
- self.auto_crop = None # auto crop data to be calculate once.
50
+ AutoCrop.__init__(self, self)
47
51
  self.calibration_data = None # camera shared data.
48
- self.charger_pos = None # vacuum data charger position.
49
- self.crop_area = None # module shared for calibration data.
50
- self.crop_img_size = None # size of the image cropped calibration data.
51
52
  self.data = ImageData # imported Image Data Module.
52
- self.draw = Drawable # imported Drawing utilities
53
+
54
+ # Initialize drawing configuration using the shared utility function
55
+ self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
56
+ self
57
+ )
58
+
53
59
  self.go_to = None # vacuum go to data
54
60
  self.img_hash = None # hash of the image calculated to check differences.
55
61
  self.img_base_layer = None # numpy array store the map base layer.
56
- self.img_size = None # size of the created image
57
- self.json_data = None # local stored and shared json data.
58
- self.json_id = None # grabbed data of the vacuum image id.
59
- self.path_pixels = None # vacuum path datas.
60
- self.robot_in_room = None # vacuum room position.
61
- self.robot_pos = None # vacuum coordinates.
62
- self.room_propriety = None # vacuum segments data.
63
- self.rooms_pos = None # vacuum room coordinates / name list.
62
+ self.img_work_layer = (
63
+ None # persistent working buffer to avoid per-frame allocations
64
+ )
64
65
  self.active_zones = None # vacuum active zones.
65
- self.frame_number = 0 # frame number of the image.
66
- self.max_frames = 1024
67
- self.zooming = False # zooming the image.
68
66
  self.svg_wait = False # SVG image creation wait.
69
- self.trim_down = 0 # memory stored trims calculated once.
70
- self.trim_left = 0 # memory stored trims calculated once.
71
- self.trim_right = 0 # memory stored trims calculated once.
72
- self.trim_up = 0 # memory stored trims calculated once.
73
- self.offset_top = self.shared.offset_top # offset top
74
- self.offset_bottom = self.shared.offset_down # offset bottom
75
- self.offset_left = self.shared.offset_left # offset left
76
- self.offset_right = self.shared.offset_right # offset right
77
- self.offset_x = 0 # offset x for the aspect ratio.
78
- self.offset_y = 0 # offset y for the aspect ratio.
79
- self.imd = ImDraw(self)
80
- self.imu = ImUtils(self)
81
- self.ac = AutoCrop(self)
82
- self.colors_manager = ColorsManagment({})
83
- self.rooms_colors = self.colors_manager.get_rooms_colors()
67
+ self.imd = ImDraw(self) # Image Draw class.
84
68
  self.color_grey = (128, 128, 128, 255)
69
+ self.file_name = self.shared.file_name # file name of the vacuum.
70
+ self.rooms_handler = RoomsHandler(
71
+ self.file_name, self.drawing_config
72
+ ) # Room data handler
73
+
74
+ @staticmethod
75
+ def get_corners(x_max, x_min, y_max, y_min):
76
+ """Get the corners of the room."""
77
+ return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
85
78
 
86
79
  async def async_extract_room_properties(self, json_data) -> RoomsProperties:
87
80
  """Extract room properties from the JSON data."""
88
-
89
- room_properties = {}
90
- self.rooms_pos = []
91
- pixel_size = json_data.get("pixelSize", [])
92
-
93
- for layer in json_data.get("layers", []):
94
- if layer["__class"] == "MapLayer":
95
- meta_data = layer.get("metaData", {})
96
- segment_id = meta_data.get("segmentId")
97
- if segment_id is not None:
98
- name = meta_data.get("name")
99
- compressed_pixels = layer.get("compressedPixels", [])
100
- pixels = self.data.sublist(compressed_pixels, 3)
101
- # Calculate x and y min/max from compressed pixels
102
- (
103
- x_min,
104
- y_min,
105
- x_max,
106
- y_max,
107
- ) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
108
- corners = [
109
- (x_min, y_min),
110
- (x_max, y_min),
111
- (x_max, y_max),
112
- (x_min, y_max),
113
- ]
114
- room_id = str(segment_id)
115
- self.rooms_pos.append(
116
- {
117
- "name": name,
118
- "corners": corners,
119
- }
120
- )
121
- room_properties[room_id] = {
122
- "number": segment_id,
123
- "outline": corners,
124
- "name": name,
125
- "x": ((x_min + x_max) // 2),
126
- "y": ((y_min + y_max) // 2),
127
- }
81
+ room_properties = await self.rooms_handler.async_extract_room_properties(
82
+ json_data
83
+ )
128
84
  if room_properties:
129
- _LOGGER.debug("%s: Rooms data extracted!", self.file_name)
85
+ rooms = RoomStore(self.file_name, room_properties)
86
+ LOGGER.debug(
87
+ "%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
88
+ )
89
+ # Convert room_properties to the format expected by async_get_robot_in_room
90
+ self.rooms_pos = []
91
+ for room_id, room_data in room_properties.items():
92
+ self.rooms_pos.append(
93
+ {
94
+ "id": room_id,
95
+ "name": room_data["name"],
96
+ "outline": room_data["outline"],
97
+ }
98
+ )
130
99
  else:
131
- _LOGGER.debug("%s: Rooms data not available!", self.file_name)
100
+ LOGGER.debug("%s: Rooms data not available!", self.file_name)
132
101
  self.rooms_pos = None
133
102
  return room_properties
134
103
 
135
- async def _async_initialize_colors(self):
136
- """Initialize and return all required colors."""
137
- return {
138
- "color_wall": self.colors_manager.get_colour(SupportedColor.WALLS),
139
- "color_no_go": self.colors_manager.get_colour(SupportedColor.NO_GO),
140
- "color_go_to": self.colors_manager.get_colour(SupportedColor.GO_TO),
141
- "color_robot": self.colors_manager.get_colour(SupportedColor.ROBOT),
142
- "color_charger": self.colors_manager.get_colour(SupportedColor.CHARGER),
143
- "color_move": self.colors_manager.get_colour(SupportedColor.PATH),
144
- "color_background": self.colors_manager.get_colour(
145
- SupportedColor.MAP_BACKGROUND
146
- ),
147
- "color_zone_clean": self.colors_manager.get_colour(
148
- SupportedColor.ZONE_CLEAN
149
- ),
150
- }
151
-
152
104
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
153
105
  async def async_get_image_from_json(
154
106
  self,
155
- m_json: json | None,
156
- ) -> Image.Image | None:
107
+ m_json: JsonType | None,
108
+ return_webp: bool = False,
109
+ ) -> WebPBytes | Image.Image | None:
157
110
  """Get the image from the JSON data.
158
111
  It uses the ImageDraw class to draw some of the elements of the image.
159
112
  The robot itself will be drawn in this function as per some of the values are needed for other tasks.
160
113
  @param m_json: The JSON data to use to draw the image.
161
- @return Image.Image: The image to display.
114
+ @param return_webp: If True, return WebP bytes; if False, return PIL Image (default).
115
+ @return WebPBytes | Image.Image: WebP bytes or PIL Image depending on return_webp parameter.
162
116
  """
163
117
  # Initialize the colors.
164
- colors = await self._async_initialize_colors()
118
+ colors: Colors = {
119
+ name: self.shared.user_colors[idx] for idx, name in enumerate(COLORS)
120
+ }
165
121
  # Check if the JSON data is not None else process the image.
166
122
  try:
167
123
  if m_json is not None:
168
- _LOGGER.debug("%s: Creating Image.", self.file_name)
124
+ LOGGER.debug("%s: Creating Image.", self.file_name)
169
125
  # buffer json data
170
126
  self.json_data = m_json
171
127
  # Get the image size from the JSON data
@@ -190,135 +146,285 @@ class HypferMapImageHandler:
190
146
  # Get the pixels size and layers from the JSON data
191
147
  pixel_size = int(m_json["pixelSize"])
192
148
  layers, active = self.data.find_layers(m_json["layers"], {}, [])
193
- new_frame_hash = await self.imd.calculate_array_hash(layers, active)
149
+ # Populate active_zones from the JSON data
150
+ self.active_zones = active
151
+ new_frame_hash = await self.calculate_array_hash(layers, active)
194
152
  if self.frame_number == 0:
195
153
  self.img_hash = new_frame_hash
196
- # empty image
154
+ # Create empty image
197
155
  img_np_array = await self.draw.create_empty_image(
198
- size_x, size_y, colors["color_background"]
156
+ size_x, size_y, colors["background"]
199
157
  )
200
- # overlapping layers and segments
201
- for layer_type, compressed_pixels_list in layers.items():
202
- room_id, img_np_array = await self.imd.async_draw_base_layer(
203
- img_np_array,
204
- compressed_pixels_list,
205
- layer_type,
206
- colors["color_wall"],
207
- colors["color_zone_clean"],
208
- pixel_size,
158
+ # Draw layers and segments if enabled
159
+ room_id = 0
160
+ # Keep track of disabled rooms to skip their walls later
161
+ disabled_rooms = set()
162
+
163
+ if self.drawing_config.is_enabled(DrawableElement.FLOOR):
164
+ # First pass: identify disabled rooms
165
+ for layer_type, compressed_pixels_list in layers.items():
166
+ # Check if this is a room layer
167
+ if layer_type == "segment":
168
+ # The room_id is the current room being processed (0-based index)
169
+ # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
170
+ current_room_id = room_id + 1
171
+ if 1 <= current_room_id <= 15:
172
+ room_element = getattr(
173
+ DrawableElement, f"ROOM_{current_room_id}", None
174
+ )
175
+ if (
176
+ room_element
177
+ and not self.drawing_config.is_enabled(
178
+ room_element
179
+ )
180
+ ):
181
+ # Add this room to the disabled rooms set
182
+ disabled_rooms.add(room_id)
183
+ LOGGER.debug(
184
+ "%s: Room %d is disabled and will be skipped",
185
+ self.file_name,
186
+ current_room_id,
187
+ )
188
+ room_id = (
189
+ room_id + 1
190
+ ) % 16 # Cycle room_id back to 0 after 15
191
+
192
+ # Reset room_id for the actual drawing pass
193
+ room_id = 0
194
+
195
+ # Second pass: draw enabled rooms and walls
196
+ for layer_type, compressed_pixels_list in layers.items():
197
+ # Check if this is a room layer
198
+ is_room_layer = layer_type == "segment"
199
+
200
+ # If it's a room layer, check if the specific room is enabled
201
+ if is_room_layer:
202
+ # The room_id is the current room being processed (0-based index)
203
+ # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
204
+ current_room_id = room_id + 1
205
+ if 1 <= current_room_id <= 15:
206
+ room_element = getattr(
207
+ DrawableElement, f"ROOM_{current_room_id}", None
208
+ )
209
+
210
+ # Skip this room if it's disabled
211
+ if not self.drawing_config.is_enabled(room_element):
212
+ room_id = (
213
+ room_id + 1
214
+ ) % 16 # Increment room_id even if we skip
215
+ continue
216
+
217
+ # Draw the layer ONLY if enabled
218
+ is_wall_layer = layer_type == "wall"
219
+ if is_wall_layer:
220
+ # Skip walls entirely if disabled
221
+ if not self.drawing_config.is_enabled(
222
+ DrawableElement.WALL
223
+ ):
224
+ continue
225
+ # Draw the layer
226
+ (
227
+ room_id,
228
+ img_np_array,
229
+ ) = await self.imd.async_draw_base_layer(
230
+ img_np_array,
231
+ compressed_pixels_list,
232
+ layer_type,
233
+ colors["wall"],
234
+ colors["zone_clean"],
235
+ pixel_size,
236
+ disabled_rooms if layer_type == "wall" else None,
237
+ )
238
+
239
+ # Draw the virtual walls if enabled
240
+ if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
241
+ img_np_array = await self.imd.async_draw_virtual_walls(
242
+ m_json, img_np_array, colors["no_go"]
209
243
  )
210
- # Draw the virtual walls if any.
211
- img_np_array = await self.imd.async_draw_virtual_walls(
212
- m_json, img_np_array, colors["color_no_go"]
213
- )
214
- # Draw charger.
215
- img_np_array = await self.imd.async_draw_charger(
216
- img_np_array, entity_dict, colors["color_charger"]
217
- )
218
- # Draw obstacles if any.
219
- img_np_array = await self.imd.async_draw_obstacle(
220
- img_np_array, entity_dict, colors["color_no_go"]
221
- )
244
+
245
+ # Draw charger if enabled
246
+ if self.drawing_config.is_enabled(DrawableElement.CHARGER):
247
+ img_np_array = await self.imd.async_draw_charger(
248
+ img_np_array, entity_dict, colors["charger"]
249
+ )
250
+
251
+ # Draw obstacles if enabled
252
+ if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
253
+ self.shared.obstacles_pos = self.data.get_obstacles(entity_dict)
254
+ if self.shared.obstacles_pos:
255
+ img_np_array = await self.imd.async_draw_obstacle(
256
+ img_np_array, self.shared.obstacles_pos, colors["no_go"]
257
+ )
222
258
  # Robot and rooms position
223
259
  if (room_id > 0) and not self.room_propriety:
224
260
  self.room_propriety = await self.async_extract_room_properties(
225
261
  self.json_data
226
262
  )
227
- if self.rooms_pos and robot_position and robot_position_angle:
228
- self.robot_pos = await self.imd.async_get_robot_in_room(
229
- robot_x=(robot_position[0]),
230
- robot_y=(robot_position[1]),
231
- angle=robot_position_angle,
232
- )
233
- _LOGGER.info("%s: Completed base Layers", self.file_name)
263
+
264
+ # Ensure room data is available for robot room detection (even if not extracted above)
265
+ if not self.rooms_pos and not self.room_propriety:
266
+ self.room_propriety = await self.async_extract_room_properties(
267
+ self.json_data
268
+ )
269
+
270
+ # Always check robot position for zooming (moved outside the condition)
271
+ if self.rooms_pos and robot_position and robot_position_angle:
272
+ self.robot_pos = await self.imd.async_get_robot_in_room(
273
+ robot_x=(robot_position[0]),
274
+ robot_y=(robot_position[1]),
275
+ angle=robot_position_angle,
276
+ )
277
+ LOGGER.info("%s: Completed base Layers", self.file_name)
234
278
  # Copy the new array in base layer.
235
- self.img_base_layer = await self.imd.async_copy_array(img_np_array)
279
+ self.img_base_layer = await self.async_copy_array(img_np_array)
280
+
236
281
  self.shared.frame_number = self.frame_number
237
282
  self.frame_number += 1
238
283
  if (self.frame_number >= self.max_frames) or (
239
284
  new_frame_hash != self.img_hash
240
285
  ):
241
286
  self.frame_number = 0
242
- _LOGGER.debug(
287
+ LOGGER.debug(
243
288
  "%s: %s at Frame Number: %s",
244
289
  self.file_name,
245
290
  str(self.json_id),
246
291
  str(self.frame_number),
247
292
  )
248
- # Copy the base layer to the new image.
249
- img_np_array = await self.imd.async_copy_array(self.img_base_layer)
250
- # All below will be drawn at each frame.
251
- # Draw zones if any.
252
- img_np_array = await self.imd.async_draw_zones(
253
- m_json,
254
- img_np_array,
255
- colors["color_zone_clean"],
256
- colors["color_no_go"],
257
- )
258
- # Draw the go_to target flag.
259
- img_np_array = await self.imd.draw_go_to_flag(
260
- img_np_array, entity_dict, colors["color_go_to"]
261
- )
262
- # Draw path prediction and paths.
263
- img_np_array = await self.imd.async_draw_paths(
264
- img_np_array, m_json, colors["color_move"], self.color_grey
293
+ # Ensure persistent working buffer exists and matches base (allocate only when needed)
294
+ if (
295
+ self.img_work_layer is None
296
+ or self.img_work_layer.shape != self.img_base_layer.shape
297
+ or self.img_work_layer.dtype != self.img_base_layer.dtype
298
+ ):
299
+ self.img_work_layer = np.empty_like(self.img_base_layer)
300
+
301
+ # Copy the base layer into the persistent working buffer (no new allocation per frame)
302
+ np.copyto(self.img_work_layer, self.img_base_layer)
303
+ img_np_array = self.img_work_layer
304
+
305
+ # Prepare parallel data extraction tasks
306
+ data_tasks = []
307
+
308
+ # Prepare zone data extraction
309
+ if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
310
+ data_tasks.append(self._prepare_zone_data(m_json))
311
+
312
+ # Prepare go_to flag data extraction
313
+ if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
314
+ data_tasks.append(self._prepare_goto_data(entity_dict))
315
+
316
+ # Prepare path data extraction
317
+ path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
318
+ LOGGER.info(
319
+ "%s: PATH element enabled: %s", self.file_name, path_enabled
265
320
  )
321
+ if path_enabled:
322
+ LOGGER.info("%s: Drawing path", self.file_name)
323
+ data_tasks.append(self._prepare_path_data(m_json))
324
+
325
+ # Await all data preparation tasks if any were created
326
+ if data_tasks:
327
+ await asyncio.gather(*data_tasks)
328
+
329
+ # Process drawing operations sequentially (since they modify the same array)
330
+ # Draw zones if enabled
331
+ if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
332
+ img_np_array = await self.imd.async_draw_zones(
333
+ m_json, img_np_array, colors["zone_clean"], colors["no_go"]
334
+ )
335
+
336
+ # Draw the go_to target flag if enabled
337
+ if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
338
+ img_np_array = await self.imd.draw_go_to_flag(
339
+ img_np_array, entity_dict, colors["go_to"]
340
+ )
341
+
342
+ # Draw paths if enabled
343
+ if path_enabled:
344
+ img_np_array = await self.imd.async_draw_paths(
345
+ img_np_array, m_json, colors["move"], self.color_grey
346
+ )
347
+ else:
348
+ LOGGER.info("%s: Skipping path drawing", self.file_name)
349
+
266
350
  # Check if the robot is docked.
267
351
  if self.shared.vacuum_state == "docked":
268
352
  # Adjust the robot angle.
269
353
  robot_position_angle -= 180
270
354
 
271
- if robot_pos:
355
+ # Draw the robot if enabled
356
+ if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
357
+ # Get robot color (allows for customization)
358
+ robot_color = self.drawing_config.get_property(
359
+ DrawableElement.ROBOT, "color", colors["robot"]
360
+ )
361
+
272
362
  # Draw the robot
273
363
  img_np_array = await self.draw.robot(
274
364
  layers=img_np_array,
275
365
  x=robot_position[0],
276
366
  y=robot_position[1],
277
367
  angle=robot_position_angle,
278
- fill=colors["color_robot"],
368
+ fill=robot_color,
279
369
  robot_state=self.shared.vacuum_state,
280
370
  )
371
+
372
+ # Update element map for robot position
373
+ if (
374
+ hasattr(self.shared, "element_map")
375
+ and self.shared.element_map is not None
376
+ ):
377
+ update_element_map_with_robot(
378
+ self.shared.element_map,
379
+ robot_position,
380
+ DrawableElement.ROBOT,
381
+ )
382
+ # Synchronize zooming state from ImageDraw to handler before auto-crop
383
+ self.zooming = self.imd.img_h.zooming
384
+
281
385
  # Resize the image
282
- img_np_array = await self.ac.async_auto_trim_and_zoom_image(
386
+ img_np_array = await self.async_auto_trim_and_zoom_image(
283
387
  img_np_array,
284
- colors["color_background"],
388
+ colors["background"],
285
389
  int(self.shared.margins),
286
390
  int(self.shared.image_rotate),
287
391
  self.zooming,
288
392
  )
289
393
  # If the image is None return None and log the error.
290
394
  if img_np_array is None:
291
- _LOGGER.warning("%s: Image array is None.", self.file_name)
395
+ LOGGER.warning("%s: Image array is None.", self.file_name)
292
396
  return None
293
397
 
294
- # Convert the numpy array to a PIL image
295
- pil_img = Image.fromarray(img_np_array, mode="RGBA")
296
- del img_np_array
297
- # reduce the image size if the zoomed image is bigger then the original.
298
- if (
299
- self.shared.image_auto_zoom
300
- and self.shared.vacuum_state == "cleaning"
301
- and self.zooming
302
- and self.shared.image_zoom_lock_ratio
303
- or self.shared.image_aspect_ratio != "None"
304
- ):
305
- width = self.shared.image_ref_width
306
- height = self.shared.image_ref_height
307
- (
308
- resized_image,
309
- self.crop_img_size,
310
- ) = await resize_to_aspect_ratio(
311
- pil_img,
312
- width,
313
- height,
314
- self.shared.image_aspect_ratio,
315
- self.async_map_coordinates_offset,
316
- )
317
- return resized_image
318
- _LOGGER.debug("%s: Frame Completed.", self.file_name)
319
- return pil_img
398
+ # Handle resizing if needed, then return based on format preference
399
+ if self.check_zoom_and_aspect_ratio():
400
+ # Convert to PIL for resizing
401
+ pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
402
+ del img_np_array
403
+ resize_params = prepare_resize_params(self, pil_img, False)
404
+ resized_image = await self.async_resize_images(resize_params)
405
+
406
+ # Return WebP bytes or PIL Image based on parameter
407
+ if return_webp:
408
+ webp_bytes = await pil_to_webp_bytes(resized_image)
409
+ return webp_bytes
410
+ else:
411
+ return resized_image
412
+ else:
413
+ # Return WebP bytes or PIL Image based on parameter
414
+ if return_webp:
415
+ # Convert directly from NumPy to WebP for better performance
416
+ webp_bytes = await numpy_to_webp_bytes(img_np_array)
417
+ del img_np_array
418
+ LOGGER.debug("%s: Frame Completed.", self.file_name)
419
+ return webp_bytes
420
+ else:
421
+ # Convert to PIL Image (original behavior)
422
+ pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
423
+ del img_np_array
424
+ LOGGER.debug("%s: Frame Completed.", self.file_name)
425
+ return pil_img
320
426
  except (RuntimeError, RuntimeWarning) as e:
321
- _LOGGER.warning(
427
+ LOGGER.warning(
322
428
  "%s: Error %s during image creation.",
323
429
  self.file_name,
324
430
  str(e),
@@ -326,38 +432,18 @@ class HypferMapImageHandler:
326
432
  )
327
433
  return None
328
434
 
329
- def get_frame_number(self) -> int:
330
- """Return the frame number of the image."""
331
- return self.frame_number
332
-
333
- def get_robot_position(self) -> RobotPosition | None:
334
- """Return the robot position."""
335
- return self.robot_pos
336
-
337
- def get_charger_position(self) -> ChargerPosition | None:
338
- """Return the charger position."""
339
- return self.charger_pos
340
-
341
- def get_img_size(self) -> ImageSize | None:
342
- """Return the size of the image."""
343
- return self.img_size
344
-
345
- def get_json_id(self) -> str | None:
346
- """Return the JSON ID from the image."""
347
- return self.json_id
348
-
349
435
  async def async_get_rooms_attributes(self) -> RoomsProperties:
350
436
  """Get the rooms attributes from the JSON data.
351
437
  :return: The rooms attribute's."""
352
438
  if self.room_propriety:
353
439
  return self.room_propriety
354
440
  if self.json_data:
355
- _LOGGER.debug("Checking %s Rooms data..", self.file_name)
441
+ LOGGER.debug("Checking %s Rooms data..", self.file_name)
356
442
  self.room_propriety = await self.async_extract_room_properties(
357
443
  self.json_data
358
444
  )
359
445
  if self.room_propriety:
360
- _LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
446
+ LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
361
447
  return self.room_propriety
362
448
 
363
449
  def get_calibration_data(self) -> CalibrationPoints:
@@ -365,20 +451,12 @@ class HypferMapImageHandler:
365
451
  this will create the attribute calibration points."""
366
452
  calibration_data = []
367
453
  rotation_angle = self.shared.image_rotate
368
- _LOGGER.info("Getting %s Calibrations points.", self.file_name)
454
+ LOGGER.info("Getting %s Calibrations points.", self.file_name)
369
455
 
370
456
  # Define the map points (fixed)
371
- map_points = [
372
- {"x": 0, "y": 0}, # Top-left corner 0
373
- {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
374
- {
375
- "x": self.crop_img_size[0],
376
- "y": self.crop_img_size[1],
377
- }, # Bottom-right corner 2
378
- {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
379
- ]
457
+ map_points = self.get_map_points()
380
458
  # Calculate the calibration points in the vacuum coordinate system
381
- vacuum_points = self.imu.get_vacuum_points(rotation_angle)
459
+ vacuum_points = self.get_vacuum_points(rotation_angle)
382
460
 
383
461
  # Create the calibration data for each point
384
462
  for vacuum_point, map_point in zip(vacuum_points, map_points):
@@ -387,32 +465,61 @@ class HypferMapImageHandler:
387
465
  del vacuum_points, map_points, calibration_point, rotation_angle # free memory.
388
466
  return calibration_data
389
467
 
390
- async def async_map_coordinates_offset(
391
- self, wsf: int, hsf: int, width: int, height: int
392
- ) -> tuple[int, int]:
393
- """
394
- Offset the coordinates to the map.
395
- :param wsf: Width scale factor.
396
- :param hsf: Height scale factor.
397
- :param width: Width of the image.
398
- :param height: Height of the image.
399
- :return: A tuple containing the adjusted (width, height) values
400
- :raises ValueError: If any input parameters are negative
401
- """
468
+ # Element selection methods
469
+ def enable_element(self, element_code: DrawableElement) -> None:
470
+ """Enable drawing of a specific element."""
471
+ self.drawing_config.enable_element(element_code)
472
+ LOGGER.info(
473
+ "%s: Enabled element %s, now enabled: %s",
474
+ self.file_name,
475
+ element_code.name,
476
+ self.drawing_config.is_enabled(element_code),
477
+ )
402
478
 
403
- if any(x < 0 for x in (wsf, hsf, width, height)):
404
- raise ValueError("All parameters must be positive integers")
405
-
406
- if wsf == 1 and hsf == 1:
407
- self.imu.set_image_offset_ratio_1_1(width, height)
408
- elif wsf == 2 and hsf == 1:
409
- self.imu.set_image_offset_ratio_2_1(width, height)
410
- elif wsf == 3 and hsf == 2:
411
- self.imu.set_image_offset_ratio_3_2(width, height)
412
- elif wsf == 5 and hsf == 4:
413
- self.imu.set_image_offset_ratio_5_4(width, height)
414
- elif wsf == 9 and hsf == 16:
415
- self.imu.set_image_offset_ratio_9_16(width, height)
416
- elif wsf == 16 and hsf == 9:
417
- self.imu.set_image_offset_ratio_16_9(width, height)
418
- return width, height
479
+ def disable_element(self, element_code: DrawableElement) -> None:
480
+ """Disable drawing of a specific element."""
481
+ manage_drawable_elements(self, "disable", element_code=element_code)
482
+
483
+ def set_elements(self, element_codes: list[DrawableElement]) -> None:
484
+ """Enable only the specified elements, disable all others."""
485
+ manage_drawable_elements(self, "set_elements", element_codes=element_codes)
486
+
487
+ def set_element_property(
488
+ self, element_code: DrawableElement, property_name: str, value
489
+ ) -> None:
490
+ """Set a drawing property for an element."""
491
+ manage_drawable_elements(
492
+ self,
493
+ "set_property",
494
+ element_code=element_code,
495
+ property_name=property_name,
496
+ value=value,
497
+ )
498
+
499
+ @staticmethod
500
+ async def async_copy_array(original_array):
501
+ """Copy the array."""
502
+ return await AsyncNumPy.async_copy(original_array)
503
+
504
+ async def _prepare_zone_data(self, m_json):
505
+ """Prepare zone data for parallel processing."""
506
+ await asyncio.sleep(0) # Yield control
507
+ try:
508
+ return self.data.find_zone_entities(m_json)
509
+ except (ValueError, KeyError):
510
+ return None
511
+
512
+ @staticmethod
513
+ async def _prepare_goto_data(entity_dict):
514
+ """Prepare go-to flag data for parallel processing."""
515
+ await asyncio.sleep(0) # Yield control
516
+ # Extract go-to target data from entity_dict
517
+ return entity_dict.get("go_to_target", None)
518
+
519
+ async def _prepare_path_data(self, m_json):
520
+ """Prepare path data for parallel processing."""
521
+ await asyncio.sleep(0) # Yield control
522
+ try:
523
+ return self.data.find_paths_entities(m_json)
524
+ except (ValueError, KeyError):
525
+ return None