valetudo-map-parser 0.1.9b42__py3-none-any.whl → 0.1.9b44__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.
@@ -6,18 +6,13 @@ Version: 0.1.9
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import logging
10
9
  from enum import IntEnum
11
10
  from typing import Dict, List, Tuple, Union
11
+ import numpy as np
12
+ from .types import LOGGER
12
13
 
13
14
  from .colors import DefaultColors, SupportedColor
14
15
 
15
-
16
- # numpy is not used in this file
17
-
18
-
19
- _LOGGER = logging.getLogger(__name__)
20
-
21
16
  # Type aliases
22
17
  Color = Tuple[int, int, int, int] # RGBA color
23
18
  PropertyDict = Dict[str, Union[Color, float, int]]
@@ -140,10 +135,10 @@ class DrawingConfig:
140
135
  """Enable drawing of a specific element."""
141
136
  if element_code in self._enabled_elements:
142
137
  self._enabled_elements[element_code] = True
143
- _LOGGER.info(
138
+ LOGGER.info(
144
139
  "Enabled element %s (%s)", element_code.name, element_code.value
145
140
  )
146
- _LOGGER.info(
141
+ LOGGER.info(
147
142
  "Element %s is now enabled: %s",
148
143
  element_code.name,
149
144
  self._enabled_elements[element_code],
@@ -153,10 +148,10 @@ class DrawingConfig:
153
148
  """Disable drawing of a specific element."""
154
149
  if element_code in self._enabled_elements:
155
150
  self._enabled_elements[element_code] = False
156
- _LOGGER.info(
151
+ LOGGER.info(
157
152
  "Disabled element %s (%s)", element_code.name, element_code.value
158
153
  )
159
- _LOGGER.info(
154
+ LOGGER.info(
160
155
  "Element %s is now enabled: %s",
161
156
  element_code.name,
162
157
  self._enabled_elements[element_code],
@@ -176,7 +171,7 @@ class DrawingConfig:
176
171
  def is_enabled(self, element_code: DrawableElement) -> bool:
177
172
  """Check if an element is enabled for drawing."""
178
173
  enabled = self._enabled_elements.get(element_code, False)
179
- _LOGGER.debug(
174
+ LOGGER.debug(
180
175
  "Checking if element %s is enabled: %s",
181
176
  element_code.name if hasattr(element_code, "name") else element_code,
182
177
  enabled,
@@ -243,7 +238,7 @@ class DrawingConfig:
243
238
  self.set_property(room_element, "color", rgba)
244
239
  self.set_property(room_element, "opacity", alpha / 255.0)
245
240
 
246
- _LOGGER.debug(
241
+ LOGGER.debug(
247
242
  "Updated room %d color to %s with alpha %s", room_id, rgb, alpha
248
243
  )
249
244
 
@@ -270,7 +265,7 @@ class DrawingConfig:
270
265
  self.set_property(element, "color", rgba)
271
266
  self.set_property(element, "opacity", alpha / 255.0)
272
267
 
273
- _LOGGER.debug(
268
+ LOGGER.debug(
274
269
  "Updated element %s color to %s with alpha %s",
275
270
  element.name,
276
271
  rgb,
@@ -297,7 +292,7 @@ class DrawingConfig:
297
292
  for disable_key, element in element_disable_mapping.items():
298
293
  if device_info.get(disable_key, False):
299
294
  self.disable_element(element)
300
- _LOGGER.info(
295
+ LOGGER.info(
301
296
  "Disabled %s element from device_info setting", element.name
302
297
  )
303
298
 
@@ -307,6 +302,586 @@ class DrawingConfig:
307
302
  if device_info.get(disable_key, False):
308
303
  room_element = getattr(DrawableElement, f"ROOM_{room_id}")
309
304
  self.disable_element(room_element)
310
- _LOGGER.info(
305
+ LOGGER.info(
311
306
  "Disabled ROOM_%d element from device_info setting", room_id
312
307
  )
308
+
309
+
310
+ class ElementMapGenerator:
311
+ """Class for generating 2D element maps from JSON data.
312
+
313
+ This class creates a 2D array where each cell contains an integer code
314
+ representing the element at that position (floor, wall, room, etc.).
315
+ It focuses only on the static structure (rooms and walls).
316
+ """
317
+
318
+ def __init__(self, drawing_config: DrawingConfig = None, shared_data=None):
319
+ """Initialize the element map generator.
320
+
321
+ Args:
322
+ drawing_config: Optional drawing configuration for element properties
323
+ """
324
+ self.drawing_config = drawing_config or DrawingConfig()
325
+ self.shared = shared_data
326
+ self.element_map = None
327
+
328
+ async def async_generate_from_json(self, json_data, existing_element_map=None):
329
+ """Generate a 2D element map from JSON data without visual rendering.
330
+
331
+ Args:
332
+ json_data: The JSON data from the vacuum
333
+ existing_element_map: Optional pre-created element map to populate
334
+
335
+ Returns:
336
+ numpy.ndarray: The 2D element map array
337
+ """
338
+ if not self.shared:
339
+ LOGGER.warning("Shared data not provided, some features may not work.")
340
+ return None
341
+
342
+ # Use existing element map if provided
343
+ if existing_element_map is not None:
344
+ self.element_map = existing_element_map
345
+
346
+ # Check if this is a Valetudo map or a Rand256 map
347
+ is_valetudo = "size" in json_data and "pixelSize" in json_data and "layers" in json_data
348
+ is_rand256 = "image" in json_data and "map_data" in json_data
349
+
350
+ # Debug logging
351
+ LOGGER.debug(f"JSON data keys: {list(json_data.keys())}")
352
+ LOGGER.debug(f"Is Valetudo: {is_valetudo}, Is Rand256: {is_rand256}")
353
+
354
+ # Create element map if not provided
355
+ if self.element_map is None:
356
+ if is_valetudo:
357
+ # Get map dimensions from Valetudo map
358
+ map_size = json_data.get("size", 0)
359
+ if isinstance(map_size, dict):
360
+ # If map_size is a dictionary, extract the values
361
+ size_x = map_size.get("width", 0)
362
+ size_y = map_size.get("height", 0)
363
+ else:
364
+ # If map_size is a number, use it for both dimensions
365
+ pixel_size = json_data.get("pixelSize", 5) # Default to 5mm per pixel
366
+ size_x = int(map_size // pixel_size)
367
+ size_y = int(map_size // pixel_size)
368
+ self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
369
+ self.element_map[:] = DrawableElement.FLOOR
370
+ elif is_rand256:
371
+ # Get map dimensions from Rand256 map
372
+ map_data = json_data.get("map_data", {})
373
+ size_x = map_data.get("width", 0)
374
+ size_y = map_data.get("height", 0)
375
+ self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
376
+
377
+ if not (is_valetudo or is_rand256):
378
+ LOGGER.error("Unknown JSON format, cannot generate element map")
379
+ return None
380
+
381
+ if is_valetudo:
382
+ # Get map dimensions from the Valetudo JSON data
383
+ size_x = json_data["size"]["x"]
384
+ size_y = json_data["size"]["y"]
385
+ pixel_size = json_data["pixelSize"]
386
+
387
+ # Calculate scale factor based on pixel size (normalize to 5mm standard)
388
+ # This helps handle maps with different scales
389
+ scale_factor = pixel_size if pixel_size != 0 else 1.0
390
+ LOGGER.info(f"Map dimensions: {size_x}x{size_y}, pixel size: {pixel_size}mm, scale factor: {scale_factor:.2f}")
391
+
392
+ # Ensure element_map is properly initialized with the correct dimensions
393
+ if self.element_map is None or self.element_map.shape[0] == 0 or self.element_map.shape[1] == 0:
394
+ # For now, create a full-sized element map to ensure coordinates match
395
+ # We'll resize it at the end for efficiency
396
+ map_width = int(size_x // pixel_size) if pixel_size != 0 else size_x
397
+ map_height = int(size_y // pixel_size) if pixel_size != 0 else size_y
398
+
399
+ LOGGER.info(f"Creating element map with dimensions: {map_width}x{map_height}")
400
+ self.element_map = np.zeros((map_height, map_width), dtype=np.int32)
401
+ self.element_map[:] = DrawableElement.FLOOR
402
+
403
+ # Process layers (rooms, walls, etc.)
404
+ for layer in json_data["layers"]:
405
+ layer_type = layer["type"]
406
+
407
+ # Process rooms (segments)
408
+ if layer_type == "segment":
409
+ # Handle different segment formats
410
+ if "segments" in layer:
411
+ segments = layer["segments"]
412
+ else:
413
+ segments = [layer] # Some formats have segment data directly in the layer
414
+
415
+ for segment in segments:
416
+ # Get room ID and check if it's enabled
417
+ if "id" in segment:
418
+ room_id = segment["id"]
419
+ room_id_int = int(room_id)
420
+ elif "metaData" in segment and "segmentId" in segment["metaData"]:
421
+ # Handle Hypfer format
422
+ room_id = segment["metaData"]["segmentId"]
423
+ room_id_int = int(room_id)
424
+ else:
425
+ # Skip segments without ID
426
+ continue
427
+
428
+ # Skip if room is disabled
429
+ room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
430
+ if room_element is None or not self.drawing_config.is_enabled(room_element):
431
+ continue
432
+
433
+ # Room element code was already retrieved above
434
+ if room_element is not None:
435
+ # Process pixels for this room
436
+ if "pixels" in segment and segment["pixels"]:
437
+ # Regular pixel format
438
+ for x, y, z in segment["pixels"]:
439
+ # Calculate pixel coordinates
440
+ col = x * pixel_size
441
+ row = y * pixel_size
442
+
443
+ # Fill the element map with room code
444
+ for i in range(z):
445
+ # Get the region to update
446
+ region_col_start = col + i * pixel_size
447
+ region_col_end = col + (i + 1) * pixel_size
448
+ region_row_start = row
449
+ region_row_end = row + pixel_size
450
+
451
+ # Update element map for this region
452
+ if region_row_start < size_y and region_col_start < size_x:
453
+ # Ensure we stay within bounds
454
+ end_row = min(region_row_end, size_y)
455
+ end_col = min(region_col_end, size_x)
456
+
457
+ # Set element code for this region
458
+ # Only set pixels that are not already set (floor is 1)
459
+ region = self.element_map[region_row_start:end_row, region_col_start:end_col]
460
+ mask = region == DrawableElement.FLOOR
461
+ region[mask] = room_element
462
+
463
+ elif "compressedPixels" in segment and segment["compressedPixels"]:
464
+ # Compressed pixel format (used in Valetudo)
465
+ compressed_pixels = segment["compressedPixels"]
466
+ i = 0
467
+ pixel_count = 0
468
+
469
+ while i < len(compressed_pixels):
470
+ x = compressed_pixels[i]
471
+ y = compressed_pixels[i+1]
472
+ count = compressed_pixels[i+2]
473
+ pixel_count += count
474
+
475
+ # Set element code for this run of pixels
476
+ for j in range(count):
477
+ px = x + j
478
+ if 0 <= px < size_x and 0 <= y < size_y:
479
+ self.element_map[y, px] = room_element
480
+
481
+ i += 3
482
+
483
+ # Debug: Log that we're adding room pixels
484
+ LOGGER.info(f"Adding room {room_id_int} pixels to element map with code {room_element}")
485
+ LOGGER.info(f"Room {room_id_int} has {len(compressed_pixels)//3} compressed runs with {pixel_count} total pixels")
486
+
487
+ # Process walls
488
+ elif layer_type == "wall":
489
+ # Skip if walls are disabled
490
+ if not self.drawing_config.is_enabled(DrawableElement.WALL):
491
+ continue
492
+
493
+ # Process wall pixels
494
+ if "pixels" in layer and layer["pixels"]:
495
+ # Regular pixel format
496
+ for x, y, z in layer["pixels"]:
497
+ # Calculate pixel coordinates
498
+ col = x * pixel_size
499
+ row = y * pixel_size
500
+
501
+ # Fill the element map with wall code
502
+ for i in range(z):
503
+ # Get the region to update
504
+ region_col_start = col + i * pixel_size
505
+ region_col_end = col + (i + 1) * pixel_size
506
+ region_row_start = row
507
+ region_row_end = row + pixel_size
508
+
509
+ # Update element map for this region
510
+ if region_row_start < size_y and region_col_start < size_x:
511
+ # Ensure we stay within bounds
512
+ end_row = min(region_row_end, size_y)
513
+ end_col = min(region_col_end, size_x)
514
+
515
+ # Set element code for this region
516
+ # Only set pixels that are not already set (floor is 1)
517
+ region = self.element_map[region_row_start:end_row, region_col_start:end_col]
518
+ mask = region == DrawableElement.FLOOR
519
+ region[mask] = DrawableElement.WALL
520
+
521
+ elif "compressedPixels" in layer and layer["compressedPixels"]:
522
+ # Compressed pixel format (used in Valetudo)
523
+ compressed_pixels = layer["compressedPixels"]
524
+ i = 0
525
+ pixel_count = 0
526
+
527
+ while i < len(compressed_pixels):
528
+ x = compressed_pixels[i]
529
+ y = compressed_pixels[i+1]
530
+ count = compressed_pixels[i+2]
531
+ pixel_count += count
532
+
533
+ # Set element code for this run of pixels
534
+ for j in range(count):
535
+ px = x + j
536
+ if 0 <= px < size_x and 0 <= y < size_y:
537
+ self.element_map[y, px] = DrawableElement.WALL
538
+
539
+ i += 3
540
+
541
+ # Debug: Log that we're adding wall pixels
542
+ LOGGER.info(f"Adding wall pixels to element map with code {DrawableElement.WALL}")
543
+ LOGGER.info(f"Wall layer has {len(compressed_pixels)//3} compressed runs with {pixel_count} total pixels")
544
+
545
+ elif is_rand256:
546
+ # Get map dimensions from the Rand256 JSON data
547
+ map_data = json_data["map_data"]
548
+ size_x = map_data["dimensions"]["width"]
549
+ size_y = map_data["dimensions"]["height"]
550
+
551
+ # Create empty element map initialized with floor
552
+ self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
553
+ self.element_map[:] = DrawableElement.FLOOR
554
+
555
+ # Process rooms
556
+ if "rooms" in map_data and map_data["rooms"]:
557
+ for room in map_data["rooms"]:
558
+ # Get room ID and check if it's enabled
559
+ room_id_int = room["id"]
560
+
561
+ # Skip if room is disabled
562
+ if not self.drawing_config.is_enabled(f"ROOM_{room_id_int}"):
563
+ continue
564
+
565
+ # Get room element code (ROOM_1, ROOM_2, etc.)
566
+ room_element = None
567
+ if 0 < room_id_int <= 15:
568
+ room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
569
+
570
+ if room_element is not None and "coordinates" in room:
571
+ # Process coordinates for this room
572
+ for coord in room["coordinates"]:
573
+ x, y = coord
574
+ # Update element map for this pixel
575
+ if 0 <= y < size_y and 0 <= x < size_x:
576
+ self.element_map[y, x] = room_element
577
+
578
+ # Process segments (alternative format for rooms)
579
+ if "segments" in map_data and map_data["segments"]:
580
+ for segment_id, coordinates in map_data["segments"].items():
581
+ # Get room ID and check if it's enabled
582
+ room_id_int = int(segment_id)
583
+
584
+ # Skip if room is disabled
585
+ if not self.drawing_config.is_enabled(f"ROOM_{room_id_int}"):
586
+ continue
587
+
588
+ # Get room element code (ROOM_1, ROOM_2, etc.)
589
+ room_element = None
590
+ if 0 < room_id_int <= 15:
591
+ room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
592
+
593
+ if room_element is not None and coordinates:
594
+ # Process individual coordinates
595
+ for coord in coordinates:
596
+ if isinstance(coord, (list, tuple)) and len(coord) == 2:
597
+ x, y = coord
598
+ # Update element map for this pixel
599
+ if 0 <= y < size_y and 0 <= x < size_x:
600
+ self.element_map[y, x] = room_element
601
+
602
+ # Process walls
603
+ if "walls" in map_data and map_data["walls"]:
604
+ # Skip if walls are disabled
605
+ if self.drawing_config.is_element_enabled("WALL"):
606
+ # Process wall coordinates
607
+ for coord in map_data["walls"]:
608
+ x, y = coord
609
+ # Update element map for this pixel
610
+ if 0 <= y < size_y and 0 <= x < size_x:
611
+ self.element_map[y, x] = DrawableElement.WALL
612
+
613
+ # Find the bounding box of non-zero elements to crop the element map
614
+ non_zero_indices = np.nonzero(self.element_map)
615
+ if len(non_zero_indices[0]) > 0: # If there are any non-zero elements
616
+ # Get the bounding box coordinates
617
+ min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0])
618
+ min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1])
619
+
620
+ # Add a margin around the bounding box
621
+ margin = 20 # Pixels of margin around the non-zero elements
622
+ crop_min_y = max(0, min_y - margin)
623
+ crop_max_y = min(self.element_map.shape[0] - 1, max_y + margin)
624
+ crop_min_x = max(0, min_x - margin)
625
+ crop_max_x = min(self.element_map.shape[1] - 1, max_x + margin)
626
+
627
+ # Log the cropping information
628
+ LOGGER.info(f"Cropping element map from {self.element_map.shape} to bounding box: "
629
+ f"({crop_min_x}, {crop_min_y}) to ({crop_max_x}, {crop_max_y})")
630
+ LOGGER.info(f"Cropped dimensions: {crop_max_x - crop_min_x + 1}x{crop_max_y - crop_min_y + 1}")
631
+
632
+ # Create a new, smaller array with just the non-zero region
633
+ cropped_map = self.element_map[crop_min_y:crop_max_y+1, crop_min_x:crop_max_x+1].copy()
634
+
635
+ # Store the cropping coordinates in the shared data for later reference
636
+ if self.shared:
637
+ self.shared.element_map_crop = {
638
+ 'min_x': crop_min_x,
639
+ 'min_y': crop_min_y,
640
+ 'max_x': crop_max_x,
641
+ 'max_y': crop_max_y,
642
+ 'original_shape': self.element_map.shape
643
+ }
644
+
645
+ # Replace the element map with the cropped version
646
+ self.element_map = cropped_map
647
+
648
+ # Now resize the element map to reduce its dimensions
649
+ # Calculate the resize factor based on the current size
650
+ resize_factor = 5 # Reduce to 1/5 of the current size
651
+ new_height = max(self.element_map.shape[0] // resize_factor, 50) # Ensure minimum size
652
+ new_width = max(self.element_map.shape[1] // resize_factor, 50) # Ensure minimum size
653
+
654
+ # Create a resized element map
655
+ resized_map = np.zeros((new_height, new_width), dtype=np.int32)
656
+ resized_map[:] = DrawableElement.FLOOR # Initialize with floor
657
+
658
+ # Calculate scaling factors
659
+ y_scale = self.element_map.shape[0] / new_height
660
+ x_scale = self.element_map.shape[1] / new_width
661
+
662
+ # Populate the resized map by sampling from the original map
663
+ for y in range(new_height):
664
+ for x in range(new_width):
665
+ # Get the corresponding position in the original map
666
+ orig_y = min(int(y * y_scale), self.element_map.shape[0] - 1)
667
+ orig_x = min(int(x * x_scale), self.element_map.shape[1] - 1)
668
+ # Copy the element code
669
+ resized_map[y, x] = self.element_map[orig_y, orig_x]
670
+
671
+ # Store the resize information in shared data
672
+ if self.shared:
673
+ if hasattr(self.shared, 'element_map_crop'):
674
+ self.shared.element_map_crop['resize_factor'] = resize_factor
675
+ self.shared.element_map_crop['resized_shape'] = resized_map.shape
676
+ self.shared.element_map_crop['original_cropped_shape'] = self.element_map.shape
677
+
678
+ # Log the resizing information
679
+ LOGGER.info(f"Resized element map from {self.element_map.shape} to {resized_map.shape} (1/{resize_factor} of cropped size)")
680
+
681
+ # Replace the element map with the resized version
682
+ self.element_map = resized_map
683
+
684
+ return self.element_map
685
+
686
+ def get_element_map(self):
687
+ """Return the element map.
688
+
689
+ Returns:
690
+ numpy.ndarray: The 2D element map array or None if not initialized
691
+ """
692
+ return self.element_map
693
+
694
+ def get_element_at_image_position(self, x: int, y: int):
695
+ """Get the element code at the specified position in the image.
696
+
697
+ This method uses calibration points to accurately map between image coordinates
698
+ and element map coordinates.
699
+
700
+ Args:
701
+ x: X coordinate in the image (e.g., 0-1984)
702
+ y: Y coordinate in the image (e.g., 0-1824)
703
+
704
+ Returns:
705
+ Element code at the specified position, or None if out of bounds
706
+ """
707
+ if self.shared is None or self.element_map is None:
708
+ return None
709
+
710
+ # Get calibration points if available
711
+ calibration_points = None
712
+ if hasattr(self.shared, 'attr_calibration_points'):
713
+ calibration_points = self.shared.attr_calibration_points
714
+
715
+ if calibration_points and len(calibration_points) >= 4:
716
+ # Extract image and vacuum coordinates from calibration points
717
+ image_points = []
718
+ vacuum_points = []
719
+ for point in calibration_points:
720
+ if 'map' in point and 'vacuum' in point:
721
+ image_points.append((point['map']['x'], point['map']['y']))
722
+ vacuum_points.append((point['vacuum']['x'], point['vacuum']['y']))
723
+
724
+ if len(image_points) >= 2:
725
+ # Calculate scaling factors
726
+ img_x_min = min(p[0] for p in image_points)
727
+ img_x_max = max(p[0] for p in image_points)
728
+ img_y_min = min(p[1] for p in image_points)
729
+ img_y_max = max(p[1] for p in image_points)
730
+
731
+ vac_x_min = min(p[0] for p in vacuum_points)
732
+ vac_x_max = max(p[0] for p in vacuum_points)
733
+ vac_y_min = min(p[1] for p in vacuum_points)
734
+ vac_y_max = max(p[1] for p in vacuum_points)
735
+
736
+ # Normalize the input coordinates to 0-1 range in image space
737
+ norm_x = (x - img_x_min) / (img_x_max - img_x_min) if img_x_max > img_x_min else 0
738
+ norm_y = (y - img_y_min) / (img_y_max - img_y_min) if img_y_max > img_y_min else 0
739
+
740
+ # Map to vacuum coordinates
741
+ vac_x = vac_x_min + norm_x * (vac_x_max - vac_x_min)
742
+ vac_y = vac_y_min + norm_y * (vac_y_max - vac_y_min)
743
+
744
+ LOGGER.debug(f"Mapped image ({x}, {y}) to vacuum ({vac_x:.1f}, {vac_y:.1f})")
745
+
746
+ # Now map from vacuum coordinates to element map coordinates
747
+ # This depends on how the element map was created
748
+ if hasattr(self.shared, 'element_map_crop') and self.shared.element_map_crop:
749
+ crop_info = self.shared.element_map_crop
750
+
751
+ # Adjust for cropping
752
+ if 'min_x' in crop_info and 'min_y' in crop_info:
753
+ elem_x = int(vac_x - crop_info['min_x'])
754
+ elem_y = int(vac_y - crop_info['min_y'])
755
+
756
+ # Adjust for resizing
757
+ if 'resize_factor' in crop_info and 'original_cropped_shape' in crop_info and 'resized_shape' in crop_info:
758
+ orig_h, orig_w = crop_info['original_cropped_shape']
759
+ resized_h, resized_w = crop_info['resized_shape']
760
+
761
+ # Scale to resized coordinates
762
+ elem_x = int(elem_x * resized_w / orig_w)
763
+ elem_y = int(elem_y * resized_h / orig_h)
764
+
765
+ LOGGER.debug(f"Mapped vacuum ({vac_x:.1f}, {vac_y:.1f}) to element map ({elem_x}, {elem_y})")
766
+
767
+ # Check bounds and return element
768
+ height, width = self.element_map.shape
769
+ if 0 <= elem_y < height and 0 <= elem_x < width:
770
+ return self.element_map[elem_y, elem_x]
771
+
772
+ # Fallback to the simpler method if calibration points aren't available
773
+ return self.get_element_at_position(x, y, is_image_coords=True)
774
+
775
+ def get_element_name(self, element_code):
776
+ """Get the name of the element from its code.
777
+
778
+ Args:
779
+ element_code: The element code (e.g., 1, 2, 101, etc.)
780
+
781
+ Returns:
782
+ The name of the element (e.g., 'FLOOR', 'WALL', 'ROOM_1', etc.)
783
+ """
784
+ if element_code is None:
785
+ return 'NONE'
786
+
787
+ # Check if it's a room
788
+ if element_code >= 100:
789
+ room_number = element_code - 100
790
+ return f'ROOM_{room_number}'
791
+
792
+ # Check standard elements
793
+ for name, code in vars(DrawableElement).items():
794
+ if not name.startswith('_') and isinstance(code, int) and code == element_code:
795
+ return name
796
+
797
+ return f'UNKNOWN_{element_code}'
798
+
799
+ def get_element_at_position(self, x: int, y: int, is_image_coords: bool = False):
800
+ """Get the element code at the specified position in the element map.
801
+
802
+ Args:
803
+ x: X coordinate in the original (uncropped) element map or image
804
+ y: Y coordinate in the original (uncropped) element map or image
805
+ is_image_coords: If True, x and y are image coordinates (e.g., 1984x1824)
806
+ If False, x and y are element map coordinates
807
+
808
+ Returns:
809
+ Element code at the specified position, or None if out of bounds
810
+ """
811
+ if self.element_map is None:
812
+ return None
813
+
814
+ # If coordinates are from the image, convert them to element map coordinates first
815
+ if is_image_coords and self.shared:
816
+ # Get image dimensions
817
+ if hasattr(self.shared, 'image_size') and self.shared.image_size is not None and len(self.shared.image_size) >= 2:
818
+ image_width = self.shared.image_size[0]
819
+ image_height = self.shared.image_size[1]
820
+ else:
821
+ # Default image dimensions if not available
822
+ image_width = 1984
823
+ image_height = 1824
824
+
825
+ # Get original element map dimensions (before resizing)
826
+ if hasattr(self.shared, 'element_map_crop') and self.shared.element_map_crop is not None and 'original_cropped_shape' in self.shared.element_map_crop:
827
+ original_map_height, original_map_width = self.shared.element_map_crop['original_cropped_shape']
828
+ else:
829
+ # Estimate based on typical values
830
+ original_map_width = 1310
831
+ original_map_height = 1310
832
+
833
+ # Calculate scaling factors between image and original element map
834
+ x_scale_to_map = original_map_width / image_width
835
+ y_scale_to_map = original_map_height / image_height
836
+
837
+ # Convert image coordinates to element map coordinates
838
+ # Apply a small offset to better align with the actual elements
839
+ # This is based on empirical testing with the sample coordinates
840
+ x_offset = 50 # Adjust as needed based on testing
841
+ y_offset = 20 # Adjust as needed based on testing
842
+ x = int((x + x_offset) * x_scale_to_map)
843
+ y = int((y + y_offset) * y_scale_to_map)
844
+
845
+ LOGGER.debug(f"Converted image coordinates ({x}, {y}) to element map coordinates")
846
+
847
+ # Adjust coordinates if the element map has been cropped and resized
848
+ if self.shared and hasattr(self.shared, 'element_map_crop'):
849
+ # Get the crop information
850
+ crop_info = self.shared.element_map_crop
851
+
852
+ # Adjust coordinates to the cropped map
853
+ x_cropped = x - crop_info['min_x']
854
+ y_cropped = y - crop_info['min_y']
855
+
856
+ # If the map has been resized, adjust coordinates further
857
+ if 'resize_factor' in crop_info:
858
+ resize_factor = crop_info['resize_factor']
859
+ original_cropped_shape = crop_info.get('original_cropped_shape')
860
+ resized_shape = crop_info.get('resized_shape')
861
+
862
+ if original_cropped_shape and resized_shape:
863
+ # Calculate scaling factors
864
+ y_scale = original_cropped_shape[0] / resized_shape[0]
865
+ x_scale = original_cropped_shape[1] / resized_shape[1]
866
+
867
+ # Scale the coordinates to the resized map
868
+ x_resized = int(x_cropped / x_scale)
869
+ y_resized = int(y_cropped / y_scale)
870
+
871
+ # Check if the coordinates are within the resized map
872
+ height, width = self.element_map.shape
873
+ if 0 <= y_resized < height and 0 <= x_resized < width:
874
+ return self.element_map[y_resized, x_resized]
875
+ return None
876
+
877
+ # If no resizing or missing resize info, use cropped coordinates
878
+ height, width = self.element_map.shape
879
+ if 0 <= y_cropped < height and 0 <= x_cropped < width:
880
+ return self.element_map[y_cropped, x_cropped]
881
+ return None
882
+ else:
883
+ # No cropping, use coordinates as is
884
+ height, width = self.element_map.shape
885
+ if 0 <= y < height and 0 <= x < width:
886
+ return self.element_map[y, x]
887
+ return None