From 4b3b100d633e053862d1209c461a120ac9b49292 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 28 Apr 2026 15:01:32 +0000 Subject: [PATCH] =?UTF-8?q?Parse=20Magic=20Plan=20XML=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/tests/__init__.py | 0 backend/magic_plan/tests/test_xml_parser.py | 480 ++++++++++++++++++++ backend/magic_plan/xml_parser.py | 5 + 3 files changed, 485 insertions(+) create mode 100644 backend/magic_plan/tests/__init__.py create mode 100644 backend/magic_plan/tests/test_xml_parser.py create mode 100644 backend/magic_plan/xml_parser.py diff --git a/backend/magic_plan/tests/__init__.py b/backend/magic_plan/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/magic_plan/tests/test_xml_parser.py b/backend/magic_plan/tests/test_xml_parser.py new file mode 100644 index 00000000..2dcf6b63 --- /dev/null +++ b/backend/magic_plan/tests/test_xml_parser.py @@ -0,0 +1,480 @@ +import json +import xml.etree.ElementTree as ET +from pathlib import Path +import pytest + +from backend.magic_plan.models import ( + MagicPlanDoor, + MagicPlanExploded, + MagicPlanExplodedOpening, + MagicPlanExplodedWall, + MagicPlanFloor, + MagicPlanFurniture, + MagicPlanMainDimension, + MagicPlanPlan, + MagicPlanRoom, + MagicPlanRoomPoint, + MagicPlanSymbolInstance, + MagicPlanWallPoint, + MagicPlanWindow, +) +from backend.magic_plan.xml_parser import parse_magicplan_xml + +FIXTURES = Path(__file__).parent.parent + + +def _load(filename: str) -> str: + content = (FIXTURES / filename).read_text().strip() + try: + ET.fromstring(content) + return content + except ET.ParseError: + return json.loads(content) + + +@pytest.fixture(scope="module") +def plan1() -> MagicPlanPlan: + return parse_magicplan_xml(_load("xml_example.xml")) + + +@pytest.fixture(scope="module") +def plan2() -> MagicPlanPlan: + return parse_magicplan_xml(_load("xml_example_2.xml")) + + +@pytest.fixture(scope="module") +def plan3() -> MagicPlanPlan: + return parse_magicplan_xml(_load("xml_example_3.xml")) + + +# --------------------------------------------------------------------------- +# Plan-level attributes +# --------------------------------------------------------------------------- + +class TestPlanAttributes: + + def test_id(self, plan1: MagicPlanPlan): + assert plan1.id == "b66bb427-33fe-4865-8d57-bad7d9d3f2e5" + + def test_uid(self, plan1: MagicPlanPlan): + assert plan1.uid == "69e5fafc.0890b3ff" + + def test_name(self, plan1: MagicPlanPlan): + assert plan1.name == "275 Carr Hill Rd NE9 5ND" + + def test_type(self, plan1: MagicPlanPlan): + assert plan1.type == "0" + + def test_interior_wall_width(self, plan1: MagicPlanPlan): + assert plan1.interior_wall_width == pytest.approx(0.12) + + def test_exterior_wall_width(self, plan1: MagicPlanPlan): + assert plan1.exterior_wall_width == pytest.approx(0.25) + + def test_schematic_false(self, plan1: MagicPlanPlan): + assert plan1.schematic is False + + def test_has_land_survey_address_false(self, plan1: MagicPlanPlan): + assert plan1.has_land_survey_address is False + + def test_last_patch_identifier(self, plan1: MagicPlanPlan): + assert plan1.last_patch_identifier == "0" + + def test_last_roll_identifier(self, plan1: MagicPlanPlan): + assert plan1.last_roll_identifier == "0" + + +class TestPlanValues: + + def test_date(self, plan1: MagicPlanPlan): + assert plan1.values["date"] == "2026-04-20" + + def test_statistics_area_of_height(self, plan1: MagicPlanPlan): + assert plan1.values["statistics.areaOfHeight"] == "2.134" + + def test_statistics_basement_account(self, plan1: MagicPlanPlan): + assert plan1.values["statistics.basement.account"] == "100" + + def test_statistics_exterior_walls(self, plan1: MagicPlanPlan): + assert plan1.values["statistics.exteriorWalls"] == "0" + + def test_statistics_interior_walls(self, plan1: MagicPlanPlan): + assert plan1.values["statistics.interiorWalls"] == "0" + + def test_plan2_date(self, plan2: MagicPlanPlan): + assert plan2.values["date"] == "2026-04-16" + + +# --------------------------------------------------------------------------- +# Floors +# --------------------------------------------------------------------------- + +class TestFloors: + + def test_floor_count_plan1(self, plan1: MagicPlanPlan): + assert len(plan1.floors) == 2 + + def test_floor_count_plan2(self, plan2: MagicPlanPlan): + assert len(plan2.floors) == 1 + + def test_floor_count_plan3(self, plan3: MagicPlanPlan): + assert len(plan3.floors) == 1 + + def test_ground_floor_name(self, plan1: MagicPlanPlan): + assert plan1.floors[0].name == "Ground Floor" + + def test_first_floor_name(self, plan1: MagicPlanPlan): + assert plan1.floors[1].name == "1st Floor" + + def test_ground_floor_type(self, plan1: MagicPlanPlan): + assert plan1.floors[0].floor_type == "0" + + def test_upper_floor_type(self, plan1: MagicPlanPlan): + assert plan1.floors[1].floor_type == "1" + + def test_floor_uid(self, plan1: MagicPlanPlan): + assert plan1.floors[0].uid == "69e5fb20.4feef7ff" + + def test_ground_floor_area_without_walls(self, plan1: MagicPlanPlan): + assert plan1.floors[0].area_without_walls == pytest.approx(40.20736) + + def test_ground_floor_area_with_walls(self, plan1: MagicPlanPlan): + assert plan1.floors[0].area_with_walls == pytest.approx(48.40593) + + def test_ground_floor_area_with_interior_walls_only(self, plan1: MagicPlanPlan): + assert plan1.floors[0].area_with_interior_walls_only == pytest.approx(40.67878) + + def test_floor_rotation(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rotation == pytest.approx(0.0) + + def test_floor_compass_angle(self, plan1: MagicPlanPlan): + assert plan1.floors[0].compass_angle == pytest.approx(-1.0) + + +# --------------------------------------------------------------------------- +# Symbol instance +# --------------------------------------------------------------------------- + +class TestSymbolInstance: + + def test_symbol_instance_id(self, plan1: MagicPlanPlan): + assert plan1.floors[0].symbol_instance.id == "floor" + + def test_symbol_instance_uid(self, plan1: MagicPlanPlan): + assert plan1.floors[0].symbol_instance.uid == "69e5fb20.4feef7ff" + + def test_symbol_instance_symbol(self, plan1: MagicPlanPlan): + assert plan1.floors[0].symbol_instance.symbol == "floor" + + def test_symbol_instance_parent_uid(self, plan1: MagicPlanPlan): + assert plan1.floors[0].symbol_instance.parent_uid == "" + + def test_symbol_instance_ceiling_height_value(self, plan1: MagicPlanPlan): + assert plan1.floors[0].symbol_instance.values["ceilingHeight"] == "2.323164" + + +# --------------------------------------------------------------------------- +# Rooms +# --------------------------------------------------------------------------- + +class TestRooms: + + def test_ground_floor_room_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms) == 2 + + def test_first_floor_room_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[1].rooms) == 4 + + def test_plan3_room_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].rooms) == 9 + + def test_room_type(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].type == "Kitchen" + + def test_room_uid(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].uid == "69e5fbc8.71027bff" + + def test_room_area(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].area == pytest.approx(10.78332) + + def test_room_perimeter(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].perimeter == pytest.approx(13.32812) + + def test_room_x(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].x == pytest.approx(3.80616) + + def test_room_y(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].y == pytest.approx(0.23162) + + def test_room_rotation(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].rotation == pytest.approx(0.0) + + def test_room_was_modified_false(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].was_modified is False + + def test_room_was_modified_true(self, plan1: MagicPlanPlan): + # Closet on 1st floor has wasModified=1 + closet = plan1.floors[1].rooms[1] + assert closet.type == "Closet" + assert closet.was_modified is True + + def test_room_linked_room_0(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].linked_room_0 == "-1" + + def test_room_linked_room_1(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].linked_room_1 == "-1" + + def test_room_ceiling_height_value(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].values["ceilingHeight"] == "2.323164" + + def test_room_label_value(self, plan2: MagicPlanPlan): + dining = plan2.floors[0].rooms[1] + assert dining.type == "Dining Room" + assert dining.values["label"] == "Room" + + def test_room_no_label_when_absent(self, plan1: MagicPlanPlan): + assert "label" not in plan1.floors[0].rooms[0].values + + +# --------------------------------------------------------------------------- +# Room points +# --------------------------------------------------------------------------- + +class TestRoomPoints: + + def test_kitchen_point_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[0].points) == 4 + + def test_living_room_point_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[1].points) == 8 + + def test_point_snapped_x(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].points[0].snapped_x == pytest.approx(-1.44357) + + def test_point_snapped_y(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].points[0].snapped_y == pytest.approx(-2.00846) + + def test_point_height(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].points[0].height == pytest.approx(2.323164) + + def test_point_uid(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].points[0].uid == "69e5fbc8.710363ff" + + def test_point_values_empty_when_absent(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].points[0].values == {} + + +# --------------------------------------------------------------------------- +# Doors (floorRoom context) +# --------------------------------------------------------------------------- + +class TestDoors: + + def test_kitchen_door_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[0].doors) == 5 + + def test_corridor_door_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].rooms[0].doors) == 9 + + def test_door_wall_point_index(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].wall_point_index == 3 + + def test_door_type(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].type == "4" + + def test_door_u(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].u == pytest.approx(0.47585) + + def test_door_snapped_width(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].snapped_width == pytest.approx(1.82394) + + def test_door_snapped_height(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].snapped_height == pytest.approx(2.08411) + + def test_door_snapped_orientation(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].snapped_orientation == 3 + + def test_door_twin_wall_item_uid_present(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].twin_wall_item_uid == "69e5fbc8.74614fff" + + def test_door_twin_wall_item_uid_absent(self, plan2: MagicPlanPlan): + # Example 2 doors have no twinWallItemUid + assert plan2.floors[0].rooms[0].doors[0].twin_wall_item_uid is None + + def test_door_symbol_instance(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].symbol_instance == "W-0-0" + + def test_door_inset_x(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].doors[0].inset_x == pytest.approx(0.0) + + +# --------------------------------------------------------------------------- +# Windows (floorRoom context) +# --------------------------------------------------------------------------- + +class TestWindows: + + def test_kitchen_window_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[0].windows) == 1 + + def test_corridor_window_count_zero(self, plan3: MagicPlanPlan): + corridor = plan3.floors[0].rooms[0] + assert corridor.type == "Corridor" + assert len(corridor.windows) == 0 + + def test_window_wall_point_index(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].wall_point_index == 1 + + def test_window_type(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].type == "1" + + def test_window_u(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].u == pytest.approx(0.68463) + + def test_window_snapped_width(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].snapped_width == pytest.approx(1.71972) + + def test_window_snapped_height(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].snapped_height == pytest.approx(0.94940) + + def test_window_symbol_instance(self, plan1: MagicPlanPlan): + assert plan1.floors[0].rooms[0].windows[0].symbol_instance == "W-0-2" + + +# --------------------------------------------------------------------------- +# Furniture (room-level) +# --------------------------------------------------------------------------- + +class TestRoomFurniture: + + def test_kitchen_furniture_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[0].furniture) == 5 + + def test_bathroom_furniture_count_zero(self, plan1: MagicPlanPlan): + bathroom = plan1.floors[1].rooms[0] + assert bathroom.type == "Bathroom" + assert len(bathroom.furniture) == 0 + + def test_furniture_type(self, plan1: MagicPlanPlan): + furn = plan1.floors[0].rooms[0].furniture[0] + assert isinstance(furn.type, str) + + def test_furniture_symbol_instance(self, plan1: MagicPlanPlan): + furn = plan1.floors[0].rooms[0].furniture[0] + assert isinstance(furn.symbol_instance, str) + + +# --------------------------------------------------------------------------- +# Floor-level furniture (example 3) +# --------------------------------------------------------------------------- + +class TestFloorLevelFurniture: + + def test_floor_furniture_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].furniture) == 1 + + def test_floor_furniture_x(self, plan3: MagicPlanPlan): + assert plan3.floors[0].furniture[0].x == pytest.approx(-2.09376) + + def test_floor_furniture_y(self, plan3: MagicPlanPlan): + assert plan3.floors[0].furniture[0].y == pytest.approx(3.13664) + + def test_floor_furniture_absent_plan1(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].furniture) == 0 + + +# --------------------------------------------------------------------------- +# Main dimensions +# --------------------------------------------------------------------------- + +class TestMainDimensions: + + def test_main_dimension_count(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].rooms[0].main_dimensions) == 2 + + def test_main_dimension_from_point(self, plan1: MagicPlanPlan): + dim = plan1.floors[0].rooms[0].main_dimensions[0] + assert isinstance(dim.from_point, int) + + def test_main_dimension_is_set(self, plan1: MagicPlanPlan): + dim = plan1.floors[0].rooms[0].main_dimensions[0] + assert isinstance(dim.is_set, bool) + + +# --------------------------------------------------------------------------- +# Exploded section +# --------------------------------------------------------------------------- + +class TestExploded: + + def test_exploded_wall_count_ground_floor(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].exploded.walls) == 11 + + def test_exploded_door_count_ground_floor(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].exploded.doors) == 6 + + def test_exploded_window_count_ground_floor(self, plan1: MagicPlanPlan): + assert len(plan1.floors[0].exploded.windows) == 2 + + def test_exploded_wall_type(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.walls[0].wall_type == "exterior" + + def test_exploded_wall_points(self, plan1: MagicPlanPlan): + pts = plan1.floors[0].exploded.walls[0].points + assert len(pts) == 2 + assert pts[0].x == pytest.approx(2.363) + assert pts[0].y == pytest.approx(-1.778) + assert pts[0].height == pytest.approx(2.323164) + + def test_exploded_door_x1(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.doors[0].x1 == pytest.approx(5.24973) + + def test_exploded_door_y1(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.doors[0].y1 == pytest.approx(-1.333153) + + def test_exploded_door_width(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.doors[0].width == pytest.approx(0.773) + + def test_exploded_door_height(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.doors[0].height == pytest.approx(2.02225) + + def test_exploded_door_symbol_instance(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.doors[0].symbol_instance == "W-0-1" + + def test_exploded_window_x1(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.windows[0].x1 == pytest.approx(5.24973) + + def test_exploded_window_width(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.windows[0].width == pytest.approx(1.71972) + + def test_exploded_window_height(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.windows[0].height == pytest.approx(0.949396) + + def test_exploded_window_symbol_instance(self, plan1: MagicPlanPlan): + assert plan1.floors[0].exploded.windows[0].symbol_instance == "W-0-2" + + def test_plan3_exploded_wall_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].exploded.walls) == 33 + + def test_plan3_exploded_door_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].exploded.doors) == 12 + + def test_plan3_exploded_window_count(self, plan3: MagicPlanPlan): + assert len(plan3.floors[0].exploded.windows) == 5 + + +# --------------------------------------------------------------------------- +# Interior room points +# --------------------------------------------------------------------------- + +class TestInteriorRoomPoints: + + def test_interior_room_floor_count_plan1(self, plan1: MagicPlanPlan): + assert len(plan1.interior_room_floors) == 2 + + def test_interior_room_floor_count_plan2(self, plan2: MagicPlanPlan): + assert len(plan2.interior_room_floors) == 1 + + def test_interior_room_floor_uid_matches(self, plan1: MagicPlanPlan): + # interiorRoomPoints floors share uids with the main floors + assert plan1.interior_room_floors[0].uid == plan1.floors[0].uid diff --git a/backend/magic_plan/xml_parser.py b/backend/magic_plan/xml_parser.py new file mode 100644 index 00000000..13d30cdd --- /dev/null +++ b/backend/magic_plan/xml_parser.py @@ -0,0 +1,5 @@ +from backend.magic_plan.models import MagicPlanPlan + + +def parse_magicplan_xml(xml_str: str) -> MagicPlanPlan: + raise NotImplementedError