CustomLabelProvider.java

/**
 * Copyright (c) 2025, 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.svg;

import com.powsybl.sld.layout.LayoutParameters;
import com.powsybl.sld.library.SldComponentLibrary;
import com.powsybl.sld.model.coordinate.Direction;
import com.powsybl.sld.model.nodes.*;
import com.powsybl.sld.model.nodes.feeders.FeederWithSides;

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

import static com.powsybl.sld.model.coordinate.Direction.*;

/**
 * Enables the configuration of content displayed in the SLD, for labels and feeder infos.
 * <p>
 * Customizations are defined in the constructor's map parameters.
 *
 * <p>
 * The labels map defines what will be displayed for the equipments, and it is indexed by the equipment ID.
 * The custom content is declared through an CustomLabels record: the label will be displayed in the diagram at its standard position;
 * The additionalLabel will be displayed on the equipment's right side.
 *
 * <p>
 * The feederInfosData map defines what will be displayed along the feeder, and it is indexed by the equipment ID (and a not-null side,
 * for feeders with sides such as lines and transformers), through the FeederContext record;
 * The custom content is declared via as a list of CustomFeederInfos records: the componentType is the info component type name;
 * labelDirection determines the direction (e.g., IN or OUT for the arrows); label is the string displayed next to the info component.
 *
 * @author Christian Biasuzzi {@literal <christian.biasuzzi at soft.it>}
 */
public class CustomLabelProvider extends AbstractLabelProvider {

    private static final double LABEL2_OFFSET = 6d;

    public record CustomLabels(String label, String additionalLabel) {
        public CustomLabels(String label) {
            this(label, null);
        }

        public boolean hasLabel() {
            return label != null && !label.isEmpty();
        }

        public boolean hasAdditionalLabel() {
            return additionalLabel != null && !additionalLabel.isEmpty();
        }
    }

    public record CustomFeederInfos(String componentType, LabelDirection labelDirection, String label) {
    }

    public record FeederContext(String feederId, NodeSide side) {
        public FeederContext(String feederId) {
            this(feederId, null);
        }
    }

    private final Map<String, CustomLabels> labels;

    private final Map<FeederContext, List<CustomFeederInfos>> feederInfosData;

    public CustomLabelProvider(Map<String, CustomLabels> labels, Map<FeederContext, List<CustomFeederInfos>> feederInfosData,
                               SldComponentLibrary componentLibrary, LayoutParameters layoutParameters, SvgParameters svgParameters) {
        super(componentLibrary, layoutParameters, svgParameters);
        this.labels = Objects.requireNonNull(labels);
        this.feederInfosData = Objects.requireNonNull(feederInfosData);
    }

    private Optional<CustomLabels> getEquipmentLabel(Node node) {
        if (node instanceof EquipmentNode eqNode) {
            return Optional.ofNullable(labels.get(eqNode.getEquipmentId()));
        } else {
            return Optional.empty();
        }
    }

    private List<FeederInfo> getCustomFeederInfos(FeederNode node, NodeSide side) {
        return feederInfosData.getOrDefault(new FeederContext(node.getEquipmentId(), side), List.of())
                .stream()
                .map(info -> new DirectionalFeederInfo(info.componentType(),
                        info.labelDirection(),
                        null,
                        info.label()))
                .collect(Collectors.toList());
    }

    @Override
    public List<FeederInfo> getFeederInfos(FeederNode node) {
        Objects.requireNonNull(node);

        Feeder feeder = node.getFeeder();
        List<FeederInfo> feederInfos = switch (feeder.getFeederType()) {
            case INJECTION -> getCustomFeederInfos(node, null);
            case BRANCH, HVDC, TWO_WINDINGS_TRANSFORMER_LEG, THREE_WINDINGS_TRANSFORMER_LEG -> getCustomFeederInfos(node, ((FeederWithSides) feeder).getSide());
            default -> List.of();
        };

        if (node.getDirection() == BOTTOM && !svgParameters.isFeederInfoSymmetry()) {
            Collections.reverse(feederInfos);
        }
        return feederInfos;
    }

    protected LabelPosition getAdditionalBusLabelPosition() {
        return new LabelPosition("NW_LABEL2", -LABEL2_OFFSET, 2 * LABEL2_OFFSET, false, 0);
    }

    protected LabelPosition getAdditionalLabelPosition(Node node, Direction direction) {
        double yShift = -LABEL2_OFFSET;
        String positionName = "";
        double angle = 0;
        if (direction != UNDEFINED) {
            yShift = direction == TOP
                    ? 2 * LABEL2_OFFSET
                    : ((int) (componentLibrary.getSize(node.getComponentType()).getHeight()) - 2 * LABEL2_OFFSET);
            positionName = direction == TOP ? "N" : "S";
            if (svgParameters.isLabelDiagonal()) {
                angle = direction == TOP ? -svgParameters.getAngleLabelShift() : svgParameters.getAngleLabelShift();
            }
        }
        double dx = (int) componentLibrary.getSize(node.getComponentType()).getWidth() + LABEL2_OFFSET;
        return new LabelPosition(positionName + "_LABEL2", dx, yShift, false, (int) angle);
    }

    private void addNodeLabels(CustomLabels labels, List<NodeLabel> nodeLabels, LabelPosition labelPosition, LabelPosition additionalLabelPosition) {
        if (labels.hasLabel()) {
            nodeLabels.add(new NodeLabel(labels.label(), labelPosition, null));
        }
        if (labels.hasAdditionalLabel()) {
            nodeLabels.add(new NodeLabel(labels.additionalLabel(), additionalLabelPosition, null));
        }
    }

    @Override
    public List<NodeLabel> getNodeLabels(Node node, Direction direction) {
        Objects.requireNonNull(node);
        Objects.requireNonNull(direction);

        List<NodeLabel> nodeLabels = new ArrayList<>();
        if (node instanceof BusNode) {
            getEquipmentLabel(node).ifPresent(l ->
                    addNodeLabels(l, nodeLabels, getBusLabelPosition(), getAdditionalBusLabelPosition()));
        } else if (node instanceof FeederNode || svgParameters.isDisplayEquipmentNodesLabel() && node instanceof EquipmentNode) {
            getEquipmentLabel(node).ifPresent(l ->
                    addNodeLabels(l, nodeLabels, getLabelPosition(node, direction), getAdditionalLabelPosition(node, direction)));
        } else if (svgParameters.isDisplayConnectivityNodesId() && node instanceof ConnectivityNode) {
            nodeLabels.add(new NodeLabel(node.getId(), getLabelPosition(node, direction), null));
        }
        return nodeLabels;
    }

    @Override
    public List<NodeDecorator> getNodeDecorators(Node node, Direction direction) {
        return List.of();
    }
}