valetudo-map-parser 0.1.9b50__py3-none-any.whl → 0.1.9b52__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.
@@ -23,6 +23,51 @@ class ImageDraw:
23
23
  self.img_h = image_handler
24
24
  self.file_name = self.img_h.shared.file_name
25
25
 
26
+ @staticmethod
27
+ def point_in_polygon(x: int, y: int, polygon: list) -> bool:
28
+ """
29
+ Check if a point is inside a polygon using ray casting algorithm.
30
+ Enhanced version with better handling of edge cases.
31
+
32
+ Args:
33
+ x: X coordinate of the point
34
+ y: Y coordinate of the point
35
+ polygon: List of (x, y) tuples forming the polygon
36
+
37
+ Returns:
38
+ True if the point is inside the polygon, False otherwise
39
+ """
40
+ # Ensure we have a valid polygon with at least 3 points
41
+ if len(polygon) < 3:
42
+ return False
43
+
44
+ # Make sure the polygon is closed (last point equals first point)
45
+ if polygon[0] != polygon[-1]:
46
+ polygon = polygon + [polygon[0]]
47
+
48
+ # Use winding number algorithm for better accuracy
49
+ wn = 0 # Winding number counter
50
+
51
+ # Loop through all edges of the polygon
52
+ for i in range(len(polygon) - 1): # Last vertex is first vertex
53
+ p1x, p1y = polygon[i]
54
+ p2x, p2y = polygon[i + 1]
55
+
56
+ # Test if a point is left/right/on the edge defined by two vertices
57
+ if p1y <= y: # Start y <= P.y
58
+ if p2y > y: # End y > P.y (upward crossing)
59
+ # Point left of edge
60
+ if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) > 0:
61
+ wn += 1 # Valid up intersect
62
+ else: # Start y > P.y
63
+ if p2y <= y: # End y <= P.y (downward crossing)
64
+ # Point right of edge
65
+ if ((p2x - p1x) * (y - p1y) - (x - p1x) * (p2y - p1y)) < 0:
66
+ wn -= 1 # Valid down intersect
67
+
68
+ # If winding number is not 0, the point is inside the polygon
69
+ return wn != 0
70
+
26
71
  async def draw_go_to_flag(
27
72
  self, np_array: NumpyArray, entity_dict: dict, color_go_to: Color
28
73
  ) -> NumpyArray:
@@ -414,75 +459,196 @@ class ImageDraw:
414
459
  _LOGGER.info("%s: Got the points in the json.", self.file_name)
415
460
  return entity_dict
416
461
 
462
+ @staticmethod
463
+ def point_in_polygon(x: int, y: int, polygon: list) -> bool:
464
+ """
465
+ Check if a point is inside a polygon using ray casting algorithm.
466
+
467
+ Args:
468
+ x: X coordinate of the point
469
+ y: Y coordinate of the point
470
+ polygon: List of (x, y) tuples forming the polygon
471
+
472
+ Returns:
473
+ True if the point is inside the polygon, False otherwise
474
+ """
475
+ n = len(polygon)
476
+ inside = False
477
+
478
+ p1x, p1y = polygon[0]
479
+ xinters = None # Initialize with default value
480
+ for i in range(1, n + 1):
481
+ p2x, p2y = polygon[i % n]
482
+ if y > min(p1y, p2y):
483
+ if y <= max(p1y, p2y):
484
+ if x <= max(p1x, p2x):
485
+ if p1y != p2y:
486
+ xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
487
+ if p1x == p2x or (xinters is not None and x <= xinters):
488
+ inside = not inside
489
+ p1x, p1y = p2x, p2y
490
+
491
+ return inside
492
+
417
493
  async def async_get_robot_in_room(
418
494
  self, robot_y: int = 0, robot_x: int = 0, angle: float = 0.0
419
495
  ) -> RobotPosition:
420
496
  """Get the robot position and return in what room is."""
497
+ # First check if we already have a cached room and if the robot is still in it
421
498
  if self.img_h.robot_in_room:
422
- # Check if the robot coordinates are inside the room's corners
423
- if (
424
- (self.img_h.robot_in_room["right"] >= int(robot_x))
425
- and (self.img_h.robot_in_room["left"] <= int(robot_x))
426
- ) and (
427
- (self.img_h.robot_in_room["down"] >= int(robot_y))
428
- and (self.img_h.robot_in_room["up"] <= int(robot_y))
499
+ # If we have outline data, use point_in_polygon for accurate detection
500
+ if "outline" in self.img_h.robot_in_room:
501
+ outline = self.img_h.robot_in_room["outline"]
502
+ if self.point_in_polygon(int(robot_x), int(robot_y), outline):
503
+ temp = {
504
+ "x": robot_x,
505
+ "y": robot_y,
506
+ "angle": angle,
507
+ "in_room": self.img_h.robot_in_room["room"],
508
+ }
509
+ # Handle active zones
510
+ if self.img_h.active_zones and (
511
+ self.img_h.robot_in_room["id"]
512
+ in range(len(self.img_h.active_zones))
513
+ ):
514
+ self.img_h.zooming = bool(
515
+ self.img_h.active_zones[self.img_h.robot_in_room["id"]]
516
+ )
517
+ else:
518
+ self.img_h.zooming = False
519
+ return temp
520
+ # Fallback to bounding box check if no outline data
521
+ elif all(
522
+ k in self.img_h.robot_in_room for k in ["left", "right", "up", "down"]
429
523
  ):
430
- temp = {
431
- "x": robot_x,
432
- "y": robot_y,
433
- "angle": angle,
434
- "in_room": self.img_h.robot_in_room["room"],
435
- }
436
- if self.img_h.active_zones and (
437
- self.img_h.robot_in_room["id"]
438
- in range(len(self.img_h.active_zones))
439
- ): # issue #100 Index out of range.
440
- self.img_h.zooming = bool(
441
- self.img_h.active_zones[self.img_h.robot_in_room["id"]]
442
- )
443
- else:
444
- self.img_h.zooming = False
445
- return temp
446
- # else we need to search and use the async method.
524
+ if (
525
+ (self.img_h.robot_in_room["right"] >= int(robot_x))
526
+ and (self.img_h.robot_in_room["left"] <= int(robot_x))
527
+ ) and (
528
+ (self.img_h.robot_in_room["down"] >= int(robot_y))
529
+ and (self.img_h.robot_in_room["up"] <= int(robot_y))
530
+ ):
531
+ temp = {
532
+ "x": robot_x,
533
+ "y": robot_y,
534
+ "angle": angle,
535
+ "in_room": self.img_h.robot_in_room["room"],
536
+ }
537
+ # Handle active zones
538
+ if self.img_h.active_zones and (
539
+ self.img_h.robot_in_room["id"]
540
+ in range(len(self.img_h.active_zones))
541
+ ):
542
+ self.img_h.zooming = bool(
543
+ self.img_h.active_zones[self.img_h.robot_in_room["id"]]
544
+ )
545
+ else:
546
+ self.img_h.zooming = False
547
+ return temp
548
+
549
+ # If we don't have a cached room or the robot is not in it, search all rooms
447
550
  last_room = None
448
551
  room_count = 0
449
552
  if self.img_h.robot_in_room:
450
553
  last_room = self.img_h.robot_in_room
451
- for room in self.img_h.rooms_pos:
452
- corners = room["corners"]
453
- self.img_h.robot_in_room = {
454
- "id": room_count,
455
- "left": int(corners[0][0]),
456
- "right": int(corners[2][0]),
457
- "up": int(corners[0][1]),
458
- "down": int(corners[2][1]),
459
- "room": str(room["name"]),
554
+
555
+ # Check if the robot is far outside the normal map boundaries
556
+ # This helps prevent false positives for points very far from any room
557
+ map_boundary = 20000 # Typical map size is around 5000-10000 units
558
+ if abs(robot_x) > map_boundary or abs(robot_y) > map_boundary:
559
+ _LOGGER.debug(
560
+ "%s robot position (%s, %s) is far outside map boundaries.",
561
+ self.file_name,
562
+ robot_x,
563
+ robot_y,
564
+ )
565
+ self.img_h.robot_in_room = last_room
566
+ self.img_h.zooming = False
567
+ temp = {
568
+ "x": robot_x,
569
+ "y": robot_y,
570
+ "angle": angle,
571
+ "in_room": last_room["room"] if last_room else None,
460
572
  }
461
- room_count += 1
462
- # Check if the robot coordinates are inside the room's corners
463
- if (
464
- (self.img_h.robot_in_room["right"] >= int(robot_x))
465
- and (self.img_h.robot_in_room["left"] <= int(robot_x))
466
- ) and (
467
- (self.img_h.robot_in_room["down"] >= int(robot_y))
468
- and (self.img_h.robot_in_room["up"] <= int(robot_y))
469
- ):
470
- temp = {
471
- "x": robot_x,
472
- "y": robot_y,
473
- "angle": angle,
474
- "in_room": self.img_h.robot_in_room["room"],
573
+ return temp
574
+
575
+ # Search through all rooms to find which one contains the robot
576
+ if self.img_h.rooms_pos is None:
577
+ _LOGGER.debug(
578
+ "%s: No rooms data available for robot position detection.",
579
+ self.file_name,
580
+ )
581
+ self.img_h.robot_in_room = last_room
582
+ self.img_h.zooming = False
583
+ temp = {
584
+ "x": robot_x,
585
+ "y": robot_y,
586
+ "angle": angle,
587
+ "in_room": last_room["room"] if last_room else None,
588
+ }
589
+ return temp
590
+
591
+ for room in self.img_h.rooms_pos:
592
+ # Check if the room has an outline (polygon points)
593
+ if "outline" in room:
594
+ outline = room["outline"]
595
+ # Use point_in_polygon for accurate detection with complex shapes
596
+ if self.point_in_polygon(int(robot_x), int(robot_y), outline):
597
+ # Robot is in this room
598
+ self.img_h.robot_in_room = {
599
+ "id": room_count,
600
+ "room": str(room["name"]),
601
+ "outline": outline,
602
+ }
603
+ temp = {
604
+ "x": robot_x,
605
+ "y": robot_y,
606
+ "angle": angle,
607
+ "in_room": self.img_h.robot_in_room["room"],
608
+ }
609
+ _LOGGER.debug(
610
+ "%s is in %s room (polygon detection).",
611
+ self.file_name,
612
+ self.img_h.robot_in_room["room"],
613
+ )
614
+ return temp
615
+ # Fallback to bounding box if no outline is available
616
+ elif "corners" in room:
617
+ corners = room["corners"]
618
+ # Create a bounding box from the corners
619
+ self.img_h.robot_in_room = {
620
+ "id": room_count,
621
+ "left": int(corners[0][0]),
622
+ "right": int(corners[2][0]),
623
+ "up": int(corners[0][1]),
624
+ "down": int(corners[2][1]),
625
+ "room": str(room["name"]),
475
626
  }
476
- _LOGGER.debug(
477
- "%s is in %s room.",
478
- self.file_name,
479
- self.img_h.robot_in_room["room"],
480
- )
481
- del room, corners, robot_x, robot_y # free memory.
482
- return temp
483
- del room, corners # free memory.
627
+ # Check if the robot is inside the bounding box
628
+ if (
629
+ (self.img_h.robot_in_room["right"] >= int(robot_x))
630
+ and (self.img_h.robot_in_room["left"] <= int(robot_x))
631
+ ) and (
632
+ (self.img_h.robot_in_room["down"] >= int(robot_y))
633
+ and (self.img_h.robot_in_room["up"] <= int(robot_y))
634
+ ):
635
+ temp = {
636
+ "x": robot_x,
637
+ "y": robot_y,
638
+ "angle": angle,
639
+ "in_room": self.img_h.robot_in_room["room"],
640
+ }
641
+ _LOGGER.debug(
642
+ "%s is in %s room (bounding box detection).",
643
+ self.file_name,
644
+ self.img_h.robot_in_room["room"],
645
+ )
646
+ return temp
647
+ room_count += 1
648
+
649
+ # Robot not found in any room
484
650
  _LOGGER.debug(
485
- "%s not located within Camera Rooms coordinates.",
651
+ "%s not located within any room coordinates.",
486
652
  self.file_name,
487
653
  )
488
654
  self.img_h.robot_in_room = last_room
@@ -493,7 +659,6 @@ class ImageDraw:
493
659
  "angle": angle,
494
660
  "in_room": last_room["room"] if last_room else None,
495
661
  }
496
- # If the robot is not inside any room, return a default value
497
662
  return temp
498
663
 
499
664
  async def async_get_robot_position(self, entity_dict: dict) -> tuple | None:
@@ -9,12 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
 
12
- import numpy as np
13
12
  from PIL import Image
14
13
 
15
14
  from .config.auto_crop import AutoCrop
16
15
  from .config.drawable_elements import DrawableElement
17
- from .config.optimized_element_map import OptimizedElementMapGenerator
18
16
  from .config.shared import CameraShared
19
17
  from .config.types import (
20
18
  COLORS,
@@ -22,19 +20,15 @@ from .config.types import (
22
20
  CalibrationPoints,
23
21
  Colors,
24
22
  RoomsProperties,
25
- RoomStore,
26
23
  )
27
24
  from .config.utils import (
28
25
  BaseHandler,
29
- get_element_at_position,
30
- get_room_at_position,
31
26
  initialize_drawing_config,
32
27
  manage_drawable_elements,
33
28
  prepare_resize_params,
34
- update_element_map_with_robot,
35
29
  )
36
- from .config.room_outline import extract_room_outline_with_scipy
37
30
  from .hypfer_draw import ImageDraw as ImDraw
31
+ from .hypfer_rooms_handler import HypferRoomsHandler
38
32
  from .map_data import ImageData
39
33
 
40
34
 
@@ -63,123 +57,62 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
63
57
  self.imd = ImDraw(self) # Image Draw class.
64
58
  self.color_grey = (128, 128, 128, 255)
65
59
  self.file_name = self.shared.file_name # file name of the vacuum.
66
- self.element_map_manager = OptimizedElementMapGenerator(
67
- self.drawing_config, self.shared
68
- ) # Map of element codes
60
+ self.rooms_handler = HypferRoomsHandler(
61
+ self.file_name, self.drawing_config
62
+ ) # Room data handler
69
63
 
70
64
  @staticmethod
71
65
  def get_corners(x_max, x_min, y_max, y_min):
72
66
  """Get the corners of the room."""
73
67
  return [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
74
68
 
75
- async def extract_room_outline_from_map(self, room_id_int, pixels, pixel_size):
76
- """Extract the outline of a room using the pixel data and element map.
77
-
78
- Args:
79
- room_id_int: The room ID as an integer
80
- pixels: List of pixel coordinates in the format [[x, y, z], ...]
81
- pixel_size: Size of each pixel
82
-
83
- Returns:
84
- List of points forming the outline of the room
85
- """
86
- # Calculate x and y min/max from compressed pixels for rectangular fallback
87
- x_values = []
88
- y_values = []
89
- for x, y, z in pixels:
90
- for i in range(z):
91
- x_values.append(x + i * pixel_size)
92
- y_values.append(y)
93
-
94
- if not x_values or not y_values:
95
- return []
96
-
97
- min_x, max_x = min(x_values), max(x_values)
98
- min_y, max_y = min(y_values), max(y_values)
99
-
100
- # If we don't have an element map, return a rectangular outline
101
- if not hasattr(self, "element_map") or self.shared.element_map is None:
102
- # Return rectangular outline
103
- return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
104
-
105
- # Create a binary mask for this room using the pixel data
106
- # This is more reliable than using the element_map since we're directly using the pixel data
107
- height, width = self.shared.element_map.shape
108
- room_mask = np.zeros((height, width), dtype=np.uint8)
109
-
110
- # Fill the mask with room pixels using the pixel data
111
- for x, y, z in pixels:
112
- for i in range(z):
113
- px = x + i * pixel_size
114
- py = y
115
- # Make sure we're within bounds
116
- if 0 <= py < height and 0 <= px < width:
117
- # Mark a pixel_size x pixel_size block in the mask
118
- for dx in range(pixel_size):
119
- for dy in range(pixel_size):
120
- if py + dy < height and px + dx < width:
121
- room_mask[py + dy, px + dx] = 1
122
-
123
- # Debug log to check if we have any room pixels
124
- num_room_pixels = np.sum(room_mask)
125
- LOGGER.debug(
126
- "%s: Room %s mask has %d pixels",
127
- self.file_name,
128
- str(room_id_int),
129
- int(num_room_pixels),
130
- )
131
-
132
- # Use the scipy-based room outline extraction
133
- return await extract_room_outline_with_scipy(
134
- room_mask, min_x, min_y, max_x, max_y, self.file_name, room_id_int
135
- )
136
-
137
69
  async def async_extract_room_properties(self, json_data) -> RoomsProperties:
138
70
  """Extract room properties from the JSON data."""
139
71
 
140
- room_properties = {}
141
- self.rooms_pos = []
142
- pixel_size = json_data.get("pixelSize", [])
143
-
144
- for layer in json_data.get("layers", []):
145
- if layer["__class"] == "MapLayer":
146
- meta_data = layer.get("metaData", {})
147
- segment_id = meta_data.get("segmentId")
148
- if segment_id is not None:
149
- name = meta_data.get("name")
150
- compressed_pixels = layer.get("compressedPixels", [])
151
- pixels = self.data.sublist(compressed_pixels, 3)
152
- # Calculate x and y min/max from compressed pixels
153
- (
154
- x_min,
155
- y_min,
156
- x_max,
157
- y_max,
158
- ) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
159
- corners = self.get_corners(x_max, x_min, y_max, y_min)
160
- room_id = str(segment_id)
161
- self.rooms_pos.append(
162
- {
163
- "name": name,
164
- "corners": corners,
165
- }
166
- )
167
- room_properties[room_id] = {
168
- "number": segment_id,
169
- "outline": corners,
170
- "name": name,
171
- "x": ((x_min + x_max) // 2),
172
- "y": ((y_min + y_max) // 2),
173
- }
174
- if room_properties:
175
- rooms = RoomStore(self.file_name, room_properties)
176
- LOGGER.debug(
177
- "%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
178
- )
179
- else:
180
- LOGGER.debug("%s: Rooms data not available!", self.file_name)
181
- self.rooms_pos = None
182
- return room_properties
72
+ return await self.rooms_handler.async_extract_room_properties(json_data)
73
+ # room_properties = {}
74
+ # self.rooms_pos = []
75
+ # pixel_size = json_data.get("pixelSize", [])
76
+ #
77
+ # for layer in json_data.get("layers", []):
78
+ # if layer["__class"] == "MapLayer":
79
+ # meta_data = layer.get("metaData", {})
80
+ # segment_id = meta_data.get("segmentId")
81
+ # if segment_id is not None:
82
+ # name = meta_data.get("name")
83
+ # compressed_pixels = layer.get("compressedPixels", [])
84
+ # pixels = self.data.sublist(compressed_pixels, 3)
85
+ # # Calculate x and y min/max from compressed pixels
86
+ # (
87
+ # x_min,
88
+ # y_min,
89
+ # x_max,
90
+ # y_max,
91
+ # ) = await self.data.async_get_rooms_coordinates(pixels, pixel_size)
92
+ # corners = self.get_corners(x_max, x_min, y_max, y_min)
93
+ # room_id = str(segment_id)
94
+ # self.rooms_pos.append(
95
+ # {
96
+ # "name": name,
97
+ # "corners": corners,
98
+ # }
99
+ # )
100
+ # room_properties[room_id] = {
101
+ # "number": segment_id,
102
+ # "outline": corners,
103
+ # "name": name,
104
+ # "x": ((x_min + x_max) // 2),
105
+ # "y": ((y_min + y_max) // 2),
106
+ # }
107
+ # if room_properties:
108
+ # rooms = RoomStore(self.file_name, room_properties)
109
+ # LOGGER.debug(
110
+ # "%s: Rooms data extracted! %s", self.file_name, rooms.get_rooms()
111
+ # )
112
+ # else:
113
+ # LOGGER.debug("%s: Rooms data not available!", self.file_name)
114
+ # self.rooms_pos = None
115
+ # return room_properties
183
116
 
184
117
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
185
118
  async def async_get_image_from_json(
@@ -471,36 +404,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
471
404
  if img_np_array is None:
472
405
  LOGGER.warning("%s: Image array is None.", self.file_name)
473
406
  return None
474
- # Debug logging for element map creation
475
- LOGGER.info(
476
- "%s: Frame number: %d, has element_map: %s",
477
- self.file_name,
478
- self.frame_number,
479
- hasattr(self.shared, "element_map"),
480
- )
481
407
 
482
- if (self.shared.element_map is None) and (self.frame_number == 1):
483
- # Create element map for tracking what's drawn where
484
- LOGGER.info(
485
- "%s: Creating element map with shape: %s",
486
- self.file_name,
487
- img_np_array.shape,
488
- )
489
-
490
- # Generate the element map directly from JSON data
491
- # This will create a cropped element map containing only the non-zero elements
492
- LOGGER.info("%s: Generating element map from JSON data", self.file_name)
493
- self.shared.element_map = (
494
- await self.element_map_manager.async_generate_from_json(m_json)
495
- )
496
-
497
- LOGGER.info(
498
- "%s: Element map created with shape: %s",
499
- self.file_name,
500
- self.shared.element_map.shape
501
- if self.shared.element_map is not None
502
- else None,
503
- )
504
408
  # Convert the numpy array to a PIL image
505
409
  pil_img = Image.fromarray(img_np_array, mode="RGBA")
506
410
  del img_np_array
@@ -584,17 +488,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
584
488
  value=value,
585
489
  )
586
490
 
587
- def get_element_at_position(self, x: int, y: int) -> DrawableElement | None:
588
- """Get the element code at a specific position."""
589
-
590
- return get_element_at_position(self.shared.element_map, x, y)
591
-
592
- def get_room_at_position(self, x: int, y: int) -> int | None:
593
- """Get the room ID at a specific position, or None if not a room."""
594
- return get_room_at_position(
595
- self.shared.element_map, x, y, DrawableElement.ROOM_1
596
- )
597
-
598
491
  @staticmethod
599
492
  async def async_copy_array(original_array):
600
493
  """Copy the array."""