/*
* This file is part of LaTeXDraw.
* Copyright (c) 2005-2017 Arnaud BLOUIN
* LaTeXDraw is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later version.
* LaTeXDraw is distributed without any warranty; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package net.sf.latexdraw.view.svg;
import java.util.Objects;
import net.sf.latexdraw.badaboom.BadaboomCollector;
import net.sf.latexdraw.models.MathUtils;
import net.sf.latexdraw.models.interfaces.shape.ArrowStyle;
import net.sf.latexdraw.models.interfaces.shape.IArrow;
import net.sf.latexdraw.models.interfaces.shape.IShape;
import net.sf.latexdraw.parsers.svg.CSSColors;
import net.sf.latexdraw.parsers.svg.SVGAttributes;
import net.sf.latexdraw.parsers.svg.SVGCircleElement;
import net.sf.latexdraw.parsers.svg.SVGDocument;
import net.sf.latexdraw.parsers.svg.SVGElement;
import net.sf.latexdraw.parsers.svg.SVGElements;
import net.sf.latexdraw.parsers.svg.SVGMarkerElement;
import net.sf.latexdraw.parsers.svg.SVGNodeList;
import net.sf.latexdraw.parsers.svg.SVGPathElement;
import net.sf.latexdraw.parsers.svg.path.SVGPathSeg;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegClosePath;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegCurvetoCubic;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegLineto;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegLinetoVertical;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegList;
import net.sf.latexdraw.parsers.svg.path.SVGPathSegMoveto;
import net.sf.latexdraw.util.LNamespace;
/**
* An SVG generator for arrows.
* @author Arnaud BLOUIN
*/
class LArrowSVGGenerator {
/** The arrowhead generated or used to generate the SVG-arrow. */
protected IArrow arrow;
/**
* Creates an SVG arrow generator.
* @param arr The arrow. Must not be null.
*/
protected LArrowSVGGenerator(final IArrow arr) {
super();
arrow = Objects.requireNonNull(arr);
}
/**
* Initialises the arrow using an SVGMarkerElement.
* @param elt The SVGMarkerElement uses to initialise the arrow.
* @param owner The figure the has the arrow.
* @since 2.0.0
*/
protected void setArrow(final SVGMarkerElement elt, final IShape owner, final String svgMarker) {
SVGNodeList nl = elt.getChildren(SVGElements.SVG_PATH);
if(nl.getLength()==0) {
nl = elt.getChildren(SVGElements.SVG_CIRCLE);
if(nl.getLength()>0)
setArrow((SVGCircleElement)nl.item(0), elt, owner);
}
else
setArrow((SVGPathElement)nl.item(0), elt, owner, svgMarker);
}
/**
* Initialises the arrowhead using a circle arrow.
* @param circle The circle element.
* @param elt The arrowhead element.
* @param owner The shape that has the arrow.
* @since 2.0.0
*/
protected void setArrow(final SVGCircleElement circle, final SVGMarkerElement elt, final IShape owner) {
final double radius = circle.getR();
final String dotSizeNumStr = circle.getAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_DOT_SIZE_NUM);
final double dotSizeDim;
double dotSizeNum;
final double lineWidth = owner.hasDbleBord() ? owner.getDbleBordSep() + 2.*owner.getThickness() : owner.getThickness();
if(dotSizeNumStr==null)
dotSizeNum = 1.;
else
try { dotSizeNum = Double.parseDouble(dotSizeNumStr); }
catch(final NumberFormatException e) {
BadaboomCollector.INSTANCE.add(e);
dotSizeNum = 1.;
}
if(circle.getStroke()==null) {
arrow.setArrowStyle(MathUtils.INST.equalsDouble(elt.getRefX(), 0.) ? ArrowStyle.DISK_END : ArrowStyle.DISK_IN);
dotSizeDim = radius*lineWidth*2.-dotSizeNum*lineWidth;
}
else {
arrow.setArrowStyle(MathUtils.INST.equalsDouble(elt.getRefX(), 0.) ? ArrowStyle.CIRCLE_END : ArrowStyle.CIRCLE_IN);
dotSizeDim = (radius*lineWidth+lineWidth/2.)*2.-dotSizeNum*lineWidth;
}
if(MathUtils.INST.equalsDouble(dotSizeDim,0.0))
arrow.setArrowStyle(ArrowStyle.ROUND_IN);
else {
arrow.setDotSizeDim(dotSizeDim);
arrow.setDotSizeNum(dotSizeNum);
}
}
private void setArrowBarBracket(final SVGPathElement path, final SVGPathSegMoveto m, final double lineWidth, final SVGPathSeg seg,
final SVGMarkerElement elt, final SVGPathSegList list, final String svgMarker) {
final String tbarNumStr = path.getAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_TBAR_SIZE_NUM);
double tbarNum;
double y = Math.abs(m.getY());
final boolean isStartArrow = SVGAttributes.SVG_MARKER_START.equals(svgMarker);
if(tbarNumStr==null)
tbarNum = 1.;
else
try { tbarNum = Double.parseDouble(tbarNumStr); }
catch(final NumberFormatException e) {
BadaboomCollector.INSTANCE.add(e);
tbarNum = 1.;
}
arrow.setTBarSizeNum(tbarNum);
arrow.setTBarSizeDim(y*lineWidth*2. - tbarNum*lineWidth);
if(seg instanceof SVGPathSegLineto && MathUtils.INST.equalsDouble(((SVGPathSegLineto)seg).getX(), m.getX()) || seg instanceof SVGPathSegLinetoVertical)
arrow.setArrowStyle(MathUtils.INST.equalsDouble(m.getX(),0.) ? ArrowStyle.BAR_IN : ArrowStyle.BAR_END);
else if(seg instanceof SVGPathSegCurvetoCubic) {
final double width = (arrow.getTBarSizeDim() + arrow.getTBarSizeNum()*lineWidth)/lineWidth;
final double rBrack = (Math.abs(m.getX())-0.5)/width;
arrow.setArrowStyle(MathUtils.INST.equalsDouble(Math.abs(m.getX()), 0.5) ? ArrowStyle.RIGHT_ROUND_BRACKET : ArrowStyle.LEFT_ROUND_BRACKET);
if(!isStartArrow)
arrow.setArrowStyle(arrow.getArrowStyle().getOppositeArrowStyle());
arrow.setRBracketNum(rBrack);
}
else // It may be a bracket.
if(list.size()==4 && seg instanceof SVGPathSegLineto && list.get(2) instanceof SVGPathSegLineto && list.get(3) instanceof SVGPathSegLineto) {
final double lgth = Math.abs(m.getX()-((SVGPathSegLineto)seg).getX());
y += m.getY() > 0. ? -0.5 : 0.5;
arrow.setTBarSizeDim(y*lineWidth*2. - tbarNum*lineWidth);
arrow.setBracketNum((lgth-0.5)*lineWidth/(arrow.getTBarSizeDim()/IShape.PPC + arrow.getTBarSizeNum()*lineWidth));
arrow.setArrowStyle(elt.getRefX()>0.? ArrowStyle.RIGHT_SQUARE_BRACKET : ArrowStyle.LEFT_SQUARE_BRACKET);
}
}
private void setArrowArrow(final SVGPathElement path, final SVGPathSegMoveto m, final double lineWidth, final SVGPathSeg seg,
final SVGPathSegList list, final String svgMarker) {
if(!(seg instanceof SVGPathSegLineto && list.get(2) instanceof SVGPathSegLineto &&
list.get(3) instanceof SVGPathSegLineto && list.get(4) instanceof SVGPathSegClosePath))
return ;
final String arrNumStr = path.getAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_SIZE_NUM);
double arrNum;
final double arrDim;
final double lgth = Math.abs(((SVGPathSegLineto)seg).getX() - m.getX());
final boolean moveIs0 = MathUtils.INST.equalsDouble(m.getX(),0.) && MathUtils.INST.equalsDouble(m.getY(),0.);
final boolean isStartArrow = SVGAttributes.SVG_MARKER_START.equals(svgMarker);
if(arrNumStr==null)
arrNum = 1.;
else
try { arrNum = Double.parseDouble(arrNumStr); }
catch(final NumberFormatException e) {
BadaboomCollector.INSTANCE.add(e);
arrNum = 1.;
}
if(list.size()==10)
arrow.setArrowStyle(moveIs0 ? ArrowStyle.LEFT_DBLE_ARROW : ArrowStyle.RIGHT_DBLE_ARROW);
else
arrow.setArrowStyle(moveIs0 ? ArrowStyle.LEFT_ARROW : ArrowStyle.RIGHT_ARROW);
if(!isStartArrow)
arrow.setArrowStyle(arrow.getArrowStyle().getOppositeArrowStyle());
arrDim = lineWidth*(((SVGPathSegLineto)seg).getY()*2. - arrNum);
arrow.setArrowLength(lgth/((arrNum*lineWidth + arrDim)/lineWidth));
arrow.setArrowSizeDim(arrDim);
arrow.setArrowSizeNum(arrNum);
arrow.setArrowInset(Math.abs(((SVGPathSegLineto)seg).getX()-((SVGPathSegLineto)list.get(2)).getX())/lgth);
}
/**
* Initialises the arrowhead using a path arrow.
* @param path The path element.
* @param elt The arrowhead element.
* @param owner The shape that has the arrow.
* @since 2.0.0
*/
protected void setArrow(final SVGPathElement path, final SVGMarkerElement elt, final IShape owner, final String svgMarker) {
final SVGPathSegList list = path.getSegList();
final SVGPathSegMoveto m = (SVGPathSegMoveto)list.get(0);
final double lineWidth = owner.hasDbleBord() ? owner.getDbleBordSep() + 2.*owner.getThickness() : owner.getThickness();
if(list.size()==2 || list.size()==4) // It may be a bar or a bracket
setArrowBarBracket(path, m, lineWidth, list.get(1), elt, list, svgMarker);
else if(list.size()==5 || list.size()==10)// It may be an arrow or a double arrow
setArrowArrow(path, m, lineWidth, list.get(1), list, svgMarker);
}
private double toSVGCircle(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGElement circle = new SVGCircleElement(doc);
final double r = (arrow.getDotSizeDim()+arrow.getDotSizeNum()*lineWidth)/2.-lineWidth/2.;
circle.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_DOT_SIZE_NUM, String.valueOf(arrow.getDotSizeNum()));
circle.setAttribute(SVGAttributes.SVG_R, String.valueOf(r/lineWidth));
circle.setAttribute(SVGAttributes.SVG_FILL, CSSColors.INSTANCE.getColorName(shape.getFillingCol(), true));
circle.setAttribute(SVGAttributes.SVG_STROKE, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
circle.setStrokeWidth(1);
marker.appendChild(circle);
return arrow.getArrowStyle()==ArrowStyle.CIRCLE_IN ? lineWidth*(arrow.isLeftArrow() ? -1. : 1.) : 0.;
}
private double toSVGDisk(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGElement circle = new SVGCircleElement(doc);
final double r = (arrow.getDotSizeDim()+arrow.getDotSizeNum()*lineWidth)/2.;
circle.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_DOT_SIZE_NUM, String.valueOf(arrow.getDotSizeNum()));
circle.setAttribute(SVGAttributes.SVG_R, String.valueOf(r/lineWidth));
circle.setAttribute(SVGAttributes.SVG_FILL, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
marker.appendChild(circle);
return arrow.getArrowStyle()==ArrowStyle.DISK_IN ? lineWidth * (arrow.isLeftArrow() ? -1. : 1.) : 0.;
}
private void toSVGBar(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGPathElement bar = new SVGPathElement(doc);
final double width = arrow.getTBarSizeDim() + arrow.getTBarSizeNum()*lineWidth;
final SVGPathSegList path = new SVGPathSegList();
final double x = arrow.getArrowStyle()==ArrowStyle.BAR_IN ? arrow.isLeftArrow() ? 0.5 : -0.5 : 0.;
bar.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_TBAR_SIZE_NUM, String.valueOf(arrow.getTBarSizeNum()));
path.add(new SVGPathSegMoveto(x, -width/(lineWidth*2.), false));
path.add(new SVGPathSegLineto(x, width/(lineWidth*2.), false));
bar.setPathData(path);
bar.setAttribute(SVGAttributes.SVG_STROKE, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
bar.setAttribute(SVGAttributes.SVG_FILL, SVGAttributes.SVG_VALUE_NONE);
bar.setPathData(path);
bar.setStrokeWidth(1.);
marker.appendChild(bar);
}
private double toSVGSquareBracket(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGPathElement bar = new SVGPathElement(doc);
final double width = arrow.getTBarSizeDim() + arrow.getTBarSizeNum()*lineWidth;
final SVGPathSegList path = new SVGPathSegList();
final double lgth = arrow.getBracketNum()*(arrow.getTBarSizeDim()/IShape.PPC + arrow.getTBarSizeNum()*lineWidth)/lineWidth;
final boolean isInverted = arrow.isInverted();//FIXME shape.PPC
final double gapPostion;
if(arrow.getArrowStyle()==ArrowStyle.LEFT_SQUARE_BRACKET) {
final double lgth2 = isInverted ? -lgth : 0.;
path.add(new SVGPathSegMoveto(lgth+lgth2+0.5, -width/(lineWidth*2)+0.5, false));
path.add(new SVGPathSegLineto(lgth2, -width/(lineWidth*2)+0.5, false));
path.add(new SVGPathSegLineto(lgth2, width/(lineWidth*2)-0.5, false));
path.add(new SVGPathSegLineto(lgth+lgth2+0.5, width/(lineWidth*2)-0.5, false));
gapPostion = isInverted ? -lineWidth/4. : -lineWidth/2.;
}
else {
final double lgth2 = isInverted ? lgth : 0.;
path.add(new SVGPathSegMoveto(-lgth+lgth2-0.5, -width/(lineWidth*2)+0.5, false));
path.add(new SVGPathSegLineto(lgth2, -width/(lineWidth*2)+0.5, false));
path.add(new SVGPathSegLineto(lgth2, width/(lineWidth*2)-0.5, false));
path.add(new SVGPathSegLineto(-lgth+lgth2-0.5, width/(lineWidth*2)-0.5, false));
gapPostion = isInverted ? lineWidth/4. : lineWidth/2.;
}
marker.appendChild(bar);
bar.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_TBAR_SIZE_NUM, String.valueOf(arrow.getTBarSizeNum()));
bar.setAttribute(SVGAttributes.SVG_STROKE, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
bar.setAttribute(SVGAttributes.SVG_FILL, SVGAttributes.SVG_VALUE_NONE);
bar.setPathData(path);
bar.setAttribute(SVGAttributes.SVG_STROKE_WIDTH, "1"); //$NON-NLS-1$
return gapPostion;
}
private void toSVGArrow(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGElement arrowSVG = new SVGPathElement(doc);
final double width = (arrow.getArrowSizeNum()*lineWidth + arrow.getArrowSizeDim())/lineWidth;
double length = arrow.getArrowLength()*width;
double inset = arrow.getArrowInset()*length;
final SVGPathSegList path = new SVGPathSegList();
if(arrow.getArrowStyle()==ArrowStyle.LEFT_ARROW) {
length *= -1.;
inset *= -1.;
}
final double lgth2 = arrow.isInverted() ? length : 0.;
path.add(new SVGPathSegMoveto(lgth2, 0., false));
path.add(new SVGPathSegLineto(-length+lgth2, width/2., false));
path.add(new SVGPathSegLineto(-length+inset+lgth2, 0., false));
path.add(new SVGPathSegLineto(-length+lgth2, -width/2., false));
path.add(new SVGPathSegClosePath());
marker.appendChild(arrowSVG);
arrowSVG.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_SIZE_NUM, String.valueOf(arrow.getArrowSizeNum()));
arrowSVG.setAttribute(SVGAttributes.SVG_FILL, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
arrowSVG.setAttribute(SVGAttributes.SVG_D, path.toString());
}
private void toSVGRoundBracket(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGPathElement rbracket = new SVGPathElement(doc);
final double width = (arrow.getTBarSizeDim() + arrow.getTBarSizeNum()*lineWidth)/lineWidth;
double lgth = arrow.getRBracketNum()*width;
final SVGPathSegList path = new SVGPathSegList();
double gap = 0.5;
if(arrow.getArrowStyle()==ArrowStyle.LEFT_ROUND_BRACKET) {
lgth *= -1.;
gap *= -1.;
}
final double lgth2 = arrow.isInverted() ? lgth : 0.;
path.add(new SVGPathSegMoveto(-lgth+lgth2-gap, width/2., false));
path.add(new SVGPathSegCurvetoCubic(-lgth+lgth2-gap, -width/2., 0., width/2., 0., -width/2., false));
marker.appendChild(rbracket);
rbracket.setAttribute(SVGAttributes.SVG_STROKE, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
rbracket.setAttribute(SVGAttributes.SVG_FILL, SVGAttributes.SVG_VALUE_NONE);
rbracket.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_TBAR_SIZE_NUM, String.valueOf(arrow.getTBarSizeNum()));
rbracket.setPathData(path);
rbracket.setStrokeWidth(1);
}
private void toSVGDoubleArrow(final SVGDocument doc, final double lineWidth, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGElement arrowSVG = new SVGPathElement(doc);
final double width = (arrow.getArrowSizeNum()*lineWidth + arrow.getArrowSizeDim())/lineWidth;
double length = arrow.getArrowLength()*width;
double inset = arrow.getArrowInset()*length;
final SVGPathSegList path = new SVGPathSegList();
if(arrow.getArrowStyle()==ArrowStyle.LEFT_DBLE_ARROW) {
inset *= -1.;
length *= -1.;
}
final double lgth2 = arrow.isInverted() ? length*2 : 0.;
path.add(new SVGPathSegMoveto(lgth2, 0., false));
path.add(new SVGPathSegLineto(-length+lgth2, width/2., false));
path.add(new SVGPathSegLineto(-length+inset+lgth2, 0., false));
path.add(new SVGPathSegLineto(-length+lgth2, -width/2., false));
path.add(new SVGPathSegClosePath());
path.add(new SVGPathSegMoveto(-length+lgth2, 0., false));
path.add(new SVGPathSegLineto(-length*2+lgth2, width/2., false));
path.add(new SVGPathSegLineto(-length*2+inset+lgth2, 0., false));
path.add(new SVGPathSegLineto(-length*2+lgth2, -width/2., false));
path.add(new SVGPathSegClosePath());
marker.appendChild(arrowSVG);
arrowSVG.setAttribute(LNamespace.LATEXDRAW_NAMESPACE+':'+LNamespace.XML_ARROW_SIZE_NUM, String.valueOf(arrow.getArrowSizeNum()));
arrowSVG.setAttribute(SVGAttributes.SVG_FILL, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
arrowSVG.setAttribute(SVGAttributes.SVG_D, path.toString());
}
private void toSVGRoundIn(final SVGDocument doc, final boolean isShadow, final SVGElement marker) {
final IShape shape = arrow.getShape();
final SVGElement circle = new SVGCircleElement(doc);
circle.setAttribute(SVGAttributes.SVG_R, "0.5"); //$NON-NLS-1$
circle.setAttribute(SVGAttributes.SVG_FILL, CSSColors.INSTANCE.getColorName(isShadow ? shape.getShadowCol() : shape.getLineColour(), true));
marker.appendChild(circle);
}
/**
* Return the SVG tree of the arrowhead or null if this arrowhead has no style.
* @param doc The document used to create elements.
* @param isShadow True: this operation is call to create the SVG shadow of the shape.
* @return The SVG tree of the arrowhead or null if doc is null.
* @since 2.0.0
*/
public SVGElement toSVG(final SVGDocument doc, final boolean isShadow) {
if(doc==null || !arrow.hasStyle())
return null;
final ArrowStyle arrowStyle = arrow.getArrowStyle();
final SVGElement marker = new SVGMarkerElement(doc);
final double lineWidth = arrow.getLineThickness();
double gapPostion = 0.;
if(arrowStyle==ArrowStyle.CIRCLE_END || arrowStyle==ArrowStyle.CIRCLE_IN)
gapPostion = toSVGCircle(doc, lineWidth, isShadow, marker);
else
if(arrowStyle==ArrowStyle.DISK_END || arrowStyle==ArrowStyle.DISK_IN)
gapPostion = toSVGDisk(doc, lineWidth, isShadow, marker);
else
if(arrowStyle.isBar())
toSVGBar(doc, lineWidth, isShadow, marker);
else if(arrowStyle.isSquareBracket())
toSVGSquareBracket(doc, lineWidth, isShadow, marker);
else
if(arrowStyle==ArrowStyle.RIGHT_ARROW || arrowStyle==ArrowStyle.LEFT_ARROW)
toSVGArrow(doc, lineWidth, isShadow, marker);
else
if(arrowStyle.isRoundBracket())
toSVGRoundBracket(doc, lineWidth, isShadow, marker);
else
if(arrowStyle==ArrowStyle.LEFT_DBLE_ARROW || arrowStyle==ArrowStyle.RIGHT_DBLE_ARROW)
toSVGDoubleArrow(doc, lineWidth, isShadow, marker);
else
if(arrowStyle==ArrowStyle.ROUND_IN)
toSVGRoundIn(doc, isShadow, marker);
if(!MathUtils.INST.equalsDouble(gapPostion,0.))
marker.setAttribute(SVGAttributes.SVG_REF_X, String.valueOf(gapPostion/lineWidth));
marker.setAttribute(SVGAttributes.SVG_OVERFLOW, SVGAttributes.SVG_VALUE_VISIBLE);
marker.setAttribute(SVGAttributes.SVG_ORIENT, SVGAttributes.SVG_VALUE_AUTO);
return marker;
}
}