Create a gui that creates network configs by having the network components and spatial data represented by geometries and let the user move them around in order to create an example of what the network's layout should look like and be based upon, here's some code to start with. Use real world logic keeping in mind that these lines need to be tenched.
import sys
import json
from PyQt6 import QtWidgets, QtCore, QtGui

# Define colors for different component types for better visual distinction
COMPONENT_COLORS = {
    "erven": QtGui.QColor(0, 255, 0),             # Green
    "centroids": QtGui.QColor(0, 0, 255),         # Blue
    "boundary_boxes": QtGui.QColor(255, 165, 0),  # Orange
    "muc350": QtGui.QColor(128, 0, 128),          # Purple
    "splitters": QtGui.QColor(255, 0, 255),       # Magenta
    "central_node": QtGui.QColor(200, 200, 200),  # Light Gray
    "road": QtGui.QColor(255, 255, 255),          # White
    "road_crossing": QtGui.QColor(139, 69, 19),    # Brown
    "drop_cable": QtGui.QColor(255, 0, 0),        # Red
    "link_cable": QtGui.QColor(0, 0, 255)         # Blue
}

# Define different pen styles for cables
CABLE_STYLES = {
    "drop_cable": QtCore.Qt.PenStyle.DashLine,
    "link_cable": QtCore.Qt.PenStyle.SolidLine
}

# Path to component icons (ensure these paths are correct or adjust accordingly)
COMPONENT_ICONS = {
    "erven": "icons/erven.png",
    "centroids": "icons/centroid.png",
    "boundary_boxes": "icons/boundary_box.png",
    "muc350": "icons/muc350.png",
    "splitters": "icons/splitter.png",
    "central_node": "icons/central_node.png"
}

class NetworkComponent(QtWidgets.QGraphicsObject):
    """Represents a network component with a label, icon, and color."""
    
    # Signal emitted when the component is moved
    moved = QtCore.pyqtSignal()
    
    def __init__(self, component_type, label, x, y, width=80, height=40, icon_path=None):
        super().__init__()
        self.component_type = component_type
        self.label = label
        self.width = width
        self.height = height
        self.icon_path = icon_path or COMPONENT_ICONS.get(component_type, None)
        self.setPos(x, y)
        
        # Initialize brush and pen
        self.brush = QtGui.QBrush(COMPONENT_COLORS.get(component_type, QtGui.QColor(200, 200, 200)))
        self.default_brush = self.brush  # To restore after hover
        self.pen = QtGui.QPen(QtGui.QColor(0, 0, 0))  # Black border
        
        # Load icon if available
        if self.icon_path:
            self.icon = QtGui.QPixmap(self.icon_path).scaled(
                self.width - 10, self.height - 10, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation
            )
        else:
            self.icon = None
        
        # Set flags to make the item movable and selectable, and to send geometry change notifications
        self.setFlags(
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable |
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
        )
        self.setAcceptHoverEvents(True)
        self.connections = []  # List to store connected cables
    
    def add_connection(self, cable):
        """Add a cable connection to this component."""
        if cable not in self.connections:
            self.connections.append(cable)
    
    def remove_connection(self, cable):
        """Remove a cable connection from this component."""
        if cable in self.connections:
            self.connections.remove(cable)
    
    def hoverEnterEvent(self, event):
        """Change color on hover."""
        self.brush = QtGui.QBrush(QtGui.QColor(255, 255, 0))  # Yellow
        self.update()  # Trigger a repaint
        super().hoverEnterEvent(event)
    
    def hoverLeaveEvent(self, event):
        """Revert color when not hovered."""
        self.brush = self.default_brush
        self.update()  # Trigger a repaint
        super().hoverLeaveEvent(event)
    
    def contextMenuEvent(self, event):
        """Provide a context menu for actions like Delete or Edit Properties."""
        menu = QtWidgets.QMenu()
        delete_action = menu.addAction("Delete")
        edit_action = menu.addAction("Edit Properties")
        action = menu.exec(event.screenPos())
        if action == delete_action:
            self.delete_component()
        elif action == edit_action:
            self.edit_properties()
    
    def delete_component(self):
        """Delete the component and its connected cables."""
        # Remove all connected cables
        for cable in self.connections.copy():
            cable.delete_cable()
        # Remove the component from the scene
        scene = self.scene()
        if scene:
            scene.removeItem(self)
        # Optionally, emit a signal or perform additional cleanup
    
    def edit_properties(self):
        """Open a dialog to edit component properties."""
        dialog = PropertiesDialog(self)
        if dialog.exec() == QtWidgets.QDialog.DialogCode.Accepted:
            self.label = dialog.name_edit.text()
            self.update()  # Refresh the display
    
    def boundingRect(self):
        """Define the bounding rectangle of the component."""
        return QtCore.QRectF(0, 0, self.width, self.height)
    
    def paint(self, painter, option, widget=None):
        """Custom paint to include the label and icon."""
        # Draw the rectangle
        painter.setBrush(self.brush)
        painter.setPen(self.pen)
        painter.drawRect(self.boundingRect())
    
        # Draw the icon
        if self.icon:
            painter.drawPixmap(5, 5, self.icon)
    
        # Draw the label below the icon
        painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0)))  # Black text
        font = painter.font()
        font.setPointSize(10)
        painter.setFont(font)
        rect = self.boundingRect()
        painter.drawText(QtCore.QRectF(0, self.height - 20, self.width, 20), QtCore.Qt.AlignmentFlag.AlignCenter, self.label)
    
    def itemChange(self, change, value):
        """Override to emit signal when moved and handle snapping."""
        if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
            self.moved.emit()
            self.snap_to_center_of_erven()
        return super().itemChange(change, value)
    
    def snap_to_center_of_erven(self, threshold=50):
        """Snap the centroid to the center of the nearest Erf within a threshold."""
        if self.component_type != "centroids":
            return
        nearest_erven = None
        min_distance = float('inf')
        for erf in self.scene().components["erven"]:
            erf_center = erf.scenePos() + erf.boundingRect().center()
            distance = QtCore.QLineF(self.scenePos() + self.boundingRect().center(), erf_center).length()
            if distance < min_distance:
                min_distance = distance
                nearest_erven = erf
        if nearest_erven and min_distance <= threshold:
            erf_center = nearest_erven.scenePos() + nearest_erven.boundingRect().center()
            new_pos = erf_center - QtCore.QPointF(self.boundingRect().width()/2, self.boundingRect().height()/2)
            self.setPos(new_pos)
    
    def mousePressEvent(self, event):
        """Handle initiating cable creation via mouse drag."""
        if event.button() == QtCore.Qt.MouseButton.LeftButton and self.component_type != "road_crossing":
            self.scene().initiate_cable_creation(self, event.pos())
        super().mousePressEvent(event)

class NetworkCable(QtWidgets.QGraphicsPathItem):
    """Represents a cable connecting two network components."""
    
    def __init__(self, start_item, end_item, cable_type='drop_cable'):
        super().__init__()
        self.start_item = start_item
        self.end_item = end_item
        self.cable_type = cable_type
        
        # Set pen style based on cable type
        pen = QtGui.QPen(COMPONENT_COLORS.get(cable_type, QtGui.QColor(0, 0, 0)), 2)
        pen.setStyle(CABLE_STYLES.get(cable_type, QtCore.Qt.PenStyle.SolidLine))
        self.setPen(pen)
        
        # Enable selection
        self.setFlags(
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
        )
        
        self.update_position()
        
        # Connect signals to update position when components move
        self.start_item.moved.connect(self.update_position)
        self.end_item.moved.connect(self.update_position)
        
        # Add this cable to the components' connections
        self.start_item.add_connection(self)
        self.end_item.add_connection(self)
        
        # Ensure the cable is behind components
        self.setZValue(-1)
        
    def update_position(self):
        """Update the cable's position based on connected components."""
        start_center = self.start_item.scenePos() + self.start_item.boundingRect().center()
        end_center = self.end_item.scenePos() + self.end_item.boundingRect().center()
        
        # Create a path that follows the trenched lines
        path = QtGui.QPainterPath(start_center)
        # Implement routing logic here; for simplicity, direct line
        path.lineTo(end_center)
        self.setPath(path)
    
    def mousePressEvent(self, event):
        """Enable dragging of cable endpoints."""
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self.dragging = True
            self.drag_start_pos = event.scenePos()
            event.accept()
        super().mousePressEvent(event)
    
    def mouseMoveEvent(self, event):
        """Handle dragging of cable endpoints with snapping."""
        if hasattr(self, 'dragging') and self.dragging:
            new_end = event.scenePos()
            # Snap to the nearest component or trench line
            snapped_point = self.scene().get_snapped_position(new_end)
            path = QtGui.QPainterPath(self.start_item.scenePos() + self.start_item.boundingRect().center())
            path.lineTo(snapped_point)
            self.setPath(path)
            event.accept()
        super().mouseMoveEvent(event)
    
    def mouseReleaseEvent(self, event):
        """Finalize dragging of cable endpoints."""
        if hasattr(self, 'dragging') and self.dragging:
            self.dragging = False
            # Find the nearest component to snap to
            snapped_component = self.scene().find_nearest_component(event.scenePos())
            if snapped_component:
                self.end_item.remove_connection(self)
                self.end_item = snapped_component
                self.end_item.add_connection(self)
                self.update_position()
            event.accept()
        super().mouseReleaseEvent(event)
    
    def contextMenuEvent(self, event):
        """Provide a context menu for deleting the cable."""
        menu = QtWidgets.QMenu()
        delete_action = menu.addAction("Delete Cable")
        action = menu.exec(event.screenPos())
        if action == delete_action:
            self.delete_cable()
    
    def delete_cable(self):
        """Delete the cable from the scene and disconnect from components."""
        # Remove connections from components
        self.start_item.remove_connection(self)
        self.end_item.remove_connection(self)
        # Remove the cable from the scene
        scene = self.scene()
        if scene:
            scene.removeItem(self)
        # Optionally, perform additional cleanup

class PropertiesDialog(QtWidgets.QDialog):
    """Dialog to edit properties of a network component."""
    
    def __init__(self, component, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Edit Properties")
        self.component = component
        self.setup_ui()
    
    def setup_ui(self):
        layout = QtWidgets.QFormLayout(self)
        
        # Name Edit
        self.name_edit = QtWidgets.QLineEdit(self)
        self.name_edit.setText(self.component.label)
        layout.addRow("Name:", self.name_edit)
        
        # Component Type (read-only)
        self.type_label = QtWidgets.QLabel(self.component.component_type)
        layout.addRow("Type:", self.type_label)
        
        # Position (read-only)
        pos = self.component.pos()
        self.position_label = QtWidgets.QLabel(f"({pos.x()}, {pos.y()})")
        layout.addRow("Position:", self.position_label)
        
        # Buttons
        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok |
            QtWidgets.QDialogButtonBox.StandardButton.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addRow(button_box)

class PropertiesPanel(QtWidgets.QDockWidget):
    """Dockable panel to display and edit properties of selected components."""
    
    def __init__(self, parent=None):
        super().__init__("Properties", parent)
        self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea | QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
        self.widget = QtWidgets.QWidget()
        self.layout = QtWidgets.QFormLayout(self.widget)
        self.setWidget(self.widget)
        
        # Properties fields
        self.name_edit = QtWidgets.QLineEdit()
        self.type_label = QtWidgets.QLabel()
        self.position_label = QtWidgets.QLabel()
        
        self.layout.addRow("Name:", self.name_edit)
        self.layout.addRow("Type:", self.type_label)
        self.layout.addRow("Position:", self.position_label)
        
        # Update button
        self.update_button = QtWidgets.QPushButton("Update")
        self.layout.addRow(self.update_button)
        self.update_button.clicked.connect(self.update_properties)
        
        self.current_component = None
    
    def display_properties(self, component):
        """Display properties of the selected component."""
        self.current_component = component
        self.name_edit.setText(component.label)
        self.type_label.setText(component.component_type)
        pos = component.pos()
        self.position_label.setText(f"({int(pos.x())}, {int(pos.y())})")
    
    def update_properties(self):
        """Update the component's properties based on user input."""
        if self.current_component:
            new_name = self.name_edit.text()
            self.current_component.label = new_name
            self.current_component.update()

class NetworkScene(QtWidgets.QGraphicsScene):
    """Custom QGraphicsScene to handle cable creation via mouse drag."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.creating_cable = False
        self.start_component = None
        self.temp_line = None

        # Trenched lines for routing cables
        self.trench_left = QtWidgets.QGraphicsLineItem(400, 300, 400, 800)
        self.trench_right = QtWidgets.QGraphicsLineItem(800, 300, 800, 800)
        trench_pen = QtGui.QPen(QtGui.QColor(160, 82, 45), 2)  # SaddleBrown color
        self.trench_left.setPen(trench_pen)
        self.trench_right.setPen(trench_pen)
        self.trench_left.setZValue(-4)  # Behind road and other elements
        self.trench_right.setZValue(-4)
        self.addItem(self.trench_left)
        self.addItem(self.trench_right)

    def initiate_cable_creation(self, component, pos):
        """Start the process of creating a cable from the given component."""
        self.creating_cable = True
        self.start_component = component
        scene_pos = component.mapToScene(pos)
        self.temp_line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(scene_pos, scene_pos))
        pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2, QtCore.Qt.PenStyle.DashLine)
        self.temp_line.setPen(pen)
        self.temp_line.setZValue(-2)  # Behind cables
        self.addItem(self.temp_line)
    
    def mouseMoveEvent(self, event):
        """Update the temporary line during cable creation."""
        if self.creating_cable and self.temp_line:
            new_end = event.scenePos()
            line = QtCore.QLineF(self.temp_line.line().p1(), new_end)
            self.temp_line.setLine(line)
        super().mouseMoveEvent(event)
    
    def mouseReleaseEvent(self, event):
        """Finalize the cable creation if a valid end component is targeted."""
        if self.creating_cable and self.temp_line:
            end_items = self.items(event.scenePos())
            # Find the topmost NetworkComponent under the cursor
            end_component = None
            for item in end_items:
                if isinstance(item, NetworkComponent) and item != self.start_component:
                    end_component = item
                    break
            if end_component:
                # Prompt user to select cable type
                cable_type, ok = QtWidgets.QInputDialog.getItem(
                    None, "Select Cable Type", "Cable Type:", ["drop_cable", "link_cable"], 0, False
                )
                if ok:
                    new_cable = NetworkCable(self.start_component, end_component, cable_type=cable_type)
                    self.parent().add_cable_to_scene(new_cable)
            # Remove the temporary line
            self.removeItem(self.temp_line)
            self.temp_line = None
            self.creating_cable = False
        super().mouseReleaseEvent(event)
    
    def get_snapped_position(self, pos, snapping_distance=20):
        """Return the nearest trench line position to snap to."""
        # Check distance to trench_left
        left_line = self.trench_left.line()
        right_line = self.trench_right.line()
        distance_left = self.distance_to_line(pos, left_line)
        distance_right = self.distance_to_line(pos, right_line)
        
        if distance_left < distance_right and distance_left <= snapping_distance:
            return self.closest_point_on_line(pos, left_line)
        elif distance_right <= snapping_distance:
            return self.closest_point_on_line(pos, right_line)
        else:
            return pos  # No snapping
        
    def distance_to_line(self, point, line):
        """Calculate the perpendicular distance from a point to a line."""
        return QtGui.QLineF(line.p1(), QtCore.QPointF(point.x(), point.y())).length() * abs(
            (line.p2().x() - line.p1().x()) * (line.p1().y() - point.y()) - 
            (line.p1().x() - point.x()) * (line.p2().y() - line.p1().y())
        ) / line.length()
    
    def closest_point_on_line(self, point, line):
        """Find the closest point on a line to a given point."""
        line_p1 = line.p1()
        line_p2 = line.p2()
        dx = line_p2.x() - line_p1.x()
        dy = line_p2.y() - line_p1.y()
        if dx == dy == 0:
            return line_p1
        t = ((point.x() - line_p1.x()) * dx + (point.y() - line_p1.y()) * dy) / (dx*dx + dy*dy)
        t = max(0, min(1, t))
        return QtCore.QPointF(line_p1.x() + t * dx, line_p1.y() + t * dy)
    
    def find_nearest_component(self, pos, search_distance=30):
        """Find the nearest NetworkComponent to a given position within a search distance."""
        nearest_component = None
        min_distance = float('inf')
        for component in self.components["erven"] + self.components["centroids"] + self.components["boundary_boxes"] + [self.parent().central_node]:
            component_center = component.scenePos() + component.boundingRect().center()
            distance = QtCore.QLineF(pos, component_center).length()
            if distance < min_distance and distance <= search_distance:
                min_distance = distance
                nearest_component = component
        return nearest_component

class NetworkCable(QtWidgets.QGraphicsPathItem):
    """Represents a cable connecting two network components."""
    
    def __init__(self, start_item, end_item, cable_type='drop_cable'):
        super().__init__()
        self.start_item = start_item
        self.end_item = end_item
        self.cable_type = cable_type
        
        # Set pen style based on cable type
        pen = QtGui.QPen(COMPONENT_COLORS.get(cable_type, QtGui.QColor(0, 0, 0)), 2)
        pen.setStyle(CABLE_STYLES.get(cable_type, QtCore.Qt.PenStyle.SolidLine))
        self.setPen(pen)
        
        # Enable selection
        self.setFlags(
            QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
        )
        
        self.update_position()
        
        # Connect signals to update position when components move
        self.start_item.moved.connect(self.update_position)
        self.end_item.moved.connect(self.update_position)
        
        # Add this cable to the components' connections
        self.start_item.add_connection(self)
        self.end_item.add_connection(self)
        
        # Ensure the cable is behind components
        self.setZValue(-1)
        
    def update_position(self):
        """Update the cable's position based on connected components."""
        start_center = self.start_item.scenePos() + self.start_item.boundingRect().center()
        end_center = self.end_item.scenePos() + self.end_item.boundingRect().center()
        
        # Create a path that follows the trenched lines
        path = QtGui.QPainterPath(start_center)
        # For simplicity, using a straight line; implement routing along trench lines here
        path.lineTo(end_center)
        self.setPath(path)
    
    def mousePressEvent(self, event):
        """Enable dragging of cable endpoints."""
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            self.dragging = True
            self.drag_start_pos = event.scenePos()
            event.accept()
        super().mousePressEvent(event)
    
    def mouseMoveEvent(self, event):
        """Handle dragging of cable endpoints with snapping."""
        if hasattr(self, 'dragging') and self.dragging:
            new_end = event.scenePos()
            # Snap to the nearest trench line or component
            snapped_point = self.scene().get_snapped_position(new_end)
            path = QtGui.QPainterPath(self.start_item.scenePos() + self.start_item.boundingRect().center())
            path.lineTo(snapped_point)
            self.setPath(path)
            event.accept()
        super().mouseMoveEvent(event)
    
    def mouseReleaseEvent(self, event):
        """Finalize dragging of cable endpoints."""
        if hasattr(self, 'dragging') and self.dragging:
            self.dragging = False
            # Find the nearest component to snap to
            snapped_component = self.scene().find_nearest_component(event.scenePos())
            if snapped_component:
                self.end_item.remove_connection(self)
                self.end_item = snapped_component
                self.end_item.add_connection(self)
                self.update_position()
            event.accept()
        super().mouseReleaseEvent(event)
    
    def contextMenuEvent(self, event):
        """Provide a context menu for deleting the cable."""
        menu = QtWidgets.QMenu()
        delete_action = menu.addAction("Delete Cable")
        action = menu.exec(event.screenPos())
        if action == delete_action:
            self.delete_cable()
    
    def delete_cable(self):
        """Delete the cable from the scene and disconnect from components."""
        # Remove connections from components
        self.start_item.remove_connection(self)
        self.end_item.remove_connection(self)
        # Remove the cable from the scene
        scene = self.scene()
        if scene:
            scene.removeItem(self)
        # Optionally, perform additional cleanup

class PropertiesDialog(QtWidgets.QDialog):
    """Dialog to edit properties of a network component."""
    
    def __init__(self, component, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Edit Properties")
        self.component = component
        self.setup_ui()
    
    def setup_ui(self):
        layout = QtWidgets.QFormLayout(self)
        
        # Name Edit
        self.name_edit = QtWidgets.QLineEdit(self)
        self.name_edit.setText(self.component.label)
        layout.addRow("Name:", self.name_edit)
        
        # Component Type (read-only)
        self.type_label = QtWidgets.QLabel(self.component.component_type)
        layout.addRow("Type:", self.type_label)
        
        # Position (read-only)
        pos = self.component.pos()
        self.position_label = QtWidgets.QLabel(f"({pos.x()}, {pos.y()})")
        layout.addRow("Position:", self.position_label)
        
        # Buttons
        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok |
            QtWidgets.QDialogButtonBox.StandardButton.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addRow(button_box)

class PropertiesPanel(QtWidgets.QDockWidget):
    """Dockable panel to display and edit properties of selected components."""
    
    def __init__(self, parent=None):
        super().__init__("Properties", parent)
        self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea | QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
        self.widget = QtWidgets.QWidget()
        self.layout = QtWidgets.QFormLayout(self.widget)
        self.setWidget(self.widget)
        
        # Properties fields
        self.name_edit = QtWidgets.QLineEdit()
        self.type_label = QtWidgets.QLabel()
        self.position_label = QtWidgets.QLabel()
        
        self.layout.addRow("Name:", self.name_edit)
        self.layout.addRow("Type:", self.type_label)
        self.layout.addRow("Position:", self.position_label)
        
        # Update button
        self.update_button = QtWidgets.QPushButton("Update")
        self.layout.addRow(self.update_button)
        self.update_button.clicked.connect(self.update_properties)
        
        self.current_component = None
    
    def display_properties(self, component):
        """Display properties of the selected component."""
        self.current_component = component
        self.name_edit.setText(component.label)
        self.type_label.setText(component.component_type)
        pos = component.pos()
        self.position_label.setText(f"({int(pos.x())}, {int(pos.y())})")
    
    def update_properties(self):
        """Update the component's properties based on user input."""
        if self.current_component:
            new_name = self.name_edit.text()
            self.current_component.label = new_name
            self.current_component.update()

class HelpDialog(QtWidgets.QDialog):
    """Dialog to provide help and documentation."""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Help - Network Configuration GUI")
        self.setMinimumSize(600, 400)
        layout = QtWidgets.QVBoxLayout(self)
        
        help_text = """
        <h2>Network Configuration GUI Help</h2>
        <p>This application allows you to configure and visualize network components such as Erven, Centroids, Boundary Boxes, MUC350s, Splitters, and cables.</p>
        
        <h3>Adding Components</h3>
        <ul>
            <li>Use the toolbox on the left to add different types of components.</li>
            <li>Components can be positioned by dragging them on the canvas.</li>
            <li>Centroids automatically snap to the center of the nearest Erf when moved close enough.</li>
        </ul>
        
        <h3>Creating Cables</h3>
        <ul>
            <li>To create a cable, click and drag from one component to another.</li>
            <li>Select the cable type (Drop Cable or Link Cable) when prompted.</li>
            <li>Drop Cables are represented in red (dashed lines), and Link Cables are in blue (solid lines).</li>
            <li>Drop Cables can be clicked and dragged; their endpoints will snap to the nearest component or trench line.</li>
        </ul>
        
        <h3>Context Menus</h3>
        <ul>
            <li>Right-click on a component to delete it or edit its properties.</li>
            <li>Right-click on a cable to delete it.</li>
        </ul>
        
        <h3>Properties Panel</h3>
        <ul>
            <li>Select a component to view and edit its properties in the Properties panel on the right.</li>
        </ul>
        
        <h3>Saving and Loading</h3>
        <ul>
            <li>Use the File menu to save the current network configuration to a JSON file.</li>
            <li>Load an existing configuration from a JSON file to recreate the network.</li>
        </ul>
        
        <h3>Simulation</h3>
        <ul>
            <li>Use the Simulation menu to test connectivity between two selected components.</li>
            <li>Components will change color based on their connectivity status.</li>
        </ul>
        
        <h3>Performance</h3>
        <ul>
            <li>The application optimizes rendering for smooth performance even with a large number of components.</li>
        </ul>
        
        """
        
        text_browser = QtWidgets.QTextBrowser()
        text_browser.setHtml(help_text)
        layout.addWidget(text_browser)
        
        button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Close)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

class NetworkGUI(QtWidgets.QMainWindow):
    """Main window for the network configuration GUI."""
    
    def __init__(self):
        super().__init__()
        
        self.setWindowTitle("Network Configuration")
        self.setGeometry(100, 100, 1400, 900)
        
        self.central_widget = QtWidgets.QWidget()
        self.setCentralWidget(self.central_widget)
        self.layout = QtWidgets.QHBoxLayout(self.central_widget)
        
        # Initialize Properties Panel
        self.properties_panel = PropertiesPanel(self)
        self.addDockWidget(QtCore.Qt.DockWidgetArea.RightDockWidgetArea, self.properties_panel)
        
        # Initialize Help Menu
        self.init_help_menu()
        
        # Initialize Simulation Menu
        self.init_simulation_menu()
        
        # Initialize File Menu
        self.init_file_menu()
        
        # Initialize Graphics View and Scene
        self.scene = NetworkScene(self)
        self.view = QtWidgets.QGraphicsView(self.scene)
        self.view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        self.layout.addWidget(self.view)
        
        # Connect selection changes
        self.scene.selectionChanged.connect(self.update_properties_panel)
        
        # Initialize component storage
        self.components = {
            "erven": [],
            "centroids": [],
            "boundary_boxes": [],
            "drop_cables": [],
            "link_cables": [],
            "muc350": [],
            "splitters": [],
            "road_crossings": []
        }
        
        # Initialize the central node and intersection line
        self.central_node = NetworkComponent("central_node", "Central Node", 550, 350, 100, 50, COMPONENT_ICONS.get("central_node"))
        self.scene.addItem(self.central_node)
        self.components["central_node"].append(self.central_node)
        
        # Create the road and T-sections
        self.create_road()
        
        # Create UI elements
        self.create_toolbox()
        self.create_initial_components()
        
    def init_help_menu(self):
        """Initialize the Help menu in the menu bar."""
        help_action = QtGui.QAction("Help", self)
        help_action.triggered.connect(self.show_help)
        menu = self.menuBar().addMenu("Help")
        menu.addAction(help_action)
    
    def show_help(self):
        """Display the help dialog."""
        help_dialog = HelpDialog(self)
        help_dialog.exec()
    
    def init_simulation_menu(self):
        """Initialize the Simulation menu with connectivity testing."""
        simulation_menu = self.menuBar().addMenu("Simulation")
        
        test_connectivity_action = QtGui.QAction("Test Connectivity", self)
        test_connectivity_action.triggered.connect(self.test_connectivity)
        
        simulation_menu.addAction(test_connectivity_action)
    
    def init_file_menu(self):
        """Initialize the File menu in the menu bar with Save and Load options."""
        file_menu = self.menuBar().addMenu("File")
        
        save_action = QtGui.QAction("Save Configuration", self)
        save_action.triggered.connect(self.save_configuration)
        
        load_action = QtGui.QAction("Load Configuration", self)
        load_action.triggered.connect(self.load_configuration)
        
        file_menu.addAction(save_action)
        file_menu.addAction(load_action)
    
    def create_toolbox(self):
        """Create a dockable toolbox for adding components."""
        toolbox = QtWidgets.QDockWidget("Components", self)
        self.addDockWidget(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea, toolbox)
        
        toolbox_widget = QtWidgets.QWidget()
        toolbox_layout = QtWidgets.QVBoxLayout(toolbox_widget)
        
        # Add buttons for different components
        self.add_component_button(toolbox_layout, "Add Erf", self.add_erven)
        self.add_component_button(toolbox_layout, "Add Centroid", self.add_centroid)
        self.add_component_button(toolbox_layout, "Add Boundary Box", self.add_boundary_box)
        self.add_component_button(toolbox_layout, "Add Drop Cable", self.add_drop_cable)
        self.add_component_button(toolbox_layout, "Add Link Cable", self.add_link_cable)
        self.add_component_button(toolbox_layout, "Add MUC350", self.add_muc350)
        self.add_component_button(toolbox_layout, "Add 1:16-way Splitter", self.add_splitter)
        self.add_component_button(toolbox_layout, "Add Road-Crossing", self.add_road_crossing)
        
        # Spacer to push buttons to the top
        toolbox_layout.addStretch()
        toolbox.setWidget(toolbox_widget)
    
    def add_component_button(self, layout, label, callback):
        """Helper method to add a button to the toolbox."""
        button = QtWidgets.QPushButton(label)
        button.clicked.connect(callback)
        layout.addWidget(button)
    
    def create_road(self):
        """Create the road with two T-sections forming a U-shape."""
        # Main road
        road = QtWidgets.QGraphicsLineItem(400, 300, 800, 300)
        road.setPen(QtGui.QPen(COMPONENT_COLORS["road"], 4))  # Width of 4 pixels
        road.setZValue(-3)  # Behind other items
        self.scene.addItem(road)
        
        # First T-section
        t_section1 = QtWidgets.QGraphicsLineItem(600, 300, 600, 350)
        t_section1.setPen(QtGui.QPen(COMPONENT_COLORS["road"], 4))
        t_section1.setZValue(-3)
        self.scene.addItem(t_section1)
        
        # Second T-section
        t_section2 = QtWidgets.QGraphicsLineItem(600, 300, 600, 250)
        t_section2.setPen(QtGui.QPen(COMPONENT_COLORS["road"], 4))
        t_section2.setZValue(-3)
        self.scene.addItem(t_section2)
    
    def create_initial_components(self):
        """Create initial network components with specified layout."""
        # Create Erven on the left side of the road
        erven_spacing = 100
        erven_start_x = 100
        erven_start_y = 200
        erven_width = 80
        erven_height = 40
    
        for i in range(4):
            x = erven_start_x + i * erven_spacing
            y = erven_start_y
            erf = NetworkComponent("erven", f"Erf {i+1}", x, y, erven_width, erven_height, COMPONENT_ICONS.get("erven"))
            self.scene.addItem(erf)
            self.components["erven"].append(erf)
        
        # Create Erven on the right side of the road
        erven_start_x_right = 700
        erven_start_y_right = 200
    
        for i in range(4):
            x = erven_start_x_right + i * erven_spacing
            y = erven_start_y_right
            erf = NetworkComponent("erven", f"Erf {i+5}", x, y, erven_width, erven_height, COMPONENT_ICONS.get("erven"))
            self.scene.addItem(erf)
            self.components["erven"].append(erf)
        
        # Create initial centroids (half the size of Erven)
        centroid_width = 40  # Half of 80
        centroid_height = 20  # Half of 40
        centroid_spacing = 100
        centroid_start_x = 150
        centroid_start_y = 270
    
        for i in range(4):
            x = centroid_start_x + i * centroid_spacing
            y = centroid_start_y
            centroid = NetworkComponent("centroids", f"Centroid {i+1}", x, y, centroid_width, centroid_height, COMPONENT_ICONS.get("centroids"))
            self.scene.addItem(centroid)
            self.components["centroids"].append(centroid)
        
        centroid_start_x_right = 750
        centroid_start_y_right = 270
    
        for i in range(4):
            x = centroid_start_x_right + i * centroid_spacing
            y = centroid_start_y_right
            centroid = NetworkComponent("centroids", f"Centroid {i+5}", x, y, centroid_width, centroid_height, COMPONENT_ICONS.get("centroids"))
            self.scene.addItem(centroid)
            self.components["centroids"].append(centroid)
        
        # Create boundary boxes
        boundary_box_spacing = 150
        boundary_box_start_x = 400
        boundary_box_start_y = 100
        for i in range(4):
            x = boundary_box_start_x + i * boundary_box_spacing
            y = boundary_box_start_y
            boundary_box = NetworkComponent("boundary_boxes", f"BB {i+1}", x, y, 80, 40, COMPONENT_ICONS.get("boundary_boxes"))
            self.scene.addItem(boundary_box)
            self.components["boundary_boxes"].append(boundary_box)
    
        # Optionally, add initial cables
        # Connect the first and last Erven to the central node
        if self.components["erven"]:
            first_erf = self.components["erven"][0]
            last_erf = self.components["erven"][7]
            drop_cable1 = NetworkCable(first_erf, self.central_node, cable_type='drop_cable')
            self.add_cable_to_scene(drop_cable1)
    
            drop_cable2 = NetworkCable(last_erf, self.central_node, cable_type='drop_cable')
            self.add_cable_to_scene(drop_cable2)
    
    def add_cable_to_scene(self, cable):
        """Add a cable to the scene and store it."""
        self.scene.addItem(cable)
        if cable.cable_type == 'drop_cable':
            self.components["drop_cables"].append(cable)
        elif cable.cable_type == 'link_cable':
            self.components["link_cables"].append(cable)
    
    def add_erven(self):
        """Add an Erf component."""
        index = len(self.components["erven"]) + 1
        # Alternate side based on position
        if index <= 4:
            x = 100 + (index - 1) * 100
            y = 200
        else:
            x = 700 + (index - 5) * 100
            y = 200
        erf = NetworkComponent("erven", f"Erf {index}", x, y, 80, 40, COMPONENT_ICONS.get("erven"))
        self.scene.addItem(erf)
        self.components["erven"].append(erf)
        self.statusBar().showMessage(f"Added Erf {index}", 2000)
    
    def add_centroid(self):
        """Add a Centroid component."""
        index = len(self.components["centroids"]) + 1
        # Alternate side based on position
        if index <= 4:
            x = 150 + (index - 1) * 100
            y = 270
        else:
            x = 750 + (index - 5) * 100
            y = 270
        centroid = NetworkComponent("centroids", f"Centroid {index}", x, y, 40, 20, COMPONENT_ICONS.get("centroids"))  # Half size
        self.scene.addItem(centroid)
        self.components["centroids"].append(centroid)
        self.statusBar().showMessage(f"Added Centroid {index}", 2000)
    
    def add_boundary_box(self):
        """Add a Boundary Box component."""
        index = len(self.components["boundary_boxes"]) + 1
        x = 400 + (index - 1) * 150
        y = 100
        boundary_box = NetworkComponent("boundary_boxes", f"BB {index}", x, y, 80, 40, COMPONENT_ICONS.get("boundary_boxes"))
        self.scene.addItem(boundary_box)
        self.components["boundary_boxes"].append(boundary_box)
        self.statusBar().showMessage(f"Added Boundary Box {index}", 2000)
    
    def add_drop_cable(self):
        """Add a Drop Cable by connecting the last Erf to the central node."""
        if not self.components["erven"]:
            QtWidgets.QMessageBox.warning(self, "No Erven", "Please add an Erf first.")
            return
        # For simplicity, connect the last Erf to the central node
        start_item = self.components["erven"][-1]
        end_item = self.central_node
        drop_cable = NetworkCable(start_item, end_item, cable_type='drop_cable')
        self.add_cable_to_scene(drop_cable)
        self.statusBar().showMessage("Added Drop Cable", 2000)
    
    def add_link_cable(self):
        """Add a Link Cable by connecting the central node to the last Centroid."""
        if not self.components["centroids"]:
            QtWidgets.QMessageBox.warning(self, "No Centroids", "Please add a Centroid first.")
            return
        # For simplicity, connect the central node to the last Centroid
        start_item = self.central_node
        end_item = self.components["centroids"][-1]
        link_cable = NetworkCable(start_item, end_item, cable_type='link_cable')
        self.add_cable_to_scene(link_cable)
        self.statusBar().showMessage("Added Link Cable", 2000)
    
    def add_muc350(self):
        """Add a MUC350 component."""
        index = len(self.components["muc350"]) + 1
        x = 700
        y = 150 + (index - 1) * 100
        muc350 = NetworkComponent("muc350", f"MUC350 {index}", x, y, 80, 40, COMPONENT_ICONS.get("muc350"))
        self.scene.addItem(muc350)
        self.components["muc350"].append(muc350)
        self.statusBar().showMessage(f"Added MUC350 {index}", 2000)
    
    def add_splitter(self):
        """Add a 1:16-way Splitter component."""
        index = len(self.components["splitters"]) + 1
        x = 800
        y = 150 + (index - 1) * 100
        splitter = NetworkComponent("splitters", f"Splitter {index}", x, y, 80, 40, COMPONENT_ICONS.get("splitters"))
        self.scene.addItem(splitter)
        self.components["splitters"].append(splitter)
        self.statusBar().showMessage(f"Added Splitter {index}", 2000)
    
    def add_road_crossing(self):
        """Add a Road-Crossing component."""
        index = len(self.components["road_crossings"]) + 1
        x = 500  # Starting x position for Road-Crossing
        y = 300 + (index - 1) * 100
        road_crossing_length = 200
        road_crossing = QtWidgets.QGraphicsLineItem(x, y, x + road_crossing_length, y)
        road_crossing.setPen(QtGui.QPen(COMPONENT_COLORS["road_crossing"], 8))  # Twice the road width (4*2)
        road_crossing.setZValue(-2)  # Behind road lines but above cables
        self.scene.addItem(road_crossing)
        self.components["road_crossings"].append(road_crossing)
        self.statusBar().showMessage(f"Added Road-Crossing {index}", 2000)
    
    def update_properties_panel(self):
        """Update the properties panel based on the selected item."""
        selected_items = self.scene.selectedItems()
        if selected_items:
            item = selected_items[0]
            if isinstance(item, NetworkComponent):
                self.properties_panel.display_properties(item)
        else:
            # Clear the properties panel if nothing is selected
            self.properties_panel.current_component = None
            self.properties_panel.name_edit.clear()
            self.properties_panel.type_label.clear()
            self.properties_panel.position_label.clear()
    
    def save_configuration(self):
        """Save the current network layout to a JSON file."""
        options = QtWidgets.QFileDialog.Options()
        file_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save Configuration", "", "JSON Files (*.json)", options=options)
        if file_path:
            data = {
                "components": [],
                "cables": []
            }
            # Save components
            for category in ["erven", "centroids", "boundary_boxes", "central_node", "muc350", "splitters"]:
                for component in self.components.get(category, []):
                    component_data = {
                        "type": component.component_type,
                        "label": component.label,
                        "x": component.scenePos().x(),
                        "y": component.scenePos().y(),
                        "width": component.width,
                        "height": component.height
                        # Add more properties as needed
                    }
                    data["components"].append(component_data)
            # Save cables
            for cable in self.components.get("drop_cables", []) + self.components.get("link_cables", []):
                cable_data = {
                    "start_label": cable.start_item.label,
                    "end_label": cable.end_item.label,
                    "type": cable.cable_type
                }
                data["cables"].append(cable_data)
            # Write to file
            try:
                with open(file_path, 'w') as f:
                    json.dump(data, f, indent=4)
                self.statusBar().showMessage(f"Configuration saved to {file_path}", 5000)
            except Exception as e:
                QtWidgets.QMessageBox.critical(self, "Save Error", f"Failed to save configuration:\n{e}")
    
    def load_configuration(self):
        """Load a network layout from a JSON file."""
        options = QtWidgets.QFileDialog.Options()
        file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Load Configuration", "", "JSON Files (*.json)", options=options)
        if file_path:
            try:
                with open(file_path, 'r') as f:
                    data = json.load(f)
                # Clear existing scene
                self.clear_scene()
                # Load components
                label_to_component = {}
                for component_data in data.get("components", []):
                    component_type = component_data["type"]
                    label = component_data["label"]
                    x = component_data["x"]
                    y = component_data["y"]
                    width = component_data.get("width", 80)
                    height = component_data.get("height", 40)
                    icon = COMPONENT_ICONS.get(component_type, None)
                    component = NetworkComponent(component_type, label, x, y, width, height, icon)
                    self.scene.addItem(component)
                    self.components.setdefault(component_type, []).append(component)
                    label_to_component[label] = component
                # Load cables
                for cable_data in data.get("cables", []):
                    start_label = cable_data["start_label"]
                    end_label = cable_data["end_label"]
                    cable_type = cable_data.get("type", "drop_cable")
                    start_item = label_to_component.get(start_label, None)
                    end_item = label_to_component.get(end_label, None)
                    if start_item and end_item:
                        cable = NetworkCable(start_item, end_item, cable_type=cable_type)
                        self.add_cable_to_scene(cable)
                self.statusBar().showMessage(f"Configuration loaded from {file_path}", 5000)
            except Exception as e:
                QtWidgets.QMessageBox.critical(self, "Load Error", f"Failed to load configuration:\n{e}")
    
    def clear_scene(self):
        """Clear all items from the scene and reset component storage."""
        self.scene.clear()
        for key in self.components:
            self.components[key] = []
        # Re-add trench lines in the scene's constructor
        self.scene = NetworkScene(self)
        self.view.setScene(self.scene)
        # Re-add central node and road
        self.central_node = NetworkComponent("central_node", "Central Node", 550, 350, 100, 50, COMPONENT_ICONS.get("central_node"))
        self.scene.addItem(self.central_node)
        self.components["central_node"].append(self.central_node)
        self.scene.create_road()  # Recreate road lines
        # Note: Must implement create_road() as a method of NetworkScene or manage accordingly
    
    def test_connectivity(self):
        """Test connectivity between two selected components."""
        selected_items = self.scene.selectedItems()
        components = [item for item in selected_items if isinstance(item, NetworkComponent)]
        if len(components) != 2:
            QtWidgets.QMessageBox.warning(self, "Selection Error", "Please select exactly two components to test connectivity.")
            return
        start = components[0]
        end = components[1]
        # Perform BFS to check connectivity
        visited = set()
        queue = [start]
        found = False
        while queue:
            current = queue.pop(0)
            if current == end:
                found = True
                break
            visited.add(current)
            for cable in current.connections:
                neighbor = cable.start_item if cable.end_item == current else cable.end_item
                if neighbor not in visited and neighbor not in queue:
                    queue.append(neighbor)
        # Update visual indicators
        if found:
            start.pen.setColor(QtGui.QColor(0, 255, 0))  # Green
            end.pen.setColor(QtGui.QColor(0, 255, 0))
            start.update()
            end.update()
            QtWidgets.QMessageBox.information(self, "Connectivity Test", f"{start.label} and {end.label} are connected.")
        else:
            start.pen.setColor(QtGui.QColor(255, 0, 0))  # Red
            end.pen.setColor(QtGui.QColor(255, 0, 0))
            start.update()
            end.update()
            QtWidgets.QMessageBox.information(self, "Connectivity Test", f"{start.label} and {end.label} are NOT connected.")
    
    def show_help(self):
        """Display the help dialog."""
        help_dialog = HelpDialog(self)
        help_dialog.exec()

def main():
    """Main function to run the application."""
    app = QtWidgets.QApplication(sys.argv)
    window = NetworkGUI()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()