valetudo-map-parser 0.1.9b54__tar.gz → 0.1.9b56__tar.gz

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-0.1.9b54 → valetudo_map_parser-0.1.9b56}/PKG-INFO +1 -1
  2. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/__init__.py +2 -1
  3. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/shared.py +1 -0
  4. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/hypfer_draw.py +7 -0
  5. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/rand25_handler.py +195 -128
  6. valetudo_map_parser-0.1.9b56/SCR/valetudo_map_parser/rooms_handler.py +470 -0
  7. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/pyproject.toml +1 -1
  8. valetudo_map_parser-0.1.9b54/SCR/valetudo_map_parser/rooms_handler.py +0 -225
  9. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/LICENSE +0 -0
  10. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/NOTICE.txt +0 -0
  11. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/README.md +0 -0
  12. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  13. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  14. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
  15. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/colors.py +0 -0
  16. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/drawable.py +0 -0
  17. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
  18. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
  19. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  20. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/rand25_parser.py +0 -0
  21. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/room_outline.py +0 -0
  22. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/types.py +0 -0
  23. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/config/utils.py +0 -0
  24. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/hypfer_handler.py +0 -0
  25. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
  26. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/map_data.py +0 -0
  27. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/py.typed +0 -0
  28. {valetudo_map_parser-0.1.9b54 → valetudo_map_parser-0.1.9b56}/SCR/valetudo_map_parser/reimg_draw.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b54
3
+ Version: 0.1.9b56
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -17,11 +17,12 @@ from .config.types import (
17
17
  )
18
18
  from .hypfer_handler import HypferMapImageHandler
19
19
  from .rand25_handler import ReImageHandler
20
- from .rooms_handler import RoomsHandler
20
+ from .rooms_handler import RoomsHandler, RandRoomsHandler
21
21
 
22
22
 
23
23
  __all__ = [
24
24
  "RoomsHandler",
25
+ "RandRoomsHandler",
25
26
  "HypferMapImageHandler",
26
27
  "ReImageHandler",
27
28
  "RRMapParser",
@@ -88,6 +88,7 @@ class CameraShared:
88
88
  self.vac_json_id = None # Vacuum json id
89
89
  self.margins = "100" # Image margins
90
90
  self.obstacles_data = None # Obstacles data
91
+ self.obstacles_pos = None # Obstacles position
91
92
  self.offset_top = 0 # Image offset top
92
93
  self.offset_down = 0 # Image offset down
93
94
  self.offset_left = 0 # Image offset left
@@ -303,6 +303,13 @@ class ImageDraw:
303
303
  await self.img_h.draw.async_draw_obstacles(
304
304
  np_array, obstacle_positions, color_no_go
305
305
  )
306
+
307
+ # Update both obstacles_pos and obstacles_data
308
+ self.img_h.shared.obstacles_pos = obstacle_positions
309
+ # Only update obstacles_data if it's None or if the number of obstacles has changed
310
+ if (self.img_h.shared.obstacles_data is None or
311
+ len(self.img_h.shared.obstacles_data) != len(obstacle_positions)):
312
+ self.img_h.shared.obstacles_data = obstacle_positions
306
313
  return np_array
307
314
 
308
315
  async def async_draw_charger(
@@ -29,13 +29,13 @@ from .config.types import (
29
29
  )
30
30
  from .config.utils import (
31
31
  BaseHandler,
32
- async_extract_room_outline,
33
32
  initialize_drawing_config,
34
33
  manage_drawable_elements,
35
34
  prepare_resize_params,
36
35
  )
37
36
  from .map_data import RandImageData
38
37
  from .reimg_draw import ImageDraw
38
+ from .rooms_handler import RandRoomsHandler
39
39
 
40
40
 
41
41
  _LOGGER = logging.getLogger(__name__)
@@ -68,38 +68,13 @@ class ReImageHandler(BaseHandler, AutoCrop):
68
68
  self.active_zones = None # Active zones
69
69
  self.file_name = self.shared.file_name # File name
70
70
  self.imd = ImageDraw(self) # Image Draw
71
-
72
- async def extract_room_outline_from_map(self, room_id_int, pixels):
73
- """Extract the outline of a room using the pixel data and element map.
74
-
75
- Args:
76
- room_id_int: The room ID as an integer
77
- pixels: List of pixel coordinates in the format [[x, y, z], ...]
78
-
79
- Returns:
80
- List of points forming the outline of the room
81
- """
82
- # Calculate x and y min/max from compressed pixels for rectangular fallback
83
- x_values = []
84
- y_values = []
85
- for x, y, _ in pixels:
86
- x_values.append(x)
87
- y_values.append(y)
88
-
89
- if not x_values or not y_values:
90
- return []
91
-
92
- min_x, max_x = min(x_values), max(x_values)
93
- min_y, max_y = min(y_values), max(y_values)
94
-
95
- # Always return a rectangular outline since element_map is removed
96
- return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
71
+ self.rooms_handler = RandRoomsHandler(self.file_name, self.drawing_config) # Room data handler
97
72
 
98
73
  async def extract_room_properties(
99
74
  self, json_data: JsonType, destinations: JsonType
100
75
  ) -> RoomsProperties:
101
76
  """Extract the room properties."""
102
- unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
77
+ # unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
103
78
  size_x, size_y = RandImageData.get_rrm_image_size(json_data)
104
79
  top, left = RandImageData.get_rrm_image_position(json_data)
105
80
  try:
@@ -110,62 +85,44 @@ class ReImageHandler(BaseHandler, AutoCrop):
110
85
  ) = await RandImageData.async_get_rrm_segments(
111
86
  json_data, size_x, size_y, top, left, True
112
87
  )
88
+
113
89
  dest_json = destinations
114
- room_data = dict(dest_json).get("rooms", [])
115
90
  zones_data = dict(dest_json).get("zones", [])
116
91
  points_data = dict(dest_json).get("spots", [])
117
- room_id_to_data = {room["id"]: room for room in room_data}
92
+
93
+ # Use the RandRoomsHandler to extract room properties
94
+ room_properties = await self.rooms_handler.async_extract_room_properties(
95
+ json_data, dest_json
96
+ )
97
+
98
+ # Update self.rooms_pos from room_properties for compatibility with other methods
118
99
  self.rooms_pos = []
119
- room_properties = {}
120
- if self.outlines:
121
- for id_x, room_id in enumerate(unsorted_id):
122
- if room_id in room_id_to_data:
123
- room_info = room_id_to_data[room_id]
124
- name = room_info.get("name")
125
- # Calculate x and y min/max from outlines
126
- x_min = self.outlines[id_x][0][0]
127
- x_max = self.outlines[id_x][1][0]
128
- y_min = self.outlines[id_x][0][1]
129
- y_max = self.outlines[id_x][1][1]
130
- corners = self.get_corners(x_max, x_min, y_max, y_min)
131
- # rand256 vacuums accept int(room_id) or str(name)
132
- # the card will soon support int(room_id) but the camera will send name
133
- # this avoids the manual change of the values in the card.
134
- self.rooms_pos.append(
135
- {
136
- "name": name,
137
- "corners": corners,
138
- }
139
- )
140
- room_properties[int(room_id)] = {
141
- "number": int(room_id),
142
- "outline": corners,
143
- "name": name,
144
- "x": (x_min + x_max) // 2,
145
- "y": (y_min + y_max) // 2,
146
- }
147
- # get the zones and points data
148
- zone_properties = await self.async_zone_propriety(zones_data)
149
- # get the points data
150
- point_properties = await self.async_points_propriety(points_data)
151
- if room_properties or zone_properties:
152
- extracted_data = [
153
- f"{len(room_properties)} Rooms" if room_properties else None,
154
- f"{len(zone_properties)} Zones" if zone_properties else None,
155
- ]
156
- extracted_data = ", ".join(filter(None, extracted_data))
157
- _LOGGER.debug("Extracted data: %s", extracted_data)
158
- else:
159
- self.rooms_pos = None
160
- _LOGGER.debug(
161
- "%s: Rooms and Zones data not available!", self.file_name
162
- )
163
- rooms = RoomStore(self.file_name, room_properties)
164
- _LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
165
- return room_properties, zone_properties, point_properties
100
+ for room_id, room_data in room_properties.items():
101
+ self.rooms_pos.append(
102
+ {"name": room_data["name"], "outline": room_data["outline"]}
103
+ )
104
+
105
+ # get the zones and points data
106
+ zone_properties = await self.async_zone_propriety(zones_data)
107
+ # get the points data
108
+ point_properties = await self.async_points_propriety(points_data)
109
+
110
+ if room_properties or zone_properties:
111
+ extracted_data = [
112
+ f"{len(room_properties)} Rooms" if room_properties else None,
113
+ f"{len(zone_properties)} Zones" if zone_properties else None,
114
+ ]
115
+ extracted_data = ", ".join(filter(None, extracted_data))
116
+ _LOGGER.debug("Extracted data: %s", extracted_data)
166
117
  else:
167
- _LOGGER.debug("%s: No outlines available", self.file_name)
168
- return None, None, None
118
+ self.rooms_pos = None
119
+ _LOGGER.debug(
120
+ "%s: Rooms and Zones data not available!", self.file_name
121
+ )
122
+
123
+ rooms = RoomStore(self.file_name, room_properties)
124
+ _LOGGER.debug("Rooms Data: %s", rooms.get_rooms())
125
+ return room_properties, zone_properties, point_properties
169
126
  except (RuntimeError, ValueError) as e:
170
127
  _LOGGER.debug(
171
128
  "No rooms Data or Error in extract_room_properties: %s",
@@ -368,79 +325,189 @@ class ReImageHandler(BaseHandler, AutoCrop):
368
325
  _LOGGER.debug("Got Rooms Attributes.")
369
326
  return self.room_propriety
370
327
 
328
+ @staticmethod
329
+ def point_in_polygon(x: int, y: int, polygon: list) -> bool:
330
+ """
331
+ Check if a point is inside a polygon using ray casting algorithm.
332
+ Enhanced version with better handling of edge cases.
333
+
334
+ Args:
335
+ x: X coordinate of the point
336
+ y: Y coordinate of the point
337
+ polygon: List of (x, y) tuples forming the polygon
338
+
339
+ Returns:
340
+ True if the point is inside the polygon, False otherwise
341
+ """
342
+ # Ensure we have a valid polygon with at least 3 points
343
+ if len(polygon) < 3:
344
+ return False
345
+
346
+ # Make sure the polygon is closed (last point equals first point)
347
+ if polygon[0] != polygon[-1]:
348
+ polygon = polygon + [polygon[0]]
349
+
350
+ # Use winding number algorithm for better accuracy
351
+ wn = 0 # Winding number counter
352
+
353
+ # Loop through all edges of the polygon
354
+ for i in range(len(polygon) - 1): # Last vertex is first vertex
355
+ p1x, p1y = polygon[i]
356
+ p2x, p2y = polygon[i + 1]
357
+
358
+ # Test if a point is left/right/on the edge defined by two vertices
359
+ if p1y <= y: # Start y <= P.y
360
+ if p2y > y: # End y > P.y (upward crossing)
361
+ # Point left of edge
362
+ if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) > 0:
363
+ wn += 1 # Valid up intersect
364
+ else: # Start y > P.y
365
+ if p2y <= y: # End y <= P.y (downward crossing)
366
+ # Point right of edge
367
+ if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) < 0:
368
+ wn -= 1 # Valid down intersect
369
+
370
+ # If winding number is not 0, the point is inside the polygon
371
+ return wn != 0
372
+
371
373
  async def async_get_robot_in_room(
372
374
  self, robot_x: int, robot_y: int, angle: float
373
375
  ) -> RobotPosition:
374
376
  """Get the robot position and return in what room is."""
377
+ # First check if we already have a cached room and if the robot is still in it
378
+ if self.robot_in_room:
379
+ # If we have outline data, use point_in_polygon for accurate detection
380
+ if "outline" in self.robot_in_room:
381
+ outline = self.robot_in_room["outline"]
382
+ if self.point_in_polygon(int(robot_x), int(robot_y), outline):
383
+ temp = {
384
+ "x": robot_x,
385
+ "y": robot_y,
386
+ "angle": angle,
387
+ "in_room": self.robot_in_room["room"],
388
+ }
389
+ # Handle active zones
390
+ self.active_zones = self.shared.rand256_active_zone
391
+ self.zooming = False
392
+ if self.active_zones and (
393
+ self.robot_in_room["id"]
394
+ in range(len(self.active_zones))
395
+ ):
396
+ self.zooming = bool(
397
+ self.active_zones[self.robot_in_room["id"]]
398
+ )
399
+ else:
400
+ self.zooming = False
401
+ return temp
402
+ # Fallback to bounding box check if no outline data
403
+ elif all(
404
+ k in self.robot_in_room for k in ["left", "right", "up", "down"]
405
+ ):
406
+ if (
407
+ (self.robot_in_room["right"] <= int(robot_x) <= self.robot_in_room["left"])
408
+ and (self.robot_in_room["up"] <= int(robot_y) <= self.robot_in_room["down"])
409
+ ):
410
+ temp = {
411
+ "x": robot_x,
412
+ "y": robot_y,
413
+ "angle": angle,
414
+ "in_room": self.robot_in_room["room"],
415
+ }
416
+ # Handle active zones
417
+ self.active_zones = self.shared.rand256_active_zone
418
+ self.zooming = False
419
+ if self.active_zones and (
420
+ self.robot_in_room["id"]
421
+ in range(len(self.active_zones))
422
+ ):
423
+ self.zooming = bool(
424
+ self.active_zones[self.robot_in_room["id"]]
425
+ )
426
+ else:
427
+ self.zooming = False
428
+ return temp
375
429
 
376
- def _check_robot_position(x: int, y: int) -> bool:
377
- # Check if the robot coordinates are inside the room's corners
378
- return (
379
- self.robot_in_room["left"] >= x >= self.robot_in_room["right"]
380
- and self.robot_in_room["up"] >= y >= self.robot_in_room["down"]
381
- )
430
+ # If we don't have a cached room or the robot is not in it, search all rooms
431
+ last_room = None
432
+ room_count = 0
433
+ if self.robot_in_room:
434
+ last_room = self.robot_in_room
382
435
 
383
- # If the robot coordinates are inside the room's
384
- if self.robot_in_room and _check_robot_position(robot_x, robot_y):
436
+ # Check if the robot is far outside the normal map boundaries
437
+ # This helps prevent false positives for points very far from any room
438
+ map_boundary = 50000 # Typical map size is around 25000-30000 units for Rand25
439
+ if abs(robot_x) > map_boundary or abs(robot_y) > map_boundary:
440
+ _LOGGER.debug(
441
+ "%s robot position (%s, %s) is far outside map boundaries.",
442
+ self.file_name,
443
+ robot_x,
444
+ robot_y,
445
+ )
446
+ self.robot_in_room = last_room
447
+ self.zooming = False
385
448
  temp = {
386
449
  "x": robot_x,
387
450
  "y": robot_y,
388
451
  "angle": angle,
389
- "in_room": self.robot_in_room["room"],
452
+ "in_room": last_room["room"] if last_room else "unknown",
390
453
  }
391
- self.active_zones = self.shared.rand256_active_zone
392
- self.zooming = False
393
- if self.active_zones and (
394
- (self.robot_in_room["id"]) in range(len(self.active_zones))
395
- ): # issue #100 Index out of range
396
- self.zooming = bool(self.active_zones[self.robot_in_room["id"]])
397
454
  return temp
398
- # else we need to search and use the async method
399
- _LOGGER.debug("%s Changed room.. searching..", self.file_name)
400
- room_count = -1
401
- last_room = None
402
455
 
403
- # If no rooms data is available, return a default position
456
+ # Search through all rooms to find which one contains the robot
404
457
  if not self.rooms_pos:
405
- _LOGGER.debug("%s: No rooms data available", self.file_name)
406
- return {"x": robot_x, "y": robot_y, "angle": angle, "in_room": "unknown"}
458
+ _LOGGER.debug(
459
+ "%s: No rooms data available for robot position detection.",
460
+ self.file_name,
461
+ )
462
+ self.robot_in_room = last_room
463
+ self.zooming = False
464
+ temp = {
465
+ "x": robot_x,
466
+ "y": robot_y,
467
+ "angle": angle,
468
+ "in_room": last_room["room"] if last_room else "unknown",
469
+ }
470
+ return temp
407
471
 
408
- # If rooms data is available, search for the room
409
- if self.robot_in_room:
410
- last_room = self.robot_in_room
472
+ _LOGGER.debug("%s: Searching for robot in rooms...", self.file_name)
411
473
  for room in self.rooms_pos:
412
- corners = room["corners"]
474
+ # Check if the room has an outline (polygon points)
475
+ if "outline" in room:
476
+ outline = room["outline"]
477
+ # Use point_in_polygon for accurate detection with complex shapes
478
+ if self.point_in_polygon(int(robot_x), int(robot_y), outline):
479
+ # Robot is in this room
480
+ self.robot_in_room = {
481
+ "id": room_count,
482
+ "room": str(room["name"]),
483
+ "outline": outline,
484
+ }
485
+ temp = {
486
+ "x": robot_x,
487
+ "y": robot_y,
488
+ "angle": angle,
489
+ "in_room": self.robot_in_room["room"],
490
+ }
491
+ _LOGGER.debug(
492
+ "%s is in %s room (polygon detection).",
493
+ self.file_name,
494
+ self.robot_in_room["room"],
495
+ )
496
+ return temp
413
497
  room_count += 1
414
- self.robot_in_room = {
415
- "id": room_count,
416
- "left": corners[0][0],
417
- "right": corners[2][0],
418
- "up": corners[0][1],
419
- "down": corners[2][1],
420
- "room": room["name"],
421
- }
422
- # Check if the robot coordinates are inside the room's corners
423
- if _check_robot_position(robot_x, robot_y):
424
- temp = {
425
- "x": robot_x,
426
- "y": robot_y,
427
- "angle": angle,
428
- "in_room": self.robot_in_room["room"],
429
- }
430
- _LOGGER.debug("%s is in %s", self.file_name, self.robot_in_room["room"])
431
- del room, corners, robot_x, robot_y # free memory.
432
- return temp
433
- # After checking all rooms and not finding a match
498
+
499
+ # Robot not found in any room
434
500
  _LOGGER.debug(
435
- "%s: Not located within Camera Rooms coordinates.", self.file_name
501
+ "%s not located within any room coordinates.",
502
+ self.file_name,
436
503
  )
437
- self.zooming = False
438
504
  self.robot_in_room = last_room
505
+ self.zooming = False
439
506
  temp = {
440
507
  "x": robot_x,
441
508
  "y": robot_y,
442
509
  "angle": angle,
443
- "in_room": self.robot_in_room["room"] if self.robot_in_room else "unknown",
510
+ "in_room": last_room["room"] if last_room else "unknown",
444
511
  }
445
512
  return temp
446
513
 
@@ -0,0 +1,470 @@
1
+ """
2
+ Hipfer Rooms Handler Module.
3
+ Handles room data extraction and processing for Valetudo Hipfer vacuum maps.
4
+ Provides async methods for room outline extraction and properties management.
5
+ Version: 0.1.9
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Any, Dict, List, Optional, Tuple
12
+
13
+ import numpy as np
14
+ from scipy.ndimage import binary_dilation, binary_erosion
15
+ from scipy.spatial import ConvexHull
16
+
17
+ from .config.drawable_elements import DrawableElement, DrawingConfig
18
+ from .config.types import LOGGER, RoomsProperties
19
+
20
+ from .map_data import RandImageData, ImageData
21
+
22
+ class RoomsHandler:
23
+ """
24
+ Handler for extracting and managing room data from Hipfer vacuum maps.
25
+
26
+ This class provides methods to:
27
+ - Extract room outlines using the Ramer-Douglas-Peucker algorithm
28
+ - Process room properties from JSON data
29
+ - Generate room masks and extract contours
30
+
31
+ All methods are async for better integration with the rest of the codebase.
32
+ """
33
+
34
+ def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
35
+ """
36
+ Initialize the HipferRoomsHandler.
37
+
38
+ Args:
39
+ vacuum_id: Identifier for the vacuum
40
+ drawing_config: Configuration for which elements to draw (optional)
41
+ """
42
+ self.vacuum_id = vacuum_id
43
+ self.drawing_config = drawing_config
44
+ self.current_json_data = (
45
+ None # Will store the current JSON data being processed
46
+ )
47
+
48
+ @staticmethod
49
+ def sublist(data: list, chunk_size: int) -> list:
50
+ return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
51
+
52
+ @staticmethod
53
+ def convex_hull_outline(mask: np.ndarray) -> list[tuple[int, int]]:
54
+ y_indices, x_indices = np.where(mask > 0)
55
+ if len(x_indices) == 0 or len(y_indices) == 0:
56
+ return []
57
+
58
+ points = np.column_stack((x_indices, y_indices))
59
+ if len(points) < 3:
60
+ return [(int(x), int(y)) for x, y in points]
61
+
62
+ hull = ConvexHull(points)
63
+ # Convert numpy.int64 values to regular Python integers
64
+ hull_points = [
65
+ (int(points[vertex][0]), int(points[vertex][1])) for vertex in hull.vertices
66
+ ]
67
+ if hull_points[0] != hull_points[-1]:
68
+ hull_points.append(hull_points[0])
69
+ return hull_points
70
+
71
+ async def _process_room_layer(
72
+ self, layer: Dict[str, Any], width: int, height: int, pixel_size: int
73
+ ) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
74
+ """Process a single room layer and extract its outline.
75
+
76
+ Args:
77
+ layer: The layer data from the JSON
78
+ width: The width of the map
79
+ height: The height of the map
80
+ pixel_size: The size of each pixel
81
+
82
+ Returns:
83
+ Tuple of (room_id, room_data) or (None, None) if processing failed
84
+ """
85
+ meta_data = layer.get("metaData", {})
86
+ segment_id = meta_data.get("segmentId")
87
+ name = meta_data.get("name", "Room {}".format(segment_id))
88
+ compressed_pixels = layer.get("compressedPixels", [])
89
+ pixels = self.sublist(compressed_pixels, 3)
90
+
91
+ # Check if this room is enabled in the drawing configuration
92
+ if self.drawing_config is not None:
93
+ # Convert segment_id to room element (ROOM_1 to ROOM_15)
94
+ try:
95
+ # Segment IDs might not be sequential, so we need to map them to room elements
96
+ # We'll use a simple approach: if segment_id is an integer, use it directly
97
+ room_element_id = int(segment_id)
98
+ if 1 <= room_element_id <= 15:
99
+ room_element = getattr(
100
+ DrawableElement, f"ROOM_{room_element_id}", None
101
+ )
102
+ if room_element:
103
+ is_enabled = self.drawing_config.is_enabled(room_element)
104
+ if not is_enabled:
105
+ # Skip this room if it's disabled
106
+ LOGGER.debug("Skipping disabled room %s", segment_id)
107
+ return None, None
108
+ except (ValueError, TypeError):
109
+ # If segment_id is not a valid integer, we can't map it to a room element
110
+ # In this case, we'll include the room (fail open)
111
+ LOGGER.debug(
112
+ "Could not convert segment_id %s to room element", segment_id
113
+ )
114
+
115
+ # Optimization: Create a smaller mask for just the room area
116
+ if not pixels:
117
+ # Skip if no pixels
118
+ return None, None
119
+
120
+ # Convert to numpy arrays for vectorized operations
121
+ pixel_data = np.array(pixels)
122
+
123
+ if pixel_data.size == 0:
124
+ return None, None
125
+
126
+ # Find the actual bounds of the room to create a smaller mask
127
+ # Add padding to ensure we don't lose edge details
128
+ padding = 10 # Add padding pixels around the room
129
+ min_x = max(0, int(np.min(pixel_data[:, 0])) - padding)
130
+ max_x = min(
131
+ width, int(np.max(pixel_data[:, 0]) + np.max(pixel_data[:, 2])) + padding
132
+ )
133
+ min_y = max(0, int(np.min(pixel_data[:, 1])) - padding)
134
+ max_y = min(height, int(np.max(pixel_data[:, 1]) + 1) + padding)
135
+
136
+ # Create a smaller mask for just the room area (much faster)
137
+ local_width = max_x - min_x
138
+ local_height = max_y - min_y
139
+
140
+ # Skip if dimensions are invalid
141
+ if local_width <= 0 or local_height <= 0:
142
+ return None, None
143
+
144
+ # Create a smaller mask
145
+ local_mask = np.zeros((local_height, local_width), dtype=np.uint8)
146
+
147
+ # Fill the mask efficiently
148
+ for x, y, length in pixel_data:
149
+ x, y, length = int(x), int(y), int(length)
150
+ # Adjust coordinates to local mask
151
+ local_x = x - min_x
152
+ local_y = y - min_y
153
+
154
+ # Ensure we're within bounds
155
+ if 0 <= local_y < local_height and 0 <= local_x < local_width:
156
+ # Calculate the end point, clamping to mask width
157
+ end_x = min(local_x + length, local_width)
158
+ if end_x > local_x: # Only process if there's a valid segment
159
+ local_mask[local_y, local_x:end_x] = 1
160
+
161
+ # Apply morphological operations
162
+ struct_elem = np.ones((3, 3), dtype=np.uint8)
163
+ eroded = binary_erosion(local_mask, structure=struct_elem, iterations=1)
164
+ mask = binary_dilation(eroded, structure=struct_elem, iterations=1).astype(
165
+ np.uint8
166
+ )
167
+
168
+ # Extract contour from the mask
169
+ outline = self.convex_hull_outline(mask)
170
+ if not outline:
171
+ return None, None
172
+
173
+ # Adjust coordinates back to global space
174
+ outline = [(x + min_x, y + min_y) for (x, y) in outline]
175
+
176
+ # Use coordinates as-is without flipping Y coordinates
177
+ xs, ys = zip(*outline)
178
+ x_min, x_max = min(xs), max(xs)
179
+ y_min, y_max = min(ys), max(ys)
180
+
181
+ room_id = str(segment_id)
182
+
183
+ # Scale coordinates by pixel_size and convert to regular Python integers
184
+ scaled_outline = [
185
+ (int(x * pixel_size), int(y * pixel_size)) for x, y in outline
186
+ ]
187
+ room_data = {
188
+ "number": segment_id,
189
+ "outline": scaled_outline,
190
+ "name": name,
191
+ "x": int(((x_min + x_max) * pixel_size) // 2),
192
+ "y": int(((y_min + y_max) * pixel_size) // 2),
193
+ }
194
+
195
+ return room_id, room_data
196
+
197
+ async def async_extract_room_properties(self, json_data) -> RoomsProperties:
198
+ """Extract room properties from the JSON data.
199
+
200
+ This method processes all room layers in the JSON data and extracts their outlines.
201
+ It respects the drawing configuration, skipping rooms that are disabled.
202
+
203
+ Args:
204
+ json_data: The JSON data from the vacuum
205
+
206
+ Returns:
207
+ Dictionary of room properties
208
+ """
209
+ start_total = time.time()
210
+ room_properties = {}
211
+ pixel_size = json_data.get("pixelSize", 5)
212
+ height = json_data["size"]["y"]
213
+ width = json_data["size"]["x"]
214
+
215
+ for layer in json_data.get("layers", []):
216
+ if layer.get("__class") == "MapLayer" and layer.get("type") == "segment":
217
+ room_id, room_data = await self._process_room_layer(
218
+ layer, width, height, pixel_size
219
+ )
220
+ if room_id is not None and room_data is not None:
221
+ room_properties[room_id] = room_data
222
+
223
+ # Log timing information
224
+ total_time = time.time() - start_total
225
+ LOGGER.debug("Room extraction Total time: %.3fs", total_time)
226
+ return room_properties
227
+
228
+ class RandRoomsHandler:
229
+ """
230
+ Handler for extracting and managing room data from Rand25 vacuum maps.
231
+
232
+ This class provides methods to:
233
+ - Extract room outlines using the Convex Hull algorithm
234
+ - Process room properties from JSON data and destinations JSON
235
+ - Generate room masks and extract contours
236
+
237
+ All methods are async for better integration with the rest of the codebase.
238
+ """
239
+
240
+ def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
241
+ """
242
+ Initialize the RandRoomsHandler.
243
+
244
+ Args:
245
+ vacuum_id: Identifier for the vacuum
246
+ drawing_config: Configuration for which elements to draw (optional)
247
+ """
248
+ self.vacuum_id = vacuum_id
249
+ self.drawing_config = drawing_config
250
+ self.current_json_data = None # Will store the current JSON data being processed
251
+ self.segment_data = None # Segment data
252
+ self.outlines = None # Outlines data
253
+
254
+ @staticmethod
255
+ def sublist(data: list, chunk_size: int) -> list:
256
+ """Split a list into chunks of specified size."""
257
+ return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
258
+
259
+ @staticmethod
260
+ def convex_hull_outline(points: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
261
+ """
262
+ Generate a convex hull outline from a set of points.
263
+
264
+ Args:
265
+ points: List of (x, y) coordinate tuples
266
+
267
+ Returns:
268
+ List of (x, y) tuples forming the convex hull outline
269
+ """
270
+ if len(points) == 0:
271
+ return []
272
+
273
+ # Convert to numpy array for processing
274
+ points_array = np.array(points)
275
+
276
+ if len(points) < 3:
277
+ # Not enough points for a convex hull, return the points as is
278
+ return [(int(x), int(y)) for x, y in points_array]
279
+
280
+ try:
281
+ # Calculate the convex hull
282
+ hull = ConvexHull(points_array)
283
+
284
+ # Extract the vertices in order
285
+ hull_points = [
286
+ (int(points_array[vertex][0]), int(points_array[vertex][1]))
287
+ for vertex in hull.vertices
288
+ ]
289
+
290
+ # Close the polygon by adding the first point at the end
291
+ if hull_points[0] != hull_points[-1]:
292
+ hull_points.append(hull_points[0])
293
+
294
+ return hull_points
295
+
296
+ except Exception as e:
297
+ LOGGER.warning(f"Error calculating convex hull: {e}")
298
+
299
+ # Fallback to bounding box if convex hull fails
300
+ x_min, y_min = np.min(points_array, axis=0)
301
+ x_max, y_max = np.max(points_array, axis=0)
302
+
303
+ return [
304
+ (int(x_min), int(y_min)), # Top-left
305
+ (int(x_max), int(y_min)), # Top-right
306
+ (int(x_max), int(y_max)), # Bottom-right
307
+ (int(x_min), int(y_max)), # Bottom-left
308
+ (int(x_min), int(y_min)), # Back to top-left to close the polygon
309
+ ]
310
+
311
+ async def _process_segment_data(
312
+ self, segment_data: List, segment_id: int, pixel_size: int
313
+ ) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
314
+ """
315
+ Process a single segment and extract its outline.
316
+
317
+ Args:
318
+ segment_data: The segment pixel data
319
+ segment_id: The ID of the segment
320
+ pixel_size: The size of each pixel
321
+
322
+ Returns:
323
+ Tuple of (room_id, room_data) or (None, None) if processing failed
324
+ """
325
+ # Check if this room is enabled in the drawing configuration
326
+ if self.drawing_config is not None:
327
+ try:
328
+ # Convert segment_id to room element (ROOM_1 to ROOM_15)
329
+ room_element_id = int(segment_id)
330
+ if 1 <= room_element_id <= 15:
331
+ room_element = getattr(
332
+ DrawableElement, f"ROOM_{room_element_id}", None
333
+ )
334
+ if room_element:
335
+ is_enabled = self.drawing_config.is_enabled(room_element)
336
+ if not is_enabled:
337
+ # Skip this room if it's disabled
338
+ LOGGER.debug("Skipping disabled room %s", segment_id)
339
+ return None, None
340
+ except (ValueError, TypeError):
341
+ # If segment_id is not a valid integer, we can't map it to a room element
342
+ # In this case, we'll include the room (fail open)
343
+ LOGGER.debug(
344
+ "Could not convert segment_id %s to room element", segment_id
345
+ )
346
+
347
+ # Skip if no pixels
348
+ if not segment_data:
349
+ return None, None
350
+
351
+ # Extract points from segment data
352
+ points = []
353
+ for x, y, _ in segment_data:
354
+ points.append((int(x), int(y)))
355
+
356
+ if not points:
357
+ return None, None
358
+
359
+ # Use convex hull to get the outline
360
+ outline = self.convex_hull_outline(points)
361
+ if not outline:
362
+ return None, None
363
+
364
+ # Calculate bounding box for the room
365
+ xs, ys = zip(*outline)
366
+ x_min, x_max = min(xs), max(xs)
367
+ y_min, y_max = min(ys), max(ys)
368
+
369
+ # Scale coordinates by pixel_size
370
+ scaled_outline = [
371
+ (int(x * pixel_size), int(y * pixel_size)) for x, y in outline
372
+ ]
373
+
374
+ room_id = str(segment_id)
375
+ room_data = {
376
+ "number": segment_id,
377
+ "outline": scaled_outline,
378
+ "name": f"Room {segment_id}", # Default name, will be updated from destinations
379
+ "x": int(((x_min + x_max) * pixel_size) // 2),
380
+ "y": int(((y_min + y_max) * pixel_size) // 2),
381
+ }
382
+
383
+ return room_id, room_data
384
+
385
+ async def async_extract_room_properties(
386
+ self, json_data: Dict[str, Any], destinations: Dict[str, Any]
387
+ ) -> RoomsProperties:
388
+ """
389
+ Extract room properties from the JSON data and destinations.
390
+
391
+ Args:
392
+ json_data: The JSON data from the vacuum
393
+ destinations: The destinations JSON containing room names and IDs
394
+
395
+ Returns:
396
+ Dictionary of room properties
397
+ """
398
+ start_total = time.time()
399
+ room_properties = {}
400
+
401
+ # Get basic map information
402
+ unsorted_id = RandImageData.get_rrm_segments_ids(json_data)
403
+ size_x, size_y = RandImageData.get_rrm_image_size(json_data)
404
+ top, left = RandImageData.get_rrm_image_position(json_data)
405
+ pixel_size = 50 # Rand25 vacuums use a larger pixel size to match the original implementation
406
+
407
+ # Get segment data and outlines if not already available
408
+ if not self.segment_data or not self.outlines:
409
+ (
410
+ self.segment_data,
411
+ self.outlines,
412
+ ) = await RandImageData.async_get_rrm_segments(
413
+ json_data, size_x, size_y, top, left, True
414
+ )
415
+
416
+ # Process destinations JSON to get room names
417
+ dest_json = destinations
418
+ room_data = dest_json.get("rooms", [])
419
+ room_id_to_data = {room["id"]: room for room in room_data}
420
+
421
+ # Process each segment
422
+ if unsorted_id and self.segment_data and self.outlines:
423
+ for idx, segment_id in enumerate(unsorted_id):
424
+ # Extract points from segment data
425
+ points = []
426
+ for x, y, _ in self.segment_data[idx]:
427
+ points.append((int(x), int(y)))
428
+
429
+ if not points:
430
+ continue
431
+
432
+ # Use convex hull to get the outline
433
+ outline = self.convex_hull_outline(points)
434
+ if not outline:
435
+ continue
436
+
437
+ # Scale coordinates by pixel_size
438
+ scaled_outline = [
439
+ (int(x * pixel_size), int(y * pixel_size)) for x, y in outline
440
+ ]
441
+
442
+ # Calculate center point
443
+ xs, ys = zip(*outline)
444
+ x_min, x_max = min(xs), max(xs)
445
+ y_min, y_max = min(ys), max(ys)
446
+ center_x = int(((x_min + x_max) * pixel_size) // 2)
447
+ center_y = int(((y_min + y_max) * pixel_size) // 2)
448
+
449
+ # Create room data
450
+ room_id = str(segment_id)
451
+ room_data = {
452
+ "number": segment_id,
453
+ "outline": scaled_outline,
454
+ "name": f"Room {segment_id}", # Default name, will be updated from destinations
455
+ "x": center_x,
456
+ "y": center_y,
457
+ }
458
+
459
+ # Update room name from destinations if available
460
+ if segment_id in room_id_to_data:
461
+ room_info = room_id_to_data[segment_id]
462
+ room_data["name"] = room_info.get("name", room_data["name"])
463
+
464
+ room_properties[room_id] = room_data
465
+
466
+ # Log timing information
467
+ total_time = time.time() - start_total
468
+ LOGGER.debug("Room extraction Total time: %.3fs", total_time)
469
+
470
+ return room_properties
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "valetudo-map-parser"
3
- version = "0.1.9b54"
3
+ version = "0.1.9b56"
4
4
  description = "A Python library to parse Valetudo map data returning a PIL Image object."
5
5
  authors = ["Sandro Cantarella <gsca075@gmail.com>"]
6
6
  license = "Apache-2.0"
@@ -1,225 +0,0 @@
1
- """
2
- Hipfer Rooms Handler Module.
3
- Handles room data extraction and processing for Valetudo Hipfer vacuum maps.
4
- Provides async methods for room outline extraction and properties management.
5
- Version: 0.1.9
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import time
11
- from typing import Any, Dict, Optional, Tuple
12
-
13
- import numpy as np
14
- from scipy.ndimage import binary_dilation, binary_erosion
15
- from scipy.spatial import ConvexHull
16
-
17
- from .config.drawable_elements import DrawableElement, DrawingConfig
18
- from .config.types import LOGGER, RoomsProperties
19
-
20
-
21
- class RoomsHandler:
22
- """
23
- Handler for extracting and managing room data from Hipfer vacuum maps.
24
-
25
- This class provides methods to:
26
- - Extract room outlines using the Ramer-Douglas-Peucker algorithm
27
- - Process room properties from JSON data
28
- - Generate room masks and extract contours
29
-
30
- All methods are async for better integration with the rest of the codebase.
31
- """
32
-
33
- def __init__(self, vacuum_id: str, drawing_config: Optional[DrawingConfig] = None):
34
- """
35
- Initialize the HipferRoomsHandler.
36
-
37
- Args:
38
- vacuum_id: Identifier for the vacuum
39
- drawing_config: Configuration for which elements to draw (optional)
40
- """
41
- self.vacuum_id = vacuum_id
42
- self.drawing_config = drawing_config
43
- self.current_json_data = (
44
- None # Will store the current JSON data being processed
45
- )
46
-
47
- @staticmethod
48
- def sublist(data: list, chunk_size: int) -> list:
49
- return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
50
-
51
- @staticmethod
52
- def convex_hull_outline(mask: np.ndarray) -> list[tuple[int, int]]:
53
- y_indices, x_indices = np.where(mask > 0)
54
- if len(x_indices) == 0 or len(y_indices) == 0:
55
- return []
56
-
57
- points = np.column_stack((x_indices, y_indices))
58
- if len(points) < 3:
59
- return [(int(x), int(y)) for x, y in points]
60
-
61
- hull = ConvexHull(points)
62
- # Convert numpy.int64 values to regular Python integers
63
- hull_points = [
64
- (int(points[vertex][0]), int(points[vertex][1])) for vertex in hull.vertices
65
- ]
66
- if hull_points[0] != hull_points[-1]:
67
- hull_points.append(hull_points[0])
68
- return hull_points
69
-
70
- async def _process_room_layer(
71
- self, layer: Dict[str, Any], width: int, height: int, pixel_size: int
72
- ) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
73
- """Process a single room layer and extract its outline.
74
-
75
- Args:
76
- layer: The layer data from the JSON
77
- width: The width of the map
78
- height: The height of the map
79
- pixel_size: The size of each pixel
80
-
81
- Returns:
82
- Tuple of (room_id, room_data) or (None, None) if processing failed
83
- """
84
- meta_data = layer.get("metaData", {})
85
- segment_id = meta_data.get("segmentId")
86
- name = meta_data.get("name", "Room {}".format(segment_id))
87
- compressed_pixels = layer.get("compressedPixels", [])
88
- pixels = self.sublist(compressed_pixels, 3)
89
-
90
- # Check if this room is enabled in the drawing configuration
91
- if self.drawing_config is not None:
92
- # Convert segment_id to room element (ROOM_1 to ROOM_15)
93
- try:
94
- # Segment IDs might not be sequential, so we need to map them to room elements
95
- # We'll use a simple approach: if segment_id is an integer, use it directly
96
- room_element_id = int(segment_id)
97
- if 1 <= room_element_id <= 15:
98
- room_element = getattr(
99
- DrawableElement, f"ROOM_{room_element_id}", None
100
- )
101
- if room_element:
102
- is_enabled = self.drawing_config.is_enabled(room_element)
103
- if not is_enabled:
104
- # Skip this room if it's disabled
105
- LOGGER.debug("Skipping disabled room %s", segment_id)
106
- return None, None
107
- except (ValueError, TypeError):
108
- # If segment_id is not a valid integer, we can't map it to a room element
109
- # In this case, we'll include the room (fail open)
110
- LOGGER.debug(
111
- "Could not convert segment_id %s to room element", segment_id
112
- )
113
-
114
- # Optimization: Create a smaller mask for just the room area
115
- if not pixels:
116
- # Skip if no pixels
117
- return None, None
118
-
119
- # Convert to numpy arrays for vectorized operations
120
- pixel_data = np.array(pixels)
121
-
122
- if pixel_data.size == 0:
123
- return None, None
124
-
125
- # Find the actual bounds of the room to create a smaller mask
126
- # Add padding to ensure we don't lose edge details
127
- padding = 10 # Add padding pixels around the room
128
- min_x = max(0, int(np.min(pixel_data[:, 0])) - padding)
129
- max_x = min(
130
- width, int(np.max(pixel_data[:, 0]) + np.max(pixel_data[:, 2])) + padding
131
- )
132
- min_y = max(0, int(np.min(pixel_data[:, 1])) - padding)
133
- max_y = min(height, int(np.max(pixel_data[:, 1]) + 1) + padding)
134
-
135
- # Create a smaller mask for just the room area (much faster)
136
- local_width = max_x - min_x
137
- local_height = max_y - min_y
138
-
139
- # Skip if dimensions are invalid
140
- if local_width <= 0 or local_height <= 0:
141
- return None, None
142
-
143
- # Create a smaller mask
144
- local_mask = np.zeros((local_height, local_width), dtype=np.uint8)
145
-
146
- # Fill the mask efficiently
147
- for x, y, length in pixel_data:
148
- x, y, length = int(x), int(y), int(length)
149
- # Adjust coordinates to local mask
150
- local_x = x - min_x
151
- local_y = y - min_y
152
-
153
- # Ensure we're within bounds
154
- if 0 <= local_y < local_height and 0 <= local_x < local_width:
155
- # Calculate the end point, clamping to mask width
156
- end_x = min(local_x + length, local_width)
157
- if end_x > local_x: # Only process if there's a valid segment
158
- local_mask[local_y, local_x:end_x] = 1
159
-
160
- # Apply morphological operations
161
- struct_elem = np.ones((3, 3), dtype=np.uint8)
162
- eroded = binary_erosion(local_mask, structure=struct_elem, iterations=1)
163
- mask = binary_dilation(eroded, structure=struct_elem, iterations=1).astype(
164
- np.uint8
165
- )
166
-
167
- # Extract contour from the mask
168
- outline = self.convex_hull_outline(mask)
169
- if not outline:
170
- return None, None
171
-
172
- # Adjust coordinates back to global space
173
- outline = [(x + min_x, y + min_y) for (x, y) in outline]
174
-
175
- # Use coordinates as-is without flipping Y coordinates
176
- xs, ys = zip(*outline)
177
- x_min, x_max = min(xs), max(xs)
178
- y_min, y_max = min(ys), max(ys)
179
-
180
- room_id = str(segment_id)
181
-
182
- # Scale coordinates by pixel_size and convert to regular Python integers
183
- scaled_outline = [
184
- (int(x * pixel_size), int(y * pixel_size)) for x, y in outline
185
- ]
186
- room_data = {
187
- "number": segment_id,
188
- "outline": scaled_outline,
189
- "name": name,
190
- "x": int(((x_min + x_max) * pixel_size) // 2),
191
- "y": int(((y_min + y_max) * pixel_size) // 2),
192
- }
193
-
194
- return room_id, room_data
195
-
196
- async def async_extract_room_properties(self, json_data) -> RoomsProperties:
197
- """Extract room properties from the JSON data.
198
-
199
- This method processes all room layers in the JSON data and extracts their outlines.
200
- It respects the drawing configuration, skipping rooms that are disabled.
201
-
202
- Args:
203
- json_data: The JSON data from the vacuum
204
-
205
- Returns:
206
- Dictionary of room properties
207
- """
208
- start_total = time.time()
209
- room_properties = {}
210
- pixel_size = json_data.get("pixelSize", 5)
211
- height = json_data["size"]["y"]
212
- width = json_data["size"]["x"]
213
-
214
- for layer in json_data.get("layers", []):
215
- if layer.get("__class") == "MapLayer" and layer.get("type") == "segment":
216
- room_id, room_data = await self._process_room_layer(
217
- layer, width, height, pixel_size
218
- )
219
- if room_id is not None and room_data is not None:
220
- room_properties[room_id] = room_data
221
-
222
- # Log timing information
223
- total_time = time.time() - start_total
224
- LOGGER.debug("Room extraction Total time: %.3fs", total_time)
225
- return room_properties