MatrixZoneLayout.java

/**
 * Copyright (c) 2023, 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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.sld.layout;

import com.powsybl.commons.*;
import com.powsybl.sld.layout.pathfinding.*;
import com.powsybl.sld.layout.zonebygrid.*;
import com.powsybl.sld.model.coordinate.*;
import com.powsybl.sld.model.coordinate.Point;
import com.powsybl.sld.model.graphs.*;
import com.powsybl.sld.model.nodes.Node;
import org.jgrapht.alg.util.*;

import java.util.*;

/**
 * @author Thomas Adam {@literal <tadam at neverhack.com>}
 */
public class MatrixZoneLayout extends AbstractZoneLayout {
    private MatrixZoneLayoutModel model;

    private final String[][] matrixUserDefinition;

    protected MatrixZoneLayout(ZoneGraph graph, String[][] matrixUserDefinition, ZoneLayoutPathFinderFactory pathFinderFactory, SubstationLayoutFactory sLayoutFactory, VoltageLevelLayoutFactory vLayoutFactory) {
        super(graph, sLayoutFactory, vLayoutFactory);
        this.pathFinder = Objects.requireNonNull(pathFinderFactory).create();
        this.matrixUserDefinition = Objects.requireNonNull(matrixUserDefinition);
    }

    /**
     * Calculate relative coordinate of substations in the zone
     */
    @Override
    protected void calculateCoordSubstations(LayoutParameters layoutParameters) {
        // Update model information
        this.model = new MatrixZoneLayoutModel(matrixUserDefinition, layoutParameters);
        for (int row = 0; row < matrixUserDefinition.length; row++) {
            for (int col = 0; col < matrixUserDefinition[row].length; col++) {
                String id = matrixUserDefinition[row][col];
                SubstationGraph sGraph = getGraph().getSubstationGraph(id);
                if (sGraph == null && !id.isEmpty()) {
                    throw new PowsyblException("Substation '" + id + "' was not found in zone graph '" + getGraph().getId() + "'");
                }
                model.addSubstationGraph(sGraph, row, col);
            }
        }

        Matrix matrix = model.getMatrix();
        // Display substations on not empty Matrix cell
        matrix.stream().filter(c -> !c.isEmpty()).map(MatrixCell::graph).forEach(graph -> layoutBySubstation.get(graph).run(layoutParameters));
        // Height by rows
        int maxHeightRow = 0;
        // Width by col
        int maxWidthCol = 0;
        // Snakeline hallway (horizontal & vertical)
        int snakelineMargin = layoutParameters.getZoneLayoutSnakeLinePadding();
        // Zone size
        int nbRows = matrix.rowCount();
        int nbCols = matrix.columnCount();
        LayoutParameters.Padding diagramPadding = layoutParameters.getDiagramPadding();
        // Move each substation into its matrix position
        for (int row = 0; row < nbRows; row++) {
            maxWidthCol = 0;
            for (int col = 0; col < nbCols; col++) {
                MatrixCell cell = matrix.get(row, col);
                BaseGraph graph = cell.graph();
                if (graph != null) {
                    // Compute delta in order to center substations into own matrix cell
                    int deltaX = (int) (matrix.getMatrixCellWidth(col) % graph.getWidth()) / 2;
                    int deltaY = (int) (matrix.getMatrixCellHeight(row) % graph.getHeight()) / 2;
                    double dx = maxWidthCol + (col + 1.0) * snakelineMargin;
                    double dy = maxHeightRow + (row + 1.0) * snakelineMargin;
                    move(graph, dx + deltaX, dy + deltaY);
                }
                maxWidthCol += matrix.getMatrixCellWidth(col);
            }
            maxHeightRow += matrix.getMatrixCellHeight(row);
        }
        double zoneWidth = maxWidthCol + (nbCols + 1.0) * snakelineMargin;
        double zoneHeight = maxHeightRow + (nbRows + 1.0) * snakelineMargin;
        getGraph().setSize(diagramPadding.getLeft() + zoneWidth + diagramPadding.getRight(),
                diagramPadding.getTop() + zoneHeight + diagramPadding.getBottom());
    }

    @Override
    protected List<Point> calculatePolylineSnakeLine(LayoutParameters layoutParameters, Pair<Node, Node> nodes,
                                                     boolean increment) {
        List<Point> polyline = new ArrayList<>();
        Node node1 = nodes.getFirst();
        Node node2 = nodes.getSecond();
        VoltageLevelGraph vlGraph1 = getGraph().getVoltageLevelGraph(node1);
        VoltageLevelGraph vlGraph2 = getGraph().getVoltageLevelGraph(node2);
        SubstationGraph ss1Graph = getGraph().getSubstationGraph(node1).orElse(null);
        SubstationGraph ss2Graph = getGraph().getSubstationGraph(node2).orElse(null);
        if (ss1Graph != null && ss2Graph != null &&
                model.contains(ss1Graph.getId()) && model.contains(ss2Graph.getId())) { // in the same Zone
            Point p1 = vlGraph1.getShiftedPoint(node1);
            Point p2 = vlGraph2.getShiftedPoint(node2);
            Direction dNode1 = getNodeDirection(node1, 1);
            Direction dNode2 = getNodeDirection(node2, 2);
            polyline = new ArrayList<>();
            // Add starting point
            polyline.add(p1);
            // Find snakeline path
            polyline.addAll(model.buildSnakeline(pathFinder, ss1Graph.getId(), p1, dNode1, ss2Graph.getId(), p2, dNode2, layoutParameters));
            // Add ending point
            polyline.add(p2);
        }
        return polyline;
    }

    @Override
    public void manageSnakeLines(LayoutParameters layoutParameters) {
        model.computePathFindingGrid(getGraph(), layoutParameters);

        // Draw snakelines between Substations
        manageSnakeLines(getGraph(), layoutParameters);
    }
}