valetudo-map-parser 0.1.9b49__py3-none-any.whl → 0.1.9b51__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.
- valetudo_map_parser/__init__.py +2 -0
- valetudo_map_parser/config/color_utils.py +71 -17
- valetudo_map_parser/config/colors.py +55 -71
- valetudo_map_parser/config/drawable.py +55 -37
- valetudo_map_parser/config/drawable_elements.py +0 -676
- valetudo_map_parser/config/enhanced_drawable.py +75 -193
- valetudo_map_parser/config/shared.py +0 -1
- valetudo_map_parser/config/types.py +19 -33
- valetudo_map_parser/config/utils.py +0 -67
- valetudo_map_parser/hypfer_draw.py +222 -57
- valetudo_map_parser/hypfer_handler.py +48 -155
- valetudo_map_parser/hypfer_rooms_handler.py +406 -0
- valetudo_map_parser/map_data.py +0 -30
- valetudo_map_parser/rand25_handler.py +2 -84
- {valetudo_map_parser-0.1.9b49.dist-info → valetudo_map_parser-0.1.9b51.dist-info}/METADATA +1 -1
- valetudo_map_parser-0.1.9b51.dist-info/RECORD +26 -0
- valetudo_map_parser-0.1.9b49.dist-info/RECORD +0 -25
- {valetudo_map_parser-0.1.9b49.dist-info → valetudo_map_parser-0.1.9b51.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b49.dist-info → valetudo_map_parser-0.1.9b51.dist-info}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b49.dist-info → valetudo_map_parser-0.1.9b51.dist-info}/WHEEL +0 -0
@@ -305,679 +305,3 @@ class DrawingConfig:
|
|
305
305
|
LOGGER.info(
|
306
306
|
"Disabled ROOM_%d element from device_info setting", room_id
|
307
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 = (
|
348
|
-
"size" in json_data and "pixelSize" in json_data and "layers" in json_data
|
349
|
-
)
|
350
|
-
is_rand256 = "image" in json_data and "map_data" in json_data
|
351
|
-
|
352
|
-
# Debug logging
|
353
|
-
LOGGER.debug(f"JSON data keys: {list(json_data.keys())}")
|
354
|
-
LOGGER.debug(f"Is Valetudo: {is_valetudo}, Is Rand256: {is_rand256}")
|
355
|
-
|
356
|
-
# Create element map if not provided
|
357
|
-
if self.element_map is None:
|
358
|
-
if is_valetudo:
|
359
|
-
# Get map dimensions from Valetudo map
|
360
|
-
map_size = json_data.get("size", 0)
|
361
|
-
if isinstance(map_size, dict):
|
362
|
-
# If map_size is a dictionary, extract the values
|
363
|
-
size_x = map_size.get("width", 0)
|
364
|
-
size_y = map_size.get("height", 0)
|
365
|
-
else:
|
366
|
-
# If map_size is a number, use it for both dimensions
|
367
|
-
pixel_size = json_data.get(
|
368
|
-
"pixelSize", 5
|
369
|
-
) # Default to 5mm per pixel
|
370
|
-
size_x = int(map_size // pixel_size)
|
371
|
-
size_y = int(map_size // pixel_size)
|
372
|
-
self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
|
373
|
-
self.element_map[:] = DrawableElement.FLOOR
|
374
|
-
elif is_rand256:
|
375
|
-
# Get map dimensions from Rand256 map
|
376
|
-
map_data = json_data.get("map_data", {})
|
377
|
-
size_x = map_data.get("width", 0)
|
378
|
-
size_y = map_data.get("height", 0)
|
379
|
-
self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
|
380
|
-
|
381
|
-
if not (is_valetudo or is_rand256):
|
382
|
-
LOGGER.error("Unknown JSON format, cannot generate element map")
|
383
|
-
return None
|
384
|
-
|
385
|
-
if is_valetudo:
|
386
|
-
# Get map dimensions from the Valetudo JSON data
|
387
|
-
size_x = json_data["size"]["x"]
|
388
|
-
size_y = json_data["size"]["y"]
|
389
|
-
pixel_size = json_data["pixelSize"]
|
390
|
-
|
391
|
-
# Calculate scale factor based on pixel size (normalize to 5mm standard)
|
392
|
-
# This helps handle maps with different scales
|
393
|
-
scale_factor = pixel_size if pixel_size != 0 else 1.0
|
394
|
-
LOGGER.info(
|
395
|
-
f"Map dimensions: {size_x}x{size_y}, pixel size: {pixel_size}mm, scale factor: {scale_factor:.2f}"
|
396
|
-
)
|
397
|
-
|
398
|
-
# Ensure element_map is properly initialized with the correct dimensions
|
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
|
-
):
|
404
|
-
# For now, create a full-sized element map to ensure coordinates match
|
405
|
-
# We'll resize it at the end for efficiency
|
406
|
-
map_width = int(size_x // pixel_size) if pixel_size != 0 else size_x
|
407
|
-
map_height = int(size_y // pixel_size) if pixel_size != 0 else size_y
|
408
|
-
|
409
|
-
LOGGER.info(
|
410
|
-
f"Creating element map with dimensions: {map_width}x{map_height}"
|
411
|
-
)
|
412
|
-
self.element_map = np.zeros((map_height, map_width), dtype=np.int32)
|
413
|
-
self.element_map[:] = DrawableElement.FLOOR
|
414
|
-
|
415
|
-
# Process layers (rooms, walls, etc.)
|
416
|
-
for layer in json_data["layers"]:
|
417
|
-
layer_type = layer["type"]
|
418
|
-
|
419
|
-
# Process rooms (segments)
|
420
|
-
if layer_type == "segment":
|
421
|
-
# Handle different segment formats
|
422
|
-
if "segments" in layer:
|
423
|
-
segments = layer["segments"]
|
424
|
-
else:
|
425
|
-
segments = [
|
426
|
-
layer
|
427
|
-
] # Some formats have segment data directly in the layer
|
428
|
-
|
429
|
-
for segment in segments:
|
430
|
-
# Get room ID and check if it's enabled
|
431
|
-
if "id" in segment:
|
432
|
-
room_id = segment["id"]
|
433
|
-
room_id_int = int(room_id)
|
434
|
-
elif (
|
435
|
-
"metaData" in segment and "segmentId" in segment["metaData"]
|
436
|
-
):
|
437
|
-
# Handle Hypfer format
|
438
|
-
room_id = segment["metaData"]["segmentId"]
|
439
|
-
room_id_int = int(room_id)
|
440
|
-
else:
|
441
|
-
# Skip segments without ID
|
442
|
-
continue
|
443
|
-
|
444
|
-
# Skip if room is disabled
|
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
|
-
):
|
451
|
-
continue
|
452
|
-
|
453
|
-
# Room element code was already retrieved above
|
454
|
-
if room_element is not None:
|
455
|
-
# Process pixels for this room
|
456
|
-
if "pixels" in segment and segment["pixels"]:
|
457
|
-
# Regular pixel format
|
458
|
-
for x, y, z in segment["pixels"]:
|
459
|
-
# Calculate pixel coordinates
|
460
|
-
col = x * pixel_size
|
461
|
-
row = y * pixel_size
|
462
|
-
|
463
|
-
# Fill the element map with room code
|
464
|
-
for i in range(z):
|
465
|
-
# Get the region to update
|
466
|
-
region_col_start = col + i * pixel_size
|
467
|
-
region_col_end = col + (i + 1) * pixel_size
|
468
|
-
region_row_start = row
|
469
|
-
region_row_end = row + pixel_size
|
470
|
-
|
471
|
-
# Update element map for this region
|
472
|
-
if (
|
473
|
-
region_row_start < size_y
|
474
|
-
and region_col_start < size_x
|
475
|
-
):
|
476
|
-
# Ensure we stay within bounds
|
477
|
-
end_row = min(region_row_end, size_y)
|
478
|
-
end_col = min(region_col_end, size_x)
|
479
|
-
|
480
|
-
# Set element code for this region
|
481
|
-
# Only set pixels that are not already set (floor is 1)
|
482
|
-
region = self.element_map[
|
483
|
-
region_row_start:end_row,
|
484
|
-
region_col_start:end_col,
|
485
|
-
]
|
486
|
-
mask = region == DrawableElement.FLOOR
|
487
|
-
region[mask] = room_element
|
488
|
-
|
489
|
-
elif (
|
490
|
-
"compressedPixels" in segment
|
491
|
-
and segment["compressedPixels"]
|
492
|
-
):
|
493
|
-
# Compressed pixel format (used in Valetudo)
|
494
|
-
compressed_pixels = segment["compressedPixels"]
|
495
|
-
i = 0
|
496
|
-
pixel_count = 0
|
497
|
-
|
498
|
-
while i < len(compressed_pixels):
|
499
|
-
x = compressed_pixels[i]
|
500
|
-
y = compressed_pixels[i + 1]
|
501
|
-
count = compressed_pixels[i + 2]
|
502
|
-
pixel_count += count
|
503
|
-
|
504
|
-
# Set element code for this run of pixels
|
505
|
-
for j in range(count):
|
506
|
-
px = x + j
|
507
|
-
if 0 <= px < size_x and 0 <= y < size_y:
|
508
|
-
self.element_map[y, px] = room_element
|
509
|
-
|
510
|
-
i += 3
|
511
|
-
|
512
|
-
# Debug: Log that we're adding room 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
|
-
)
|
519
|
-
|
520
|
-
# Process walls
|
521
|
-
elif layer_type == "wall":
|
522
|
-
# Skip if walls are disabled
|
523
|
-
if not self.drawing_config.is_enabled(DrawableElement.WALL):
|
524
|
-
continue
|
525
|
-
|
526
|
-
# Process wall pixels
|
527
|
-
if "pixels" in layer and layer["pixels"]:
|
528
|
-
# Regular pixel format
|
529
|
-
for x, y, z in layer["pixels"]:
|
530
|
-
# Calculate pixel coordinates
|
531
|
-
col = x * pixel_size
|
532
|
-
row = y * pixel_size
|
533
|
-
|
534
|
-
# Fill the element map with wall code
|
535
|
-
for i in range(z):
|
536
|
-
# Get the region to update
|
537
|
-
region_col_start = col + i * pixel_size
|
538
|
-
region_col_end = col + (i + 1) * pixel_size
|
539
|
-
region_row_start = row
|
540
|
-
region_row_end = row + pixel_size
|
541
|
-
|
542
|
-
# Update element map for this region
|
543
|
-
if (
|
544
|
-
region_row_start < size_y
|
545
|
-
and region_col_start < size_x
|
546
|
-
):
|
547
|
-
# Ensure we stay within bounds
|
548
|
-
end_row = min(region_row_end, size_y)
|
549
|
-
end_col = min(region_col_end, size_x)
|
550
|
-
|
551
|
-
# Set element code for this region
|
552
|
-
# Only set pixels that are not already set (floor is 1)
|
553
|
-
region = self.element_map[
|
554
|
-
region_row_start:end_row,
|
555
|
-
region_col_start:end_col,
|
556
|
-
]
|
557
|
-
mask = region == DrawableElement.FLOOR
|
558
|
-
region[mask] = DrawableElement.WALL
|
559
|
-
|
560
|
-
elif "compressedPixels" in layer and layer["compressedPixels"]:
|
561
|
-
# Compressed pixel format (used in Valetudo)
|
562
|
-
compressed_pixels = layer["compressedPixels"]
|
563
|
-
i = 0
|
564
|
-
pixel_count = 0
|
565
|
-
|
566
|
-
while i < len(compressed_pixels):
|
567
|
-
x = compressed_pixels[i]
|
568
|
-
y = compressed_pixels[i + 1]
|
569
|
-
count = compressed_pixels[i + 2]
|
570
|
-
pixel_count += count
|
571
|
-
|
572
|
-
# Set element code for this run of pixels
|
573
|
-
for j in range(count):
|
574
|
-
px = x + j
|
575
|
-
if 0 <= px < size_x and 0 <= y < size_y:
|
576
|
-
self.element_map[y, px] = DrawableElement.WALL
|
577
|
-
|
578
|
-
i += 3
|
579
|
-
|
580
|
-
# Debug: Log that we're adding wall 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
|
-
)
|
587
|
-
|
588
|
-
elif is_rand256:
|
589
|
-
# Get map dimensions from the Rand256 JSON data
|
590
|
-
map_data = json_data["map_data"]
|
591
|
-
size_x = map_data["dimensions"]["width"]
|
592
|
-
size_y = map_data["dimensions"]["height"]
|
593
|
-
|
594
|
-
# Create empty element map initialized with floor
|
595
|
-
self.element_map = np.zeros((size_y, size_x), dtype=np.int32)
|
596
|
-
self.element_map[:] = DrawableElement.FLOOR
|
597
|
-
|
598
|
-
# Process rooms
|
599
|
-
if "rooms" in map_data and map_data["rooms"]:
|
600
|
-
for room in map_data["rooms"]:
|
601
|
-
# Get room ID and check if it's enabled
|
602
|
-
room_id_int = room["id"]
|
603
|
-
|
604
|
-
# Skip if room is disabled
|
605
|
-
if not self.drawing_config.is_enabled(f"ROOM_{room_id_int}"):
|
606
|
-
continue
|
607
|
-
|
608
|
-
# Get room element code (ROOM_1, ROOM_2, etc.)
|
609
|
-
room_element = None
|
610
|
-
if 0 < room_id_int <= 15:
|
611
|
-
room_element = getattr(
|
612
|
-
DrawableElement, f"ROOM_{room_id_int}", None
|
613
|
-
)
|
614
|
-
|
615
|
-
if room_element is not None and "coordinates" in room:
|
616
|
-
# Process coordinates for this room
|
617
|
-
for coord in room["coordinates"]:
|
618
|
-
x, y = coord
|
619
|
-
# Update element map for this pixel
|
620
|
-
if 0 <= y < size_y and 0 <= x < size_x:
|
621
|
-
self.element_map[y, x] = room_element
|
622
|
-
|
623
|
-
# Process segments (alternative format for rooms)
|
624
|
-
if "segments" in map_data and map_data["segments"]:
|
625
|
-
for segment_id, coordinates in map_data["segments"].items():
|
626
|
-
# Get room ID and check if it's enabled
|
627
|
-
room_id_int = int(segment_id)
|
628
|
-
|
629
|
-
# Skip if room is disabled
|
630
|
-
if not self.drawing_config.is_enabled(f"ROOM_{room_id_int}"):
|
631
|
-
continue
|
632
|
-
|
633
|
-
# Get room element code (ROOM_1, ROOM_2, etc.)
|
634
|
-
room_element = None
|
635
|
-
if 0 < room_id_int <= 15:
|
636
|
-
room_element = getattr(
|
637
|
-
DrawableElement, f"ROOM_{room_id_int}", None
|
638
|
-
)
|
639
|
-
|
640
|
-
if room_element is not None and coordinates:
|
641
|
-
# Process individual coordinates
|
642
|
-
for coord in coordinates:
|
643
|
-
if isinstance(coord, (list, tuple)) and len(coord) == 2:
|
644
|
-
x, y = coord
|
645
|
-
# Update element map for this pixel
|
646
|
-
if 0 <= y < size_y and 0 <= x < size_x:
|
647
|
-
self.element_map[y, x] = room_element
|
648
|
-
|
649
|
-
# Process walls
|
650
|
-
if "walls" in map_data and map_data["walls"]:
|
651
|
-
# Skip if walls are disabled
|
652
|
-
if self.drawing_config.is_element_enabled("WALL"):
|
653
|
-
# Process wall coordinates
|
654
|
-
for coord in map_data["walls"]:
|
655
|
-
x, y = coord
|
656
|
-
# Update element map for this pixel
|
657
|
-
if 0 <= y < size_y and 0 <= x < size_x:
|
658
|
-
self.element_map[y, x] = DrawableElement.WALL
|
659
|
-
|
660
|
-
# Find the bounding box of non-zero elements to crop the element map
|
661
|
-
non_zero_indices = np.nonzero(self.element_map)
|
662
|
-
if len(non_zero_indices[0]) > 0: # If there are any non-zero elements
|
663
|
-
# Get the bounding box coordinates
|
664
|
-
min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0])
|
665
|
-
min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1])
|
666
|
-
|
667
|
-
# Add a margin around the bounding box
|
668
|
-
margin = 20 # Pixels of margin around the non-zero elements
|
669
|
-
crop_min_y = max(0, min_y - margin)
|
670
|
-
crop_max_y = min(self.element_map.shape[0] - 1, max_y + margin)
|
671
|
-
crop_min_x = max(0, min_x - margin)
|
672
|
-
crop_max_x = min(self.element_map.shape[1] - 1, max_x + margin)
|
673
|
-
|
674
|
-
# Log the cropping information
|
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
|
-
)
|
682
|
-
|
683
|
-
# Create a new, smaller array with just the non-zero region
|
684
|
-
cropped_map = self.element_map[
|
685
|
-
crop_min_y : crop_max_y + 1, crop_min_x : crop_max_x + 1
|
686
|
-
].copy()
|
687
|
-
|
688
|
-
# Store the cropping coordinates in the shared data for later reference
|
689
|
-
if self.shared:
|
690
|
-
self.shared.element_map_crop = {
|
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,
|
696
|
-
}
|
697
|
-
|
698
|
-
# Replace the element map with the cropped version
|
699
|
-
self.element_map = cropped_map
|
700
|
-
|
701
|
-
# Now resize the element map to reduce its dimensions
|
702
|
-
# Calculate the resize factor based on the current size
|
703
|
-
resize_factor = 5 # Reduce to 1/5 of the current 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
|
710
|
-
|
711
|
-
# Create a resized element map
|
712
|
-
resized_map = np.zeros((new_height, new_width), dtype=np.int32)
|
713
|
-
resized_map[:] = DrawableElement.FLOOR # Initialize with floor
|
714
|
-
|
715
|
-
# Calculate scaling factors
|
716
|
-
y_scale = self.element_map.shape[0] / new_height
|
717
|
-
x_scale = self.element_map.shape[1] / new_width
|
718
|
-
|
719
|
-
# Populate the resized map by sampling from the original map
|
720
|
-
for y in range(new_height):
|
721
|
-
for x in range(new_width):
|
722
|
-
# Get the corresponding position in the original map
|
723
|
-
orig_y = min(int(y * y_scale), self.element_map.shape[0] - 1)
|
724
|
-
orig_x = min(int(x * x_scale), self.element_map.shape[1] - 1)
|
725
|
-
# Copy the element code
|
726
|
-
resized_map[y, x] = self.element_map[orig_y, orig_x]
|
727
|
-
|
728
|
-
# Store the resize information in shared data
|
729
|
-
if self.shared:
|
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
|
-
)
|
736
|
-
|
737
|
-
# Log the resizing information
|
738
|
-
LOGGER.info(
|
739
|
-
f"Resized element map from {self.element_map.shape} to {resized_map.shape} (1/{resize_factor} of cropped size)"
|
740
|
-
)
|
741
|
-
|
742
|
-
# Replace the element map with the resized version
|
743
|
-
self.element_map = resized_map
|
744
|
-
|
745
|
-
return self.element_map
|
746
|
-
|
747
|
-
def get_element_map(self):
|
748
|
-
"""Return the element map.
|
749
|
-
|
750
|
-
Returns:
|
751
|
-
numpy.ndarray: The 2D element map array or None if not initialized
|
752
|
-
"""
|
753
|
-
return self.element_map
|
754
|
-
|
755
|
-
def get_element_at_image_position(self, x: int, y: int):
|
756
|
-
"""Get the element code at the specified position in the image.
|
757
|
-
|
758
|
-
This method uses calibration points to accurately map between image coordinates
|
759
|
-
and element map coordinates.
|
760
|
-
|
761
|
-
Args:
|
762
|
-
x: X coordinate in the image (e.g., 0-1984)
|
763
|
-
y: Y coordinate in the image (e.g., 0-1824)
|
764
|
-
|
765
|
-
Returns:
|
766
|
-
Element code at the specified position, or None if out of bounds
|
767
|
-
"""
|
768
|
-
if self.shared is None or self.element_map is None:
|
769
|
-
return None
|
770
|
-
|
771
|
-
# Get calibration points if available
|
772
|
-
calibration_points = None
|
773
|
-
if hasattr(self.shared, "attr_calibration_points"):
|
774
|
-
calibration_points = self.shared.attr_calibration_points
|
775
|
-
|
776
|
-
if calibration_points and len(calibration_points) >= 4:
|
777
|
-
# Extract image and vacuum coordinates from calibration points
|
778
|
-
image_points = []
|
779
|
-
vacuum_points = []
|
780
|
-
for point in calibration_points:
|
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"]))
|
784
|
-
|
785
|
-
if len(image_points) >= 2:
|
786
|
-
# Calculate scaling factors
|
787
|
-
img_x_min = min(p[0] for p in image_points)
|
788
|
-
img_x_max = max(p[0] for p in image_points)
|
789
|
-
img_y_min = min(p[1] for p in image_points)
|
790
|
-
img_y_max = max(p[1] for p in image_points)
|
791
|
-
|
792
|
-
vac_x_min = min(p[0] for p in vacuum_points)
|
793
|
-
vac_x_max = max(p[0] for p in vacuum_points)
|
794
|
-
vac_y_min = min(p[1] for p in vacuum_points)
|
795
|
-
vac_y_max = max(p[1] for p in vacuum_points)
|
796
|
-
|
797
|
-
# Normalize the input coordinates to 0-1 range in image space
|
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
|
-
)
|
808
|
-
|
809
|
-
# Map to vacuum coordinates
|
810
|
-
vac_x = vac_x_min + norm_x * (vac_x_max - vac_x_min)
|
811
|
-
vac_y = vac_y_min + norm_y * (vac_y_max - vac_y_min)
|
812
|
-
|
813
|
-
LOGGER.debug(
|
814
|
-
f"Mapped image ({x}, {y}) to vacuum ({vac_x:.1f}, {vac_y:.1f})"
|
815
|
-
)
|
816
|
-
|
817
|
-
# Now map from vacuum coordinates to element map coordinates
|
818
|
-
# This depends on how the element map was created
|
819
|
-
if (
|
820
|
-
hasattr(self.shared, "element_map_crop")
|
821
|
-
and self.shared.element_map_crop
|
822
|
-
):
|
823
|
-
crop_info = self.shared.element_map_crop
|
824
|
-
|
825
|
-
# Adjust for cropping
|
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"])
|
829
|
-
|
830
|
-
# Adjust for resizing
|
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"]
|
838
|
-
|
839
|
-
# Scale to resized coordinates
|
840
|
-
elem_x = int(elem_x * resized_w / orig_w)
|
841
|
-
elem_y = int(elem_y * resized_h / orig_h)
|
842
|
-
|
843
|
-
LOGGER.debug(
|
844
|
-
f"Mapped vacuum ({vac_x:.1f}, {vac_y:.1f}) to element map ({elem_x}, {elem_y})"
|
845
|
-
)
|
846
|
-
|
847
|
-
# Check bounds and return element
|
848
|
-
height, width = self.element_map.shape
|
849
|
-
if 0 <= elem_y < height and 0 <= elem_x < width:
|
850
|
-
return self.element_map[elem_y, elem_x]
|
851
|
-
|
852
|
-
# Fallback to the simpler method if calibration points aren't available
|
853
|
-
return self.get_element_at_position(x, y, is_image_coords=True)
|
854
|
-
|
855
|
-
def get_element_name(self, element_code):
|
856
|
-
"""Get the name of the element from its code.
|
857
|
-
|
858
|
-
Args:
|
859
|
-
element_code: The element code (e.g., 1, 2, 101, etc.)
|
860
|
-
|
861
|
-
Returns:
|
862
|
-
The name of the element (e.g., 'FLOOR', 'WALL', 'ROOM_1', etc.)
|
863
|
-
"""
|
864
|
-
if element_code is None:
|
865
|
-
return "NONE"
|
866
|
-
|
867
|
-
# Check if it's a room
|
868
|
-
if element_code >= 100:
|
869
|
-
room_number = element_code - 100
|
870
|
-
return f"ROOM_{room_number}"
|
871
|
-
|
872
|
-
# Check standard elements
|
873
|
-
for name, code in vars(DrawableElement).items():
|
874
|
-
if (
|
875
|
-
not name.startswith("_")
|
876
|
-
and isinstance(code, int)
|
877
|
-
and code == element_code
|
878
|
-
):
|
879
|
-
return name
|
880
|
-
|
881
|
-
return f"UNKNOWN_{element_code}"
|
882
|
-
|
883
|
-
def get_element_at_position(self, x: int, y: int, is_image_coords: bool = False):
|
884
|
-
"""Get the element code at the specified position in the element map.
|
885
|
-
|
886
|
-
Args:
|
887
|
-
x: X coordinate in the original (uncropped) element map or image
|
888
|
-
y: Y coordinate in the original (uncropped) element map or image
|
889
|
-
is_image_coords: If True, x and y are image coordinates (e.g., 1984x1824)
|
890
|
-
If False, x and y are element map coordinates
|
891
|
-
|
892
|
-
Returns:
|
893
|
-
Element code at the specified position, or None if out of bounds
|
894
|
-
"""
|
895
|
-
if self.element_map is None:
|
896
|
-
return None
|
897
|
-
|
898
|
-
# If coordinates are from the image, convert them to element map coordinates first
|
899
|
-
if is_image_coords and self.shared:
|
900
|
-
# Get image dimensions
|
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
|
-
):
|
906
|
-
image_width = self.shared.image_size[0]
|
907
|
-
image_height = self.shared.image_size[1]
|
908
|
-
else:
|
909
|
-
# Default image dimensions if not available
|
910
|
-
image_width = 1984
|
911
|
-
image_height = 1824
|
912
|
-
|
913
|
-
# Get original element map dimensions (before resizing)
|
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
|
-
]
|
922
|
-
else:
|
923
|
-
# Estimate based on typical values
|
924
|
-
original_map_width = 1310
|
925
|
-
original_map_height = 1310
|
926
|
-
|
927
|
-
# Calculate scaling factors between image and original element map
|
928
|
-
x_scale_to_map = original_map_width / image_width
|
929
|
-
y_scale_to_map = original_map_height / image_height
|
930
|
-
|
931
|
-
# Convert image coordinates to element map coordinates
|
932
|
-
# Apply a small offset to better align with the actual elements
|
933
|
-
# This is based on empirical testing with the sample coordinates
|
934
|
-
x_offset = 50 # Adjust as needed based on testing
|
935
|
-
y_offset = 20 # Adjust as needed based on testing
|
936
|
-
x = int((x + x_offset) * x_scale_to_map)
|
937
|
-
y = int((y + y_offset) * y_scale_to_map)
|
938
|
-
|
939
|
-
LOGGER.debug(
|
940
|
-
f"Converted image coordinates ({x}, {y}) to element map coordinates"
|
941
|
-
)
|
942
|
-
|
943
|
-
# Adjust coordinates if the element map has been cropped and resized
|
944
|
-
if self.shared and hasattr(self.shared, "element_map_crop"):
|
945
|
-
# Get the crop information
|
946
|
-
crop_info = self.shared.element_map_crop
|
947
|
-
|
948
|
-
# Adjust coordinates to the cropped map
|
949
|
-
x_cropped = x - crop_info["min_x"]
|
950
|
-
y_cropped = y - crop_info["min_y"]
|
951
|
-
|
952
|
-
# If the map has been resized, adjust coordinates further
|
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")
|
957
|
-
|
958
|
-
if original_cropped_shape and resized_shape:
|
959
|
-
# Calculate scaling factors
|
960
|
-
y_scale = original_cropped_shape[0] / resized_shape[0]
|
961
|
-
x_scale = original_cropped_shape[1] / resized_shape[1]
|
962
|
-
|
963
|
-
# Scale the coordinates to the resized map
|
964
|
-
x_resized = int(x_cropped / x_scale)
|
965
|
-
y_resized = int(y_cropped / y_scale)
|
966
|
-
|
967
|
-
# Check if the coordinates are within the resized map
|
968
|
-
height, width = self.element_map.shape
|
969
|
-
if 0 <= y_resized < height and 0 <= x_resized < width:
|
970
|
-
return self.element_map[y_resized, x_resized]
|
971
|
-
return None
|
972
|
-
|
973
|
-
# If no resizing or missing resize info, use cropped coordinates
|
974
|
-
height, width = self.element_map.shape
|
975
|
-
if 0 <= y_cropped < height and 0 <= x_cropped < width:
|
976
|
-
return self.element_map[y_cropped, x_cropped]
|
977
|
-
return None
|
978
|
-
else:
|
979
|
-
# No cropping, use coordinates as is
|
980
|
-
height, width = self.element_map.shape
|
981
|
-
if 0 <= y < height and 0 <= x < width:
|
982
|
-
return self.element_map[y, x]
|
983
|
-
return None
|