All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sencha.gxt.chart.client.draw.engine.SVG Maven / Gradle / Ivy

The newest version!
/**
 * Sencha GXT 3.1.1 - Sencha for GWT
 * Copyright(c) 2007-2014, Sencha, Inc.
 * [email protected]
 *
 * http://www.sencha.com/products/gxt/license/
 */
package com.sencha.gxt.chart.client.draw.engine;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.ui.RootPanel;
import com.sencha.gxt.chart.client.draw.Color;
import com.sencha.gxt.chart.client.draw.DrawComponent;
import com.sencha.gxt.chart.client.draw.Gradient;
import com.sencha.gxt.chart.client.draw.Matrix;
import com.sencha.gxt.chart.client.draw.Rotation;
import com.sencha.gxt.chart.client.draw.Scaling;
import com.sencha.gxt.chart.client.draw.Stop;
import com.sencha.gxt.chart.client.draw.Translation;
import com.sencha.gxt.chart.client.draw.path.PathSprite;
import com.sencha.gxt.chart.client.draw.sprite.CircleSprite;
import com.sencha.gxt.chart.client.draw.sprite.EllipseSprite;
import com.sencha.gxt.chart.client.draw.sprite.ImageSprite;
import com.sencha.gxt.chart.client.draw.sprite.RectangleSprite;
import com.sencha.gxt.chart.client.draw.sprite.Sprite;
import com.sencha.gxt.chart.client.draw.sprite.TextSprite;
import com.sencha.gxt.chart.client.draw.sprite.TextSprite.TextAnchor;
import com.sencha.gxt.chart.client.draw.sprite.TextSprite.TextBaseline;
import com.sencha.gxt.core.client.Style.HideMode;
import com.sencha.gxt.core.client.dom.XDOM;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.core.client.util.PreciseRectangle;

/**
 * Provides specific methods to draw with SVG.
 */
public class SVG extends DomSurface {
  /**
   * {@link JavaScriptObject} representing bounding box results of an
   * {@link SVG} text element.
   */
  public static final class TextBBox extends JavaScriptObject {

    protected TextBBox() {
    }

    /**
     * Returns the height of the SVG text element
     * 
     * @return the height of the SVG text element
     */
    public native double getHeight() /*-{
			return this.height;
    }-*/;

    /**
     * Returns the width of the SVG text element
     * 
     * @return the width of the SVG text element
     */
    public native double getWidth() /*-{
			return this.width;
    }-*/;

    /**
     * Returns the x-coordinate of the SVG text element
     * 
     * @return the x-coordinate of the SVG text element
     */
    public native double getX() /*-{
			return this.x;
    }-*/;

    /**
     * Returns the y-coordinate of the SVG text element
     * 
     * @return the y-coordinate of the SVG text element
     */
    public native double getY() /*-{
			return this.y;
    }-*/;

  }

  private Element defs;
  private Map gradientIds = new HashMap();
  protected Map clipElements = new HashMap();

  @Override
  public void addGradient(Gradient gradient) {
    Element gradientElement;
    double radAngle = Math.toRadians(gradient.getAngle());
    double[] vector = {0, 0, Math.cos(radAngle), Math.sin(radAngle)};
    double temp = Math.max(Math.abs(vector[2]), Math.abs(vector[3]));
    if (temp == 0) temp = 1;
    double max = 1 / temp;
    vector[2] *= max;
    vector[3] *= max;
    if (vector[2] < 0) {
      vector[0] = -vector[2];
      vector[2] = 0;
    }
    if (vector[3] < 0) {
      vector[1] = -vector[3];
      vector[3] = 0;
    }
    gradientElement = this.createSVGElement("linearGradient");
    gradientElement.setAttribute("x1", String.valueOf(vector[0]));
    gradientElement.setAttribute("y1", String.valueOf(vector[1]));
    gradientElement.setAttribute("x2", String.valueOf(vector[2]));
    gradientElement.setAttribute("y2", String.valueOf(vector[3]));
    String id = XDOM.getUniqueId();
    gradientIds.put(gradient, id);
    gradientElement.setId(id);
    getDefinitions().appendChild(gradientElement);
    for (int i = 0; i < gradient.getStops().size(); i++) {
      Stop stop = gradient.getStops().get(i);
      Element stopEl = createSVGElement("stop");
      stopEl.setAttribute("offset", String.valueOf(stop.getOffset()) + "%");
      stopEl.setAttribute("stop-color", stop.getColor().getColor());
      stopEl.setAttribute("stop-opacity", String.valueOf(stop.getOpacity()));
      gradientElement.appendChild(stopEl);
    }

  }

  @Override
  public void draw() {
    super.draw();
    if (surfaceElement == null) {
      surfaceElement = this.createSVGElement("svg");
      surfaceElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
      surfaceElement.setAttribute("version", "1.1");
      surfaceElement.setAttribute("width", String.valueOf(width));
      surfaceElement.setAttribute("height", String.valueOf(height));
      //Ensure SVG element follows the spec http://www.w3.org/TR/SVG/styling.html#UAStyleSheet
      // e.g. IE9
      surfaceElement.getStyle().setOverflow(Overflow.HIDDEN);
      Element bgRect = this.createSVGElement("rect");
      bgRect.setAttribute("width", "100%");
      bgRect.setAttribute("height", "100%");
      bgRect.setAttribute("fill", "#000");
      bgRect.setAttribute("stroke", "none");
      bgRect.setAttribute("opacity", "0");

      surfaceElement.appendChild(getDefinitions());
      surfaceElement.appendChild(bgRect);
      container.appendChild(surfaceElement);
    }

    renderAll();
  }

  @Override
  public void deleteSprite(Sprite sprite) {
    super.deleteSprite(sprite);
    clipElements.remove(sprite);
  }

  @Override
  public void renderSprite(Sprite sprite) {
    if (surfaceElement == null) {
      return;
    }
    if (getElement(sprite) == null) {
      createSprite(sprite);
    }
    if (!sprite.isDirty()) {
      return;
    }
    if (sprite.isZIndexDirty()) {
      applyZIndex(sprite);
    }

    applyAttributes(sprite);
    if (sprite.isTransformDirty()) {
      transform(sprite);
    }
    sprite.clearDirtyFlags();
  }

  /**
   * Associates the given sprite with the given clip element.
   * 
   * @param sprite the sprite
   * @param element the clip element
   */
  protected void setClipElement(Sprite sprite, XElement element) {
    clipElements.put(sprite, element);
  }

  @Override
  public void setCursor(Sprite sprite, String property) {
    XElement element = getElement(sprite);
    if (element != null) {
      element.getStyle().setProperty("cursor", property);
    }
  }

  @Override
  public void setViewBox(double x, double y, double width, double height) {
    surfaceElement.setAttribute(
        "viewBox",
        new StringBuilder().append(x).append(", ").append(y).append(", ").append(width).append(", ").append(height).toString());
  }

  @Override
  protected PreciseRectangle getBBoxText(TextSprite sprite) {
    PreciseRectangle bbox = new PreciseRectangle();
    XElement element = getElement(sprite);

    if (element != null) {
      try {
        TextBBox box = getTextBBox(element);
        bbox.setX(box.getX());
        bbox.setY(box.getY());
        bbox.setWidth(box.getWidth());
        bbox.setHeight(box.getHeight());
      } catch (JavaScriptException e) {
        //ignore, some browsers return 0s without exception (webkit), some blow up (gecko)
      }
      // If we read out a zero, but the sprite doesn't have blank text, we're probably
      // drawing while hidemode=hidden - try again with offsets
      if (bbox.equals(new PreciseRectangle()) && !"".equals(sprite.getText())) {
        DrawComponent hidden = getHiddenSVG();

        TextSprite copy = sprite.copy();
        hidden.addSprite(copy);
        copy.redraw();

        try {
          TextBBox box = getTextBBox(((DomSurface)hidden.getSurface()).getElement(copy));
          bbox.setX(box.getX());
          bbox.setY(box.getY());
          bbox.setWidth(box.getWidth());
          bbox.setHeight(box.getHeight());
        } catch (JavaScriptException e2) {
          // leave the bbox zeroed out if the second try with getTextBBox returns an error
        }
      }
    }
    return bbox;
  }

  /**
   * Returns the clip element associated with the given sprite.
   * 
   * @param sprite the sprite
   * @return the clip element
   */
  protected XElement getClipElement(Sprite sprite) {
    return clipElements.get(sprite);
  }

  private static DrawComponent hiddenTextSpriteMeasuring;
  /** 
   * Returns a hidden draw component that is hidden with offsets. Do not retain an instance, it will be removed from
   * the dom after this event loop.
   */
  private DrawComponent getHiddenSVG() {
    if (hiddenTextSpriteMeasuring == null) {
      hiddenTextSpriteMeasuring = new DrawComponent();
      hiddenTextSpriteMeasuring.addStyleName(HideMode.OFFSETS.value());
      RootPanel.get().add(hiddenTextSpriteMeasuring);
      hiddenTextSpriteMeasuring.redrawSurfaceForced();
      Scheduler.get().scheduleFinally(new ScheduledCommand() {
        @Override
        public void execute() {
          assert hiddenTextSpriteMeasuring != null : "More than one finally was invoked.";
          hiddenTextSpriteMeasuring.removeFromParent();
          hiddenTextSpriteMeasuring = null;
        }
      });
    }
    return hiddenTextSpriteMeasuring;
  }

  /**
   * Applies the attributes of the given sprite to its SVG element.
   * 
   * @param sprite the sprite to have its attributes set
   */
  private void applyAttributes(Sprite sprite) {
    XElement element = getElement(sprite);

    if (sprite instanceof PathSprite) {
      PathSprite path = (PathSprite) sprite;
      if (path.size() > 0) {
        element.setAttribute("d", path.toString());
      }
      if (path.isStrokeLineCapDirty()) {
        setAttribute(element, "stroke-linecap", path.getStrokeLineCap());
      }
      if (path.isStrokeLineJoinDirty()) {
        setAttribute(element, "stroke-linejoin", path.getStrokeLineJoin());
      }
      if (path.isMiterLimitDirty()) {
        setAttribute(element, "stroke-miterlimit", path.getMiterLimit());
      }
    } else if (sprite instanceof TextSprite) {
      TextSprite text = (TextSprite) sprite;
      if (text.isTextDirty() || text.isXDirty()) {
        tuneText(text);
      }
      if (text.isFontSizeDirty()) {
        if (text.getFontSize() > 0) {
          element.getStyle().setFontSize(text.getFontSize(), Unit.PX);
        } else {
          element.getStyle().clearFontSize();
        }
      }
      if (text.isFontStyleDirty()) {
        if (text.getFontStyle() != null) {
          element.getStyle().setFontStyle(text.getFontStyle());
        } else {
          element.getStyle().clearFontStyle();
        }
      }
      if (text.isFontWeightDirty()) {
        if (text.getFontWeight() != null) {
          element.getStyle().setFontWeight(text.getFontWeight());
        } else {
          element.getStyle().clearFontWeight();
        }
      }
      if (text.isFontDirty()) {
        setAttribute(element, "font-family", text.getFont());
      }
      if (text.isTextAnchorDirty()) {
        if (text.getTextAnchor() == TextAnchor.START) {
          element.setAttribute("text-anchor", "start");
        } else if (text.getTextAnchor() == TextAnchor.MIDDLE) {
          element.setAttribute("text-anchor", "middle");
        } else if (text.getTextAnchor() == TextAnchor.END) {
          element.setAttribute("text-anchor", "end");
        } else {
          element.removeAttribute("text-anchor");
        }
      }
      if (text.isXDirty()) {
        setAttribute(element, "x", text.getX());
      }
      if (text.isYDirty()) {
        setAttribute(element, "y", text.getY());
      }
    } else if (sprite instanceof RectangleSprite) {
      RectangleSprite rect = (RectangleSprite) sprite;
      if (rect.isXDirty()) {
        setAttribute(element, "x", rect.getX());
      }
      if (rect.isYDirty()) {
        setAttribute(element, "y", rect.getY());
      }
      if (rect.isWidthDirty()) {
        setAttribute(element, "width", rect.getWidth());

      }
      if (rect.isHeightDirty()) {
        setAttribute(element, "height", rect.getHeight());
      }
      if (rect.isRadiusDirty()) {
        setAttribute(element, "rx", rect.getRadius());
        setAttribute(element, "ry", rect.getRadius());

      }
    } else if (sprite instanceof CircleSprite) {
      CircleSprite circle = (CircleSprite) sprite;
      if (circle.isCenterXDirty()) {
        setAttribute(element, "cx", circle.getCenterX());
      }
      if (circle.isCenterYDirty()) {
        setAttribute(element, "cy", circle.getCenterY());
      }
      if (circle.isRadiusDirty()) {
        setAttribute(element, "r", circle.getRadius());
      }
    } else if (sprite instanceof EllipseSprite) {
      EllipseSprite ellipse = (EllipseSprite) sprite;
      if (ellipse.isCenterXDirty()) {
        setAttribute(element, "cx", ellipse.getCenterX());
      }
      if (ellipse.isCenterYDirty()) {
        setAttribute(element, "cy", ellipse.getCenterY());
      }
      if (ellipse.isRadiusXDirty()) {
        setAttribute(element, "rx", ellipse.getRadiusX());
      }
      if (ellipse.isRadiusYDirty()) {
        setAttribute(element, "ry", ellipse.getRadiusY());
      }
    } else if (sprite instanceof ImageSprite) {
      ImageSprite image = (ImageSprite) sprite;
      if (image.isResourceDirty()) {
        if (image.getResource() != null) {
          element.setAttributeNS("http://www.w3.org/1999/xlink", "href", image.getResource().getSafeUri().asString());
        } else {
          element.removeAttribute("href");
        }
      }
      if (image.isXDirty()) {
        setAttribute(element, "x", image.getX());
      }
      if (image.isYDirty()) {
        setAttribute(element, "y", image.getY());
      }
      if (image.isHeightDirty()) {
        setAttribute(element, "height", image.getHeight() + "px");
      }
      if (image.isWidthDirty()) {
        setAttribute(element, "width", image.getWidth() + "px");
      }
    }

    if (sprite.isStrokeDirty()) {
      setColorAttribute(element, "stroke", sprite.getStroke());
    }
    if (sprite.isStrokeWidthDirty()) {
      setAttribute(element, "stroke-width", sprite.getStrokeWidth());
    }
    if (sprite.isFillDirty()) {
      setColorAttribute(element, "fill", sprite.getFill());
    }
    if (sprite.isFillOpacityDirty()) {
      setAttribute(element, "fill-opacity", sprite.getFillOpacity());
    }
    if (sprite.isStrokeOpacityDirty()) {
      setAttribute(element, "stroke-opacity", sprite.getStrokeOpacity());
    }
    if (sprite.isOpacityDirty()) {
      setAttribute(element, "opacity", sprite.getOpacity());
    }
    String id = spriteIds.get(sprite);
    if (id != null) {
      element.setId(id);
    }

    // Hide or show the sprite
    if (sprite.isHiddenDirty()) {
      if (sprite.isHidden()) {
        element.setAttribute("visibility", "hidden");
      } else {
        element.removeAttribute("visibility");
      }
    }

    // Apply clip rectangle to the sprite
    if (sprite.isClipRectangleDirty()) {
      if (sprite.getClipRectangle() != null) {
        applyClip(sprite);
      } else {
        removeClip(sprite);
      }
    }
  }

  /**
   * Applies the clipping rectangle of the given sprite
   * 
   * @param sprite the sprite to apply its clipping rectangle
   */
  private void applyClip(Sprite sprite) {
    PreciseRectangle rect = sprite.getClipRectangle();
    XElement clipPath = getClipElement(sprite);

    if (clipPath != null) {
      Element clipParent = clipPath.getParentElement();
      clipParent.getParentElement().removeChild(clipParent);
    }

    XElement clipElement = createSVGElement("clipPath");
    clipPath = createSVGElement("rect");
    clipPath.setAttribute("x", String.valueOf(rect.getX()));
    clipPath.setAttribute("y", String.valueOf(rect.getY()));
    clipPath.setAttribute("width", String.valueOf(rect.getWidth()));
    clipPath.setAttribute("height", String.valueOf(rect.getHeight()));
    clipElement.appendChild(clipPath);
    getDefinitions().appendChild(clipElement);
    getElement(sprite).setAttribute("clip-path", "url(#" + clipElement.getId() + ")");
    setClipElement(sprite, clipElement);
  }

  /**
   * Insert or move a given {@link Sprite}'s element to the correct place in the DOM list for its zIndex.
   *
   * This should only need to be run once per set of changes, provided sprites are only rendered *after* all
   * z-index changes.
   * 
   * @param sprite - the {@link Sprite} to insert into the dom
   */
  private void applyZIndex(Sprite sprite) {
    int index = sprites.indexOf(sprite);
    int zIndex = sprite.getZIndex();
    int leftZIndex = Integer.MIN_VALUE;
    int rightZIndex = Integer.MAX_VALUE;
    if (index >= 1) {
      leftZIndex = sprites.get(index - 1).getZIndex();
    }
    if (index < sprites.size() - 1) {
      rightZIndex = sprites.get(index + 1).getZIndex();
    }

    if (leftZIndex > zIndex || rightZIndex < zIndex) {
      // if it doesnt match either the item on the right or the left, something may be out of order, rebuild it all
      Collections.sort(sprites, zIndexComparator());

      for (int i = 0; i < sprites.size(); i++) {
        XElement elt = getElement(sprites.get(i));
        if (elt != null) {
          getSurfaceElement().appendChild(elt);
        }
      }
    } else {
      // if no reordering is needed this pass but we have a detached element, insert it into the correct position
      XElement elt = getElement(sprite);
      if (elt != null && elt.getParentElement() == null) {
        // need to insert in the correct position in the dom, three possible cases, the end, the beginning, and the middle
        if (index == sprites.size() - 1) {// fast case, last element
          getSurfaceElement().appendChild(elt);
        } else if (index == 0) {// base case, first element
          getSurfaceElement().insertChild(elt, 2);
        } else {// otherwise, make sure prev element was inserted, then insert right after it
          Sprite neighborSprite = sprites.get(index - 1);
          Node prevNeighbor = getElement(neighborSprite);
          if (prevNeighbor == null) {
            // Force element creation - this will almost certainly invoke this method recursively, and will eventually
            // insert at beginning, then other nodes in the stack will be appended after it.
            // This is not presently a concern for three main reasons: first, this is how the code has worked for a long
            // time, typically items will be drawn in order anyway, and typically items are inserted in small batches
            // when sprite.redraw() is used individually.
            neighborSprite.redraw();
            prevNeighbor = getElement(neighborSprite);
            assert prevNeighbor != null;
          }
          assert prevNeighbor.getParentElement() == getSurfaceElement() : prevNeighbor.getParentElement();
          getSurfaceElement().insertAfter(elt, prevNeighbor);
        }
      }
    }


  }

  /**
   * Generates an SVG element for the given sprite and inserts it into the DOM
   * based on its z-index.
   * 
   * @param sprite the sprite to have its element generated
   * @return the generated element
   */
  private Element createSprite(Sprite sprite) {
    // Create svg element and append to the DOM.
    final XElement element;
    if (sprite instanceof CircleSprite) {
      element = createSVGElement("circle");
    } else if (sprite instanceof EllipseSprite) {
      element = createSVGElement("ellipse");
    } else if (sprite instanceof ImageSprite) {
      element = createSVGElement("image");
    } else if (sprite instanceof PathSprite) {
      element = createSVGElement("path");
    } else if (sprite instanceof RectangleSprite) {
      element = createSVGElement("rect");
    } else if (sprite instanceof TextSprite) {
      element = createSVGElement("text");
    } else {
      element = null;
      assert false : "unsupported sprite type " + sprite.getClass();
    }
    setElement(sprite, element);
    element.getStyle().setProperty("webkitTapHighlightColor", "rgba(0,0,0,0)");
    applyZIndex(sprite); // performs the insertion
    return element;
  }

  /**
   * Creates an SVG element using the give type.
   * 
   * @param type the type of element to create
   * @return the created SVG element
   */
  private XElement createSVGElement(String type) {
    return XElement.as(XDOM.createElementNS("http://www.w3.org/2000/svg", type));
  }

  /**
   * Returns the definitions element of the SVG element. If not already created
   * it will be instantiated.
   * 
   * @return the definitions element of the SVG element
   */
  private Element getDefinitions() {
    if (defs == null) {
      defs = createSVGElement("defs");
    }
    return defs;
  }

  /**
   * Returns the {@link TextBBox} for the given text element.
   * 
   * @param text the text element to get the bounding box
   * @return the bounding box
   */
  private native TextBBox getTextBBox(Element text) throws JavaScriptException /*-{
		return text.getBBox();
  }-*/;

  private void removeClip(Sprite sprite) {
    XElement clipElement = getClipElement(sprite);
    setClipElement(sprite, null);
    getElement(sprite).removeAttribute("clip-path");
    getDefinitions().removeChild(clipElement);
  }

  private void setAttribute(XElement element, String attribute, double value) {
    if (!(Double.isNaN(value))) {
      element.setAttribute(attribute, String.valueOf(value));
    } else {
      element.removeAttribute(attribute);
    }
  }

  private void setAttribute(XElement element, String attribute, Object value) {
    if (value != null) {
      element.setAttribute(attribute, value.toString());
    } else {
      element.removeAttribute(attribute);
    }
  }

  private void setColorAttribute(XElement element, String attribute, Color value) {
    if (value != null) {
      if (value instanceof Gradient) {
        element.setAttribute(attribute, "url(#" + gradientIds.get(value) + ")");
      } else {
        element.setAttribute(attribute, value.getColor());
      }
    } else {
      element.removeAttribute(attribute);
    }
  }

  /**
   * Divides up the text of the given {@link TextSprite}. Tspans are created
   * based on the new line character. The resultant tspans are added to the text
   * element.
   * 
   * @param sprite the sprite to have its text used
   * @return the generated tspans
   */
  private List setText(TextSprite sprite) {
    List tspans = new ArrayList();
    XElement spriteElement = getElement(sprite);

    while (spriteElement.hasChildNodes()) {
      spriteElement.removeChild(spriteElement.getFirstChild());
    }

    String parentX = String.valueOf(sprite.getX());

    // Wrap each row into tspan to emulate rows
    String[] texts = sprite.getText().split("\n");
    for (int i = 0; i < texts.length; i++) {
      if (texts[i] != null) {
        Element tspan = createSVGElement("tspan");
        tspan.appendChild(XDOM.createTextNode(texts[i]));
        tspan.setAttribute("x", parentX);
        spriteElement.appendChild(tspan);
        tspans.add(tspan);
      }
    }

    return tspans;
  }

  /**
   * Applies the transformations of the given sprite to its SVG element. The
   * transformations applied include {@link Rotation}, {@link Scaling} and
   * {@link Translation}.
   * 
   * @param sprite the sprite to have its transformations applied
   */
  private void transform(Sprite sprite) {
    Matrix matrix = sprite.transformMatrix();
    getElement(sprite).setAttribute(
        "transform",
        new StringBuilder().append("matrix( ").append(matrix.get(0, 0)).append(", ").append(matrix.get(1, 0)).append(
            ", ").append(matrix.get(0, 1)).append(", ").append(matrix.get(1, 1)).append(", ").append(matrix.get(0, 2)).append(
            ", ").append(matrix.get(1, 2)).append(")").toString());
  }

  /**
   * Wrap SVG text inside a tspan to allow for line wrapping. In addition this
   * normalizes the baseline for text the vertical middle of the text to be the
   * same as VML.
   * 
   * @param sprite - the sprite to have its text tuned
   */
  private void tuneText(TextSprite sprite) {
    List tspans = new ArrayList();

    if (sprite.getText() != null) {
      tspans = setText(sprite);
    }

    // Normalize baseline via a DY shift of first tspan.
    if (tspans.size() > 0) {
      double height = sprite.getFontSize();
      double initialHeight = height;
      if (sprite.getTextBaseline() == TextBaseline.BOTTOM) {
        double totalHeight = height * (tspans.size() - 1) * 1.2 + height;
        initialHeight = height - (totalHeight);
      } else if (sprite.getTextBaseline() == TextBaseline.MIDDLE) {
        double totalHeight = height * (tspans.size() - 1) * 1.2 + height;
        initialHeight = height - (totalHeight / 2.0);
      }
      for (int i = 0; i < tspans.size(); i++) {
        tspans.get(i).setAttribute("dy", String.valueOf(i != 0 ? height * 1.2 : initialHeight));
      }
    }
  }

  /**
   * Generates a {@link Comparator} for use in sorting sprites by their z-index.
   * 
   * @return the generated comparator
   */
  private Comparator zIndexComparator() {
    return new Comparator() {
      @Override
      public int compare(Sprite o1, Sprite o2) {
        return o1.getZIndex() - o2.getZIndex();
      }
    };
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy