valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b42__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.
@@ -2,9 +2,11 @@
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import logging
6
+ import numpy as np
5
7
  from dataclasses import dataclass
6
8
  from logging import getLogger
7
- from typing import Callable, List, Optional
9
+ from typing import Callable, Dict, List, Optional, Tuple, Union
8
10
 
9
11
  from PIL import ImageOps
10
12
 
@@ -530,3 +532,410 @@ def prepare_resize_params(handler, pil_img, rand):
530
532
  offset_func=handler.async_map_coordinates_offset,
531
533
  is_rand=rand,
532
534
  )
535
+
536
+
537
+ def initialize_drawing_config(handler):
538
+ """
539
+ Initialize drawing configuration from device_info.
540
+
541
+ Args:
542
+ handler: The handler instance with shared data and file_name attributes
543
+
544
+ Returns:
545
+ Tuple of (DrawingConfig, Drawable, EnhancedDrawable)
546
+ """
547
+ from .drawable import Drawable
548
+ from .drawable_elements import DrawableElement, DrawingConfig
549
+ from .enhanced_drawable import EnhancedDrawable
550
+
551
+ # Initialize drawing configuration
552
+ drawing_config = DrawingConfig()
553
+
554
+ # Get logger from the handler
555
+ _LOGGER = logging.getLogger(handler.__class__.__module__)
556
+
557
+ if hasattr(handler.shared, "device_info") and handler.shared.device_info is not None:
558
+ _LOGGER.info(
559
+ "%s: Initializing drawing config from device_info", handler.file_name
560
+ )
561
+ _LOGGER.info(
562
+ "%s: device_info contains disable_obstacles: %s",
563
+ handler.file_name,
564
+ "disable_obstacles" in handler.shared.device_info,
565
+ )
566
+ _LOGGER.info(
567
+ "%s: device_info contains disable_path: %s",
568
+ handler.file_name,
569
+ "disable_path" in handler.shared.device_info,
570
+ )
571
+ _LOGGER.info(
572
+ "%s: device_info contains disable_elements: %s",
573
+ handler.file_name,
574
+ "disable_elements" in handler.shared.device_info,
575
+ )
576
+
577
+ if "disable_obstacles" in handler.shared.device_info:
578
+ _LOGGER.info(
579
+ "%s: disable_obstacles value: %s",
580
+ handler.file_name,
581
+ handler.shared.device_info["disable_obstacles"],
582
+ )
583
+ if "disable_path" in handler.shared.device_info:
584
+ _LOGGER.info(
585
+ "%s: disable_path value: %s",
586
+ handler.file_name,
587
+ handler.shared.device_info["disable_path"],
588
+ )
589
+ if "disable_elements" in handler.shared.device_info:
590
+ _LOGGER.info(
591
+ "%s: disable_elements value: %s",
592
+ handler.file_name,
593
+ handler.shared.device_info["disable_elements"],
594
+ )
595
+
596
+ drawing_config.update_from_device_info(handler.shared.device_info)
597
+
598
+ # Verify elements are disabled
599
+ _LOGGER.info(
600
+ "%s: After initialization, PATH enabled: %s",
601
+ handler.file_name,
602
+ drawing_config.is_enabled(DrawableElement.PATH),
603
+ )
604
+ _LOGGER.info(
605
+ "%s: After initialization, OBSTACLE enabled: %s",
606
+ handler.file_name,
607
+ drawing_config.is_enabled(DrawableElement.OBSTACLE),
608
+ )
609
+
610
+ # Initialize both drawable systems for backward compatibility
611
+ draw = Drawable() # Legacy drawing utilities
612
+ enhanced_draw = EnhancedDrawable(drawing_config) # New enhanced drawing system
613
+
614
+ return drawing_config, draw, enhanced_draw
615
+
616
+
617
+ def blend_colors(base_color, overlay_color):
618
+ """
619
+ Blend two RGBA colors using alpha compositing.
620
+
621
+ Args:
622
+ base_color: Base RGBA color tuple (r, g, b, a)
623
+ overlay_color: Overlay RGBA color tuple (r, g, b, a)
624
+
625
+ Returns:
626
+ Blended RGBA color tuple (r, g, b, a)
627
+ """
628
+ r1, g1, b1, a1 = base_color
629
+ r2, g2, b2, a2 = overlay_color
630
+
631
+ # Convert alpha to 0-1 range
632
+ a1 = a1 / 255.0
633
+ a2 = a2 / 255.0
634
+
635
+ # Calculate resulting alpha
636
+ a_out = a1 + a2 * (1 - a1)
637
+
638
+ # Avoid division by zero
639
+ if a_out < 0.0001:
640
+ return (0, 0, 0, 0)
641
+
642
+ # Calculate blended RGB components
643
+ r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
644
+ g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
645
+ b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
646
+
647
+ # Convert back to 0-255 range and return as tuple
648
+ return (
649
+ int(max(0, min(255, r_out))),
650
+ int(max(0, min(255, g_out))),
651
+ int(max(0, min(255, b_out))),
652
+ int(max(0, min(255, a_out * 255))),
653
+ )
654
+
655
+
656
+ def blend_pixel(array, x, y, color, element, element_map=None, drawing_config=None):
657
+ """
658
+ Blend a pixel color with the existing color at the specified position.
659
+ Also updates the element map if the new element has higher z-index.
660
+
661
+ Args:
662
+ array: The image array to modify
663
+ x: X coordinate
664
+ y: Y coordinate
665
+ color: RGBA color tuple to blend
666
+ element: Element code for the pixel
667
+ element_map: Optional element map to update
668
+ drawing_config: Optional drawing configuration for z-index lookup
669
+
670
+ Returns:
671
+ None
672
+ """
673
+ # Check bounds
674
+ if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
675
+ return
676
+
677
+ # Get current element at this position
678
+ current_element = None
679
+ if element_map is not None:
680
+ current_element = element_map[y, x]
681
+
682
+ # Get z-index values for comparison
683
+ current_z = 0
684
+ new_z = 0
685
+
686
+ if drawing_config is not None:
687
+ current_z = (
688
+ drawing_config.get_property(current_element, "z_index", 0)
689
+ if current_element
690
+ else 0
691
+ )
692
+ new_z = drawing_config.get_property(element, "z_index", 0)
693
+
694
+ # Update element map if new element has higher z-index
695
+ if element_map is not None and new_z >= current_z:
696
+ element_map[y, x] = element
697
+
698
+ # Blend colors
699
+ base_color = array[y, x]
700
+ blended_color = blend_colors(base_color, color)
701
+ array[y, x] = blended_color
702
+
703
+
704
+ def get_element_at_position(element_map, x, y):
705
+ """
706
+ Get the element code at a specific position in the element map.
707
+
708
+ Args:
709
+ element_map: The element map array
710
+ x: X coordinate
711
+ y: Y coordinate
712
+
713
+ Returns:
714
+ Element code or None if out of bounds
715
+ """
716
+ if element_map is not None:
717
+ if 0 <= y < element_map.shape[0] and 0 <= x < element_map.shape[1]:
718
+ return element_map[y, x]
719
+ return None
720
+
721
+
722
+ def get_room_at_position(element_map, x, y, room_base=101):
723
+ """
724
+ Get the room ID at a specific position, or None if not a room.
725
+
726
+ Args:
727
+ element_map: The element map array
728
+ x: X coordinate
729
+ y: Y coordinate
730
+ room_base: Base value for room elements (default: 101 for DrawableElement.ROOM_1)
731
+
732
+ Returns:
733
+ Room ID (1-15) or None if not a room
734
+ """
735
+ element = get_element_at_position(element_map, x, y)
736
+ if element is not None and room_base <= element <= room_base + 14: # 15 rooms max
737
+ return element - room_base + 1
738
+ return None
739
+
740
+
741
+ def update_element_map_with_robot(element_map, robot_position, robot_element=3, robot_radius=25):
742
+ """
743
+ Update the element map with the robot position.
744
+
745
+ Args:
746
+ element_map: The element map to update
747
+ robot_position: Tuple of (x, y) coordinates for the robot
748
+ robot_element: Element code for the robot (default: 3 for DrawableElement.ROBOT)
749
+ robot_radius: Radius of the robot in pixels
750
+
751
+ Returns:
752
+ None
753
+ """
754
+ if element_map is None or robot_position is None:
755
+ return
756
+
757
+ # Update element map for robot position
758
+ for dy in range(-robot_radius, robot_radius + 1):
759
+ for dx in range(-robot_radius, robot_radius + 1):
760
+ if dx * dx + dy * dy <= robot_radius * robot_radius:
761
+ rx, ry = (
762
+ int(robot_position[0] + dx),
763
+ int(robot_position[1] + dy),
764
+ )
765
+ if (
766
+ 0 <= ry < element_map.shape[0]
767
+ and 0 <= rx < element_map.shape[1]
768
+ ):
769
+ element_map[ry, rx] = robot_element
770
+
771
+
772
+ def manage_drawable_elements(handler, action, element_code=None, element_codes=None, property_name=None, value=None):
773
+ """
774
+ Manage drawable elements (enable, disable, set elements, set properties).
775
+
776
+ Args:
777
+ handler: The handler instance with drawing_config attribute
778
+ action: Action to perform ('enable', 'disable', 'set_elements', 'set_property')
779
+ element_code: Element code for enable/disable/set_property actions
780
+ element_codes: List of element codes for set_elements action
781
+ property_name: Property name for set_property action
782
+ value: Property value for set_property action
783
+
784
+ Returns:
785
+ None
786
+ """
787
+ if not hasattr(handler, "drawing_config") or handler.drawing_config is None:
788
+ return
789
+
790
+ if action == "enable" and element_code is not None:
791
+ handler.drawing_config.enable_element(element_code)
792
+ elif action == "disable" and element_code is not None:
793
+ handler.drawing_config.disable_element(element_code)
794
+ elif action == "set_elements" and element_codes is not None:
795
+ handler.drawing_config.set_elements(element_codes)
796
+ elif action == "set_property" and element_code is not None and property_name is not None:
797
+ handler.drawing_config.set_property(element_code, property_name, value)
798
+
799
+
800
+ def handle_room_outline_error(file_name, room_id, error, logger=None):
801
+ """
802
+ Handle errors during room outline extraction.
803
+
804
+ Args:
805
+ file_name: Name of the file for logging
806
+ room_id: Room ID for logging
807
+ error: The error that occurred
808
+ logger: Logger instance (optional)
809
+
810
+ Returns:
811
+ None
812
+ """
813
+ _LOGGER = logger or logging.getLogger(__name__)
814
+
815
+ _LOGGER.warning(
816
+ "%s: Failed to trace outline for room %s: %s",
817
+ file_name, str(room_id), str(error)
818
+ )
819
+
820
+
821
+ async def async_extract_room_outline(room_mask, min_x, min_y, max_x, max_y, file_name, room_id_int, logger=None):
822
+ """
823
+ Extract the outline of a room from a binary mask.
824
+
825
+ Args:
826
+ room_mask: Binary mask where room pixels are 1 and non-room pixels are 0
827
+ min_x: Minimum x coordinate of the room
828
+ min_y: Minimum y coordinate of the room
829
+ max_x: Maximum x coordinate of the room
830
+ max_y: Maximum y coordinate of the room
831
+ file_name: Name of the file for logging
832
+ room_id_int: Room ID for logging
833
+ logger: Logger instance (optional)
834
+
835
+ Returns:
836
+ List of (x, y) points forming the room outline
837
+ """
838
+ # Use the provided logger or create a new one
839
+ _LOGGER = logger or logging.getLogger(__name__)
840
+
841
+ # Get the dimensions of the mask
842
+ height, width = room_mask.shape
843
+
844
+ # Find the coordinates of all room pixels
845
+ room_y, room_x = np.where(room_mask > 0)
846
+ if len(room_y) == 0:
847
+ return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
848
+
849
+ # Get the bounding box of the room
850
+ min_y, max_y = np.min(room_y), np.max(room_y)
851
+ min_x, max_x = np.min(room_x), np.max(room_x)
852
+
853
+ # For simple rooms, just use the rectangular outline
854
+ rect_outline = [
855
+ (min_x, min_y), # Top-left
856
+ (max_x, min_y), # Top-right
857
+ (max_x, max_y), # Bottom-right
858
+ (min_x, max_y), # Bottom-left
859
+ ]
860
+
861
+ # For more complex room shapes, trace the boundary
862
+ # This is a custom boundary tracing algorithm that works without OpenCV
863
+ try:
864
+ # Create a padded mask to handle edge cases
865
+ padded_mask = np.zeros((height + 2, width + 2), dtype=np.uint8)
866
+ padded_mask[1:-1, 1:-1] = room_mask
867
+
868
+ # Find boundary pixels (pixels that have at least one non-room neighbor)
869
+ boundary_points = []
870
+
871
+ # More efficient boundary detection - only check pixels that are part of the room
872
+ for y, x in zip(room_y, room_x):
873
+ # Check if this is a boundary pixel (at least one neighbor is 0)
874
+ is_boundary = False
875
+ for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
876
+ ny, nx = y + dy, x + dx
877
+ if (ny < 0 or ny >= height or nx < 0 or nx >= width or
878
+ room_mask[ny, nx] == 0):
879
+ is_boundary = True
880
+ break
881
+ if is_boundary:
882
+ boundary_points.append((x, y))
883
+
884
+ # Log the number of boundary points found
885
+ _LOGGER.debug(
886
+ "%s: Room %s has %d boundary points",
887
+ file_name, str(room_id_int), len(boundary_points)
888
+ )
889
+
890
+ # If we found too few boundary points, use the rectangular outline
891
+ if len(boundary_points) < 8: # Need at least 8 points for a meaningful shape
892
+ _LOGGER.debug(
893
+ "%s: Room %s has too few boundary points (%d), using rectangular outline",
894
+ file_name, str(room_id_int), len(boundary_points)
895
+ )
896
+ return rect_outline
897
+
898
+ # Use a more sophisticated algorithm to create a coherent outline
899
+ # We'll use a convex hull approach to get the main shape
900
+ # Sort points by angle from centroid
901
+ centroid_x = np.mean([p[0] for p in boundary_points])
902
+ centroid_y = np.mean([p[1] for p in boundary_points])
903
+
904
+ # Calculate angles from centroid
905
+ def calculate_angle(point):
906
+ return np.arctan2(point[1] - centroid_y, point[0] - centroid_x)
907
+
908
+ # Sort boundary points by angle
909
+ boundary_points.sort(key=calculate_angle)
910
+
911
+ # Simplify the outline if it has too many points
912
+ if len(boundary_points) > 20:
913
+ # Take every Nth point to simplify
914
+ step = len(boundary_points) // 20
915
+ simplified_outline = [boundary_points[i] for i in range(0, len(boundary_points), step)]
916
+ # Make sure we have at least 8 points
917
+ if len(simplified_outline) < 8:
918
+ simplified_outline = boundary_points[::len(boundary_points)//8]
919
+ else:
920
+ simplified_outline = boundary_points
921
+
922
+ # Make sure to close the loop
923
+ if simplified_outline[0] != simplified_outline[-1]:
924
+ simplified_outline.append(simplified_outline[0])
925
+
926
+ # Convert NumPy int64 values to regular Python integers
927
+ simplified_outline = [(int(x), int(y)) for x, y in simplified_outline]
928
+
929
+ _LOGGER.debug(
930
+ "%s: Room %s outline has %d points",
931
+ file_name, str(room_id_int), len(simplified_outline)
932
+ )
933
+
934
+ return simplified_outline
935
+
936
+ except (ValueError, IndexError, TypeError, ArithmeticError) as e:
937
+ _LOGGER.warning(
938
+ "%s: Error tracing room outline: %s. Using rectangular outline instead.",
939
+ file_name, str(e)
940
+ )
941
+ return rect_outline