valetudo-map-parser 0.1.9b4__py3-none-any.whl → 0.1.9b6__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,448 @@
1
+ """Utility code for the valetudo map parser."""
2
+
3
+ import hashlib
4
+ import json
5
+ from logging import getLogger
6
+
7
+ from PIL import ImageOps
8
+
9
+ from .types import ChargerPosition, ImageSize, NumpyArray, RobotPosition
10
+
11
+ _LOGGER = getLogger(__name__)
12
+
13
+
14
+ class BaseHandler:
15
+ """Avoid Code duplication"""
16
+
17
+ def __init__(self):
18
+ self.file_name = None
19
+ self.img_size = None
20
+ self.json_data = None
21
+ self.json_id = None
22
+ self.path_pixels = None
23
+ self.robot_in_room = None
24
+ self.robot_pos = None
25
+ self.room_propriety = None
26
+ self.rooms_pos = None
27
+ self.charger_pos = None
28
+ self.frame_number = 0
29
+ self.max_frames = 1024
30
+ self.crop_img_size = [0, 0]
31
+ self.offset_x = 0
32
+ self.offset_y = 0
33
+ self.shared = None
34
+
35
+ def get_frame_number(self) -> int:
36
+ """Return the frame number of the image."""
37
+ return self.frame_number
38
+
39
+ def get_robot_position(self) -> RobotPosition | None:
40
+ """Return the robot position."""
41
+ return self.robot_pos
42
+
43
+ def get_charger_position(self) -> ChargerPosition | None:
44
+ """Return the charger position."""
45
+ return self.charger_pos
46
+
47
+ def get_img_size(self) -> ImageSize | None:
48
+ """Return the size of the image."""
49
+ return self.img_size
50
+
51
+ def get_json_id(self) -> str | None:
52
+ """Return the JSON ID from the image."""
53
+ return self.json_id
54
+
55
+ async def async_resize_image(
56
+ self, pil_img, width, height, aspect_ratio=None, is_rand=False
57
+ ):
58
+ """Resize the image to the given dimensions and aspect ratio."""
59
+ if aspect_ratio:
60
+ wsf, hsf = [int(x) for x in aspect_ratio.split(",")]
61
+ if wsf == 0 or hsf == 0:
62
+ return pil_img
63
+ new_aspect_ratio = wsf / hsf
64
+ if width / height > new_aspect_ratio:
65
+ new_width = int(pil_img.height * new_aspect_ratio)
66
+ new_height = pil_img.height
67
+ else:
68
+ new_width = pil_img.width
69
+ new_height = int(pil_img.width / new_aspect_ratio)
70
+ _LOGGER.debug(
71
+ "%s: Image Aspect Ratio: %s, %s",
72
+ self.file_name,
73
+ str(wsf),
74
+ str(hsf),
75
+ )
76
+ (
77
+ self.crop_img_size[0],
78
+ self.crop_img_size[1],
79
+ ) = await self.async_map_coordinates_offset(
80
+ wsf, hsf, new_width, new_height, is_rand
81
+ )
82
+ return ImageOps.pad(pil_img, (new_width, new_height))
83
+ return ImageOps.pad(pil_img, (width, height))
84
+
85
+ async def async_map_coordinates_offset(
86
+ self, wsf: int, hsf: int, width: int, height: int, rand256: bool = False
87
+ ) -> tuple[int, int]:
88
+ """
89
+ Offset the coordinates to the map.
90
+ """
91
+
92
+ if wsf == 1 and hsf == 1:
93
+ self.set_image_offset_ratio_1_1(width, height, rand256)
94
+ elif wsf == 2 and hsf == 1:
95
+ self.set_image_offset_ratio_2_1(width, height, rand256)
96
+ elif wsf == 3 and hsf == 2:
97
+ self.set_image_offset_ratio_3_2(width, height, rand256)
98
+ elif wsf == 5 and hsf == 4:
99
+ self.set_image_offset_ratio_5_4(width, height, rand256)
100
+ elif wsf == 9 and hsf == 16:
101
+ self.set_image_offset_ratio_9_16(width, height, rand256)
102
+ elif wsf == 16 and hsf == 9:
103
+ self.set_image_offset_ratio_16_9(width, height, rand256)
104
+ return width, height
105
+
106
+ @staticmethod
107
+ async def calculate_array_hash(layers: dict, active: list[int] = None) -> str:
108
+ """Calculate the hash of the image based on layers and active zones."""
109
+ if layers and active:
110
+ data_to_hash = {
111
+ "layers": len(layers["wall"][0]),
112
+ "active_segments": tuple(active),
113
+ }
114
+ data_json = json.dumps(data_to_hash, sort_keys=True)
115
+ return hashlib.sha256(data_json.encode()).hexdigest()
116
+ return None
117
+
118
+ @staticmethod
119
+ async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
120
+ """Copy the array."""
121
+ return NumpyArray.copy(original_array)
122
+
123
+ def get_map_points(self) -> dict:
124
+ """Return the map points."""
125
+ return [
126
+ {"x": 0, "y": 0}, # Top-left corner 0
127
+ {"x": self.crop_img_size[0], "y": 0}, # Top-right corner 1
128
+ {
129
+ "x": self.crop_img_size[0],
130
+ "y": self.crop_img_size[1],
131
+ }, # Bottom-right corner 2
132
+ {"x": 0, "y": self.crop_img_size[1]}, # Bottom-left corner (optional) 3
133
+ ]
134
+
135
+ def set_image_offset_ratio_1_1(
136
+ self, width: int, height: int, rand256: bool = False
137
+ ) -> None:
138
+ """Set the image offset ratio to 1:1."""
139
+
140
+ rotation = self.shared.image_rotate
141
+ if not rand256:
142
+ if rotation in [0, 180]:
143
+ self.offset_y = self.crop_img_size[0] - width
144
+ self.offset_x = (height - self.crop_img_size[1]) // 2
145
+ elif rotation in [90, 270]:
146
+ self.offset_y = width - self.crop_img_size[0]
147
+ self.offset_x = (self.crop_img_size[1] - height) // 2
148
+ else:
149
+ if rotation in [0, 180]:
150
+ self.offset_x = (width - self.crop_img_size[0]) // 2
151
+ self.offset_y = height - self.crop_img_size[1]
152
+ elif rotation in [90, 270]:
153
+ self.offset_y = (self.crop_img_size[0] - width) // 2
154
+ self.offset_x = self.crop_img_size[1] - height
155
+ _LOGGER.debug(
156
+ "%s Image Coordinates Offsets (x,y): %s. %s",
157
+ self.file_name,
158
+ self.offset_x,
159
+ self.offset_y,
160
+ )
161
+
162
+ def set_image_offset_ratio_2_1(
163
+ self, width: int, height: int, rand256: bool = False
164
+ ) -> None:
165
+ """Set the image offset ratio to 2:1."""
166
+
167
+ rotation = self.shared.image_rotate
168
+ if not rand256:
169
+ if rotation in [0, 180]:
170
+ self.offset_y = width - self.crop_img_size[0]
171
+ self.offset_x = height - self.crop_img_size[1]
172
+ elif rotation in [90, 270]:
173
+ self.offset_x = width - self.crop_img_size[0]
174
+ self.offset_y = height - self.crop_img_size[1]
175
+ else:
176
+ if rotation in [0, 180]:
177
+ self.offset_y = width - self.crop_img_size[0]
178
+ self.offset_x = height - self.crop_img_size[1]
179
+ elif rotation in [90, 270]:
180
+ self.offset_x = width - self.crop_img_size[0]
181
+ self.offset_y = height - self.crop_img_size[1]
182
+
183
+ _LOGGER.debug(
184
+ "%s Image Coordinates Offsets (x,y): %s. %s",
185
+ self.file_name,
186
+ self.offset_x,
187
+ self.offset_y,
188
+ )
189
+
190
+ def set_image_offset_ratio_3_2(
191
+ self, width: int, height: int, rand256: bool = False
192
+ ) -> None:
193
+ """Set the image offset ratio to 3:2."""
194
+
195
+ rotation = self.shared.image_rotate
196
+
197
+ if not rand256:
198
+ if rotation in [0, 180]:
199
+ self.offset_y = width - self.crop_img_size[0]
200
+ self.offset_x = ((height - self.crop_img_size[1]) // 2) - (
201
+ self.crop_img_size[1] // 10
202
+ )
203
+ elif rotation in [90, 270]:
204
+ self.offset_y = (self.crop_img_size[0] - width) // 2
205
+ self.offset_x = (self.crop_img_size[1] - height) + (
206
+ (height // 10) // 2
207
+ )
208
+ else:
209
+ if rotation in [0, 180]:
210
+ self.offset_x = (width - self.crop_img_size[0]) // 2
211
+ self.offset_y = height - self.crop_img_size[1]
212
+ elif rotation in [90, 270]:
213
+ self.offset_y = (self.crop_img_size[0] - width) // 2
214
+ self.offset_x = self.crop_img_size[1] - height
215
+
216
+ _LOGGER.debug(
217
+ "%s Image Coordinates Offsets (x,y): %s. %s",
218
+ self.file_name,
219
+ self.offset_x,
220
+ self.offset_y,
221
+ )
222
+
223
+ def set_image_offset_ratio_5_4(
224
+ self, width: int, height: int, rand256: bool = False
225
+ ) -> None:
226
+ """Set the image offset ratio to 5:4."""
227
+
228
+ rotation = self.shared.image_rotate
229
+ if not rand256:
230
+ if rotation in [0, 180]:
231
+ self.offset_x = ((width - self.crop_img_size[0]) // 2) - (
232
+ self.crop_img_size[0] // 2
233
+ )
234
+ self.offset_y = (self.crop_img_size[1] - height) - (
235
+ self.crop_img_size[1] // 2
236
+ )
237
+ elif rotation in [90, 270]:
238
+ self.offset_y = ((self.crop_img_size[0] - width) // 2) - 10
239
+ self.offset_x = (self.crop_img_size[1] - height) + (
240
+ height // 10
241
+ )
242
+ else:
243
+ if rotation in [0, 180]:
244
+ self.offset_y = (width - self.crop_img_size[0]) // 2
245
+ self.offset_x = self.crop_img_size[1] - height
246
+ elif rotation in [90, 270]:
247
+ self.offset_y = (self.crop_img_size[0] - width) // 2
248
+ self.offset_x = self.crop_img_size[1] - height
249
+
250
+ _LOGGER.debug(
251
+ "%s Image Coordinates Offsets (x,y): %s. %s",
252
+ self.file_name,
253
+ self.offset_x,
254
+ self.offset_y,
255
+ )
256
+
257
+ def set_image_offset_ratio_9_16(
258
+ self, width: int, height: int, rand256: bool = False
259
+ ) -> None:
260
+ """Set the image offset ratio to 9:16."""
261
+
262
+ rotation = self.shared.image_rotate
263
+ if not rand256:
264
+ if rotation in [0, 180]:
265
+ self.offset_y = width - self.crop_img_size[0]
266
+ self.offset_x = height - self.crop_img_size[1]
267
+ elif rotation in [90, 270]:
268
+ self.offset_x = (width - self.crop_img_size[0]) + (height // 10)
269
+ self.offset_y = height - self.crop_img_size[1]
270
+ else:
271
+ if rotation in [0, 180]:
272
+ self.offset_y = width - self.crop_img_size[0]
273
+ self.offset_x = height - self.crop_img_size[1]
274
+ elif rotation in [90, 270]:
275
+ self.offset_x = width - self.crop_img_size[0]
276
+ self.offset_y = height - self.crop_img_size[1]
277
+
278
+ _LOGGER.debug(
279
+ "%s Image Coordinates Offsets (x,y): %s. %s",
280
+ self.file_name,
281
+ self.offset_x,
282
+ self.offset_y,
283
+ )
284
+
285
+ def set_image_offset_ratio_16_9(
286
+ self, width: int, height: int, rand256: bool = False
287
+ ) -> None:
288
+ """Set the image offset ratio to 16:9."""
289
+
290
+ rotation = self.shared.image_rotate
291
+ if not rand256:
292
+ if rotation in [0, 180]:
293
+ self.offset_y = width - self.crop_img_size[0]
294
+ self.offset_x = height - self.crop_img_size[1]
295
+ elif rotation in [90, 270]:
296
+ self.offset_x = width - self.crop_img_size[0]
297
+ self.offset_y = height - self.crop_img_size[1]
298
+ else:
299
+ if rotation in [0, 180]:
300
+ self.offset_y = width - self.crop_img_size[0]
301
+ self.offset_x = height - self.crop_img_size[1]
302
+ elif rotation in [90, 270]:
303
+ self.offset_x = width - self.crop_img_size[0]
304
+ self.offset_y = height - self.crop_img_size[1]
305
+
306
+ _LOGGER.debug(
307
+ "%s Image Coordinates Offsets (x,y): %s. %s",
308
+ self.file_name,
309
+ self.offset_x,
310
+ self.offset_y,
311
+ )
312
+
313
+ def get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
314
+ """Calculate the calibration points based on the rotation angle."""
315
+
316
+ # get_calibration_data
317
+ vacuum_points = [
318
+ {
319
+ "x": self.img.crop_area[0] + self.img.offset_x,
320
+ "y": self.img.crop_area[1] + self.img.offset_y,
321
+ }, # Top-left corner 0
322
+ {
323
+ "x": self.img.crop_area[2] - self.img.offset_x,
324
+ "y": self.img.crop_area[1] + self.img.offset_y,
325
+ }, # Top-right corner 1
326
+ {
327
+ "x": self.img.crop_area[2] - self.img.offset_x,
328
+ "y": self.img.crop_area[3] - self.img.offset_y,
329
+ }, # Bottom-right corner 2
330
+ {
331
+ "x": self.img.crop_area[0] + self.img.offset_x,
332
+ "y": self.img.crop_area[3] - self.img.offset_y,
333
+ }, # Bottom-left corner (optional)3
334
+ ]
335
+
336
+ # Rotate the vacuum points based on the rotation angle
337
+ if rotation_angle == 90:
338
+ vacuum_points = [
339
+ vacuum_points[1],
340
+ vacuum_points[2],
341
+ vacuum_points[3],
342
+ vacuum_points[0],
343
+ ]
344
+ elif rotation_angle == 180:
345
+ vacuum_points = [
346
+ vacuum_points[2],
347
+ vacuum_points[3],
348
+ vacuum_points[0],
349
+ vacuum_points[1],
350
+ ]
351
+ elif rotation_angle == 270:
352
+ vacuum_points = [
353
+ vacuum_points[3],
354
+ vacuum_points[0],
355
+ vacuum_points[1],
356
+ vacuum_points[2],
357
+ ]
358
+
359
+ return vacuum_points
360
+
361
+ def re_get_vacuum_points(self, rotation_angle: int) -> list[dict[str, int]]:
362
+ """Recalculate the calibration points based on the rotation angle.
363
+ RAND256 Vacuums Calibration Points are in 10th of a mm."""
364
+ vacuum_points = [
365
+ {
366
+ "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
367
+ "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
368
+ }, # Top-left corner 0
369
+ {
370
+ "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
371
+ "y": ((self.img.crop_area[1] + self.img.offset_y) * 10),
372
+ }, # Top-right corner 1
373
+ {
374
+ "x": ((self.img.crop_area[2] - self.img.offset_x) * 10),
375
+ "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
376
+ }, # Bottom-right corner 2
377
+ {
378
+ "x": ((self.img.crop_area[0] + self.img.offset_x) * 10),
379
+ "y": ((self.img.crop_area[3] - self.img.offset_y) * 10),
380
+ }, # Bottom-left corner (optional)3
381
+ ]
382
+
383
+ # Rotate the vacuum points based on the rotation angle
384
+ if rotation_angle == 90:
385
+ vacuum_points = [
386
+ vacuum_points[1],
387
+ vacuum_points[2],
388
+ vacuum_points[3],
389
+ vacuum_points[0],
390
+ ]
391
+ elif rotation_angle == 180:
392
+ vacuum_points = [
393
+ vacuum_points[2],
394
+ vacuum_points[3],
395
+ vacuum_points[0],
396
+ vacuum_points[1],
397
+ ]
398
+ elif rotation_angle == 270:
399
+ vacuum_points = [
400
+ vacuum_points[3],
401
+ vacuum_points[0],
402
+ vacuum_points[1],
403
+ vacuum_points[2],
404
+ ]
405
+
406
+ return vacuum_points
407
+
408
+ async def async_zone_propriety(self, zones_data) -> dict:
409
+ """Get the zone propriety"""
410
+ zone_properties = {}
411
+ id_count = 1
412
+ for zone in zones_data:
413
+ zone_name = zone.get("name")
414
+ coordinates = zone.get("coordinates")
415
+ if coordinates and len(coordinates) > 0:
416
+ coordinates[0].pop()
417
+ x1, y1, x2, y2 = coordinates[0]
418
+ zone_properties[zone_name] = {
419
+ "zones": coordinates,
420
+ "name": zone_name,
421
+ "x": ((x1 + x2) // 2),
422
+ "y": ((y1 + y2) // 2),
423
+ }
424
+ id_count += 1
425
+ if id_count > 1:
426
+ _LOGGER.debug("%s: Zones Properties updated.", self.file_name)
427
+ return zone_properties
428
+
429
+ async def async_points_propriety(self, points_data) -> dict:
430
+ """Get the point propriety"""
431
+ point_properties = {}
432
+ id_count = 1
433
+ for point in points_data:
434
+ point_name = point.get("name")
435
+ coordinates = point.get("coordinates")
436
+ if coordinates and len(coordinates) > 0:
437
+ coordinates = point.get("coordinates")
438
+ x1, y1 = coordinates
439
+ point_properties[id_count] = {
440
+ "position": coordinates,
441
+ "name": point_name,
442
+ "x": x1,
443
+ "y": y1,
444
+ }
445
+ id_count += 1
446
+ if id_count > 1:
447
+ _LOGGER.debug("%s: Point Properties updated.", self.file_name)
448
+ return point_properties
@@ -6,8 +6,6 @@ Version: 2024.07.2
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import hashlib
10
- import json
11
9
  import logging
12
10
 
13
11
  from .config.types import Color, JsonType, NumpyArray, RobotPosition
@@ -283,25 +281,6 @@ class ImageDraw:
283
281
  _LOGGER.info("%s: Got the points in the json.", self.file_name)
284
282
  return entity_dict
285
283
 
286
- @staticmethod
287
- async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
288
- """Copy the array."""
289
- return NumpyArray.copy(original_array)
290
-
291
- async def calculate_array_hash(self, layers: dict, active: list[int] = None) -> str:
292
- """Calculate the hash of the image based on the layers and active segments walls."""
293
- self.img_h.active_zones = active
294
- if layers and active:
295
- data_to_hash = {
296
- "layers": len(layers["wall"][0]),
297
- "active_segments": tuple(active),
298
- }
299
- data_json = json.dumps(data_to_hash, sort_keys=True)
300
- hash_value = hashlib.sha256(data_json.encode()).hexdigest()
301
- else:
302
- hash_value = None
303
- return hash_value
304
-
305
284
  async def async_get_robot_in_room(
306
285
  self, robot_y: int = 0, robot_x: int = 0, angle: float = 0.0
307
286
  ) -> RobotPosition: