PositionPredefined.java

/**
 * Copyright (c) 2020, 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.layout.position.predefined;

import com.powsybl.sld.layout.position.AbstractPositionFinder;
import com.powsybl.sld.layout.position.BSCluster;
import com.powsybl.sld.layout.position.Subsection;
import com.powsybl.sld.layout.position.VerticalBusSet;
import com.powsybl.sld.model.cells.*;
import com.powsybl.sld.model.coordinate.Side;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.BusNode;
import com.powsybl.sld.model.nodes.Node;
import com.powsybl.sld.model.coordinate.Direction;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.powsybl.sld.model.cells.Cell.CellType.EXTERN;

/**
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 */
public class PositionPredefined extends AbstractPositionFinder {
    private static final Logger LOGGER = LoggerFactory.getLogger(PositionPredefined.class);

    private static final Direction DEFAULTDIRECTION = Direction.TOP;

    private static final Comparator<VerticalBusSet> VBS_COMPARATOR = new Comparator<>() {
        @Override
        public int compare(VerticalBusSet vbs1, VerticalBusSet vbs2) {
            for (BusNode busNode : vbs1.getBusNodeSet()) {
                Optional<Integer> optionalSectionIndex2 = vbs2.getBusNodeSet().stream()
                        .filter(busNode2 -> busNode2.getBusbarIndex() == busNode.getBusbarIndex())
                        .findFirst().map(BusNode::getSectionIndex);
                if (optionalSectionIndex2.isPresent() && optionalSectionIndex2.get() != busNode.getSectionIndex()) {
                    return busNode.getSectionIndex() - optionalSectionIndex2.get();
                }
            }

            Optional<Integer> order1 = externCellOrderNb(vbs1);
            Optional<Integer> order2 = externCellOrderNb(vbs2);
            if (order1.isPresent() && order2.isPresent()) {
                return order1.get() - order2.get();
            }

            int h1max = getMaxPos(vbs1.getBusNodeSet(), BusNode::getSectionIndex);
            int h2max = getMaxPos(vbs2.getBusNodeSet(), BusNode::getSectionIndex);
            if (h1max != h2max) {
                return h1max - h2max;
            }

            int v1max = getMaxPos(vbs1.getBusNodeSet(), BusNode::getBusbarIndex);
            int v2max = getMaxPos(vbs2.getBusNodeSet(), BusNode::getBusbarIndex);
            if (v1max != v2max) {
                return v1max - v2max;
            }
            return vbs1.getBusNodeSet().size() - vbs2.getBusNodeSet().size();
        }

        private int getMaxPos(Set<BusNode> busNodes, Function<BusNode, Integer> fun) {
            return busNodes.stream()
                    .map(fun).max(Integer::compareTo).orElse(0);
        }

        private Optional<Integer> externCellOrderNb(VerticalBusSet vbs) {
            return vbs.getExternCells().stream().findFirst().map(exCell -> exCell.getOrder().orElse(-1));
        }

    };

    /**
     * Builds the layout of the bus nodes, and organises cells (order and directions)
     */

    @Override
    public Map<BusNode, Integer> indexBusPosition(List<BusNode> busNodes, List<BusCell> busCells) {
        Map<BusNode, Integer> busToNb = new HashMap<>();
        setMissingPositionIndices(busNodes, busCells);
        List<BusNode> busNodesSorted = busNodes.stream()
                .sorted(Comparator.comparingInt(BusNode::getBusbarIndex).thenComparing(BusNode::getSectionIndex))
                .collect(Collectors.toList());
        int i = 1;
        for (BusNode busNode : busNodesSorted) {
            busToNb.put(busNode, i++);
        }
        return busToNb;
    }

    /**
     * Look for missing/incoherent position indices in given bus nodes, and replace them with an appropriate value.
     * A missing/incoherent position index means a zero or negative value for busbar index and/or section index.
     * @param busNodes all voltageLevelGraph bus nodes
     * @param busCells all voltageLevelGraph bus cells
     */
    private static void setMissingPositionIndices(List<BusNode> busNodes, List<BusCell> busCells) {
        List<BusNode> missingIndicesBusNodes = busNodes.stream()
                .filter(busNode -> busNode.getBusbarIndex() <= 0 || busNode.getSectionIndex() <= 0)
                .collect(Collectors.toList());
        if (!missingIndicesBusNodes.isEmpty()) {
            int maxSectionIndex = busNodes.stream().mapToInt(BusNode::getSectionIndex).max().orElse(0);
            for (BusNode busNode : missingIndicesBusNodes) {
                setMissingPositionIndices(busNode, busNodes, busCells, maxSectionIndex);
                maxSectionIndex = Math.max(maxSectionIndex, busNode.getSectionIndex());
            }
        }
    }

    /**
     * Replace position indices in given bus node with an appropriate value: either with the same section index as the
     * first busNode which shares a BusCell with, or if no such busNode with a new section index.
     * @param busNode bus node with a missing/incoherent position index/indices
     * @param busNodes all voltageLevelGraph bus nodes
     * @param busCells all voltageLevelGraph bus cells
     * @param maxSectionIndex up-to-date max section index
     */
    private static void setMissingPositionIndices(BusNode busNode, List<BusNode> busNodes, List<BusCell> busCells, int maxSectionIndex) {
        int newSectionIndex = maxSectionIndex + 1;
        int newBusbarIndex = 1;
        for (BusCell busCell : busCells) {
            List<BusNode> cellBusNodes = busCell.getBusNodes();
            if (cellBusNodes.contains(busNode)) {
                int section = cellBusNodes.stream().mapToInt(BusNode::getSectionIndex).max().getAsInt();
                if (section > 0) {
                    newSectionIndex = section;
                    newBusbarIndex = 1 + busNodes.stream()
                            .filter(bn -> bn.getSectionIndex() == section)
                            .mapToInt(BusNode::getBusbarIndex)
                            .max().orElse(0);
                    break;
                }
            }
        }

        LOGGER.warn("Incoherent position extension on busbar {} (busbar index: {}, section index: {}): setting busbar index to {} and section index to {}",
                busNode.getId(), busNode.getBusbarIndex(), busNode.getSectionIndex(), newBusbarIndex, newSectionIndex);
        busNode.setBusBarIndexSectionIndex(newBusbarIndex, newSectionIndex);
    }

    @Override
    public BSCluster organizeBusSets(VoltageLevelGraph graph, List<VerticalBusSet> verticalBusSets) {
        gatherLayoutExtensionInformation(graph);

        List<BSCluster> bsClusters = BSCluster.createBSClusters(
                verticalBusSets.stream().sorted(VBS_COMPARATOR).collect(Collectors.toList()));

        BSCluster bsCluster = bsClusters.get(0);

        while (bsClusters.size() != 1) {
            bsCluster.merge(Side.RIGHT, bsClusters.get(1), Side.LEFT, PositionPredefined::mergeHorizontalBusLists);
            bsClusters.remove(1);
        }
        bsCluster.sortHblByVPos();
        return bsCluster;
    }

    private static void gatherLayoutExtensionInformation(VoltageLevelGraph graph) {

        graph.getBusCellStream().forEach(bc -> {
            setDirection(bc);
            setOrder(bc);
        });

        List<ExternCell> problematicCells = graph.getExternCellStream()
                .filter(cell -> cell.getOrder().isEmpty()).collect(Collectors.toList());
        if (!problematicCells.isEmpty()) {
            LOGGER.warn("Unable to build the layout only with Extension\nproblematic cells :");
            problematicCells.forEach(cell -> LOGGER
                    .info("Cell Nb : {}, Order : {}, Type : {}",
                            cell.getNumber(),
                            cell.getOrder().orElse(null),
                            cell.getType()));
        }
    }

    private static void setDirection(BusCell bc) {
        List<Direction> listOfDirectionsInsideCell = bc.getNodes().stream().map(Node::getDirection)
                .filter(d -> d != Direction.UNDEFINED).distinct().collect(Collectors.toList());
        int numberOfDirectionsInsideCell = listOfDirectionsInsideCell.size();
        if (numberOfDirectionsInsideCell == 0) {
            if (bc.getType() == EXTERN) {
                bc.setDirection(DEFAULTDIRECTION);
            }
            // The intern cells with undefined direction cannot be forced to a default position, as they are dealt with
            // later to avoid overlap, see BlockPositionner::manageInternCellOverlaps. This cannot be done now, as the
            // flat intern cells haven't been yet identified
        } else {
            bc.setDirection(listOfDirectionsInsideCell.get(0));
            if (numberOfDirectionsInsideCell > 1) {
                LOGGER.warn("Directions inside cell are not consistent: {} directions found instead of 1", numberOfDirectionsInsideCell);
            }
        }
    }

    private static void setOrder(BusCell bc) {
        bc.getNodes().stream().map(Node::getOrder)
                .filter(Optional::isPresent)
                .mapToInt(Optional::get)
                .min()
                .ifPresent(bc::setOrder);
    }

    public static void mergeHorizontalBusLists(BSCluster leftCluster, BSCluster rightCluster) {
        //for this implementation, the busBar structuralPosition are already defined,
        // we must ensure that structuralPosition vPos when merging left and right HorizontalPosition,
        // and structuralPosition hPos are ordered
        leftCluster.getHorizontalBusLists().forEach(hbl -> {
            BusNode rightNodeOfLeftHbl = hbl.getSideNode(Side.RIGHT);
            rightCluster.getHorizontalBusLists().stream()
                    .filter(hbl2 -> hbl2.getSideNode(Side.LEFT).getBusbarIndex() == rightNodeOfLeftHbl.getBusbarIndex())
                    .findFirst()
                    .ifPresent(rightHbl -> {
                        BusNode leftNodeOfRightHbl = rightHbl.getSideNode(Side.LEFT);
                        if (leftNodeOfRightHbl == rightNodeOfLeftHbl
                                || rightNodeOfLeftHbl.getSectionIndex() < leftNodeOfRightHbl.getSectionIndex()) {
                            hbl.merge(rightHbl);
                            rightCluster.removeHbl(rightHbl);
                        }
                    });
        });
        mergeHblWithNoLink(leftCluster, rightCluster);
    }

    @Override
    public void organizeDirections(VoltageLevelGraph graph, List<Subsection> subsections) {
        // Force same orientation for shunted cells
        graph.getShuntCellStream().forEach(sc -> sc.alignDirections(Side.LEFT));
    }
}