VoltageLevelGraph.java

/**
 * Copyright (c) 2019, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.powsybl.sld.model.graphs;

import com.fasterxml.jackson.core.JsonGenerator;
import com.powsybl.commons.PowsyblException;
import com.powsybl.sld.library.ComponentTypeName;
import com.powsybl.sld.model.cells.*;
import com.powsybl.sld.model.coordinate.Direction;
import com.powsybl.sld.model.coordinate.Orientation;
import com.powsybl.sld.model.coordinate.Point;
import com.powsybl.sld.model.nodes.*;
import com.powsybl.sld.model.nodes.Node.NodeType;
import com.powsybl.sld.model.nodes.feeders.FeederTwLeg;
import com.powsybl.sld.util.GraphTraversal;
import org.jgrapht.graph.Pseudograph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.powsybl.sld.model.coordinate.Position.Dimension.H;
import static com.powsybl.sld.model.coordinate.Position.Dimension.V;

/**
 * This class builds the connectivity among the elements of a voltageLevel
 * buildGraphAndDetectCell establishes the List of nodes, edges and nodeBuses
 * cells is built by the PatternCellDetector Class
 *
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 * @author Nicolas Duchene
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 * @author Franck Lecuyer {@literal <franck.lecuyer at rte-france.com>}
 */
public class VoltageLevelGraph extends AbstractBaseGraph {

    private static final Logger LOGGER = LoggerFactory.getLogger(VoltageLevelGraph.class);

    private final VoltageLevelInfos voltageLevelInfos;

    private final List<Node> nodes = new ArrayList<>();

    private final List<Edge> edges = new ArrayList<>();

    private final SortedSet<Cell> cells = new TreeSet<>(
            Comparator.comparingInt(Cell::getNumber)); // cells sorted to avoid randomness

    private final Map<Node.NodeType, List<Node>> nodesByType = new EnumMap<>(Node.NodeType.class);

    private final Map<String, Node> nodesById = new HashMap<>();

    private final Map<Node, Cell> nodeToCell = new HashMap<>();

    private int maxHorizontalBusPosition = 0;
    private int maxVerticalBusPosition = 0;

    private int cellCounter = 0;

    private final Point coord = new Point(0, 0);

    private final boolean forVoltageLevelDiagram;  // true if voltageLevel diagram
    // false if substation diagram

    // by direction, max calculated height of the extern cells
    // If no extern cell found taking into account intern cells too
    // (filled and used only when using the adapt cell height to content option)
    private Map<Direction, Double> maxCellHeight = new EnumMap<>(Direction.class);

    public VoltageLevelGraph(VoltageLevelInfos voltageLevelInfos, Graph parentGraph) {
        super(parentGraph);
        this.voltageLevelInfos = Objects.requireNonNull(voltageLevelInfos);
        this.forVoltageLevelDiagram = parentGraph == null;
    }

    @Override
    public String getId() {
        return voltageLevelInfos.getId();
    }

    public boolean isForVoltageLevelDiagram() {
        return forVoltageLevelDiagram;
    }

    public int getNextCellNumber() {
        return cellCounter++;
    }

    public void removeUnnecessaryConnectivityNodes() {
        List<Node> fictitiousNodesToRemove = nodes.stream()
                .filter(ConnectivityNode.class::isInstance)
                .collect(Collectors.toList());
        for (Node n : fictitiousNodesToRemove) {
            if (n.getAdjacentEdges().size() == 2) {
                List<Node> adjNodes = n.getAdjacentNodes();
                Node node1 = adjNodes.get(0);
                Node node2 = adjNodes.get(1);
                LOGGER.info("Remove fictitious node {} between {} and {}", n.getId(), node1.getId(), node2.getId());
                removeNode(n);
                addEdge(node1, node2);
            } else {
                LOGGER.info("Working on fictitious node {} with {} adjacent nodes", n.getId(), n.getAdjacentNodes().size());
                Node busNode = n.getAdjacentNodes().stream().filter(node -> node.getType() == Node.NodeType.BUS).findFirst().orElse(null);
                if (busNode != null) {
                    n.getAdjacentNodes().stream().filter(node -> !node.equals(busNode)).forEach(node -> {
                        LOGGER.info("Connecting {} to {}", node.getId(), busNode.getId());
                        addEdge(node, busNode);
                    });
                    LOGGER.info("Remove fictitious node {}", n.getId());
                    removeNode(n);
                } else {
                    LOGGER.warn("Cannot remove fictitious node {} because there are no adjacent BUS nodes", n.getId());
                }
            }
        }
    }

    public void removeFictitiousSwitchNode() {
        List<SwitchNode> fictitiousSwitchNodesToRemove = this.getNodes().stream()
                .filter(SwitchNode.class::isInstance)
                .map(SwitchNode.class::cast)
                .filter(SwitchNode::isFictitious)
                .filter(node -> node.getAdjacentNodes().size() == 2)
                .collect(Collectors.toList());
        for (SwitchNode n : fictitiousSwitchNodesToRemove) {
            Node node1 = n.getAdjacentNodes().get(0);
            Node node2 = n.getAdjacentNodes().get(1);
            LOGGER.info("Remove fictitious switch node {} between {} and {}", n.getName(), node1.getId(), node2.getId());
            this.removeNode(n);
            this.addEdge(node1, node2);
        }
    }

    public void logCellDetectionStatus() {
        Set<Cell> cellsLog = new HashSet<>();
        Map<Cell.CellType, Integer> cellCountByType = new EnumMap<>(Cell.CellType.class);
        for (Cell.CellType cellType : Cell.CellType.values()) {
            cellCountByType.put(cellType, 0);
        }
        int remainingNodeCount = 0;
        Map<Node.NodeType, Integer> remainingNodeCountByType = new EnumMap<>(Node.NodeType.class);
        for (Node.NodeType nodeType : Node.NodeType.values()) {
            remainingNodeCountByType.put(nodeType, 0);
        }
        for (Node node : nodes) {
            Optional<Cell> oCell = getCell(node);
            if (oCell.isPresent()) {
                Cell cell = oCell.get();
                if (cellsLog.add(cell)) {
                    cellCountByType.put(cell.getType(), cellCountByType.get(cell.getType()) + 1);
                }
            } else {
                remainingNodeCount++;
                remainingNodeCountByType.put(node.getType(), remainingNodeCountByType.get(node.getType()) + 1);
            }
        }
        if (cellsLog.isEmpty()) {
            LOGGER.warn("No cell detected");
        } else {
            LOGGER.info("{} cells detected ({})", cellsLog.size(), cellCountByType);
        }
        if (remainingNodeCount > 0) {
            LOGGER.warn("{}/{} nodes not associated to a cell ({})",
                    remainingNodeCount, nodes.size(), remainingNodeCountByType);
        }
    }

    public org.jgrapht.Graph<Node, Edge> toJgrapht() {
        org.jgrapht.Graph<Node, Edge> graph = new Pseudograph<>(Edge.class);
        for (Node node : nodes) {
            graph.addVertex(node);
        }
        for (Edge edge : edges) {
            graph.addEdge(edge.getNode1(), edge.getNode2(), edge);
        }
        return graph;
    }

    public void addNode(Node node) {
        if (nodes.contains(node)) {
            throw new AssertionError("The node cannot be added, it is already in the graph");
        }
        super.addNode(this, node);
        nodes.add(node);
        nodesByType.computeIfAbsent(node.getType(), nodeType -> new ArrayList<>()).add(node);
        nodesById.put(node.getId(), node);
    }

    @Override
    public void removeNode(Node node) {
        nodes.remove(node);
        super.removeNode(node);
        nodesByType.computeIfAbsent(node.getType(), nodeType -> new ArrayList<>()).remove(node);
        nodesById.remove(node.getId(), node);
        for (Edge edge : new ArrayList<>(node.getAdjacentEdges())) {
            removeEdge(edge);
        }
    }

    public Node getNode(String id) {
        Objects.requireNonNull(id);
        return nodesById.get(id);
    }

    @Override
    public VoltageLevelGraph getVoltageLevel(String voltageLevelId) {
        Objects.requireNonNull(voltageLevelId);
        return voltageLevelId.equals(voltageLevelInfos.getId()) ? this : null;
    }

    @Override
    public List<VoltageLevelGraph> getVoltageLevels() {
        return Collections.singletonList(this);
    }

    @Override
    public Stream<VoltageLevelGraph> getVoltageLevelStream() {
        return Stream.of(this);
    }

    @Override
    public Stream<Node> getAllNodesStream() {
        return nodes.stream();
    }

    /**
     * Add an edge between the two nodes
     *
     * @param n1 first node
     * @param n2 second node
     */
    public Edge addEdge(Node n1, Node n2) {
        Edge edge = new Edge(n1, n2);
        edges.add(edge);
        n1.addAdjacentEdge(edge);
        n2.addAdjacentEdge(edge);
        return edge;
    }

    /**
     * Remove an edge between two nodes
     *
     * @param n1 first node
     * @param n2 second node
     * @return the removed edge
     */
    private Edge removeEdge(Node n1, Node n2) {
        for (Edge edge : n1.getAdjacentEdges()) {
            if (edge.getNode1().equals(n2) || edge.getNode2().equals(n2)) {
                removeEdge(edge);
                return edge;
            }
        }
        throw new PowsyblException("Cannot remove edge, the two node are not connected");
    }

    private void removeEdge(Edge edge) {
        edge.getNode1().removeAdjacentEdge(edge);
        edge.getNode2().removeAdjacentEdge(edge);
        edges.remove(edge);
    }

    /**
     * Resolve when one EQ is connected with 2 switchs component
     */
    private void rIdentifyConnexComponent(Node node, List<Node> nodesIn, List<Node> connexComponent) {
        if (!connexComponent.contains(node)) {
            connexComponent.add(node);
            node.getAdjacentNodes()
                    .stream()
                    .filter(nodesIn::contains)
                    .forEach(n -> rIdentifyConnexComponent(n, nodesIn, connexComponent));
        }
    }

    public List<List<Node>> getConnexComponents(List<Node> nodesIn) {
        List<Node> nodesToHandle = new ArrayList<>(nodesIn);
        List<List<Node>> result = new ArrayList<>();
        while (!nodesToHandle.isEmpty()) {
            Node n = nodesToHandle.get(0);
            List<Node> connexComponent = new ArrayList<>();
            rIdentifyConnexComponent(n, nodesIn, connexComponent);
            nodesToHandle.removeAll(connexComponent);
            result.add(connexComponent);
        }
        return result;
    }

    public void setMaxBusPosition() {
        List<Integer> h = new ArrayList<>();
        List<Integer> v = new ArrayList<>();
        getNodeBuses().forEach(nodeBus -> {
            v.add(nodeBus.getBusbarIndex());
            h.add(nodeBus.getSectionIndex());
        });
        if (h.isEmpty() || v.isEmpty()) {
            return;
        }
        setMaxHorizontalBusPosition(Collections.max(h));
        setMaxVerticalBusPosition(Collections.max(v));
    }

    public void insertBusConnections(Predicate<Node> nodesOnBus) {
        getNodeBuses().forEach(busNode -> insertBusConnections(busNode, nodesOnBus));
    }

    private void insertBusConnections(BusNode busNode, Predicate<Node> nodesOnBus) {
        busNode.getAdjacentNodes().stream()
                .filter(node -> !nodesOnBus.test(node))
                .forEach(node -> insertBusConnection(busNode, node));
    }

    private void insertBusConnection(BusNode busNode, Node nodeConnectedToBusNode) {
        Node fNodeToBus = NodeFactory.createBusConnection(this, busNode.getId() + "_" + nodeConnectedToBusNode.getId());
        insertNode(busNode, fNodeToBus, nodeConnectedToBusNode);
    }

    private void insertNode(Node nodeA, Node nodeToInsert, Node nodeB) {
        Edge removedEdge = removeEdge(nodeA, nodeB);
        if (removedEdge.getNode1() == nodeA) {
            addEdge(nodeA, nodeToInsert);
            addEdge(nodeToInsert, nodeB);
        } else {
            addEdge(nodeB, nodeToInsert);
            addEdge(nodeToInsert, nodeA);
        }
    }

    public void insertHookNodesAtBuses() {
        getNodeBuses().forEach(this::insertHookNodesAtBuses);
    }

    private void insertHookNodesAtBuses(BusNode busNode) {
        busNode.getAdjacentNodes()
                .forEach(nodeOnBus -> nodeOnBus.getAdjacentNodes().stream()
                        .filter(n -> n.getType() != NodeType.BUS)
                        .filter(n -> !(n.getType() == NodeType.INTERNAL && !(n instanceof EquipmentNode)))
                        .forEach(n -> insertBusHookNode(nodeOnBus, n)));
    }

    private void insertBusHookNode(Node nodeOnBus, Node node) {
        // Create hook node
        Node fStackNode = NodeFactory.createConnectivityNode(this, nodeOnBus.getId() + "-" + node.getId());

        // Update edges
        if (node.getType() == NodeType.FEEDER) {
            // The feeder node might have several adjacent nodes (feeder fork for instance):
            // for display reasons we want the stack node to carry those adjacent nodes.
            transferEdges(node, fStackNode);
            addEdge(fStackNode, node);
        } else {
            insertNode(nodeOnBus, fStackNode, node);
        }
    }

    /**
     * Insert fictitious node(s) before feeders in order for the feeder to be properly displayed:
     * feeders need at least one inserted fictitious node to have enough space to display the feeder arrows.
     */
    public void insertHookNodesAtFeeders() {
        // Each feeder node needs a fictitious node to have enough place for the feeder infos (arrows)
        // FeederNode linked to Middle3WTNode do not need any fictitious node inserted, because of the fictitious Middle3WTNode
        List<Node> feederNodes = nodesByType.computeIfAbsent(Node.NodeType.FEEDER, nodeType -> new ArrayList<>());
        feederNodes.stream()
                .filter(feederNode -> !isInternal3wtFeederNode((FeederNode) feederNode))
                .forEach(this::insertFeederHookNode);
    }

    private boolean isInternal3wtFeederNode(FeederNode feederNode) {
        return feederNode.getFeeder().getFeederType() == FeederType.THREE_WINDINGS_TRANSFORMER_LEG
                && is3wtComponent(feederNode.getAdjacentNodes().get(0).getComponentType());
    }

    private boolean is3wtComponent(String componentName) {
        return ComponentTypeName.THREE_WINDINGS_TRANSFORMER_COMPONENTS.contains(componentName);
    }

    private void insertFeederHookNode(Node feederNode) {
        // Create a new hook node to insert before feeder node
        Node hookNode = NodeFactory.createConnectivityNode(this, feederNode.getId());

        List<Node> adjacentNodes = feederNode.getAdjacentNodes();
        if (adjacentNodes.size() == 1) {
            // Update edges: create the 2 new ones and remove the old one
            Node singleNeighbor = adjacentNodes.get(0);
            insertNode(singleNeighbor, hookNode, feederNode);
        } else {
            // Create an extra fork node, otherwise the hook-node is a node with several neighbors (fork node)
            Node forkNode = NodeFactory.createConnectivityNode(this, feederNode.getId() + "_fork");

            // Transfer the neighbours of the feeder node to that new forkNode
            transferEdges(feederNode, forkNode);

            addEdge(forkNode, hookNode);
            addEdge(hookNode, feederNode);
        }
    }

    public void extendBusesConnectedToBuses() {
        getNodeBuses().forEach(n1 ->
                n1.getAdjacentNodes().stream()
                        .filter(BusNode.class::isInstance)
                        .forEach(n2 -> extendBusConnectedToBus(n1, (BusNode) n2)));
    }

    private void extendBusConnectedToBus(BusNode n1, BusNode n2) {
        removeEdge(n1, n2);
        String busToBusId = n1.getId() + "-" + n2.getId();
        Node cNode1 = NodeFactory.createConnectivityNode(this, busToBusId + "_1");
        Node cNode2 = NodeFactory.createConnectivityNode(this, busToBusId + "_2");
        addEdge(n1, cNode1);
        addEdge(cNode1, cNode2);
        addEdge(n2, cNode2);
    }

    public ConnectivityNode insertConnectivityNode(Node node1, Node node2, String id) {
        ConnectivityNode iNode = NodeFactory.createConnectivityNode(this, id);
        insertNode(node1, iNode, node2);
        return iNode;
    }

    /**
     * Substitute a node with another node already in the graph.
     *
     * @param nodeOrigin: node which will be substituted
     * @param newNode:    node which will substitute the first one
     */
    public void substituteNode(Node nodeOrigin, Node newNode) {
        transferEdges(nodeOrigin, newNode);
        removeNode(nodeOrigin);
    }

    private void transferEdges(Node nodeOrigin, Node newNode) {
        transferEdges(nodeOrigin, newNode, new ArrayList<>(nodeOrigin.getAdjacentEdges()));
    }

    public void transferEdges(Node nodeOrigin, Node newNode, List<Edge> edgesToTransfer) {
        if (!nodesById.containsKey(newNode.getId())) {
            throw new PowsyblException("New node [" + newNode.getId() + "] is not in current voltage level graph");
        }
        for (Edge edge : edgesToTransfer) {
            if (!edge.getNodes().contains(nodeOrigin)) {
                throw new PowsyblException("Edge to transfer not in adjacent edges of given node");
            }
            Node node1 = edge.getNode1() == nodeOrigin ? newNode : edge.getNode1();
            Node node2 = edge.getNode2() == nodeOrigin ? newNode : edge.getNode2();
            addEdge(node1, node2);
            removeEdge(edge);
        }
    }

    public void substituteFictitiousNodesMirroringBusNodes() {
        getNodeBuses().forEach(busNode -> {
            List<Node> adjs = busNode.getAdjacentNodes();
            if (adjs.size() == 1 && adjs.get(0).getType() == Node.NodeType.INTERNAL) {
                Node adj = adjs.get(0);
                removeEdge(adj, busNode);
                substituteNode(adj, busNode);
            }
        });
    }

    public void substituteSingularFictitiousByFeederNode() {
        getNodes().stream()
                .filter(n -> n.getType() == Node.NodeType.INTERNAL && n.getAdjacentEdges().size() == 1)
                .forEach(n -> substituteNode(n, NodeFactory.createFictitiousFeederNode(this, n.getId(), Orientation.UP)));
    }

    public void addCell(Cell c) {
        cells.add(c);
        List<Node> cellNodes = c.getNodes();
        if (c.getType() == Cell.CellType.SHUNT) {
            cellNodes.stream().skip(1).limit(((long) cellNodes.size()) - 2) // skip first and last nodes that are part of the ExternalNodes
                    .forEach(n -> nodeToCell.put(n, c));
        } else {
            cellNodes.stream().filter(n -> n.getType() != NodeType.BUS)
                    .forEach(n -> nodeToCell.put(n, c));
        }
    }

    public void removeCell(Cell c) {
        cells.remove(c);
        c.getNodes().forEach(n -> nodeToCell.remove(n, c));
    }

    public List<BusNode> getNodeBuses() {
        return nodesByType.computeIfAbsent(Node.NodeType.BUS, nodeType -> new ArrayList<>())
                .stream()
                .map(BusNode.class::cast)
                .collect(Collectors.toList());
    }

    public List<FeederNode> getFeederNodes() {
        return getFeederNodeStream().collect(Collectors.toList());
    }

    private Stream<FeederNode> getFeederNodeStream() {
        return nodesByType.computeIfAbsent(NodeType.FEEDER, nodeType -> new ArrayList<>())
                .stream()
                .map(FeederNode.class::cast);
    }

    private Stream<FeederNode> getFeederNodeStream(FeederType feederType) {
        return getFeederNodeStream().filter(feederNode -> feederNode.getFeeder().getFeederType() == feederType);
    }

    public List<Node> getNodes() {
        return new ArrayList<>(nodes);
    }

    public Stream<ConnectivityNode> getConnectivityNodeStream() {
        return nodesByType.computeIfAbsent(NodeType.INTERNAL, nodeType -> new ArrayList<>())
                .stream()
                .filter(ConnectivityNode.class::isInstance)
                .map(ConnectivityNode.class::cast);
    }

    public List<Edge> getEdges() {
        return new ArrayList<>(edges);
    }

    public Set<Node> getNodeSet() {
        return new LinkedHashSet<>(nodes);
    }

    public Set<Edge> getEdgeSet() {
        return new LinkedHashSet<>(edges);
    }

    public Stream<Cell> getCellStream() {
        return cells.stream();
    }

    public Stream<BusCell> getBusCellStream() {
        return cells.stream().filter(BusCell.class::isInstance).map(BusCell.class::cast);
    }

    public List<BusCell> getBusCells() {
        return getBusCellStream().collect(Collectors.toList());
    }

    public Stream<InternCell> getInternCellStream() {
        return cells.stream().filter(InternCell.class::isInstance).map(InternCell.class::cast);
    }

    public Stream<ExternCell> getExternCellStream() {
        return cells.stream().filter(ExternCell.class::isInstance).map(ExternCell.class::cast);
    }

    public Stream<ArchCell> getArchCellStream() {
        return cells.stream().filter(ArchCell.class::isInstance).map(ArchCell.class::cast);
    }

    public Stream<ShuntCell> getShuntCellStream() {
        return cells.stream().filter(ShuntCell.class::isInstance).map(ShuntCell.class::cast);
    }

    @Override
    public Optional<Cell> getCell(Node node) {
        return Optional.ofNullable(nodeToCell.get(node));
    }

    public VoltageLevelInfos getVoltageLevelInfos() {
        return voltageLevelInfos;
    }

    public Point getCoord() {
        return coord;
    }

    public void setCoord(double x, double y) {
        coord.setX(x);
        coord.setY(y);
    }

    public double getX() {
        return coord.getX();
    }

    public double getY() {
        return coord.getY();
    }

    public boolean isPositionNodeBusesCalculated() {
        return getNodeBuses().stream().allMatch(n -> n.getPosition().get(H) != -1 && n.getPosition().get(V) != -1);
    }

    @Override
    public void writeJson(JsonGenerator generator, boolean includeCoordinates) throws IOException {
        generator.writeStartObject();

        generator.writeFieldName("voltageLevelInfos");
        voltageLevelInfos.writeJsonContent(generator);

        if (includeCoordinates) {
            generator.writeNumberField("x", getX());
            generator.writeNumberField("y", getY());
        }

        generator.writeArrayFieldStart("nodes");
        Iterator<Node> nodesIt = nodes.stream().sorted(Comparator.comparing(Node::getId)).iterator();
        while (nodesIt.hasNext()) {
            nodesIt.next().writeJson(generator, includeCoordinates);
        }
        generator.writeEndArray();

        generator.writeArrayFieldStart("cells");
        for (Cell cell : cells) {
            cell.writeJson(generator, includeCoordinates);
        }
        generator.writeEndArray();

        generator.writeArrayFieldStart("edges");
        for (Edge edge : edges) {
            edge.writeJson(generator);
        }
        generator.writeEndArray();

        writeBranchFields(generator, includeCoordinates);

        generator.writeEndObject();
    }

    public int getMaxH() {
        return getNodeBuses().stream()
                .mapToInt(nodeBus -> nodeBus.getPosition().get(H) + nodeBus.getPosition().getSpan(H))
                .max().orElse(0);
    }

    public int getMaxV() {
        return getNodeBuses().stream()
                .mapToInt(nodeBus -> nodeBus.getPosition().get(V) + nodeBus.getPosition().getSpan(V))
                .max().orElse(0);
    }

    public Double getExternCellHeight(Direction direction) {
        if (maxCellHeight.isEmpty() || direction == Direction.MIDDLE || direction == Direction.UNDEFINED) {
            return 0.;
        }
        return maxCellHeight.get(direction);
    }

    public void setMaxCellHeight(Map<Direction, Double> maxCellHeight) {
        this.maxCellHeight = maxCellHeight;
    }

    public int getMaxHorizontalBusPosition() {
        return maxHorizontalBusPosition;
    }

    public void setMaxHorizontalBusPosition(int maxHorizontalBusPosition) {
        this.maxHorizontalBusPosition = maxHorizontalBusPosition;
    }

    public int getMaxVerticalBusPosition() {
        return maxVerticalBusPosition;
    }

    public void setMaxVerticalBusPosition(int maxVerticalBusPosition) {
        this.maxVerticalBusPosition = maxVerticalBusPosition;
    }

    public double getFirstBusY() {
        return getExternCellHeight(Direction.TOP);
    }

    public double getLastBusY(double verticalSpaceBus) {
        return getFirstBusY() + (getMaxVerticalBusPosition() - 1) * verticalSpaceBus;
    }

    public double getInnerHeight(double verticalSpaceBus) {
        return getExternCellHeight(Direction.TOP) + verticalSpaceBus * getMaxV() + getExternCellHeight(Direction.BOTTOM);
    }

    public void substituteNodesMirroringGroundDisconnectionComponent() {
        List<GroundDisconnection> groundDisconnections = getFeederNodeStream(FeederType.GROUND)
                .map(this::getGroundDisconnection)
                .filter(Objects::nonNull)
                .toList();
        for (GroundDisconnection gd : groundDisconnections) {
            gd.nodes().forEach(this::removeNode);
            substituteNode(gd.forkNode(), NodeFactory.createGroundDisconnectionNode(this, gd.disconnector(), gd.ground()));
        }
    }

    private GroundDisconnection getGroundDisconnection(FeederNode groundFeederNode) {
        Set<Node> setFromGround = new LinkedHashSet<>();
        AtomicReference<SwitchNode> aSwitch = new AtomicReference<>();
        boolean groundDisconnectionSetIdentified = GraphTraversal.run(groundFeederNode,
                node -> node.getAdjacentEdges().size() > 2, // traversal ends successfully when encountering a fork node, which will be replaced by the ground disconnection component
                node -> node.getType() == NodeType.BUS || node.getType() == NodeType.FEEDER // traversal fails and stops when encountering a bus, a feeder, or more than one switch
                        || node.getType() == NodeType.SWITCH && aSwitch.getAndSet((SwitchNode) node) != null,
                setFromGround,
                Collections.emptySet());
        if (groundDisconnectionSetIdentified && aSwitch.get() != null && aSwitch.get().getKind() == SwitchNode.SwitchKind.DISCONNECTOR) {
            List<Node> list = setFromGround.stream().toList(); // the list contains the fork node which should not be removed but substituted
            return new GroundDisconnection(list.subList(0, list.size() - 1), groundFeederNode, aSwitch.get(), list.get(list.size() - 1));
        }
        return null;
    }

    public void substituteInternalMiddle2wtByEquipmentNodes() {
        for (FeederNode feederNode : getFeederNodeStream(FeederType.TWO_WINDINGS_TRANSFORMER_LEG).toList()) {
            if (nodesById.get(feederNode.getId()) == feederNode) { // check if not already removed
                substituteInternalMiddle2wtByEquipmentNode(feederNode);
            }
        }
    }

    private void substituteInternalMiddle2wtByEquipmentNode(FeederNode feederNode) {
        MiddleTwtNode middleNode = getMultiTermNodes().stream()
                .filter(m2wtn -> m2wtn.getAdjacentNodes().stream().anyMatch(n -> n == feederNode))
                .findFirst().orElse(null);
        Node otherSideNode = Optional.ofNullable(middleNode)
                .flatMap(m2wtn -> m2wtn.getAdjacentNodes().stream().filter(n -> n != feederNode).findFirst())
                .orElse(null);
        if (middleNode instanceof Middle2WTNode middle2WTNode
                && otherSideNode instanceof FeederNode otherSideFeederNode
                && otherSideFeederNode.getFeeder() instanceof FeederTwLeg otherSideFeederTwLeg
                && otherSideFeederTwLeg.getOwnVoltageLevelInfos().getId().equals(voltageLevelInfos.getId())) {

            ConnectivityNode connectivityNodeA = NodeFactory.createConnectivityNode(this, feederNode.getId(), ComponentTypeName.NODE);
            ConnectivityNode connectivityNodeB = NodeFactory.createConnectivityNode(this, otherSideNode.getId(), ComponentTypeName.NODE);

            // substitute nodes in voltage level
            substituteNode(feederNode, connectivityNodeA);
            substituteNode(otherSideNode, connectivityNodeB);

            // substitute the node which was "outside" the voltage level (multiTermNode / snake line) by a node inside the voltage level
            ConnectivityNode connectivityNode1 = otherSideFeederTwLeg.getSide() == NodeSide.ONE ? connectivityNodeB : connectivityNodeA;
            ConnectivityNode connectivityNode2 = otherSideFeederTwLeg.getSide() == NodeSide.ONE ? connectivityNodeA : connectivityNodeB;
            NodeFactory.createInternal2WTNode(this,
                    middle2WTNode.getId(), middle2WTNode.getName(), middle2WTNode.getEquipmentId(),
                    connectivityNode1, connectivityNode2, middle2WTNode.getComponentType());
            multiTermNodes.remove(middleNode);
            twtEdges.removeAll(middleNode.getAdjacentEdges());
        }
    }

    public void insertNodeNextTo(Node nodeToInsert, Node adjacentNode) {
        List<Node> neighbours = adjacentNode.getAdjacentNodes();
        if (neighbours.isEmpty()) {
            addEdge(nodeToInsert, adjacentNode);
        } else {
            insertNode(adjacentNode, nodeToInsert, neighbours.get(0));
        }
    }

    private record GroundDisconnection(List<Node> nodes, FeederNode ground, SwitchNode disconnector, Node forkNode) {
    }
}