SvgWriter.java

/**
 * Copyright (c) 2021-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/.
 */
package com.powsybl.nad.svg;

import com.powsybl.commons.exceptions.UncheckedXmlStreamException;
import com.powsybl.commons.xml.XmlUtil;
import com.powsybl.nad.model.*;
import org.apache.commons.io.output.WriterOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.jgrapht.alg.util.Pair;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
 */
public class SvgWriter {

    private static final String INDENT = "    ";
    public static final String SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
    public static final String XHTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml";
    private static final String SVG_ROOT_ELEMENT_NAME = "svg";
    private static final String STYLE_ELEMENT_NAME = "style";
    private static final String GROUP_ELEMENT_NAME = "g";
    private static final String POLYLINE_ELEMENT_NAME = "polyline";
    private static final String PATH_ELEMENT_NAME = "path";
    private static final String CIRCLE_ELEMENT_NAME = "circle";
    private static final String TEXT_ELEMENT_NAME = "text";
    private static final String FOREIGN_OBJECT_ELEMENT_NAME = "foreignObject";
    private static final String DIV_ELEMENT_NAME = "div";
    private static final String SPAN_ELEMENT_NAME = "span";
    private static final String USE_ELEMENT_NAME = "use";
    private static final String ID_ATTRIBUTE = "id";
    private static final String WIDTH_ATTRIBUTE = "width";
    private static final String HEIGHT_ATTRIBUTE = "height";
    private static final String VIEW_BOX_ATTRIBUTE = "viewBox";
    private static final String DESCRIPTION_ATTRIBUTE = "desc";
    private static final String CLASS_ATTRIBUTE = "class";
    private static final String STYLE_ATTRIBUTE = "style";
    private static final String TRANSFORM_ATTRIBUTE = "transform";
    private static final String CIRCLE_RADIUS_ATTRIBUTE = "r";
    private static final String PATH_D_ATTRIBUTE = "d";
    private static final String X_ATTRIBUTE = "x";
    private static final String Y_ATTRIBUTE = "y";
    private static final String POINTS_ATTRIBUTE = "points";
    private static final String HREF_ATTRIBUTE = "href";

    private final SvgParameters svgParameters;
    private final StyleProvider styleProvider;
    private final LabelProvider labelProvider;
    private final EdgeRendering edgeRendering;

    public SvgWriter(SvgParameters svgParameters, StyleProvider styleProvider, LabelProvider labelProvider) {
        this.svgParameters = Objects.requireNonNull(svgParameters);
        this.styleProvider = Objects.requireNonNull(styleProvider);
        this.labelProvider = Objects.requireNonNull(labelProvider);
        this.edgeRendering = new DefaultEdgeRendering();
    }

    public void writeSvg(Graph graph, Path svgFile) {
        Objects.requireNonNull(svgFile);
        Path dir = svgFile.toAbsolutePath().getParent();
        String svgFileName = svgFile.getFileName().toString();
        if (!svgFileName.endsWith(".svg")) {
            svgFileName = svgFileName + ".svg";
        }
        try (OutputStream svgOs = new BufferedOutputStream(Files.newOutputStream(dir.resolve(svgFileName)))) {
            writeSvg(graph, svgOs);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void writeSvg(Graph graph, Writer svgWriter) {
        try (WriterOutputStream svgOs = new WriterOutputStream(svgWriter, StandardCharsets.UTF_8)) {
            writeSvg(graph, svgOs);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void writeSvg(Graph graph, OutputStream svgOs) {
        Objects.requireNonNull(graph);
        Objects.requireNonNull(svgOs);

        // Edge coordinates need to be computed first, based on svg parameters
        edgeRendering.run(graph, svgParameters);

        try {
            XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, svgOs);
            addSvgRoot(graph, writer);
            addStyle(writer);
            if (this.svgParameters.isHighlightGraph()) {
                drawHighlightedSection(graph, writer);
            }
            drawVoltageLevelNodes(graph, writer);
            drawBranchEdges(graph, writer);
            drawThreeWtEdges(graph, writer);
            drawThreeWtNodes(graph, writer);
            drawTextEdges(graph, writer);
            drawTextNodes(graph, writer);

            writer.writeEndDocument();
        } catch (XMLStreamException e) {
            throw new UncheckedXmlStreamException(e);
        }
    }

    private void drawHighlightedSection(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.HIGHLIGHT_CLASS);
        drawHighlightVoltageLevelNodes(graph, writer);
        drawHighlightBranchEdges(graph, writer);
        drawHighlightThreeWtEdges(graph, writer);
        writer.writeEndElement();
    }

    private void drawBranchEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.BRANCH_EDGES_CLASS);
        for (BranchEdge edge : graph.getBranchEdges()) {
            writer.writeStartElement(GROUP_ELEMENT_NAME);
            writeId(writer, edge);
            writeStyleClasses(writer, styleProvider.getBranchEdgeStyleClasses(edge));
            insertName(writer, edge::getName);
            drawHalfEdge(graph, writer, edge, BranchEdge.Side.ONE);
            drawHalfEdge(graph, writer, edge, BranchEdge.Side.TWO);
            drawEdgeCenter(writer, edge);
            writer.writeEndElement();
        }
        writer.writeEndElement();
    }

    private void drawHighlightBranchEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.BRANCH_EDGES_CLASS);
        for (BranchEdge edge : graph.getBranchEdges()) {
            drawHighlightHalfEdge(graph, writer, edge, BranchEdge.Side.ONE);
            drawHighlightHalfEdge(graph, writer, edge, BranchEdge.Side.TWO);
        }
        writer.writeEndElement();
    }

    private void drawEdgeLabel(XMLStreamWriter writer, BranchEdge edge, String edgeLabel) throws XMLStreamException {

        if (edgeLabel == null || edgeLabel.isEmpty()) {
            return;
        }

        List<Point> points1 = edge.getPoints1();
        List<Point> points2 = edge.getPoints2();
        Point anchorPoint = Point.createMiddlePoint(points1.get(points1.size() - 1), points2.get(points2.size() - 1));

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_LABEL_CLASS);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(anchorPoint));

        if (edge.isVisible(BranchEdge.Side.ONE) && edge.isVisible(BranchEdge.Side.TWO)) {
            drawEdgeMiddleLabel(edgeLabel, edge, writer);
        } else if (edge.isVisible(BranchEdge.Side.ONE)) {
            drawHalfEdgeLabel(edgeLabel, edge, BranchEdge.Side.ONE, writer);
        } else {
            drawHalfEdgeLabel(edgeLabel, edge, BranchEdge.Side.TWO, writer);
        }

        writer.writeEndElement();
    }

    private void drawEdgeMiddleLabel(String edgeLabel, BranchEdge edge, XMLStreamWriter writer) throws XMLStreamException {
        double edgeEndAngle = edge.getEdgeEndAngle(BranchEdge.Side.ONE);
        drawLabel(writer, edgeLabel, 0, "text-anchor:middle", computeTextAngle(edgeEndAngle), X_ATTRIBUTE);
    }

    private void drawHalfEdgeLabel(String edgeLabel, BranchEdge edge, BranchEdge.Side side, XMLStreamWriter writer) throws XMLStreamException {
        double edgeEndAngle = edge.getEdgeEndAngle(side);
        String style = Math.cos(edgeEndAngle) < 0 ? "text-anchor:end" : "text-anchor:start";
        drawLabel(writer, edgeLabel, 0, style, computeTextAngle(edgeEndAngle), X_ATTRIBUTE);
    }

    private double computeTextAngle(double edgeEndAngle) {
        return Math.cos(edgeEndAngle) < 0 ? edgeEndAngle - Math.PI : edgeEndAngle;
    }

    private void drawEdgeCenter(XMLStreamWriter writer, BranchEdge edge) throws XMLStreamException {
        if (BranchEdge.DANGLING_LINE_EDGE.equals(edge.getType())) {
            return;
        }
        String edgeLabel = labelProvider.getLabel(edge);
        if (!BranchEdge.LINE_EDGE.equals(edge.getType()) || !StringUtils.isEmpty(edgeLabel)) {
            writer.writeStartElement(GROUP_ELEMENT_NAME);
            switch (edge.getType()) {
                case BranchEdge.PST_EDGE, BranchEdge.TWO_WT_EDGE:
                    draw2Wt(writer, edge);
                    break;
                case BranchEdge.HVDC_LINE_EDGE:
                    drawConverterStation(writer, edge);
                    break;
                default:
                    break;
            }
            drawEdgeLabel(writer, edge, edgeLabel);
            writer.writeEndElement();
        }
    }

    private void draw2Wt(XMLStreamWriter writer, BranchEdge edge) throws XMLStreamException {
        draw2WtWinding(writer, edge, BranchEdge.Side.ONE);
        draw2WtWinding(writer, edge, BranchEdge.Side.TWO);
        if (BranchEdge.PST_EDGE.equals(edge.getType())) {
            drawPstArrow(writer, edge);
        }
    }

    private void drawConverterStation(XMLStreamWriter writer, BranchEdge edge) throws XMLStreamException {
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        List<Point> line1 = edge.getPoints(BranchEdge.Side.ONE);
        List<Point> line2 = edge.getPoints(BranchEdge.Side.TWO);
        List<Point> points = new ArrayList<>(2);
        double halfWidth = svgParameters.getConverterStationWidth() / 2;
        if (line1.size() > 2) {
            points.add(line1.get(2).atDistance(halfWidth, line1.get(1)));
            points.add(line2.get(2).atDistance(halfWidth, line2.get(1)));
        } else {
            points.add(line1.get(1).atDistance(halfWidth, line1.get(0)));
            points.add(line2.get(1).atDistance(halfWidth, line2.get(0)));
        }
        String lineFormatted = points.stream()
                .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
                .collect(Collectors.joining(" "));
        writer.writeAttribute(POINTS_ATTRIBUTE, lineFormatted);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.HVDC_CLASS);
    }

    private void drawThreeWtEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        List<ThreeWtEdge> threeWtEdges = graph.getThreeWtEdges();
        if (threeWtEdges.isEmpty()) {
            return;
        }

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.THREE_WT_EDGES_CLASS);
        for (ThreeWtEdge edge : threeWtEdges) {
            drawThreeWtEdge(graph, writer, edge);
        }
        writer.writeEndElement();
    }

    private void drawHighlightThreeWtEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        List<ThreeWtEdge> threeWtEdges = graph.getThreeWtEdges();
        if (threeWtEdges.isEmpty()) {
            return;
        }

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.THREE_WT_EDGES_CLASS);
        for (ThreeWtEdge edge : threeWtEdges) {
            drawHighlightThreeWtEdge(writer, edge);
        }
        writer.writeEndElement();
    }

    private void drawHalfEdge(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side) throws XMLStreamException {
        // the half edge is only drawn if visible, but if the edge is a TwoWtEdge, the transformer is still drawn
        if (!edge.isVisible(side) && !(edge.isTransformerEdge())) {
            return;
        }
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(edge.getDiagramId() + "." + side.getNum()));
        writeStyleClasses(writer, styleProvider.getSideEdgeStyleClasses(edge, side));
        if (edge.isVisible(side)) {
            Optional<EdgeInfo> edgeInfo = labelProvider.getEdgeInfo(graph, edge, side);
            if (!graph.isLoop(edge)) {
                drawHalfEdge(graph, writer, edge, side, edgeInfo.orElse(null));
            } else {
                drawLoopEdge(writer, edge, side, edgeInfo.orElse(null));
            }
        }
        writer.writeEndElement();
    }

    private void drawHalfEdge(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, EdgeInfo edgeInfo) throws XMLStreamException {
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        writeStyleClasses(writer, StyleProvider.EDGE_PATH_CLASS);
        writeStyleAttribute(writer, styleProvider.getSideEdgeStyle(edge, side));
        writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge, side));
        if (edgeInfo != null) {
            drawBranchEdgeInfo(graph, writer, edge, side, edgeInfo);
        }
    }

    private void drawHighlightHalfEdge(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side) throws XMLStreamException {
        if (edge.isVisible(side) && !graph.isLoop(edge)) {
            drawHighlightHalfEdge(writer, edge, side);
        }
    }

    private void drawHighlightHalfEdge(XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side) throws XMLStreamException {
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getHighlightSideEdgeStyleClasses(edge, side));
        writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge, side));
    }

    private void drawLoopEdge(XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, EdgeInfo edgeInfo) throws XMLStreamException {
        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_PATH_CLASS);
        writer.writeAttribute(PATH_D_ATTRIBUTE, getLoopPathString(edge, side));
        writeStyleAttribute(writer, styleProvider.getSideEdgeStyle(edge, side));
        if (edgeInfo != null) {
            drawLoopEdgeInfo(writer, edge, side, edgeInfo);
        }
    }

    private String getPolylinePointsString(BranchEdge edge, BranchEdge.Side side) {
        return getPolylinePointsString(edge.getPoints(side));
    }

    private String getPolylinePointsString(ThreeWtEdge edge) {
        return getPolylinePointsString(edge.getPoints());
    }

    private String getPolylinePointsString(List<Point> points) {
        return points.stream()
                .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
                .collect(Collectors.joining(" "));
    }

    private String getLoopPathString(BranchEdge edge, BranchEdge.Side side) {
        Object[] points = edge.getPoints(side).stream().flatMap(p -> Stream.of(p.getX(), p.getY())).toArray();
        return String.format(Locale.US, "M%.2f,%.2f L%.2f,%.2f C%.2f,%.2f %.2f,%.2f %.2f,%.2f", points);
    }

    private void drawThreeWtEdge(Graph graph, XMLStreamWriter writer, ThreeWtEdge edge) throws XMLStreamException {
        if (!edge.isVisible()) {
            return;
        }
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writeId(writer, edge);
        writeStyleClasses(writer, styleProvider.getThreeWtEdgeStyleClasses(edge));
        insertName(writer, edge::getName);
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        writeStyleClasses(writer, StyleProvider.EDGE_PATH_CLASS);
        writeStyleAttribute(writer, styleProvider.getThreeWtEdgeStyle(edge));
        writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge));

        Optional<EdgeInfo> edgeInfo = labelProvider.getEdgeInfo(graph, edge);
        if (edgeInfo.isPresent()) {
            drawThreeWtEdgeInfo(graph, writer, edge, edgeInfo.get());
        }
        writer.writeEndElement();
    }

    private void drawHighlightThreeWtEdge(XMLStreamWriter writer, ThreeWtEdge edge) throws XMLStreamException {
        if (!edge.isVisible()) {
            return;
        }
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getThreeWtEdgeStyleClasses(edge));
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getHighlightThreeWtEdgStyleClasses(edge));
        writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge));
        writer.writeEndElement();
    }

    private void drawThreeWtNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        List<ThreeWtNode> threeWtNodes = graph.getThreeWtNodesStream().collect(Collectors.toList());
        if (threeWtNodes.isEmpty()) {
            return;
        }

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.THREE_WT_NODES_CLASS);
        for (ThreeWtNode threeWtNode : threeWtNodes) {
            writer.writeStartElement(GROUP_ELEMENT_NAME);
            writeId(writer, threeWtNode);
            writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(threeWtNode.getPosition()));
            writeStyleClasses(writer, styleProvider.getNodeStyleClasses(threeWtNode));
            List<ThreeWtEdge> edges = graph.getThreeWtEdgeStream(threeWtNode).collect(Collectors.toList());
            for (ThreeWtEdge edge : edges) {
                draw3WtWinding(edge, threeWtNode, writer);
                if (ThreeWtEdge.PST_EDGE.equals(edge.getType())) {
                    drawPstArrow(writer, threeWtNode, edge);
                }
            }
            writer.writeEndElement();
        }
        writer.writeEndElement();
    }

    private void draw3WtWinding(ThreeWtEdge edge, ThreeWtNode threeWtNode, XMLStreamWriter writer) throws XMLStreamException {
        double radius = svgParameters.getTransformerCircleRadius();
        Point circleCenter = edge.getPoints().get(1).atDistance(radius, threeWtNode.getPosition());
        writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getThreeWtEdgeStyleClasses(edge), StyleProvider.WINDING_CLASS);
        writeStyleAttribute(writer, styleProvider.getThreeWtEdgeStyle(edge));
        writer.writeAttribute("cx", getFormattedValue(circleCenter.getX() - threeWtNode.getX()));
        writer.writeAttribute("cy", getFormattedValue(circleCenter.getY() - threeWtNode.getY()));
        writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(svgParameters.getTransformerCircleRadius()));
    }

    private void drawPstArrow(XMLStreamWriter writer, ThreeWtNode threeWtNode, ThreeWtEdge edge) throws XMLStreamException {
        double arrowSize = 3 * svgParameters.getTransformerCircleRadius();

        double delta = switch (edge.getSide()) {
            case ONE -> 1.5 * Math.PI;
            case TWO -> 0.75 * Math.PI;
            case THREE -> Math.PI;
        };

        double rotationAngle = edge.getEdgeAngle() + delta;

        double radius = svgParameters.getTransformerCircleRadius();
        Point circleCenter = edge.getPoints().get(1).atDistance(radius, threeWtNode.getPosition());
        Point p = new Point(circleCenter.getX() - threeWtNode.getX(), circleCenter.getY() - threeWtNode.getY());
        double[] matrix = getTransformMatrix(arrowSize, arrowSize, rotationAngle, p);

        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        writer.writeAttribute(PATH_D_ATTRIBUTE, getPstArrowPath(arrowSize));
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getMatrixString(matrix));
        writeStyleClasses(writer, styleProvider.getThreeWtEdgeStyleClasses(edge), StyleProvider.WINDING_CLASS);
        writeStyleAttribute(writer, styleProvider.getThreeWtEdgeStyle(edge));
    }

    private void drawLoopEdgeInfo(XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, EdgeInfo edgeInfo) throws XMLStreamException {
        drawEdgeInfo(writer, edgeInfo, edge.getPoints(side).get(1), edge.getEdgeStartAngle(side));
    }

    private void drawBranchEdgeInfo(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, EdgeInfo edgeInfo) throws XMLStreamException {
        VoltageLevelNode vlNode = graph.getVoltageLevelNode(edge, side);
        BusNode busNode = graph.getBusGraphNode(edge, side);
        drawEdgeInfo(writer, edgeInfo, getArrowCenter(vlNode, busNode, edge.getPoints(side)), edge.getEdgeEndAngle(side));
    }

    private void drawThreeWtEdgeInfo(Graph graph, XMLStreamWriter writer, ThreeWtEdge edge, EdgeInfo edgeInfo) throws XMLStreamException {
        VoltageLevelNode vlNode = graph.getVoltageLevelNode(edge);
        BusNode busNode = graph.getBusGraphNode(edge);
        drawEdgeInfo(writer, edgeInfo, getArrowCenter(vlNode, busNode, edge.getPoints()), edge.getEdgeAngle());
    }

    private void drawEdgeInfo(XMLStreamWriter writer, EdgeInfo edgeInfo, Point infoCenter, double edgeAngle) throws XMLStreamException {
        drawEdgeInfo(writer, Collections.emptyList(), edgeInfo, infoCenter, edgeAngle);
    }

    private void drawEdgeInfo(XMLStreamWriter writer, List<String> additionalStyles, EdgeInfo edgeInfo, Point infoCenter, double edgeAngle) throws XMLStreamException {

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writeStyleClasses(writer, additionalStyles, StyleProvider.EDGE_INFOS_CLASS);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(infoCenter));

        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getEdgeInfoStyleClasses(edgeInfo));
        drawInAndOutArrows(writer, edgeAngle);
        Optional<String> externalLabel = edgeInfo.getExternalLabel();
        if (externalLabel.isPresent()) {
            drawLabel(writer, externalLabel.get(), edgeAngle, true);
        }
        Optional<String> internalLabel = edgeInfo.getInternalLabel();
        if (internalLabel.isPresent()) {
            drawLabel(writer, internalLabel.get(), edgeAngle, false);
        }
        writer.writeEndElement();

        writer.writeEndElement();
    }

    private void drawInAndOutArrows(XMLStreamWriter writer, double edgeAngle) throws XMLStreamException {
        double rotationAngle = edgeAngle + (edgeAngle > Math.PI / 2 ? -3 * Math.PI / 2 : Math.PI / 2);
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getRotateString(rotationAngle));
        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.ARROW_IN_CLASS);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getScaleString(svgParameters.getArrowHeight()));
        writer.writeAttribute(PATH_D_ATTRIBUTE, labelProvider.getArrowPathDIn());
        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.ARROW_OUT_CLASS);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getScaleString(svgParameters.getArrowHeight()));
        writer.writeAttribute(PATH_D_ATTRIBUTE, labelProvider.getArrowPathDOut());
        writer.writeEndElement();
    }

    private void drawLabel(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
        if (svgParameters.isEdgeInfoAlongEdge()) {
            drawLabelAlongEdge(writer, label, edgeAngle, externalLabel);
        } else {
            drawLabelPerpendicularToEdge(writer, label, edgeAngle, externalLabel);
        }
    }

    private void drawLabelAlongEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
        boolean textFlipped = Math.cos(edgeAngle) < 0;
        String style = externalLabel == textFlipped ? "text-anchor:end" : null;
        double textAngle = textFlipped ? edgeAngle - Math.PI : edgeAngle;
        double shift = svgParameters.getArrowLabelShift() * (externalLabel ? 1 : -1);
        drawLabel(writer, label, textFlipped ? -shift : shift, style, textAngle, X_ATTRIBUTE);
    }

    private void drawLabelPerpendicularToEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
        boolean textFlipped = Math.sin(edgeAngle) > 0;
        double textAngle = textFlipped ? -Math.PI / 2 + edgeAngle : Math.PI / 2 + edgeAngle;
        double shift = svgParameters.getArrowLabelShift();
        double shiftAdjusted = externalLabel == textFlipped ? shift * 1.15 : -shift; // to have a nice compact rendering, shift needs to be adjusted, because of dominant-baseline:middle (text is expected to be a number, hence not below the line)
        drawLabel(writer, label, shiftAdjusted, "text-anchor:middle", textAngle, Y_ATTRIBUTE);
    }

    private void drawLabel(XMLStreamWriter writer, String label, double shift, String style, double textAngle, String shiftAxis) throws XMLStreamException {
        writer.writeStartElement(TEXT_ELEMENT_NAME);
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getRotateString(textAngle));
        writer.writeAttribute(shiftAxis, getFormattedValue(shift));
        writeStyleAttribute(writer, style);
        writer.writeCharacters(label);
        writer.writeEndElement();
    }

    private String getRotateString(double angleRad) {
        return "rotate(" + getFormattedValue(Math.toDegrees(angleRad)) + ")";
    }

    private String getScaleString(double scale) {
        return "scale(" + getFormattedValue(scale) + ")";
    }

    private String getMatrixString(double[] matrix) {
        return "matrix("
                + getFormattedValue(matrix[0]) + "," + getFormattedValue(matrix[1]) + ","
                + getFormattedValue(matrix[2]) + "," + getFormattedValue(matrix[3]) + ","
                + getFormattedValue(matrix[4]) + "," + getFormattedValue(matrix[5]) + ")";
    }

    private double[] getTransformMatrix(double width, double height, double angle, Point center) {
        double centerPosX = center.getX();
        double centerPosY = center.getY();

        double cosRo = Math.cos(angle);
        double sinRo = Math.sin(angle);
        double cdx = width / 2;
        double cdy = height / 2;

        double e1 = centerPosX - cdx * cosRo + cdy * sinRo;
        double f1 = centerPosY - cdx * sinRo - cdy * cosRo;

        return new double[]{+cosRo, sinRo, -sinRo, cosRo, e1, f1};
    }

    private Point getArrowCenter(VoltageLevelNode vlNode, BusNode busNode, List<Point> line) {
        double shift = svgParameters.getArrowShift();
        if (line.size() == 2) { // straight line; in case of a forking line it is the middle point which is the starting point
            double nodeOuterRadius = getVoltageLevelCircleRadius(vlNode);
            double busAnnulusOuterRadius = getBusAnnulusOuterRadius(busNode, vlNode, svgParameters);
            shift += nodeOuterRadius - busAnnulusOuterRadius;
        }
        return line.get(line.size() - 2).atDistance(shift, line.get(line.size() - 1));
    }

    private void draw2WtWinding(XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side) throws XMLStreamException {
        writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
        writeStyleClasses(writer, styleProvider.getSideEdgeStyleClasses(edge, side), StyleProvider.WINDING_CLASS);
        writeStyleAttribute(writer, styleProvider.getSideEdgeStyle(edge, side));
        List<Point> halfPoints = edge.getPoints(side);
        Point point1 = halfPoints.get(halfPoints.size() - 1); // point near 2wt
        Point point2 = halfPoints.get(halfPoints.size() - 2); // point near voltage level, or control point for loops
        double radius = svgParameters.getTransformerCircleRadius();
        Point circleCenter = point1.atDistance(-radius, point2);
        writer.writeAttribute("cx", getFormattedValue(circleCenter.getX()));
        writer.writeAttribute("cy", getFormattedValue(circleCenter.getY()));
        writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(radius));
    }

    private void drawPstArrow(XMLStreamWriter writer, BranchEdge edge) throws XMLStreamException {
        double arrowSize = 3 * svgParameters.getTransformerCircleRadius();
        double rotationAngle = edge.getEdgeEndAngle(BranchEdge.Side.ONE);

        List<Point> points1 = edge.getPoints1();
        List<Point> points2 = edge.getPoints2();
        Point middle = Point.createMiddlePoint(points1.get(points1.size() - 1), points2.get(points2.size() - 1));
        double[] matrix = getTransformMatrix(arrowSize, arrowSize, rotationAngle, middle);

        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        writer.writeAttribute(PATH_D_ATTRIBUTE, getPstArrowPath(arrowSize));
        writer.writeAttribute(TRANSFORM_ATTRIBUTE, getMatrixString(matrix));
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.PST_ARROW_CLASS);
    }

    private String getPstArrowPath(double arrowSize) {
        String d1 = getFormattedValue(arrowSize); // arrow size
        String dh = getFormattedValue(svgParameters.getPstArrowHeadSize()); // arrow head size
        String d2 = getFormattedValue(arrowSize - svgParameters.getPstArrowHeadSize()); // arrow size without the arrow head
        return String.format("M%s,0 0,%s M%s,0 %s,0 %s,%s", d1, d1, d2, d1, d1, dh);
    }

    private void drawVoltageLevelNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.VOLTAGE_LEVEL_NODES_CLASS);
        for (VoltageLevelNode vlNode : graph.getVoltageLevelNodesStream().filter(VoltageLevelNode::isVisible).collect(Collectors.toList())) {
            writer.writeStartElement(GROUP_ELEMENT_NAME);
            writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(vlNode));
            drawNode(graph, writer, vlNode);
            writer.writeEndElement();
        }
        writer.writeEndElement();
    }

    private void drawHighlightVoltageLevelNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.VOLTAGE_LEVEL_NODES_CLASS);
        for (VoltageLevelNode vlNode : graph.getVoltageLevelNodesStream().filter(VoltageLevelNode::isVisible).collect(Collectors.toList())) {
            drawHighlightedNode(writer, vlNode);
        }
        writer.writeEndElement();
    }

    private void drawHighlightedNode(XMLStreamWriter writer, VoltageLevelNode vlNode) throws XMLStreamException {
        writer.writeStartElement(USE_ELEMENT_NAME);
        writer.writeAttribute(HREF_ATTRIBUTE, "#" + getPrefixedId(vlNode.getDiagramId()));
        writeStyleClasses(writer, styleProvider.getHighlightNodeStyleClasses(vlNode));
        writer.writeEndElement();
    }

    private void drawTextNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        List<Pair<VoltageLevelNode, TextNode>> textNodes = graph.getVoltageLevelTextPairs().stream()
                .filter(nodePair -> nodePair.getSecond() != null)
                .toList();

        if (!textNodes.isEmpty()) {
            writeForeignObject(writer);
            writer.writeStartElement("", DIV_ELEMENT_NAME, XHTML_NAMESPACE_URI);
            writer.writeDefaultNamespace(XHTML_NAMESPACE_URI);
            for (Pair<VoltageLevelNode, TextNode> nodePair : textNodes) {
                writeDetailedTextNode(writer, nodePair.getSecond(), nodePair.getFirst());
            }
            writer.writeEndElement();
            writer.writeEndElement();
        }
    }

    private String getTranslateString(Node node) {
        return getTranslateString(node.getPosition());
    }

    private String getTranslateString(Point point) {
        return getTranslateString(point.getX(), point.getY());
    }

    private String getTranslateString(double x, double y) {
        return "translate(" + getFormattedValue(x) + "," + getFormattedValue(y) + ")";
    }

    private void writeForeignObject(XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(FOREIGN_OBJECT_ELEMENT_NAME);
        // width and height can be set neither to auto nor 0, due to firefox not displaying it in those cases
        // using a fixed size of 1x1 and CSS {overflow: visible} to display it
        writer.writeAttribute(HEIGHT_ATTRIBUTE, "1");
        writer.writeAttribute(WIDTH_ATTRIBUTE, "1");
        writeStyleClasses(writer, StyleProvider.TEXT_NODES_CLASS);
    }

    private void writeDetailedTextNode(XMLStreamWriter writer, TextNode textNode, VoltageLevelNode vlNode) throws XMLStreamException {
        writer.writeStartElement("", DIV_ELEMENT_NAME, XHTML_NAMESPACE_URI);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.LABEL_BOX_CLASS);
        long top = Math.round(textNode.getY());
        long left = Math.round(textNode.getX());
        writeStyleAttribute(writer, String.format("position: absolute; top: %spx; left: %spx", top, left));
        writeId(writer, textNode);

        List<String> vlDescription = labelProvider.getVoltageLevelDescription(vlNode);
        writeLines(vlDescription, writer);

        writeBusNodeLegend(writer, vlNode);

        List<String> vlDetails = labelProvider.getVoltageLevelDetails(vlNode);
        writeLines(vlDetails, writer);

        writer.writeEndElement();
    }

    private void writeLines(List<String> lines, XMLStreamWriter writer) throws XMLStreamException {
        for (String line : lines) {
            writer.writeStartElement(DIV_ELEMENT_NAME);
            writer.writeCharacters(line);
            writer.writeEndElement();
        }
    }

    private void writeBusNodeLegend(XMLStreamWriter writer, VoltageLevelNode vlNode) throws XMLStreamException {
        List<BusNode> notEmptyDescrBusNodes = vlNode.getBusNodeStream()
                .filter(busNode -> StringUtils.isNotEmpty(labelProvider.getBusDescription(busNode)))
                .toList();
        for (BusNode busNode : notEmptyDescrBusNodes) {
            writer.writeStartElement(DIV_ELEMENT_NAME);
            writer.writeEmptyElement(SPAN_ELEMENT_NAME);
            writeStyleClasses(writer, styleProvider.getBusNodeStyleClasses(busNode), StyleProvider.LEGEND_SQUARE_CLASS);
            writeStyleAttribute(writer, styleProvider.getBusNodeStyle(busNode));
            writer.writeCharacters(labelProvider.getBusDescription(busNode));
            writer.writeEndElement();
        }
    }

    private void drawNode(Graph graph, XMLStreamWriter writer, VoltageLevelNode vlNode) throws XMLStreamException {
        writeId(writer, vlNode);
        writeStyleClasses(writer, styleProvider.getNodeStyleClasses(vlNode));
        insertName(writer, vlNode::getName);

        double nodeOuterRadius = getVoltageLevelCircleRadius(vlNode);

        if (vlNode.hasUnknownBusNode()) {
            writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
            writeStyleClasses(writer, styleProvider.getBusNodeStyleClasses(BusNode.UNKNOWN));
            writeStyleAttribute(writer, styleProvider.getBusNodeStyle(BusNode.UNKNOWN));
            writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(nodeOuterRadius + svgParameters.getUnknownBusNodeExtraRadius()));
        }

        List<Edge> traversingBusEdges = new ArrayList<>();

        for (BusNode busNode : vlNode.getBusNodes()) {
            double busInnerRadius = getBusAnnulusInnerRadius(busNode, vlNode, svgParameters);
            double busOuterRadius = getBusAnnulusOuterRadius(busNode, vlNode, svgParameters);
            if (busInnerRadius == 0) {
                if (busNode instanceof BoundaryBusNode) {
                    // Boundary nodes are always at side two of a dangling line edge, dangling line is its only edge
                    double edgeStartAngle = getEdgeStartAngle(graph.getBusEdges(busNode).iterator().next(), BranchEdge.Side.TWO);
                    drawBoundarySemicircle(writer, busOuterRadius, edgeStartAngle);
                } else {
                    writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
                    writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(busOuterRadius));
                }
            } else {
                writer.writeEmptyElement(PATH_ELEMENT_NAME);
                writer.writeAttribute(PATH_D_ATTRIBUTE, getFragmentedAnnulusPath(busInnerRadius, busOuterRadius, traversingBusEdges, graph, vlNode, busNode));
            }
            writeId(writer, busNode);
            writeStyleClasses(writer, styleProvider.getBusNodeStyleClasses(busNode), StyleProvider.BUSNODE_CLASS);
            writeStyleAttribute(writer, styleProvider.getBusNodeStyle(busNode));

            traversingBusEdges.addAll(graph.getBusEdges(busNode));
        }
    }

    private void drawBoundarySemicircle(XMLStreamWriter writer, double radius, double edgeStartAngle) throws XMLStreamException {
        writer.writeEmptyElement(PATH_ELEMENT_NAME);
        double startAngle = -Math.PI / 2 + edgeStartAngle;
        String semiCircle = "M" + getCirclePath(radius, startAngle, startAngle + Math.PI, true);
        writer.writeAttribute(PATH_D_ATTRIBUTE, semiCircle);
    }

    private String getFragmentedAnnulusPath(double innerRadius, double outerRadius, List<Edge> traversingBusEdges, Graph graph, VoltageLevelNode vlNode, BusNode busNode) {
        if (traversingBusEdges.isEmpty()) {
            String path = "M" + getCirclePath(outerRadius, 0, Math.PI, true)
                    + " M" + getCirclePath(outerRadius, Math.PI, 0, true);
            if (innerRadius > 0) { // going the other way around (counter-clockwise) to subtract the inner circle
                path += "M" + getCirclePath(innerRadius, 0, Math.PI, false)
                        + "M" + getCirclePath(innerRadius, Math.PI, 0, false);
            }
            return path;
        }

        List<Double> angles = createSortedTraversingAnglesList(traversingBusEdges, graph, vlNode, busNode);

        // adding first angle to close the circle annulus, and adding 360�� to keep the list ordered
        angles.add(angles.get(0) + 2 * Math.PI);

        double halfWidth = svgParameters.getNodeHollowWidth() / 2;
        double deltaAngle0 = halfWidth / outerRadius;
        double deltaAngle1 = halfWidth / innerRadius;

        StringBuilder path = new StringBuilder();
        for (int i = 0; i < angles.size() - 1; i++) {
            double outerArcStart = angles.get(i) + deltaAngle0;
            double outerArcEnd = angles.get(i + 1) - deltaAngle0;
            double innerArcStart = angles.get(i + 1) - deltaAngle1;
            double innerArcEnd = angles.get(i) + deltaAngle1;
            if (outerArcEnd > outerArcStart && innerArcEnd < innerArcStart) {
                path.append("M").append(getCirclePath(outerRadius, outerArcStart, outerArcEnd, true))
                        .append(" L").append(getCirclePath(innerRadius, innerArcStart, innerArcEnd, false))
                        .append(" Z ");
            }
        }

        return path.toString();
    }

    private List<Double> createSortedTraversingAnglesList(List<Edge> traversingBusEdges, Graph graph, VoltageLevelNode vlNode, BusNode busNode) {
        List<Double> angles = new ArrayList<>(traversingBusEdges.size());
        for (Edge edge : traversingBusEdges) {
            Node node1 = graph.getNode1(edge);
            Node node2 = graph.getNode2(edge);
            if (node1 == node2) {
                // For looping edges we need to consider the two angles
                if (isBusNodeDrawn(graph.getBusGraphNode1(edge), busNode)) {
                    angles.add(getEdgeStartAngle(edge, BranchEdge.Side.ONE));
                }
                if (isBusNodeDrawn(graph.getBusGraphNode2(edge), busNode)) {
                    angles.add(getEdgeStartAngle(edge, BranchEdge.Side.TWO));
                }
            } else {
                angles.add(getEdgeStartAngle(edge, node1 == vlNode ? BranchEdge.Side.ONE : BranchEdge.Side.TWO));
            }
        }
        Collections.sort(angles);

        return angles;
    }

    private boolean isBusNodeDrawn(Node busGraphNode, BusNode busNodeCurrentlyDrawn) {
        if (busGraphNode == BusNode.UNKNOWN) {
            return false;
        }
        if (busGraphNode instanceof BusNode busGraphBusNode) {
            return busGraphBusNode.getRingIndex() < busNodeCurrentlyDrawn.getRingIndex();
        }
        return true;
    }

    private double getEdgeStartAngle(Edge edge, BranchEdge.Side side) {
        if (edge instanceof ThreeWtEdge) {
            return ((ThreeWtEdge) edge).getEdgeAngle();
        } else if (edge instanceof BranchEdge) {
            return ((BranchEdge) edge).getEdgeStartAngle(side);
        }
        return 0;
    }

    private String getCirclePath(double radius, double angleStart, double angleEnd, boolean clockWise) {
        double arcAngle = angleEnd - angleStart;
        double xStart = radius * Math.cos(angleStart);
        double yStart = radius * Math.sin(angleStart);
        double xEnd = radius * Math.cos(angleEnd);
        double yEnd = radius * Math.sin(angleEnd);
        int largeArc = Math.abs(arcAngle) > Math.PI ? 1 : 0;
        return String.format(Locale.US, "%.3f,%.3f A%.3f,%.3f %.3f %d %d %.3f,%.3f",
                xStart, yStart, radius, radius, Math.toDegrees(arcAngle), largeArc, clockWise ? 1 : 0, xEnd, yEnd);
    }

    private void insertName(XMLStreamWriter writer, Supplier<Optional<String>> getName) throws XMLStreamException {
        if (svgParameters.isInsertNameDesc()) {
            Optional<String> nodeName = getName.get();
            if (nodeName.isPresent()) {
                writer.writeStartElement(DESCRIPTION_ATTRIBUTE);
                writer.writeCharacters(nodeName.get());
                writer.writeEndElement();
            }
        }
    }

    private void drawTextEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement(GROUP_ELEMENT_NAME);
        writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.TEXT_EDGES_CLASS);
        for (TextEdge edge : graph.getTextEdges()) {
            drawTextEdge(writer, edge, graph.getVoltageLevelNode(edge));
        }
        writer.writeEndElement();
    }

    private void drawTextEdge(XMLStreamWriter writer, TextEdge edge, VoltageLevelNode vlNode) throws XMLStreamException {
        writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
        writeId(writer, edge);
        List<Point> points = edge.getPoints();
        shiftEdgeStart(points, vlNode);
        String lineFormatted1 = points.stream()
                .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
                .collect(Collectors.joining(" "));
        writer.writeAttribute(POINTS_ATTRIBUTE, lineFormatted1);
    }

    private void writeStyleClasses(XMLStreamWriter writer, String... additionalClasses) throws XMLStreamException {
        writeStyleClasses(writer, Collections.emptyList(), additionalClasses);
    }

    private void writeStyleClasses(XMLStreamWriter writer, List<String> edgeStyleClasses, String... additionalClasses) throws XMLStreamException {
        if (edgeStyleClasses.isEmpty() && additionalClasses.length == 0) {
            return;
        }
        List<String> allClasses = new ArrayList<>(edgeStyleClasses);
        allClasses.addAll(Arrays.asList(additionalClasses));
        writer.writeAttribute(CLASS_ATTRIBUTE, String.join(" ", allClasses));
    }

    private void writeStyleAttribute(XMLStreamWriter writer, String style) throws XMLStreamException {
        if (!StringUtils.isEmpty(style)) {
            writer.writeAttribute(STYLE_ATTRIBUTE, style);
        }
    }

    private void writeId(XMLStreamWriter writer, Identifiable identifiable) throws XMLStreamException {
        writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(identifiable.getDiagramId()));
    }

    private void shiftEdgeStart(List<Point> points, VoltageLevelNode vlNode) {
        double circleRadius = getVoltageLevelCircleRadius(vlNode);
        points.set(0, points.get(0).atDistance(circleRadius, points.get(1)));
    }

    private void addSvgRoot(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement("", SVG_ROOT_ELEMENT_NAME, SVG_NAMESPACE_URI);
        if (svgParameters.isSvgWidthAndHeightAdded()) {
            double[] diagramDimension = getDiagramDimensions(graph);
            writer.writeAttribute(WIDTH_ATTRIBUTE, getFormattedValue(diagramDimension[0]));
            writer.writeAttribute(HEIGHT_ATTRIBUTE, getFormattedValue(diagramDimension[1]));
        }
        writer.writeAttribute(VIEW_BOX_ATTRIBUTE, getViewBoxValue(graph));
        writer.writeDefaultNamespace(SVG_NAMESPACE_URI);
    }

    private double[] getDiagramDimensions(Graph graph) {
        double width = getDiagramWidth(graph);
        double height = getDiagramHeight(graph);
        double scale;
        switch (svgParameters.getSizeConstraint()) {
            case FIXED_WIDTH:
                scale = svgParameters.getFixedWidth() / width;
                break;
            case FIXED_HEIGHT:
                scale = svgParameters.getFixedHeight() / height;
                break;
            case FIXED_SCALE:
                scale = svgParameters.getFixedScale();
                break;
            default:
                scale = 1;
                break;
        }
        return new double[] {width * scale, height * scale};
    }

    private double getDiagramHeight(Graph graph) {
        Padding diagramPadding = svgParameters.getDiagramPadding();
        return graph.getHeight() + diagramPadding.getTop() + diagramPadding.getBottom();
    }

    private double getDiagramWidth(Graph graph) {
        Padding diagramPadding = svgParameters.getDiagramPadding();
        return graph.getWidth() + diagramPadding.getLeft() + diagramPadding.getRight();
    }

    private String getViewBoxValue(Graph graph) {
        Padding diagramPadding = svgParameters.getDiagramPadding();
        return getFormattedValue(graph.getMinX() - diagramPadding.getLeft()) + " "
                + getFormattedValue(graph.getMinY() - diagramPadding.getTop()) + " "
                + getFormattedValue(getDiagramWidth(graph)) + " " + getFormattedValue(getDiagramHeight(graph));
    }

    private void addStyle(XMLStreamWriter writer) throws XMLStreamException {
        switch (svgParameters.getCssLocation()) {
            case INSERTED_IN_SVG:
                writer.writeStartElement(STYLE_ELEMENT_NAME);
                writer.writeCData(styleProvider.getStyleDefs());
                writer.writeEndElement();
                break;
            case EXTERNAL_IMPORTED:
                writer.writeStartElement(STYLE_ELEMENT_NAME);
                for (String cssFilename : styleProvider.getCssFilenames()) {
                    writer.writeCharacters("@import url(" + cssFilename + ");");
                }
                writer.writeEndElement();
                break;
            case EXTERNAL_NO_IMPORT:
                // nothing to do
                break;
        }
    }

    private static String getFormattedValue(double value) {
        return String.format(Locale.US, "%.2f", value);
    }

    protected double getVoltageLevelCircleRadius(VoltageLevelNode vlNode) {
        return getVoltageLevelCircleRadius(vlNode, svgParameters);
    }

    protected static double getVoltageLevelCircleRadius(VoltageLevelNode vlNode, SvgParameters svgParameters) {
        if (vlNode.isFictitious()) {
            return svgParameters.getFictitiousVoltageLevelCircleRadius();
        }
        int nbBuses = vlNode.getBusNodes().size();
        return Math.min(Math.max(nbBuses, 1), 2) * svgParameters.getVoltageLevelCircleRadius();
    }

    public static double getBusAnnulusInnerRadius(BusNode node, VoltageLevelNode vlNode, SvgParameters svgParameters) {
        if (node.getRingIndex() == 0) {
            return 0;
        }
        int nbNeighbours = node.getNbNeighbouringBusNodes();
        double unitaryRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) / (nbNeighbours + 1);
        return node.getRingIndex() * unitaryRadius + svgParameters.getInterAnnulusSpace() / 2;
    }

    public static double getBusAnnulusOuterRadius(BusNode node, VoltageLevelNode vlNode, SvgParameters svgParameters) {
        int nbNeighbours = node.getNbNeighbouringBusNodes();
        double unitaryRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) / (nbNeighbours + 1);
        return (node.getRingIndex() + 1) * unitaryRadius - svgParameters.getInterAnnulusSpace() / 2;
    }

    public String getPrefixedId(String id) {
        return svgParameters.getSvgPrefix() + id;
    }
}