valetudo-map-parser 0.1.11b2__py3-none-any.whl → 0.1.12b0__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.
@@ -16,8 +16,8 @@ from PIL import Image
16
16
  from .config.async_utils import AsyncPIL
17
17
  from .config.drawable_elements import DrawableElement
18
18
  from .config.shared import CameraShared
19
+ from .const import COLORS
19
20
  from .config.types import (
20
- COLORS,
21
21
  LOGGER,
22
22
  CalibrationPoints,
23
23
  Colors,
@@ -88,6 +88,189 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
88
88
  self.rooms_pos = None
89
89
  return room_properties
90
90
 
91
+ def _identify_disabled_rooms(self) -> set:
92
+ """Identify which rooms are disabled in the drawing configuration."""
93
+ disabled_rooms = set()
94
+ room_id = 0
95
+ for layer_type, _ in self.json_data.layers.items():
96
+ if layer_type == "segment":
97
+ current_room_id = room_id + 1
98
+ if 1 <= current_room_id <= 15:
99
+ room_element = getattr(
100
+ DrawableElement, f"ROOM_{current_room_id}", None
101
+ )
102
+ if room_element and not self.drawing_config.is_enabled(
103
+ room_element
104
+ ):
105
+ disabled_rooms.add(room_id)
106
+ room_id = (room_id + 1) % 16
107
+ return disabled_rooms
108
+
109
+ async def _draw_layer_if_enabled(
110
+ self,
111
+ img_np_array,
112
+ layer_type,
113
+ compressed_pixels_list,
114
+ colors,
115
+ pixel_size,
116
+ disabled_rooms,
117
+ room_id,
118
+ ):
119
+ """Draw a layer if it's enabled in the drawing configuration."""
120
+ is_room_layer = layer_type == "segment"
121
+
122
+ if is_room_layer:
123
+ current_room_id = room_id + 1
124
+ if 1 <= current_room_id <= 15:
125
+ room_element = getattr(DrawableElement, f"ROOM_{current_room_id}", None)
126
+ if not self.drawing_config.is_enabled(room_element):
127
+ return room_id + 1, img_np_array # Skip this room
128
+
129
+ is_wall_layer = layer_type == "wall"
130
+ if is_wall_layer and not self.drawing_config.is_enabled(DrawableElement.WALL):
131
+ return room_id, img_np_array # Skip walls
132
+
133
+ # Draw the layer
134
+ room_id, img_np_array = await self.imd.async_draw_base_layer(
135
+ img_np_array,
136
+ compressed_pixels_list,
137
+ layer_type,
138
+ colors["wall"],
139
+ colors["zone_clean"],
140
+ pixel_size,
141
+ disabled_rooms if layer_type == "wall" else None,
142
+ )
143
+ return room_id, img_np_array
144
+
145
+ async def _draw_base_layers(self, img_np_array, colors, pixel_size):
146
+ """Draw all base layers (rooms, walls, floors)."""
147
+ disabled_rooms = self._identify_disabled_rooms()
148
+ room_id = 0
149
+
150
+ for layer_type, compressed_pixels_list in self.json_data.layers.items():
151
+ room_id, img_np_array = await self._draw_layer_if_enabled(
152
+ img_np_array,
153
+ layer_type,
154
+ compressed_pixels_list,
155
+ colors,
156
+ pixel_size,
157
+ disabled_rooms,
158
+ room_id,
159
+ )
160
+
161
+ return img_np_array, room_id
162
+
163
+ async def _draw_additional_elements(
164
+ self, img_np_array, m_json, entity_dict, colors
165
+ ):
166
+ """Draw additional elements like walls, charger, and obstacles."""
167
+ if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
168
+ img_np_array = await self.imd.async_draw_virtual_walls(
169
+ m_json, img_np_array, colors["no_go"]
170
+ )
171
+
172
+ if self.drawing_config.is_enabled(DrawableElement.CHARGER):
173
+ img_np_array = await self.imd.async_draw_charger(
174
+ img_np_array, entity_dict, colors["charger"]
175
+ )
176
+
177
+ if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
178
+ self.shared.obstacles_pos = self.data.get_obstacles(entity_dict)
179
+ if self.shared.obstacles_pos:
180
+ img_np_array = await self.imd.async_draw_obstacle(
181
+ img_np_array, self.shared.obstacles_pos, colors["no_go"]
182
+ )
183
+
184
+ return img_np_array
185
+
186
+ async def _setup_room_and_robot_data(
187
+ self, room_id, robot_position, robot_position_angle
188
+ ):
189
+ """Setup room properties and robot position data."""
190
+ if (room_id > 0) and not self.room_propriety:
191
+ self.room_propriety = await self.async_extract_room_properties(
192
+ self.json_data.json_data
193
+ )
194
+
195
+ if not self.rooms_pos and not self.room_propriety:
196
+ self.room_propriety = await self.async_extract_room_properties(
197
+ self.json_data.json_data
198
+ )
199
+
200
+ if self.rooms_pos and robot_position and robot_position_angle:
201
+ self.robot_pos = await self.imd.async_get_robot_in_room(
202
+ robot_x=(robot_position[0]),
203
+ robot_y=(robot_position[1]),
204
+ angle=robot_position_angle,
205
+ )
206
+
207
+ async def _prepare_data_tasks(self, m_json, entity_dict):
208
+ """Prepare and execute data extraction tasks in parallel."""
209
+ data_tasks = []
210
+
211
+ if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
212
+ data_tasks.append(self._prepare_zone_data(m_json))
213
+
214
+ if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
215
+ data_tasks.append(self._prepare_goto_data(entity_dict))
216
+
217
+ path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
218
+ LOGGER.info("%s: PATH element enabled: %s", self.file_name, path_enabled)
219
+ if path_enabled:
220
+ LOGGER.info("%s: Drawing path", self.file_name)
221
+ data_tasks.append(self._prepare_path_data(m_json))
222
+
223
+ if data_tasks:
224
+ await asyncio.gather(*data_tasks)
225
+
226
+ return path_enabled
227
+
228
+ async def _draw_dynamic_elements(
229
+ self, img_np_array, m_json, entity_dict, colors, path_enabled
230
+ ):
231
+ """Draw dynamic elements like zones, paths, and go-to targets."""
232
+ if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
233
+ img_np_array = await self.imd.async_draw_zones(
234
+ m_json, img_np_array, colors["zone_clean"], colors["no_go"]
235
+ )
236
+
237
+ if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
238
+ img_np_array = await self.imd.draw_go_to_flag(
239
+ img_np_array, entity_dict, colors["go_to"]
240
+ )
241
+
242
+ if path_enabled:
243
+ img_np_array = await self.imd.async_draw_paths(
244
+ img_np_array, m_json, colors["move"], self.color_grey
245
+ )
246
+ else:
247
+ LOGGER.info("%s: Skipping path drawing", self.file_name)
248
+
249
+ return img_np_array
250
+
251
+ async def _draw_robot_if_enabled(
252
+ self, img_np_array, robot_pos, robot_position, robot_position_angle, colors
253
+ ):
254
+ """Draw the robot on the map if enabled."""
255
+ if self.shared.vacuum_state == "docked":
256
+ robot_position_angle -= 180
257
+
258
+ if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
259
+ robot_color = self.drawing_config.get_property(
260
+ DrawableElement.ROBOT, "color", colors["robot"]
261
+ )
262
+ img_np_array = await self.draw.robot(
263
+ layers=img_np_array,
264
+ x=robot_position[0],
265
+ y=robot_position[1],
266
+ angle=robot_position_angle,
267
+ fill=robot_color,
268
+ radius=self.shared.robot_size,
269
+ robot_state=self.shared.vacuum_state,
270
+ )
271
+
272
+ return img_np_array
273
+
91
274
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
92
275
  async def async_get_image_from_json(
93
276
  self,
@@ -132,126 +315,21 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
132
315
  img_np_array = await self.draw.create_empty_image(
133
316
  self.img_size["x"], self.img_size["y"], colors["background"]
134
317
  )
135
- # Draw layers and segments if enabled
136
318
  room_id = 0
137
- # Keep track of disabled rooms to skip their walls later
138
- disabled_rooms = set()
139
319
 
140
320
  if self.drawing_config.is_enabled(DrawableElement.FLOOR):
141
- # First pass: identify disabled rooms
142
- for (
143
- layer_type,
144
- compressed_pixels_list,
145
- ) in self.json_data.layers.items():
146
- # Check if this is a room layer
147
- if layer_type == "segment":
148
- # The room_id is the current room being processed (0-based index)
149
- # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
150
- current_room_id = room_id + 1
151
- if 1 <= current_room_id <= 15:
152
- room_element = getattr(
153
- DrawableElement, f"ROOM_{current_room_id}", None
154
- )
155
- if (
156
- room_element
157
- and not self.drawing_config.is_enabled(
158
- room_element
159
- )
160
- ):
161
- # Add this room to the disabled rooms set
162
- disabled_rooms.add(room_id)
163
- room_id = (
164
- room_id + 1
165
- ) % 16 # Cycle room_id back to 0 after 15
166
-
167
- # Reset room_id for the actual drawing pass
168
- room_id = 0
169
-
170
- # Second pass: draw enabled rooms and walls
171
- for (
172
- layer_type,
173
- compressed_pixels_list,
174
- ) in self.json_data.layers.items():
175
- # Check if this is a room layer
176
- is_room_layer = layer_type == "segment"
177
-
178
- # If it's a room layer, check if the specific room is enabled
179
- if is_room_layer:
180
- # The room_id is the current room being processed (0-based index)
181
- # We need to check if ROOM_{room_id+1} is enabled (1-based in DrawableElement)
182
- current_room_id = room_id + 1
183
- if 1 <= current_room_id <= 15:
184
- room_element = getattr(
185
- DrawableElement, f"ROOM_{current_room_id}", None
186
- )
187
-
188
- # Skip this room if it's disabled
189
- if not self.drawing_config.is_enabled(room_element):
190
- room_id = (
191
- room_id + 1
192
- ) % 16 # Increment room_id even if we skip
193
- continue
194
-
195
- # Draw the layer ONLY if enabled
196
- is_wall_layer = layer_type == "wall"
197
- if is_wall_layer:
198
- # Skip walls entirely if disabled
199
- if not self.drawing_config.is_enabled(
200
- DrawableElement.WALL
201
- ):
202
- continue
203
- # Draw the layer
204
- (
205
- room_id,
206
- img_np_array,
207
- ) = await self.imd.async_draw_base_layer(
208
- img_np_array,
209
- compressed_pixels_list,
210
- layer_type,
211
- colors["wall"],
212
- colors["zone_clean"],
213
- pixel_size,
214
- disabled_rooms if layer_type == "wall" else None,
215
- )
216
-
217
- # Draw the virtual walls if enabled
218
- if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
219
- img_np_array = await self.imd.async_draw_virtual_walls(
220
- m_json, img_np_array, colors["no_go"]
321
+ img_np_array, room_id = await self._draw_base_layers(
322
+ img_np_array, colors, pixel_size
221
323
  )
222
324
 
223
- # Draw charger if enabled
224
- if self.drawing_config.is_enabled(DrawableElement.CHARGER):
225
- img_np_array = await self.imd.async_draw_charger(
226
- img_np_array, entity_dict, colors["charger"]
227
- )
228
-
229
- # Draw obstacles if enabled
230
- if self.drawing_config.is_enabled(DrawableElement.OBSTACLE):
231
- self.shared.obstacles_pos = self.data.get_obstacles(entity_dict)
232
- if self.shared.obstacles_pos:
233
- img_np_array = await self.imd.async_draw_obstacle(
234
- img_np_array, self.shared.obstacles_pos, colors["no_go"]
235
- )
236
- # Robot and rooms position
237
- if (room_id > 0) and not self.room_propriety:
238
- self.room_propriety = await self.async_extract_room_properties(
239
- self.json_data.json_data
240
- )
325
+ img_np_array = await self._draw_additional_elements(
326
+ img_np_array, m_json, entity_dict, colors
327
+ )
241
328
 
242
- # Ensure room data is available for robot room detection (even if not extracted above)
243
- if not self.rooms_pos and not self.room_propriety:
244
- self.room_propriety = await self.async_extract_room_properties(
245
- self.json_data.json_data
246
- )
329
+ await self._setup_room_and_robot_data(
330
+ room_id, robot_position, robot_position_angle
331
+ )
247
332
 
248
- # Always check robot position for zooming (moved outside the condition)
249
- if self.rooms_pos and robot_position and robot_position_angle:
250
- self.robot_pos = await self.imd.async_get_robot_in_room(
251
- robot_x=(robot_position[0]),
252
- robot_y=(robot_position[1]),
253
- angle=robot_position_angle,
254
- )
255
333
  LOGGER.info("%s: Completed base Layers", self.file_name)
256
334
  # Copy the new array in base layer.
257
335
  # Delete old base layer before creating new one to free memory
@@ -282,73 +360,22 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
282
360
  np.copyto(self.img_work_layer, self.img_base_layer)
283
361
  img_np_array = self.img_work_layer
284
362
 
285
- # Prepare parallel data extraction tasks
286
- data_tasks = []
287
-
288
- # Prepare zone data extraction
289
- if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
290
- data_tasks.append(self._prepare_zone_data(m_json))
363
+ # Prepare and execute data extraction tasks
364
+ path_enabled = await self._prepare_data_tasks(m_json, entity_dict)
291
365
 
292
- # Prepare go_to flag data extraction
293
- if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
294
- data_tasks.append(self._prepare_goto_data(entity_dict))
295
-
296
- # Prepare path data extraction
297
- path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
298
- LOGGER.info(
299
- "%s: PATH element enabled: %s", self.file_name, path_enabled
366
+ # Draw dynamic elements
367
+ img_np_array = await self._draw_dynamic_elements(
368
+ img_np_array, m_json, entity_dict, colors, path_enabled
300
369
  )
301
- if path_enabled:
302
- LOGGER.info("%s: Drawing path", self.file_name)
303
- data_tasks.append(self._prepare_path_data(m_json))
304
-
305
- # Await all data preparation tasks if any were created
306
- if data_tasks:
307
- await asyncio.gather(*data_tasks)
308
-
309
- # Process drawing operations sequentially (since they modify the same array)
310
- # Draw zones if enabled
311
- if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
312
- img_np_array = await self.imd.async_draw_zones(
313
- m_json, img_np_array, colors["zone_clean"], colors["no_go"]
314
- )
315
-
316
- # Draw the go_to target flag if enabled
317
- if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
318
- img_np_array = await self.imd.draw_go_to_flag(
319
- img_np_array, entity_dict, colors["go_to"]
320
- )
321
370
 
322
- # Draw paths if enabled
323
- if path_enabled:
324
- img_np_array = await self.imd.async_draw_paths(
325
- img_np_array, m_json, colors["move"], self.color_grey
326
- )
327
- else:
328
- LOGGER.info("%s: Skipping path drawing", self.file_name)
329
-
330
- # Check if the robot is docked.
331
- if self.shared.vacuum_state == "docked":
332
- # Adjust the robot angle.
333
- robot_position_angle -= 180
334
-
335
- # Draw the robot if enabled
336
- if robot_pos and self.drawing_config.is_enabled(DrawableElement.ROBOT):
337
- # Get robot color (allows for customization)
338
- robot_color = self.drawing_config.get_property(
339
- DrawableElement.ROBOT, "color", colors["robot"]
340
- )
341
-
342
- # Draw the robot
343
- img_np_array = await self.draw.robot(
344
- layers=img_np_array,
345
- x=robot_position[0],
346
- y=robot_position[1],
347
- angle=robot_position_angle,
348
- fill=robot_color,
349
- radius=self.shared.robot_size,
350
- robot_state=self.shared.vacuum_state,
351
- )
371
+ # Draw robot
372
+ img_np_array = await self._draw_robot_if_enabled(
373
+ img_np_array,
374
+ robot_pos,
375
+ robot_position,
376
+ robot_position_angle,
377
+ colors,
378
+ )
352
379
 
353
380
  # Synchronize zooming state from ImageDraw to handler before auto-crop
354
381
  self.zooming = self.imd.img_h.zooming
@@ -376,11 +403,11 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
376
403
 
377
404
  # Return PIL Image
378
405
  return resized_image
379
- else:
380
- # Return PIL Image (convert from NumPy)
381
- pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
382
- del img_np_array
383
- return pil_img
406
+
407
+ # Return PIL Image (convert from NumPy)
408
+ pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
409
+ del img_np_array
410
+ return pil_img
384
411
  except (RuntimeError, RuntimeWarning) as e:
385
412
  LOGGER.warning(
386
413
  "%s: Error %s during image creation.",
@@ -30,6 +30,8 @@ T = TypeVar("T")
30
30
 
31
31
 
32
32
  class RangeStats(TypedDict):
33
+ """Statistics for a range of values (min, max, mid, avg)."""
34
+
33
35
  min: int
34
36
  max: int
35
37
  mid: int
@@ -37,6 +39,8 @@ class RangeStats(TypedDict):
37
39
 
38
40
 
39
41
  class Dimensions(TypedDict):
42
+ """Dimensions with x/y range statistics and pixel count."""
43
+
40
44
  x: RangeStats
41
45
  y: RangeStats
42
46
  pixelCount: int
@@ -46,10 +50,14 @@ class Dimensions(TypedDict):
46
50
 
47
51
 
48
52
  class FloorWallMeta(TypedDict, total=False):
53
+ """Metadata for floor and wall layers."""
54
+
49
55
  area: int
50
56
 
51
57
 
52
58
  class SegmentMeta(TypedDict, total=False):
59
+ """Metadata for segment layers including segment ID and active state."""
60
+
53
61
  segmentId: str
54
62
  active: bool
55
63
  source: str
@@ -57,6 +65,8 @@ class SegmentMeta(TypedDict, total=False):
57
65
 
58
66
 
59
67
  class MapLayerBase(TypedDict):
68
+ """Base structure for map layers with pixels and dimensions."""
69
+
60
70
  __class__: Literal["MapLayer"]
61
71
  type: str
62
72
  pixels: list[int]
@@ -65,11 +75,15 @@ class MapLayerBase(TypedDict):
65
75
 
66
76
 
67
77
  class FloorWallLayer(MapLayerBase):
78
+ """Map layer representing floor or wall areas."""
79
+
68
80
  metaData: FloorWallMeta
69
81
  type: Literal["floor", "wall"]
70
82
 
71
83
 
72
84
  class SegmentLayer(MapLayerBase):
85
+ """Map layer representing a room segment."""
86
+
73
87
  metaData: SegmentMeta
74
88
  type: Literal["segment"]
75
89
 
@@ -78,12 +92,16 @@ class SegmentLayer(MapLayerBase):
78
92
 
79
93
 
80
94
  class PointMeta(TypedDict, total=False):
95
+ """Metadata for point entities including angle, label, and ID."""
96
+
81
97
  angle: float
82
98
  label: str
83
99
  id: str
84
100
 
85
101
 
86
102
  class PointMapEntity(TypedDict):
103
+ """Point-based map entity (robot, charger, obstacle, etc.)."""
104
+
87
105
  __class__: Literal["PointMapEntity"]
88
106
  type: str
89
107
  points: list[int]
@@ -91,6 +109,8 @@ class PointMapEntity(TypedDict):
91
109
 
92
110
 
93
111
  class PathMapEntity(TypedDict):
112
+ """Path-based map entity representing robot movement paths."""
113
+
94
114
  __class__: Literal["PathMapEntity"]
95
115
  type: str
96
116
  points: list[int]
@@ -103,16 +123,22 @@ Entity = PointMapEntity | PathMapEntity
103
123
 
104
124
 
105
125
  class MapMeta(TypedDict, total=False):
126
+ """Metadata for the Valetudo map including version and total area."""
127
+
106
128
  version: int
107
129
  totalLayerArea: int
108
130
 
109
131
 
110
132
  class Size(TypedDict):
133
+ """Map size with x and y dimensions."""
134
+
111
135
  x: int
112
136
  y: int
113
137
 
114
138
 
115
139
  class ValetudoMap(TypedDict):
140
+ """Complete Valetudo map structure with layers and entities."""
141
+
116
142
  __class__: Literal["ValetudoMap"]
117
143
  metaData: MapMeta
118
144
  size: Size
@@ -673,6 +699,11 @@ class RandImageData:
673
699
  img = RandImageData.get_rrm_image(json_data)
674
700
  seg_data = img.get("segments", {})
675
701
  seg_ids = seg_data.get("id")
702
+
703
+ # Handle missing or invalid segment IDs gracefully
704
+ if not seg_ids:
705
+ return ([], []) if out_lines else []
706
+
676
707
  segments = []
677
708
  outlines = []
678
709
  count_seg = 0