valetudo-map-parser 0.1.8__py3-none-any.whl → 0.1.9a1__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 (28) hide show
  1. valetudo_map_parser/__init__.py +19 -12
  2. valetudo_map_parser/config/auto_crop.py +174 -116
  3. valetudo_map_parser/config/color_utils.py +105 -0
  4. valetudo_map_parser/config/colors.py +662 -13
  5. valetudo_map_parser/config/drawable.py +624 -279
  6. valetudo_map_parser/config/drawable_elements.py +292 -0
  7. valetudo_map_parser/config/enhanced_drawable.py +324 -0
  8. valetudo_map_parser/config/optimized_element_map.py +406 -0
  9. valetudo_map_parser/config/rand25_parser.py +42 -28
  10. valetudo_map_parser/config/room_outline.py +148 -0
  11. valetudo_map_parser/config/shared.py +29 -5
  12. valetudo_map_parser/config/types.py +102 -51
  13. valetudo_map_parser/config/utils.py +841 -0
  14. valetudo_map_parser/hypfer_draw.py +398 -132
  15. valetudo_map_parser/hypfer_handler.py +259 -241
  16. valetudo_map_parser/hypfer_rooms_handler.py +599 -0
  17. valetudo_map_parser/map_data.py +45 -64
  18. valetudo_map_parser/rand25_handler.py +429 -310
  19. valetudo_map_parser/reimg_draw.py +55 -74
  20. valetudo_map_parser/rooms_handler.py +470 -0
  21. valetudo_map_parser-0.1.9a1.dist-info/METADATA +93 -0
  22. valetudo_map_parser-0.1.9a1.dist-info/RECORD +27 -0
  23. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a1.dist-info}/WHEEL +1 -1
  24. valetudo_map_parser/images_utils.py +0 -398
  25. valetudo_map_parser-0.1.8.dist-info/METADATA +0 -23
  26. valetudo_map_parser-0.1.8.dist-info/RECORD +0 -20
  27. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a1.dist-info}/LICENSE +0 -0
  28. {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a1.dist-info}/NOTICE.txt +0 -0
@@ -2,153 +2,92 @@
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
10
  import json
11
- import logging
12
11
 
13
12
  from PIL import Image
14
13
 
14
+ from .config.auto_crop import AutoCrop
15
+ from .config.drawable_elements import DrawableElement
16
+ from .config.shared import CameraShared
15
17
  from .config.types import (
18
+ COLORS,
19
+ LOGGER,
16
20
  CalibrationPoints,
17
- ChargerPosition,
18
- ImageSize,
19
- RobotPosition,
21
+ Colors,
20
22
  RoomsProperties,
23
+ RoomStore,
21
24
  )
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,
25
+ from .config.utils import (
26
+ BaseHandler,
27
+ initialize_drawing_config,
28
+ manage_drawable_elements,
29
+ prepare_resize_params,
32
30
  )
33
- from .config.colors import ColorsManagment, SupportedColor
34
-
35
- _LOGGER = logging.getLogger(__name__)
31
+ from .hypfer_draw import ImageDraw as ImDraw
32
+ from .map_data import ImageData
33
+ from .rooms_handler import RoomsHandler
36
34
 
37
35
 
38
- class HypferMapImageHandler:
36
+ class HypferMapImageHandler(BaseHandler, AutoCrop):
39
37
  """Map Image Handler Class.
40
38
  This class is used to handle the image data and the drawing of the map."""
41
39
 
42
40
  def __init__(self, shared_data: CameraShared):
43
41
  """Initialize the Map Image Handler."""
42
+ BaseHandler.__init__(self)
44
43
  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.
44
+ AutoCrop.__init__(self, self)
47
45
  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
46
  self.data = ImageData # imported Image Data Module.
52
- self.draw = Drawable # imported Drawing utilities
47
+
48
+ # Initialize drawing configuration using the shared utility function
49
+ self.drawing_config, self.draw, self.enhanced_draw = initialize_drawing_config(
50
+ self
51
+ )
52
+
53
53
  self.go_to = None # vacuum go to data
54
54
  self.img_hash = None # hash of the image calculated to check differences.
55
55
  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.
64
56
  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
57
  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()
58
+ self.imd = ImDraw(self) # Image Draw class.
84
59
  self.color_grey = (128, 128, 128, 255)
60
+ self.file_name = self.shared.file_name # file name of the vacuum.
61
+ self.rooms_handler = RoomsHandler(
62
+ self.file_name, self.drawing_config
63
+ ) # Room data handler
64
+
65
+ @staticmethod
66
+ def get_corners(x_max, x_min, y_max, y_min):
67
+ """Get the corners of the room."""
68
+ return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
85
69
 
86
70
  async def async_extract_room_properties(self, json_data) -> RoomsProperties:
87
71
  """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
- }
72
+ room_properties = await self.rooms_handler.async_extract_room_properties(
73
+ json_data
74
+ )
128
75
  if room_properties:
129
- _LOGGER.debug("%s: Rooms data extracted!", self.file_name)
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
+ )
130
86
  else:
131
- _LOGGER.debug("%s: Rooms data not available!", self.file_name)
87
+ LOGGER.debug("%s: Rooms data not available!", self.file_name)
132
88
  self.rooms_pos = None
133
89
  return room_properties
134
90
 
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
91
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
153
92
  async def async_get_image_from_json(
154
93
  self,
@@ -161,11 +100,13 @@ class HypferMapImageHandler:
161
100
  @return Image.Image: The image to display.
162
101
  """
163
102
  # Initialize the colors.
164
- colors = await self._async_initialize_colors()
103
+ colors: Colors = {
104
+ name: self.shared.user_colors[idx] for idx, name in enumerate(COLORS)
105
+ }
165
106
  # Check if the JSON data is not None else process the image.
166
107
  try:
167
108
  if m_json is not None:
168
- _LOGGER.debug("%s: Creating Image.", self.file_name)
109
+ LOGGER.debug("%s: Creating Image.", self.file_name)
169
110
  # buffer json data
170
111
  self.json_data = m_json
171
112
  # Get the image size from the JSON data
@@ -190,35 +131,120 @@ class HypferMapImageHandler:
190
131
  # Get the pixels size and layers from the JSON data
191
132
  pixel_size = int(m_json["pixelSize"])
192
133
  layers, active = self.data.find_layers(m_json["layers"], {}, [])
193
- new_frame_hash = await self.imd.calculate_array_hash(layers, active)
134
+ new_frame_hash = await self.calculate_array_hash(layers, active)
194
135
  if self.frame_number == 0:
195
136
  self.img_hash = new_frame_hash
196
- # empty image
137
+ # Create empty image
197
138
  img_np_array = await self.draw.create_empty_image(
198
- size_x, size_y, colors["color_background"]
139
+ size_x, size_y, colors["background"]
199
140
  )
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,
141
+ # Draw layers and segments if enabled
142
+ room_id = 0
143
+ # Keep track of disabled rooms to skip their walls later
144
+ disabled_rooms = set()
145
+
146
+ if self.drawing_config.is_enabled(DrawableElement.FLOOR):
147
+ # First pass: identify disabled rooms
148
+ for layer_type, compressed_pixels_list in layers.items():
149
+ # Check if this is a room layer
150
+ if layer_type == "segment":
151
+ # The room_id is the current room being processed (0-based index)
152
+ # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
153
+ current_room_id = room_id + 1
154
+ if 1 <= current_room_id <= 15:
155
+ room_element = getattr(
156
+ DrawableElement, f"ROOM_{current_room_id}", None
157
+ )
158
+ if (
159
+ room_element
160
+ and not self.drawing_config.is_enabled(
161
+ room_element
162
+ )
163
+ ):
164
+ # Add this room to the disabled rooms set
165
+ disabled_rooms.add(room_id)
166
+ LOGGER.debug(
167
+ "%s: Room %d is disabled and will be skipped",
168
+ self.file_name,
169
+ current_room_id,
170
+ )
171
+ room_id = (
172
+ room_id + 1
173
+ ) % 16 # Cycle room_id back to 0 after 15
174
+
175
+ # Reset room_id for the actual drawing pass
176
+ room_id = 0
177
+
178
+ # Second pass: draw enabled rooms and walls
179
+ for layer_type, compressed_pixels_list in layers.items():
180
+ # Check if this is a room layer
181
+ is_room_layer = layer_type == "segment"
182
+
183
+ # If it's a room layer, check if the specific room is enabled
184
+ if is_room_layer:
185
+ # The room_id is the current room being processed (0-based index)
186
+ # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
187
+ current_room_id = room_id + 1
188
+ if 1 <= current_room_id <= 15:
189
+ room_element = getattr(
190
+ DrawableElement, f"ROOM_{current_room_id}", None
191
+ )
192
+
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
199
+
200
+ # Check if this is a wall layer and if walls are enabled
201
+ is_wall_layer = layer_type == "wall"
202
+ if is_wall_layer:
203
+ if not self.drawing_config.is_enabled(
204
+ DrawableElement.WALL
205
+ ):
206
+ pass
207
+
208
+ # Draw the layer
209
+ (
210
+ room_id,
211
+ img_np_array,
212
+ ) = await self.imd.async_draw_base_layer(
213
+ img_np_array,
214
+ compressed_pixels_list,
215
+ layer_type,
216
+ colors["wall"],
217
+ colors["zone_clean"],
218
+ pixel_size,
219
+ disabled_rooms if layer_type == "wall" else None,
220
+ )
221
+
222
+ # Update element map for this layer
223
+ if is_room_layer and 0 < room_id <= 15:
224
+ # Mark the room in the element map
225
+ room_element = getattr(
226
+ DrawableElement, f"ROOM_{room_id}", None
227
+ )
228
+
229
+ # Draw the virtual walls if enabled
230
+ if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
231
+ img_np_array = await self.imd.async_draw_virtual_walls(
232
+ m_json, img_np_array, colors["no_go"]
209
233
  )
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
- )
234
+
235
+ # Draw charger if enabled
236
+ if self.drawing_config.is_enabled(DrawableElement.CHARGER):
237
+ img_np_array = await self.imd.async_draw_charger(
238
+ img_np_array, entity_dict, colors["charger"]
239
+ )
240
+
241
+ # Draw obstacles if enabled
242
+ if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
243
+ self.shared.obstacles_pos = self.data.get_obstacles(entity_dict)
244
+ if self.shared.obstacles_pos:
245
+ img_np_array = await self.imd.async_draw_obstacle(
246
+ img_np_array, self.shared.obstacles_pos, colors["no_go"]
247
+ )
222
248
  # Robot and rooms position
223
249
  if (room_id > 0) and not self.room_propriety:
224
250
  self.room_propriety = await self.async_extract_room_properties(
@@ -230,95 +256,109 @@ class HypferMapImageHandler:
230
256
  robot_y=(robot_position[1]),
231
257
  angle=robot_position_angle,
232
258
  )
233
- _LOGGER.info("%s: Completed base Layers", self.file_name)
259
+ LOGGER.info("%s: Completed base Layers", self.file_name)
234
260
  # Copy the new array in base layer.
235
- self.img_base_layer = await self.imd.async_copy_array(img_np_array)
261
+ self.img_base_layer = await self.async_copy_array(img_np_array)
236
262
  self.shared.frame_number = self.frame_number
237
263
  self.frame_number += 1
238
264
  if (self.frame_number >= self.max_frames) or (
239
265
  new_frame_hash != self.img_hash
240
266
  ):
241
267
  self.frame_number = 0
242
- _LOGGER.debug(
268
+ LOGGER.debug(
243
269
  "%s: %s at Frame Number: %s",
244
270
  self.file_name,
245
271
  str(self.json_id),
246
272
  str(self.frame_number),
247
273
  )
248
274
  # Copy the base layer to the new image.
249
- img_np_array = await self.imd.async_copy_array(self.img_base_layer)
275
+ img_np_array = await self.async_copy_array(self.img_base_layer)
250
276
  # 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
277
+ # Draw zones if any and if enabled
278
+ if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
279
+ img_np_array = await self.imd.async_draw_zones(
280
+ m_json,
281
+ img_np_array,
282
+ colors["zone_clean"],
283
+ colors["no_go"],
284
+ )
285
+
286
+ # Draw the go_to target flag if enabled
287
+ if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
288
+ img_np_array = await self.imd.draw_go_to_flag(
289
+ img_np_array, entity_dict, colors["go_to"]
290
+ )
291
+
292
+ # Draw path prediction and paths if enabled
293
+ path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
294
+ LOGGER.info(
295
+ "%s: PATH element enabled: %s", self.file_name, path_enabled
265
296
  )
297
+ if path_enabled:
298
+ LOGGER.info("%s: Drawing path", self.file_name)
299
+ img_np_array = await self.imd.async_draw_paths(
300
+ img_np_array, m_json, colors["move"], self.color_grey
301
+ )
302
+ else:
303
+ LOGGER.info("%s: Skipping path drawing", self.file_name)
304
+
266
305
  # Check if the robot is docked.
267
306
  if self.shared.vacuum_state == "docked":
268
307
  # Adjust the robot angle.
269
308
  robot_position_angle -= 180
270
309
 
271
- if robot_pos:
310
+ # Draw the robot if enabled
311
+ if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
312
+ # Get robot color (allows for customization)
313
+ robot_color = self.drawing_config.get_property(
314
+ DrawableElement.ROBOT, "color", colors["robot"]
315
+ )
316
+
272
317
  # Draw the robot
273
318
  img_np_array = await self.draw.robot(
274
319
  layers=img_np_array,
275
320
  x=robot_position[0],
276
321
  y=robot_position[1],
277
322
  angle=robot_position_angle,
278
- fill=colors["color_robot"],
323
+ fill=robot_color,
279
324
  robot_state=self.shared.vacuum_state,
280
325
  )
326
+
327
+ # Update element map for robot position
328
+ if (
329
+ hasattr(self.shared, "element_map")
330
+ and self.shared.element_map is not None
331
+ ):
332
+ update_element_map_with_robot(
333
+ self.shared.element_map,
334
+ robot_position,
335
+ DrawableElement.ROBOT,
336
+ )
281
337
  # Resize the image
282
- img_np_array = await self.ac.async_auto_trim_and_zoom_image(
338
+ img_np_array = await self.async_auto_trim_and_zoom_image(
283
339
  img_np_array,
284
- colors["color_background"],
340
+ colors["background"],
285
341
  int(self.shared.margins),
286
342
  int(self.shared.image_rotate),
287
343
  self.zooming,
288
344
  )
289
345
  # If the image is None return None and log the error.
290
346
  if img_np_array is None:
291
- _LOGGER.warning("%s: Image array is None.", self.file_name)
347
+ LOGGER.warning("%s: Image array is None.", self.file_name)
292
348
  return None
293
349
 
294
350
  # Convert the numpy array to a PIL image
295
351
  pil_img = Image.fromarray(img_np_array, mode="RGBA")
296
352
  del img_np_array
297
353
  # 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
- )
354
+ if self.check_zoom_and_aspect_ratio():
355
+ resize_params = prepare_resize_params(self, pil_img, False)
356
+ resized_image = await self.async_resize_images(resize_params)
317
357
  return resized_image
318
- _LOGGER.debug("%s: Frame Completed.", self.file_name)
358
+ LOGGER.debug("%s: Frame Completed.", self.file_name)
319
359
  return pil_img
320
360
  except (RuntimeError, RuntimeWarning) as e:
321
- _LOGGER.warning(
361
+ LOGGER.warning(
322
362
  "%s: Error %s during image creation.",
323
363
  self.file_name,
324
364
  str(e),
@@ -326,38 +366,18 @@ class HypferMapImageHandler:
326
366
  )
327
367
  return None
328
368
 
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
369
  async def async_get_rooms_attributes(self) -> RoomsProperties:
350
370
  """Get the rooms attributes from the JSON data.
351
371
  :return: The rooms attribute's."""
352
372
  if self.room_propriety:
353
373
  return self.room_propriety
354
374
  if self.json_data:
355
- _LOGGER.debug("Checking %s Rooms data..", self.file_name)
375
+ LOGGER.debug("Checking %s Rooms data..", self.file_name)
356
376
  self.room_propriety = await self.async_extract_room_properties(
357
377
  self.json_data
358
378
  )
359
379
  if self.room_propriety:
360
- _LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
380
+ LOGGER.debug("Got %s Rooms Attributes.", self.file_name)
361
381
  return self.room_propriety
362
382
 
363
383
  def get_calibration_data(self) -> CalibrationPoints:
@@ -365,20 +385,12 @@ class HypferMapImageHandler:
365
385
  this will create the attribute calibration points."""
366
386
  calibration_data = []
367
387
  rotation_angle = self.shared.image_rotate
368
- _LOGGER.info("Getting %s Calibrations points.", self.file_name)
388
+ LOGGER.info("Getting %s Calibrations points.", self.file_name)
369
389
 
370
390
  # 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
- ]
391
+ map_points = self.get_map_points()
380
392
  # Calculate the calibration points in the vacuum coordinate system
381
- vacuum_points = self.imu.get_vacuum_points(rotation_angle)
393
+ vacuum_points = self.get_vacuum_points(rotation_angle)
382
394
 
383
395
  # Create the calibration data for each point
384
396
  for vacuum_point, map_point in zip(vacuum_points, map_points):
@@ -387,32 +399,38 @@ class HypferMapImageHandler:
387
399
  del vacuum_points, map_points, calibration_point, rotation_angle # free memory.
388
400
  return calibration_data
389
401
 
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
- """
402
+ # Element selection methods
403
+ def enable_element(self, element_code: DrawableElement) -> None:
404
+ """Enable drawing of a specific element."""
405
+ self.drawing_config.enable_element(element_code)
406
+ LOGGER.info(
407
+ "%s: Enabled element %s, now enabled: %s",
408
+ self.file_name,
409
+ element_code.name,
410
+ self.drawing_config.is_enabled(element_code),
411
+ )
412
+
413
+ def disable_element(self, element_code: DrawableElement) -> None:
414
+ """Disable drawing of a specific element."""
415
+ manage_drawable_elements(self, "disable", element_code=element_code)
416
+
417
+ def set_elements(self, element_codes: list[DrawableElement]) -> None:
418
+ """Enable only the specified elements, disable all others."""
419
+ manage_drawable_elements(self, "set_elements", element_codes=element_codes)
420
+
421
+ def set_element_property(
422
+ self, element_code: DrawableElement, property_name: str, value
423
+ ) -> None:
424
+ """Set a drawing property for an element."""
425
+ manage_drawable_elements(
426
+ self,
427
+ "set_property",
428
+ element_code=element_code,
429
+ property_name=property_name,
430
+ value=value,
431
+ )
402
432
 
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
433
+ @staticmethod
434
+ async def async_copy_array(original_array):
435
+ """Copy the array."""
436
+ return original_array.copy()