valetudo-map-parser 0.1.2__py3-none-any.whl → 0.1.3__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.
@@ -0,0 +1,422 @@
1
+ """
2
+ Image Draw Class for Valetudo Hypfer Image Handling.
3
+ This class is used to simplify the ImageHandler class.
4
+ Version: 2024.07.2
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+
13
+ from .config.types import (
14
+ Color,
15
+ JsonType,
16
+ NumpyArray,
17
+ RobotPosition,
18
+ )
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ class ImageDraw:
24
+ """Class to handle the image creation.
25
+ It Draws each element of the images, like the walls, zones, paths, etc."""
26
+
27
+ def __init__(self, image_handler):
28
+ self.img_h = image_handler
29
+ self.file_name = self.img_h.shared.file_name
30
+
31
+ async def draw_go_to_flag(
32
+ self, np_array: NumpyArray, entity_dict: dict, color_go_to: Color
33
+ ) -> NumpyArray:
34
+ """Draw the goto target flag on the map."""
35
+ go_to = entity_dict.get("go_to_target")
36
+ if go_to:
37
+ np_array = await self.img_h.draw.go_to_flag(
38
+ np_array,
39
+ (go_to[0]["points"][0], go_to[0]["points"][1]),
40
+ self.img_h.shared.image_rotate,
41
+ color_go_to,
42
+ )
43
+ return np_array
44
+
45
+ async def async_draw_base_layer(
46
+ self,
47
+ img_np_array,
48
+ compressed_pixels_list,
49
+ layer_type,
50
+ color_wall,
51
+ color_zone_clean,
52
+ pixel_size,
53
+ ):
54
+ """Draw the base layer of the map."""
55
+ room_id = 0
56
+
57
+ for compressed_pixels in compressed_pixels_list:
58
+ pixels = self.img_h.data.sublist(compressed_pixels, 3)
59
+
60
+ if layer_type in ["segment", "floor"]:
61
+ img_np_array, room_id = await self._process_room_layer(
62
+ img_np_array,
63
+ pixels,
64
+ layer_type,
65
+ room_id,
66
+ pixel_size,
67
+ color_zone_clean,
68
+ )
69
+ elif layer_type == "wall":
70
+ img_np_array = await self._process_wall_layer(
71
+ img_np_array, pixels, pixel_size, color_wall
72
+ )
73
+
74
+ return room_id, img_np_array
75
+
76
+ async def _process_room_layer(
77
+ self, img_np_array, pixels, layer_type, room_id, pixel_size, color_zone_clean
78
+ ):
79
+ """Process a room layer (segment or floor)."""
80
+ room_color = self.img_h.rooms_colors[room_id]
81
+
82
+ try:
83
+ if layer_type == "segment":
84
+ room_color = self._get_active_room_color(
85
+ room_id, room_color, color_zone_clean
86
+ )
87
+
88
+ img_np_array = await self.img_h.draw.from_json_to_image(
89
+ img_np_array, pixels, pixel_size, room_color
90
+ )
91
+ room_id = (room_id + 1) % 16 # Cycle room_id back to 0 after 15
92
+
93
+ except IndexError as e:
94
+ _LOGGER.warning("%s: Image Draw Error: %s", self.file_name, str(e))
95
+ _LOGGER.debug(
96
+ "%s Active Zones: %s and Room ID: %s",
97
+ self.file_name,
98
+ str(self.img_h.active_zones),
99
+ str(room_id),
100
+ )
101
+
102
+ return img_np_array, room_id
103
+
104
+ def _get_active_room_color(self, room_id, room_color, color_zone_clean):
105
+ """Adjust the room color if the room is active."""
106
+ if self.img_h.active_zones and room_id < len(self.img_h.active_zones):
107
+ if self.img_h.active_zones[room_id] == 1:
108
+ return tuple(
109
+ ((2 * room_color[i]) + color_zone_clean[i]) // 3 for i in range(4)
110
+ )
111
+ return room_color
112
+
113
+ async def _process_wall_layer(self, img_np_array, pixels, pixel_size, color_wall):
114
+ """Process a wall layer."""
115
+ return await self.img_h.draw.from_json_to_image(
116
+ img_np_array, pixels, pixel_size, color_wall
117
+ )
118
+
119
+ async def async_draw_obstacle(
120
+ self, np_array: NumpyArray, entity_dict: dict, color_no_go: Color
121
+ ) -> NumpyArray:
122
+ """Get the obstacle positions from the entity data."""
123
+ try:
124
+ obstacle_data = entity_dict.get("obstacle")
125
+ except KeyError:
126
+ _LOGGER.info("%s No obstacle found.", self.file_name)
127
+ else:
128
+ obstacle_positions = []
129
+ if obstacle_data:
130
+ for obstacle in obstacle_data:
131
+ label = obstacle.get("metaData", {}).get("label")
132
+ points = obstacle.get("points", [])
133
+
134
+ if label and points:
135
+ obstacle_pos = {
136
+ "label": label,
137
+ "points": {"x": points[0], "y": points[1]},
138
+ }
139
+ obstacle_positions.append(obstacle_pos)
140
+
141
+ # List of dictionaries containing label and points for each obstacle
142
+ # and draw obstacles on the map
143
+ if obstacle_positions:
144
+ self.img_h.draw.draw_obstacles(
145
+ np_array, obstacle_positions, color_no_go
146
+ )
147
+ return np_array
148
+
149
+ async def async_draw_charger(
150
+ self,
151
+ np_array: NumpyArray,
152
+ entity_dict: dict,
153
+ color_charger: Color,
154
+ ) -> NumpyArray:
155
+ """Get the charger position from the entity data."""
156
+ try:
157
+ charger_pos = entity_dict.get("charger_location")
158
+ except KeyError:
159
+ _LOGGER.warning("%s: No charger position found.", self.file_name)
160
+ else:
161
+ if charger_pos:
162
+ charger_pos = charger_pos[0]["points"]
163
+ self.img_h.charger_pos = {
164
+ "x": charger_pos[0],
165
+ "y": charger_pos[1],
166
+ }
167
+ np_array = await self.img_h.draw.battery_charger(
168
+ np_array, charger_pos[0], charger_pos[1], color_charger
169
+ )
170
+ return np_array
171
+ return np_array
172
+
173
+ async def async_get_json_id(self, my_json: JsonType) -> str | None:
174
+ """Return the JSON ID from the image."""
175
+ try:
176
+ json_id = my_json["metaData"]["nonce"]
177
+ except (ValueError, KeyError) as e:
178
+ _LOGGER.debug("%s: No JsonID provided: %s", self.file_name, str(e))
179
+ json_id = None
180
+ return json_id
181
+
182
+ async def async_draw_zones(
183
+ self,
184
+ m_json: JsonType,
185
+ np_array: NumpyArray,
186
+ color_zone_clean: Color,
187
+ color_no_go: Color,
188
+ ) -> NumpyArray:
189
+ """Get the zone clean from the JSON data."""
190
+ try:
191
+ zone_clean = self.img_h.data.find_zone_entities(m_json)
192
+ except (ValueError, KeyError):
193
+ zone_clean = None
194
+ else:
195
+ _LOGGER.info("%s: Got zones.", self.file_name)
196
+ if zone_clean:
197
+ try:
198
+ zones_active = zone_clean.get("active_zone")
199
+ except KeyError:
200
+ zones_active = None
201
+ if zones_active:
202
+ np_array = await self.img_h.draw.zones(
203
+ np_array, zones_active, color_zone_clean
204
+ )
205
+ try:
206
+ no_go_zones = zone_clean.get("no_go_area")
207
+ except KeyError:
208
+ no_go_zones = None
209
+
210
+ if no_go_zones:
211
+ np_array = await self.img_h.draw.zones(
212
+ np_array, no_go_zones, color_no_go
213
+ )
214
+
215
+ try:
216
+ no_mop_zones = zone_clean.get("no_mop_area")
217
+ except KeyError:
218
+ no_mop_zones = None
219
+
220
+ if no_mop_zones:
221
+ np_array = await self.img_h.draw.zones(
222
+ np_array, no_mop_zones, color_no_go
223
+ )
224
+ return np_array
225
+
226
+ async def async_draw_virtual_walls(
227
+ self, m_json: JsonType, np_array: NumpyArray, color_no_go: Color
228
+ ) -> NumpyArray:
229
+ """Get the virtual walls from the JSON data."""
230
+ try:
231
+ virtual_walls = self.img_h.data.find_virtual_walls(m_json)
232
+ except (ValueError, KeyError):
233
+ virtual_walls = None
234
+ else:
235
+ _LOGGER.info("%s: Got virtual walls.", self.file_name)
236
+ if virtual_walls:
237
+ np_array = await self.img_h.draw.draw_virtual_walls(
238
+ np_array, virtual_walls, color_no_go
239
+ )
240
+ return np_array
241
+
242
+ async def async_draw_paths(
243
+ self,
244
+ np_array: NumpyArray,
245
+ m_json: JsonType,
246
+ color_move: Color,
247
+ color_gray: Color,
248
+ ) -> NumpyArray:
249
+ """Get the paths from the JSON data."""
250
+ # Initialize the variables
251
+ path_pixels = None
252
+ predicted_path = None
253
+ # Extract the paths data from the JSON data.
254
+ try:
255
+ paths_data = self.img_h.data.find_paths_entities(m_json)
256
+ predicted_path = paths_data.get("predicted_path", [])
257
+ path_pixels = paths_data.get("path", [])
258
+ except KeyError as e:
259
+ _LOGGER.warning("%s: Error extracting paths data:", str(e))
260
+
261
+ if predicted_path:
262
+ predicted_path = predicted_path[0]["points"]
263
+ predicted_path = self.img_h.data.sublist(predicted_path, 2)
264
+ predicted_pat2 = self.img_h.data.sublist_join(predicted_path, 2)
265
+ np_array = await self.img_h.draw.lines(
266
+ np_array, predicted_pat2, 2, color_gray
267
+ )
268
+ if path_pixels:
269
+ for path in path_pixels:
270
+ # Get the points from the current path and extend multiple paths.
271
+ points = path.get("points", [])
272
+ sublists = self.img_h.data.sublist(points, 2)
273
+ self.img_h.shared.map_new_path = self.img_h.data.sublist_join(
274
+ sublists, 2
275
+ )
276
+ np_array = await self.img_h.draw.lines(
277
+ np_array, self.img_h.shared.map_new_path, 5, color_move
278
+ )
279
+ return np_array
280
+
281
+ async def async_get_entity_data(self, m_json: JsonType) -> dict or None:
282
+ """Get the entity data from the JSON data."""
283
+ try:
284
+ entity_dict = self.img_h.data.find_points_entities(m_json)
285
+ except (ValueError, KeyError):
286
+ entity_dict = None
287
+ else:
288
+ _LOGGER.info("%s: Got the points in the json.", self.file_name)
289
+ return entity_dict
290
+
291
+ @staticmethod
292
+ async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
293
+ """Copy the array."""
294
+ return NumpyArray.copy(original_array)
295
+
296
+ async def calculate_array_hash(self, layers: dict, active: list[int] = None) -> str:
297
+ """Calculate the hash of the image based on the layers and active segments walls."""
298
+ self.img_h.active_zones = active
299
+ if layers and active:
300
+ data_to_hash = {
301
+ "layers": len(layers["wall"][0]),
302
+ "active_segments": tuple(active),
303
+ }
304
+ data_json = json.dumps(data_to_hash, sort_keys=True)
305
+ hash_value = hashlib.sha256(data_json.encode()).hexdigest()
306
+ else:
307
+ hash_value = None
308
+ return hash_value
309
+
310
+ async def async_get_robot_in_room(
311
+ self, robot_y: int = 0, robot_x: int = 0, angle: float = 0.0
312
+ ) -> RobotPosition:
313
+ """Get the robot position and return in what room is."""
314
+ if self.img_h.robot_in_room:
315
+ # Check if the robot coordinates are inside the room's corners
316
+ if (
317
+ (self.img_h.robot_in_room["right"] >= int(robot_x))
318
+ and (self.img_h.robot_in_room["left"] <= int(robot_x))
319
+ ) and (
320
+ (self.img_h.robot_in_room["down"] >= int(robot_y))
321
+ and (self.img_h.robot_in_room["up"] <= int(robot_y))
322
+ ):
323
+ temp = {
324
+ "x": robot_x,
325
+ "y": robot_y,
326
+ "angle": angle,
327
+ "in_room": self.img_h.robot_in_room["room"],
328
+ }
329
+ if self.img_h.active_zones and (
330
+ self.img_h.robot_in_room["id"]
331
+ in range(len(self.img_h.active_zones))
332
+ ): # issue #100 Index out of range.
333
+ self.img_h.zooming = bool(
334
+ self.img_h.active_zones[self.img_h.robot_in_room["id"]]
335
+ )
336
+ else:
337
+ self.img_h.zooming = False
338
+ return temp
339
+ # else we need to search and use the async method.
340
+ if self.img_h.rooms_pos:
341
+ last_room = None
342
+ room_count = 0
343
+ if self.img_h.robot_in_room:
344
+ last_room = self.img_h.robot_in_room
345
+ for room in self.img_h.rooms_pos:
346
+ corners = room["corners"]
347
+ self.img_h.robot_in_room = {
348
+ "id": room_count,
349
+ "left": int(corners[0][0]),
350
+ "right": int(corners[2][0]),
351
+ "up": int(corners[0][1]),
352
+ "down": int(corners[2][1]),
353
+ "room": str(room["name"]),
354
+ }
355
+ room_count += 1
356
+ # Check if the robot coordinates are inside the room's corners
357
+ if (
358
+ (self.img_h.robot_in_room["right"] >= int(robot_x))
359
+ and (self.img_h.robot_in_room["left"] <= int(robot_x))
360
+ ) and (
361
+ (self.img_h.robot_in_room["down"] >= int(robot_y))
362
+ and (self.img_h.robot_in_room["up"] <= int(robot_y))
363
+ ):
364
+ temp = {
365
+ "x": robot_x,
366
+ "y": robot_y,
367
+ "angle": angle,
368
+ "in_room": self.img_h.robot_in_room["room"],
369
+ }
370
+ _LOGGER.debug(
371
+ "%s is in %s room.",
372
+ self.file_name,
373
+ self.img_h.robot_in_room["room"],
374
+ )
375
+ del room, corners, robot_x, robot_y # free memory.
376
+ return temp
377
+ del room, corners # free memory.
378
+ _LOGGER.debug(
379
+ "%s not located within Camera Rooms coordinates.",
380
+ self.file_name,
381
+ )
382
+ self.img_h.robot_in_room = last_room
383
+ self.img_h.zooming = False
384
+ temp = {
385
+ "x": robot_x,
386
+ "y": robot_y,
387
+ "angle": angle,
388
+ "in_room": last_room["room"] if last_room else None,
389
+ }
390
+ # If the robot is not inside any room, return a default value
391
+ return temp
392
+
393
+ async def async_get_robot_position(self, entity_dict: dict) -> tuple | None:
394
+ """Get the robot position from the entity data."""
395
+ robot_pos = None
396
+ robot_position = None
397
+ robot_position_angle = None
398
+ try:
399
+ robot_pos = entity_dict.get("robot_position")
400
+ except KeyError:
401
+ _LOGGER.warning("%s No robot position found.", self.file_name)
402
+ return None, None, None
403
+ finally:
404
+ if robot_pos:
405
+ robot_position = robot_pos[0]["points"]
406
+ robot_position_angle = round(
407
+ float(robot_pos[0]["metaData"]["angle"]), 1
408
+ )
409
+ if self.img_h.rooms_pos is None:
410
+ self.img_h.robot_pos = {
411
+ "x": robot_position[0],
412
+ "y": robot_position[1],
413
+ "angle": robot_position_angle,
414
+ }
415
+ else:
416
+ self.img_h.robot_pos = await self.async_get_robot_in_room(
417
+ robot_y=(robot_position[1]),
418
+ robot_x=(robot_position[0]),
419
+ angle=robot_position_angle,
420
+ )
421
+
422
+ return robot_pos, robot_position, robot_position_angle