BlockPositionner.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.layout.position;

import com.powsybl.sld.model.cells.BusCell;
import com.powsybl.sld.model.cells.InternCell;
import com.powsybl.sld.model.coordinate.Side;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.BusNode;

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

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

/**
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 */
public class BlockPositionner {

    public void determineBlockPositions(VoltageLevelGraph graph, List<Subsection> subsections, Map<String, Side> busInfoMap) {
        int hPos = 0;
        int prevHPos = 0;
        int hSpace = 0;
        int maxV = graph.getMaxVerticalBusPosition();
        List<InternCell> nonFlatCellsToClose = new ArrayList<>();

        // Set vertical position of node buses based on busbarIndex
        // Note that busbarIndex starts at 1 but positions start at 0
        graph.getNodeBuses().forEach(nodeBus -> nodeBus.getPosition().set(V, nodeBus.getBusbarIndex() - 1));

        Subsection prevSs = new Subsection(maxV);
        for (Subsection ss : subsections) {
            List<BusNode> busNodesToClose = getBusNodesToClose(prevSs, ss);
            List<BusNode> busNodesToOpen = getBusNodesToOpen(prevSs, ss);

            if (busNodesToClose.stream().anyMatch(busNode -> busInfoMap.get(busNode.getId()) == Side.RIGHT)) {
                // Adding one cell on previous subsection right side
                hPos += 2; // A cell is 2 units wide
            }

            for (BusNode busNode : busNodesToClose) {
                closeBusNode(busNode, hPos - hSpace);
            }

            for (BusNode busNode : busNodesToOpen) {
                openBusNode(busNode, hPos);
            }

            if (busNodesToOpen.stream().anyMatch(busNode -> busInfoMap.get(busNode.getId()) == Side.LEFT)) {
                // Adding one cell on current subsection left side
                hPos += 2; // A cell is 2 units wide
            }

            hPos = placeCrossOverInternCells(hPos, ss.getInternCells(InternCell.Shape.CROSSOVER, Side.RIGHT), Side.RIGHT, nonFlatCellsToClose);
            hPos = placeVerticalCells(hPos, getVerticalCells(ss));
            hPos = placeCrossOverInternCells(hPos, ss.getInternCells(InternCell.Shape.CROSSOVER, Side.LEFT), Side.LEFT, nonFlatCellsToClose);
            if (hPos == prevHPos) {
                // Bus minimum width is one cell
                hPos += 2; // A cell is 2 units wide
            }
            hSpace = placeFlatInternCells(hPos, ss.getInternCells(InternCell.Shape.FLAT, Side.LEFT)) - hPos;
            hPos += hSpace;
            prevHPos = hPos;
            prevSs = ss;
        }

        List<BusNode> busNodesToClose = getBusNodesToClose(prevSs, new Subsection(maxV));
        if (busNodesToClose.stream().anyMatch(busNode -> busInfoMap.get(busNode.getId()) == Side.RIGHT)) {
            // Adding one cell on previous subsection right side
            hPos += 2; // A cell is 2 units wide
        }

        for (BusNode busNode : busNodesToClose) {
            closeBusNode(busNode, hPos - hSpace);
        }

        manageInternCellOverlaps(graph);
    }

    private static List<BusCell> getVerticalCells(Subsection ss) {
        List<BusCell> verticalCells = new ArrayList<>();
        verticalCells.addAll(ss.getVerticalInternCells());
        verticalCells.addAll(ss.getExternCells());
        verticalCells.addAll(ss.getArchCells());
        verticalCells.sort(Comparator.comparingInt(bc -> bc.getOrder().orElse(-1)));
        return verticalCells;
    }

    private List<BusNode> getBusNodesToClose(Subsection prevSS, Subsection ss) {
        List<BusNode> busNodesToClose = new ArrayList<>();
        for (int v = 0; v < prevSS.getSize(); v++) {
            BusNode prevBusNode = prevSS.getBusNode(v);
            BusNode actualBusNode = ss.getBusNode(v);
            if (prevBusNode != null && (actualBusNode == null || prevBusNode != actualBusNode)) {
                busNodesToClose.add(prevBusNode);
            }
        }
        return busNodesToClose;
    }

    private List<BusNode> getBusNodesToOpen(Subsection prevSS, Subsection ss) {
        List<BusNode> busNodesToOpen = new ArrayList<>();
        for (int v = 0; v < prevSS.getSize(); v++) {
            BusNode prevBusNode = prevSS.getBusNode(v);
            BusNode actualBusNode = ss.getBusNode(v);
            if (actualBusNode != null && (prevBusNode == null || prevBusNode != actualBusNode)) {
                busNodesToOpen.add(actualBusNode);
            }
        }
        return busNodesToOpen;
    }

    private void closeBusNode(BusNode busNode, int hPositionRight) {
        int positionLeft = Math.max(busNode.getPosition().get(H), 0);
        busNode.getPosition().setSpan(H, hPositionRight - positionLeft);
    }

    private void openBusNode(BusNode busNode, int hPositionLeft) {
        busNode.getPosition().set(H, hPositionLeft);
    }

    private int placeVerticalCells(int hPos, Collection<BusCell> busCells) {
        int hPosRes = hPos;
        for (BusCell cell : busCells) {
            hPosRes = cell.newHPosition(hPosRes);
        }
        return hPosRes;
    }

    private int placeFlatInternCells(int hPos, List<InternCell> cells) {
        int hPosRes = hPos;
        for (BusCell cell : cells) {
            hPosRes = Math.max(hPosRes, cell.newHPosition(hPos));
        }
        return hPosRes;
    }

    private int placeCrossOverInternCells(int hPos,
                                          List<InternCell> cells,
                                          Side side, List<InternCell> nonFlatCellsToClose) {
        // side, is the side from the InternCell standpoint. The left side of the internCell shall be on the right of the subsection
        int hPosRes = hPos;
        cells.sort(Comparator.comparingInt(c -> -nonFlatCellsToClose.indexOf(c)));
        for (InternCell cell : cells) {
            hPosRes = cell.newHPosition(hPosRes, side);
        }
        if (side == Side.LEFT) {
            nonFlatCellsToClose.addAll(cells);
        } else {
            nonFlatCellsToClose.removeAll(cells);
        }
        return hPosRes;
    }

    private void manageInternCellOverlaps(VoltageLevelGraph graph) {
        List<InternCell> cellsToHandle = graph.getInternCellStream()
                .filter(internCell -> internCell.checkIsNotShape(InternCell.Shape.FLAT, InternCell.Shape.UNDEFINED, InternCell.Shape.UNHANDLED_PATTERN))
                .collect(Collectors.toList());
        InternCellsLanes lane = new InternCellsLanes(cellsToHandle);
        lane.run();
    }

    /**
     * The class lane manages the overlaps of internCells.
     * After bundleToCompatibleLanes each lane contents non overlapping cells
     * arrangeLane at this stage balance the lanes on TOP and BOTTOM this could be improved by having various VPos per lane
     */
    private class InternCellsLanes {
        InternCellsLanes nextLane;
        List<InternCellsLanes> lanes;
        List<InternCell> cells;

        InternCellsLanes(List<InternCell> cells) {
            this.cells = new ArrayList<>(cells);
            lanes = new ArrayList<>();
            lanes.add(this);
        }

        InternCellsLanes(InternCell cell, List<InternCellsLanes> lanes) {
            this.lanes = lanes;
            lanes.add(this);
            cells = new ArrayList<>();
            addCell(cell);
        }

        void addCell(InternCell cell) {
            cells.add(cell);
        }

        void run() {
            bundleToCompatibleLanes();
            arrangeLanes();
        }

        private void bundleToCompatibleLanes() {
            Map<InternCell, List<InternCell>> incompatibilities = identifyIncompatibilities();
            while (!incompatibilities.isEmpty()) {
                shiftIncompatibilities(incompatibilities);
                incompatibilities = identifyIncompatibilities();
            }
            if (nextLane != null) {
                nextLane.bundleToCompatibleLanes();
            }
        }

        private Map<InternCell, List<InternCell>> identifyIncompatibilities() {
            Map<InternCell, List<InternCell>> incompatibilities = new LinkedHashMap<>();
            for (int i = 0; i < cells.size(); i++) {
                InternCell internCellA = cells.get(i);
                int hAmin = getHfromSide(internCellA, Side.LEFT);
                int hAmax = getHfromSide(internCellA, Side.RIGHT);

                for (int j = i + 1; j < cells.size(); j++) {
                    InternCell internCellB = cells.get(j);
                    int hBmin = getHfromSide(internCellB, Side.LEFT);
                    int hBmax = getHfromSide(internCellB, Side.RIGHT);
                    if (hAmin < hBmax && hBmin < hAmax) {
                        incompatibilities.putIfAbsent(internCellA, new ArrayList<>());
                        incompatibilities.get(internCellA).add(internCellB);
                        incompatibilities.putIfAbsent(internCellB, new ArrayList<>());
                        incompatibilities.get(internCellB).add(internCellA);
                    }
                }
            }
            return incompatibilities;
        }

        private int getHfromSide(InternCell cell, Side side) {
            if (cell.checkIsShape(InternCell.Shape.ONE_LEG)) {
                return cell.getSideHPos(Side.UNDEFINED);
            }
            return cell.getSideHPos(side);
        }

        private void shiftIncompatibilities(Map<InternCell, List<InternCell>> incompatibilities) {
            incompatibilities.entrySet().stream()
                    .filter(e -> !e.getValue().isEmpty())
                    .max(Comparator.comparingInt(e -> e.getValue().size()))
                    .map(Map.Entry::getKey)
                    .ifPresent(cell -> {
                        cells.remove(cell);
                        if (nextLane == null) {
                            nextLane = new InternCellsLanes(cell, lanes);
                        } else {
                            nextLane.addCell(cell);
                        }
                    });
        }

        private void arrangeLanes() {
            int i = 0;
            for (InternCellsLanes lane : lanes) {
                final int j = i % 2;
                final int newV = i / 2;
                lane.cells.forEach(c -> {
                    if (c.getDirection() == UNDEFINED) {
                        c.setDirection(j == 0 ? TOP : BOTTOM);
                    }
                    if (!c.checkIsShape(InternCell.Shape.ONE_LEG)) {
                        c.getBodyBlock().getPosition().set(V, newV);
                    }
                });
                i++;
            }
        }

        public String toString() {
            return (lanes.indexOf(this) + 1) + "/" + lanes.size() + " " + cells.toString();
        }
    }
}