valetudo-map-parser 0.1.9b45__py3-none-any.whl → 0.1.9b46__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.
@@ -13,11 +13,11 @@ import asyncio
13
13
  import logging
14
14
  import math
15
15
 
16
- # cv2 is imported but not used directly in this file
17
- # It's needed for other modules that import from here
18
16
  import numpy as np
19
17
  from PIL import ImageDraw, ImageFont
18
+ from scipy import ndimage
20
19
 
20
+ from .colors import ColorsManagement
21
21
  from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
22
22
 
23
23
 
@@ -187,7 +187,6 @@ class Drawable:
187
187
  width: Width of the line
188
188
  blend: Whether to blend the color with the background
189
189
  """
190
- from .colors import ColorsManagment
191
190
 
192
191
  x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
193
192
 
@@ -195,13 +194,17 @@ class Drawable:
195
194
  if blend:
196
195
  # Sample at start point
197
196
  if 0 <= y1 < layer.shape[0] and 0 <= x1 < layer.shape[1]:
198
- start_blended_color = ColorsManagment.sample_and_blend_color(layer, x1, y1, color)
197
+ start_blended_color = ColorsManagement.sample_and_blend_color(
198
+ layer, x1, y1, color
199
+ )
199
200
  else:
200
201
  start_blended_color = color
201
202
 
202
203
  # Sample at end point
203
204
  if 0 <= y2 < layer.shape[0] and 0 <= x2 < layer.shape[1]:
204
- end_blended_color = ColorsManagment.sample_and_blend_color(layer, x2, y2, color)
205
+ end_blended_color = ColorsManagement.sample_and_blend_color(
206
+ layer, x2, y2, color
207
+ )
205
208
  else:
206
209
  end_blended_color = color
207
210
 
@@ -210,7 +213,7 @@ class Drawable:
210
213
  (start_blended_color[0] + end_blended_color[0]) // 2,
211
214
  (start_blended_color[1] + end_blended_color[1]) // 2,
212
215
  (start_blended_color[2] + end_blended_color[2]) // 2,
213
- (start_blended_color[3] + end_blended_color[3]) // 2
216
+ (start_blended_color[3] + end_blended_color[3]) // 2,
214
217
  )
215
218
  else:
216
219
  blended_color = color
@@ -256,28 +259,27 @@ class Drawable:
256
259
  """
257
260
  Join the coordinates creating a continuous line (path).
258
261
  """
259
- import logging
260
- from .colors import ColorsManagment
261
262
 
262
- _LOGGER = logging.getLogger(__name__)
263
- _LOGGER.debug("Drawing lines with %d coordinates, width %d, color %s", len(coords), width, color)
264
263
  for coord in coords:
265
264
  x0, y0 = coord[0]
266
265
  try:
267
266
  x1, y1 = coord[1]
268
267
  except IndexError:
269
268
  x1, y1 = x0, y0
270
- _LOGGER.debug("Drawing line from (%d, %d) to (%d, %d)", x0, y0, x1, y1)
271
269
 
272
270
  # Sample background color at the endpoints and blend with foreground color
273
271
  # This is more efficient than sampling at every pixel
274
272
  if 0 <= y0 < arr.shape[0] and 0 <= x0 < arr.shape[1]:
275
- start_blended_color = ColorsManagment.sample_and_blend_color(arr, x0, y0, color)
273
+ start_blended_color = ColorsManagement.sample_and_blend_color(
274
+ arr, x0, y0, color
275
+ )
276
276
  else:
277
277
  start_blended_color = color
278
278
 
279
279
  if 0 <= y1 < arr.shape[0] and 0 <= x1 < arr.shape[1]:
280
- end_blended_color = ColorsManagment.sample_and_blend_color(arr, x1, y1, color)
280
+ end_blended_color = ColorsManagement.sample_and_blend_color(
281
+ arr, x1, y1, color
282
+ )
281
283
  else:
282
284
  end_blended_color = color
283
285
 
@@ -286,7 +288,7 @@ class Drawable:
286
288
  (start_blended_color[0] + end_blended_color[0]) // 2,
287
289
  (start_blended_color[1] + end_blended_color[1]) // 2,
288
290
  (start_blended_color[2] + end_blended_color[2]) // 2,
289
- (start_blended_color[3] + end_blended_color[3]) // 2
291
+ (start_blended_color[3] + end_blended_color[3]) // 2,
290
292
  )
291
293
 
292
294
  dx = abs(x1 - x0)
@@ -422,7 +424,6 @@ class Drawable:
422
424
  """
423
425
  Draw the zones on the input layer with color blending.
424
426
  """
425
- from .colors import ColorsManagment
426
427
 
427
428
  dot_radius = 1 # Number of pixels for the dot
428
429
  dot_spacing = 4 # Space between dots
@@ -440,14 +441,18 @@ class Drawable:
440
441
 
441
442
  # Blend the color with the background color at the sample point
442
443
  if 0 <= sample_y < layers.shape[0] and 0 <= sample_x < layers.shape[1]:
443
- blended_color = ColorsManagment.sample_and_blend_color(layers, sample_x, sample_y, color)
444
+ blended_color = ColorsManagement.sample_and_blend_color(
445
+ layers, sample_x, sample_y, color
446
+ )
444
447
  else:
445
448
  blended_color = color
446
449
 
447
450
  for y in range(min_y, max_y, dot_spacing):
448
451
  for x in range(min_x, max_x, dot_spacing):
449
452
  for _ in range(dot_radius):
450
- layers = Drawable._ellipse(layers, (x, y), dot_radius, blended_color)
453
+ layers = Drawable._ellipse(
454
+ layers, (x, y), dot_radius, blended_color
455
+ )
451
456
  return layers
452
457
 
453
458
  @staticmethod
@@ -542,7 +547,7 @@ class Drawable:
542
547
 
543
548
  @staticmethod
544
549
  async def async_draw_obstacles(
545
- image: np.ndarray, obstacle_info_list, color: Tuple[int, int, int, int]
550
+ image: np.ndarray, obstacle_info_list, color: Color
546
551
  ) -> np.ndarray:
547
552
  """
548
553
  Optimized async version of draw_obstacles using asyncio.gather().
@@ -592,3 +597,60 @@ class Drawable:
592
597
  else:
593
598
  draw.text((x, y), text, font=font, fill=color)
594
599
  x += draw.textlength(text, font=default_font)
600
+
601
+ @staticmethod
602
+ def draw_filled_polygon(image_array, vertices, color):
603
+ """
604
+ Draw a filled polygon on the image array using scipy.ndimage.
605
+
606
+ Args:
607
+ image_array: NumPy array of shape (height, width, 4) containing RGBA image data
608
+ vertices: List of (x,y) tuples defining the polygon vertices
609
+ color: RGBA color tuple to fill the polygon with
610
+
611
+ Returns:
612
+ Modified image array with the filled polygon
613
+ """
614
+ if len(vertices) < 3:
615
+ return image_array # Need at least 3 points for a polygon
616
+
617
+ height, width = image_array.shape[:2]
618
+
619
+ # Create a mask for the polygon
620
+ polygon_mask = np.zeros((height, width), dtype=bool)
621
+
622
+ # Convert vertices to numpy arrays for processing
623
+ vertices_array = np.array(vertices)
624
+ x_coords = vertices_array[:, 0]
625
+ y_coords = vertices_array[:, 1]
626
+
627
+ # Clip coordinates to image boundaries
628
+ x_coords = np.clip(x_coords, 0, width - 1)
629
+ y_coords = np.clip(y_coords, 0, height - 1)
630
+
631
+ # Create a polygon using scipy.ndimage
632
+ # First create the boundary
633
+ for i in range(len(vertices)):
634
+ x1, y1 = int(x_coords[i]), int(y_coords[i])
635
+ x2, y2 = (
636
+ int(x_coords[(i + 1) % len(vertices)]),
637
+ int(y_coords[(i + 1) % len(vertices)]),
638
+ )
639
+
640
+ # Draw line between consecutive vertices
641
+ length = max(abs(x2 - x1), abs(y2 - y1)) * 2
642
+ if length == 0:
643
+ continue
644
+
645
+ t = np.linspace(0, 1, length)
646
+ x = np.round(x1 * (1 - t) + x2 * t).astype(int)
647
+ y = np.round(y1 * (1 - t) + y2 * t).astype(int)
648
+
649
+ # Add boundary points to mask
650
+ polygon_mask[y, x] = True
651
+
652
+ # Fill the polygon using scipy.ndimage.binary_fill_holes
653
+ filled_polygon = ndimage.binary_fill_holes(polygon_mask)
654
+
655
+ # Apply color to the filled polygon
656
+ return ColorsManagement.batch_blend_colors(image_array, filled_polygon, color)
@@ -344,7 +344,9 @@ class ElementMapGenerator:
344
344
  self.element_map = existing_element_map
345
345
 
346
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
347
+ is_valetudo = (
348
+ "size" in json_data and "pixelSize" in json_data and "layers" in json_data
349
+ )
348
350
  is_rand256 = "image" in json_data and "map_data" in json_data
349
351
 
350
352
  # Debug logging
@@ -362,7 +364,9 @@ class ElementMapGenerator:
362
364
  size_y = map_size.get("height", 0)
363
365
  else:
364
366
  # If map_size is a number, use it for both dimensions
365
- pixel_size = json_data.get("pixelSize", 5) # Default to 5mm per pixel
367
+ pixel_size = json_data.get(
368
+ "pixelSize", 5
369
+ ) # Default to 5mm per pixel
366
370
  size_x = int(map_size // pixel_size)
367
371
  size_y = int(map_size // pixel_size)
368
372
  self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
@@ -387,16 +391,24 @@ class ElementMapGenerator:
387
391
  # Calculate scale factor based on pixel size (normalize to 5mm standard)
388
392
  # This helps handle maps with different scales
389
393
  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}")
394
+ LOGGER.info(
395
+ f"Map dimensions: {size_x}x{size_y}, pixel size: {pixel_size}mm, scale factor: {scale_factor:.2f}"
396
+ )
391
397
 
392
398
  # 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:
399
+ if (
400
+ self.element_map is None
401
+ or self.element_map.shape[0] == 0
402
+ or self.element_map.shape[1] == 0
403
+ ):
394
404
  # For now, create a full-sized element map to ensure coordinates match
395
405
  # We'll resize it at the end for efficiency
396
406
  map_width = int(size_x // pixel_size) if pixel_size != 0 else size_x
397
407
  map_height = int(size_y // pixel_size) if pixel_size != 0 else size_y
398
408
 
399
- LOGGER.info(f"Creating element map with dimensions: {map_width}x{map_height}")
409
+ LOGGER.info(
410
+ f"Creating element map with dimensions: {map_width}x{map_height}"
411
+ )
400
412
  self.element_map = np.zeros((map_height, map_width), dtype=np.int32)
401
413
  self.element_map[:] = DrawableElement.FLOOR
402
414
 
@@ -410,14 +422,18 @@ class ElementMapGenerator:
410
422
  if "segments" in layer:
411
423
  segments = layer["segments"]
412
424
  else:
413
- segments = [layer] # Some formats have segment data directly in the layer
425
+ segments = [
426
+ layer
427
+ ] # Some formats have segment data directly in the layer
414
428
 
415
429
  for segment in segments:
416
430
  # Get room ID and check if it's enabled
417
431
  if "id" in segment:
418
432
  room_id = segment["id"]
419
433
  room_id_int = int(room_id)
420
- elif "metaData" in segment and "segmentId" in segment["metaData"]:
434
+ elif (
435
+ "metaData" in segment and "segmentId" in segment["metaData"]
436
+ ):
421
437
  # Handle Hypfer format
422
438
  room_id = segment["metaData"]["segmentId"]
423
439
  room_id_int = int(room_id)
@@ -426,8 +442,12 @@ class ElementMapGenerator:
426
442
  continue
427
443
 
428
444
  # 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):
445
+ room_element = getattr(
446
+ DrawableElement, f"ROOM_{room_id_int}", None
447
+ )
448
+ if room_element is None or not self.drawing_config.is_enabled(
449
+ room_element
450
+ ):
431
451
  continue
432
452
 
433
453
  # Room element code was already retrieved above
@@ -449,18 +469,27 @@ class ElementMapGenerator:
449
469
  region_row_end = row + pixel_size
450
470
 
451
471
  # Update element map for this region
452
- if region_row_start < size_y and region_col_start < size_x:
472
+ if (
473
+ region_row_start < size_y
474
+ and region_col_start < size_x
475
+ ):
453
476
  # Ensure we stay within bounds
454
477
  end_row = min(region_row_end, size_y)
455
478
  end_col = min(region_col_end, size_x)
456
479
 
457
480
  # Set element code for this region
458
481
  # 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]
482
+ region = self.element_map[
483
+ region_row_start:end_row,
484
+ region_col_start:end_col,
485
+ ]
460
486
  mask = region == DrawableElement.FLOOR
461
487
  region[mask] = room_element
462
488
 
463
- elif "compressedPixels" in segment and segment["compressedPixels"]:
489
+ elif (
490
+ "compressedPixels" in segment
491
+ and segment["compressedPixels"]
492
+ ):
464
493
  # Compressed pixel format (used in Valetudo)
465
494
  compressed_pixels = segment["compressedPixels"]
466
495
  i = 0
@@ -468,8 +497,8 @@ class ElementMapGenerator:
468
497
 
469
498
  while i < len(compressed_pixels):
470
499
  x = compressed_pixels[i]
471
- y = compressed_pixels[i+1]
472
- count = compressed_pixels[i+2]
500
+ y = compressed_pixels[i + 1]
501
+ count = compressed_pixels[i + 2]
473
502
  pixel_count += count
474
503
 
475
504
  # Set element code for this run of pixels
@@ -481,8 +510,12 @@ class ElementMapGenerator:
481
510
  i += 3
482
511
 
483
512
  # 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")
513
+ LOGGER.info(
514
+ f"Adding room {room_id_int} pixels to element map with code {room_element}"
515
+ )
516
+ LOGGER.info(
517
+ f"Room {room_id_int} has {len(compressed_pixels) // 3} compressed runs with {pixel_count} total pixels"
518
+ )
486
519
 
487
520
  # Process walls
488
521
  elif layer_type == "wall":
@@ -507,14 +540,20 @@ class ElementMapGenerator:
507
540
  region_row_end = row + pixel_size
508
541
 
509
542
  # Update element map for this region
510
- if region_row_start < size_y and region_col_start < size_x:
543
+ if (
544
+ region_row_start < size_y
545
+ and region_col_start < size_x
546
+ ):
511
547
  # Ensure we stay within bounds
512
548
  end_row = min(region_row_end, size_y)
513
549
  end_col = min(region_col_end, size_x)
514
550
 
515
551
  # Set element code for this region
516
552
  # 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]
553
+ region = self.element_map[
554
+ region_row_start:end_row,
555
+ region_col_start:end_col,
556
+ ]
518
557
  mask = region == DrawableElement.FLOOR
519
558
  region[mask] = DrawableElement.WALL
520
559
 
@@ -526,8 +565,8 @@ class ElementMapGenerator:
526
565
 
527
566
  while i < len(compressed_pixels):
528
567
  x = compressed_pixels[i]
529
- y = compressed_pixels[i+1]
530
- count = compressed_pixels[i+2]
568
+ y = compressed_pixels[i + 1]
569
+ count = compressed_pixels[i + 2]
531
570
  pixel_count += count
532
571
 
533
572
  # Set element code for this run of pixels
@@ -539,8 +578,12 @@ class ElementMapGenerator:
539
578
  i += 3
540
579
 
541
580
  # 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")
581
+ LOGGER.info(
582
+ f"Adding wall pixels to element map with code {DrawableElement.WALL}"
583
+ )
584
+ LOGGER.info(
585
+ f"Wall layer has {len(compressed_pixels) // 3} compressed runs with {pixel_count} total pixels"
586
+ )
544
587
 
545
588
  elif is_rand256:
546
589
  # Get map dimensions from the Rand256 JSON data
@@ -565,7 +608,9 @@ class ElementMapGenerator:
565
608
  # Get room element code (ROOM_1, ROOM_2, etc.)
566
609
  room_element = None
567
610
  if 0 < room_id_int <= 15:
568
- room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
611
+ room_element = getattr(
612
+ DrawableElement, f"ROOM_{room_id_int}", None
613
+ )
569
614
 
570
615
  if room_element is not None and "coordinates" in room:
571
616
  # Process coordinates for this room
@@ -588,7 +633,9 @@ class ElementMapGenerator:
588
633
  # Get room element code (ROOM_1, ROOM_2, etc.)
589
634
  room_element = None
590
635
  if 0 < room_id_int <= 15:
591
- room_element = getattr(DrawableElement, f"ROOM_{room_id_int}", None)
636
+ room_element = getattr(
637
+ DrawableElement, f"ROOM_{room_id_int}", None
638
+ )
592
639
 
593
640
  if room_element is not None and coordinates:
594
641
  # Process individual coordinates
@@ -625,21 +672,27 @@ class ElementMapGenerator:
625
672
  crop_max_x = min(self.element_map.shape[1] - 1, max_x + margin)
626
673
 
627
674
  # 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}")
675
+ LOGGER.info(
676
+ f"Cropping element map from {self.element_map.shape} to bounding box: "
677
+ f"({crop_min_x}, {crop_min_y}) to ({crop_max_x}, {crop_max_y})"
678
+ )
679
+ LOGGER.info(
680
+ f"Cropped dimensions: {crop_max_x - crop_min_x + 1}x{crop_max_y - crop_min_y + 1}"
681
+ )
631
682
 
632
683
  # 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()
684
+ cropped_map = self.element_map[
685
+ crop_min_y : crop_max_y + 1, crop_min_x : crop_max_x + 1
686
+ ].copy()
634
687
 
635
688
  # Store the cropping coordinates in the shared data for later reference
636
689
  if self.shared:
637
690
  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
691
+ "min_x": crop_min_x,
692
+ "min_y": crop_min_y,
693
+ "max_x": crop_max_x,
694
+ "max_y": crop_max_y,
695
+ "original_shape": self.element_map.shape,
643
696
  }
644
697
 
645
698
  # Replace the element map with the cropped version
@@ -648,8 +701,12 @@ class ElementMapGenerator:
648
701
  # Now resize the element map to reduce its dimensions
649
702
  # Calculate the resize factor based on the current size
650
703
  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
704
+ new_height = max(
705
+ self.element_map.shape[0] // resize_factor, 50
706
+ ) # Ensure minimum size
707
+ new_width = max(
708
+ self.element_map.shape[1] // resize_factor, 50
709
+ ) # Ensure minimum size
653
710
 
654
711
  # Create a resized element map
655
712
  resized_map = np.zeros((new_height, new_width), dtype=np.int32)
@@ -670,13 +727,17 @@ class ElementMapGenerator:
670
727
 
671
728
  # Store the resize information in shared data
672
729
  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
730
+ if hasattr(self.shared, "element_map_crop"):
731
+ self.shared.element_map_crop["resize_factor"] = resize_factor
732
+ self.shared.element_map_crop["resized_shape"] = resized_map.shape
733
+ self.shared.element_map_crop["original_cropped_shape"] = (
734
+ self.element_map.shape
735
+ )
677
736
 
678
737
  # 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)")
738
+ LOGGER.info(
739
+ f"Resized element map from {self.element_map.shape} to {resized_map.shape} (1/{resize_factor} of cropped size)"
740
+ )
680
741
 
681
742
  # Replace the element map with the resized version
682
743
  self.element_map = resized_map
@@ -709,7 +770,7 @@ class ElementMapGenerator:
709
770
 
710
771
  # Get calibration points if available
711
772
  calibration_points = None
712
- if hasattr(self.shared, 'attr_calibration_points'):
773
+ if hasattr(self.shared, "attr_calibration_points"):
713
774
  calibration_points = self.shared.attr_calibration_points
714
775
 
715
776
  if calibration_points and len(calibration_points) >= 4:
@@ -717,9 +778,9 @@ class ElementMapGenerator:
717
778
  image_points = []
718
779
  vacuum_points = []
719
780
  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']))
781
+ if "map" in point and "vacuum" in point:
782
+ image_points.append((point["map"]["x"], point["map"]["y"]))
783
+ vacuum_points.append((point["vacuum"]["x"], point["vacuum"]["y"]))
723
784
 
724
785
  if len(image_points) >= 2:
725
786
  # Calculate scaling factors
@@ -734,35 +795,54 @@ class ElementMapGenerator:
734
795
  vac_y_max = max(p[1] for p in vacuum_points)
735
796
 
736
797
  # 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
798
+ norm_x = (
799
+ (x - img_x_min) / (img_x_max - img_x_min)
800
+ if img_x_max > img_x_min
801
+ else 0
802
+ )
803
+ norm_y = (
804
+ (y - img_y_min) / (img_y_max - img_y_min)
805
+ if img_y_max > img_y_min
806
+ else 0
807
+ )
739
808
 
740
809
  # Map to vacuum coordinates
741
810
  vac_x = vac_x_min + norm_x * (vac_x_max - vac_x_min)
742
811
  vac_y = vac_y_min + norm_y * (vac_y_max - vac_y_min)
743
812
 
744
- LOGGER.debug(f"Mapped image ({x}, {y}) to vacuum ({vac_x:.1f}, {vac_y:.1f})")
813
+ LOGGER.debug(
814
+ f"Mapped image ({x}, {y}) to vacuum ({vac_x:.1f}, {vac_y:.1f})"
815
+ )
745
816
 
746
817
  # Now map from vacuum coordinates to element map coordinates
747
818
  # This depends on how the element map was created
748
- if hasattr(self.shared, 'element_map_crop') and self.shared.element_map_crop:
819
+ if (
820
+ hasattr(self.shared, "element_map_crop")
821
+ and self.shared.element_map_crop
822
+ ):
749
823
  crop_info = self.shared.element_map_crop
750
824
 
751
825
  # 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'])
826
+ if "min_x" in crop_info and "min_y" in crop_info:
827
+ elem_x = int(vac_x - crop_info["min_x"])
828
+ elem_y = int(vac_y - crop_info["min_y"])
755
829
 
756
830
  # 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']
831
+ if (
832
+ "resize_factor" in crop_info
833
+ and "original_cropped_shape" in crop_info
834
+ and "resized_shape" in crop_info
835
+ ):
836
+ orig_h, orig_w = crop_info["original_cropped_shape"]
837
+ resized_h, resized_w = crop_info["resized_shape"]
760
838
 
761
839
  # Scale to resized coordinates
762
840
  elem_x = int(elem_x * resized_w / orig_w)
763
841
  elem_y = int(elem_y * resized_h / orig_h)
764
842
 
765
- LOGGER.debug(f"Mapped vacuum ({vac_x:.1f}, {vac_y:.1f}) to element map ({elem_x}, {elem_y})")
843
+ LOGGER.debug(
844
+ f"Mapped vacuum ({vac_x:.1f}, {vac_y:.1f}) to element map ({elem_x}, {elem_y})"
845
+ )
766
846
 
767
847
  # Check bounds and return element
768
848
  height, width = self.element_map.shape
@@ -782,19 +862,23 @@ class ElementMapGenerator:
782
862
  The name of the element (e.g., 'FLOOR', 'WALL', 'ROOM_1', etc.)
783
863
  """
784
864
  if element_code is None:
785
- return 'NONE'
865
+ return "NONE"
786
866
 
787
867
  # Check if it's a room
788
868
  if element_code >= 100:
789
869
  room_number = element_code - 100
790
- return f'ROOM_{room_number}'
870
+ return f"ROOM_{room_number}"
791
871
 
792
872
  # Check standard elements
793
873
  for name, code in vars(DrawableElement).items():
794
- if not name.startswith('_') and isinstance(code, int) and code == element_code:
874
+ if (
875
+ not name.startswith("_")
876
+ and isinstance(code, int)
877
+ and code == element_code
878
+ ):
795
879
  return name
796
880
 
797
- return f'UNKNOWN_{element_code}'
881
+ return f"UNKNOWN_{element_code}"
798
882
 
799
883
  def get_element_at_position(self, x: int, y: int, is_image_coords: bool = False):
800
884
  """Get the element code at the specified position in the element map.
@@ -814,7 +898,11 @@ class ElementMapGenerator:
814
898
  # If coordinates are from the image, convert them to element map coordinates first
815
899
  if is_image_coords and self.shared:
816
900
  # 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:
901
+ if (
902
+ hasattr(self.shared, "image_size")
903
+ and self.shared.image_size is not None
904
+ and len(self.shared.image_size) >= 2
905
+ ):
818
906
  image_width = self.shared.image_size[0]
819
907
  image_height = self.shared.image_size[1]
820
908
  else:
@@ -823,8 +911,14 @@ class ElementMapGenerator:
823
911
  image_height = 1824
824
912
 
825
913
  # 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']
914
+ if (
915
+ hasattr(self.shared, "element_map_crop")
916
+ and self.shared.element_map_crop is not None
917
+ and "original_cropped_shape" in self.shared.element_map_crop
918
+ ):
919
+ original_map_height, original_map_width = self.shared.element_map_crop[
920
+ "original_cropped_shape"
921
+ ]
828
922
  else:
829
923
  # Estimate based on typical values
830
924
  original_map_width = 1310
@@ -842,22 +936,24 @@ class ElementMapGenerator:
842
936
  x = int((x + x_offset) * x_scale_to_map)
843
937
  y = int((y + y_offset) * y_scale_to_map)
844
938
 
845
- LOGGER.debug(f"Converted image coordinates ({x}, {y}) to element map coordinates")
939
+ LOGGER.debug(
940
+ f"Converted image coordinates ({x}, {y}) to element map coordinates"
941
+ )
846
942
 
847
943
  # Adjust coordinates if the element map has been cropped and resized
848
- if self.shared and hasattr(self.shared, 'element_map_crop'):
944
+ if self.shared and hasattr(self.shared, "element_map_crop"):
849
945
  # Get the crop information
850
946
  crop_info = self.shared.element_map_crop
851
947
 
852
948
  # Adjust coordinates to the cropped map
853
- x_cropped = x - crop_info['min_x']
854
- y_cropped = y - crop_info['min_y']
949
+ x_cropped = x - crop_info["min_x"]
950
+ y_cropped = y - crop_info["min_y"]
855
951
 
856
952
  # 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')
953
+ if "resize_factor" in crop_info:
954
+ resize_factor = crop_info["resize_factor"]
955
+ original_cropped_shape = crop_info.get("original_cropped_shape")
956
+ resized_shape = crop_info.get("resized_shape")
861
957
 
862
958
  if original_cropped_shape and resized_shape:
863
959
  # Calculate scaling factors