/*
* 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.parsers.svg;
import net.sf.latexdraw.models.MathUtils;
/**
* Defines an SVG transformation.
* @author Arnaud BLOUIN
* @since 0.1
*/
public class SVGTransform {
public static final int SVG_TRANSFORM_UNKNOWN = 0;
public static final int SVG_TRANSFORM_MATRIX = 1;
public static final int SVG_TRANSFORM_TRANSLATE = 2;
public static final int SVG_TRANSFORM_SCALE = 3;
public static final int SVG_TRANSFORM_ROTATE = 4;
public static final int SVG_TRANSFORM_SKEWX = 5;
public static final int SVG_TRANSFORM_SKEWY = 6;
/** The type of the transformation. */
protected int type;
/** The matrix of the transformation. */
protected SVGMatrix matrix;
/** The angle of a possible rotation or skew. */
protected double angle;
/** The possible rotation X-position. */
protected double cx;
/** The possible rotation Y-position. */
protected double cy;
/**
* Creates a transformation with no type.
*/
public SVGTransform() {
super();
type = SVG_TRANSFORM_UNKNOWN;
matrix = new SVGMatrix();
angle = Double.NaN;
cx = Double.NaN;
cy = Double.NaN;
}
/**
* The constructor using a string containing the transformation. The transformation is not added if it is not valid.
* @param transformation The SVG transformation.
* @throws IllegalArgumentException If the transformation is no valid.
* @since 0.1
*/
public SVGTransform(final String transformation) {
this();
setTransformation(transformation);
}
/**
* Parses a string containing the SVG transformation in order to set the transformation.
* @param transformation The string to parse.
* @throws IllegalArgumentException If the transformation is no valid.
* @since 0.1
*/
public void setTransformation(final String transformation) {
if(transformation==null)
throw new IllegalArgumentException();
String code = transformation.replaceAll("[ \t\n\r\f]+", " ");//$NON-NLS-1$//$NON-NLS-2$
code = code.replaceAll("^[ ]", "");//$NON-NLS-1$//$NON-NLS-2$
code = code.replaceAll("[ ]$", "");//$NON-NLS-1$//$NON-NLS-2$
code = code.replaceAll("[ ]?[(][ ]?", "(");//$NON-NLS-1$//$NON-NLS-2$
code = code.replaceAll("[ ]?[)]", ")");//$NON-NLS-1$//$NON-NLS-2$
code = code.replaceAll("[ ]?,[ ]?", ",");//$NON-NLS-1$//$NON-NLS-2$
int i;
final int lgth = code.length();
final int j;
int k;
if(code.startsWith(SVGAttributes.SVG_TRANSFORM_ROTATE)) {
i = SVGAttributes.SVG_TRANSFORM_ROTATE.length();
j = code.indexOf(')');
int k1 = code.indexOf(' ');
int k2 = code.indexOf(',');
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
k = k1==-1 ? k2 : k2==-1 ? k1 : Math.min(k1, k2);
if(k==-1)
setRotate(Double.parseDouble(code.substring(i+1, j)), 0., 0.);
else {
final double cx2;
final double cy2;
final double rotAngle;
rotAngle = Double.parseDouble(code.substring(i+1, k));
i = k+1;
k1 = code.indexOf(' ', k+1);
k2 = code.indexOf(',', k+1);
k = k1==-1 ? k2 : k2==-1 ? k1 : Math.min(k1, k2);
if(k==-1)
throw new IllegalArgumentException();
cx2 = Double.parseDouble(code.substring(i, k));
cy2 = Double.parseDouble(code.substring(k+1, j));
setRotate(rotAngle, cx2, cy2);
}
}
else if(code.startsWith(SVGAttributes.SVG_TRANSFORM_SCALE)) {
k = code.indexOf(',');
i = SVGAttributes.SVG_TRANSFORM_SCALE.length();
j = code.indexOf(')');
final double sx;
final double sy;
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
if(k==-1)
k = code.indexOf(' ');
if(k==-1) {
final double val = Double.parseDouble(code.substring(i+1, j));
sx = val;
sy = val;
}
else {
sx = Double.parseDouble(code.substring(i+1, k));
sy = Double.parseDouble(code.substring(k+1, j));
}
setScale(sx, sy);
}
else if(code.startsWith(SVGAttributes.SVG_TRANSFORM_TRANSLATE)) {
k = code.indexOf(',');
i = SVGAttributes.SVG_TRANSFORM_TRANSLATE.length();
j = code.indexOf(')');
final double tx;
final double ty;
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
if(k==-1)
k = code.indexOf(' ');
if(k==-1) {
final double val = Double.parseDouble(code.substring(i+1, j));
tx = val;
ty = val;
}
else {
tx = Double.parseDouble(code.substring(i+1, k));
ty = Double.parseDouble(code.substring(k+1, j));
}
setTranslate(tx, ty);
}
else if(code.startsWith(SVGAttributes.SVG_TRANSFORM_MATRIX)) {
final int nbPts = 6;
int l;
int k1;
int k2;
i = SVGAttributes.SVG_TRANSFORM_MATRIX.length();
j = code.indexOf(')');
final double[] coords = new double[nbPts];
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
i++;
for(l=0; l<nbPts-1; l++) {
k1 = code.indexOf(' ', i);
k2 = code.indexOf(',', i);
k = k1==-1 ? k2 : k2==-1 ? k1 : Math.min(k1, k2);
if(k==-1)
throw new IllegalArgumentException();
coords[l] = Double.parseDouble(code.substring(i, k));
i = k+1;
}
coords[nbPts-1] = Double.parseDouble(code.substring(i, j));
setMatrix(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
}
else if(code.startsWith(SVGAttributes.SVG_TRANSFORM_SKEW_X)) {
i = SVGAttributes.SVG_TRANSFORM_SKEW_X.length();
j = code.indexOf(')');
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
setSkewX(Double.parseDouble(code.substring(i+1, j)));
}
else if(code.startsWith(SVGAttributes.SVG_TRANSFORM_SKEW_Y)) {
i = SVGAttributes.SVG_TRANSFORM_SKEW_Y.length();
j = code.indexOf(')');
if(i>=lgth || code.charAt(i)!='(' || j==-1)
throw new IllegalArgumentException();
setSkewY(Double.parseDouble(code.substring(i+1, j)));
}
else
throw new IllegalArgumentException();
}
/**
* The transformation will be a translation.
* @param tx The X translation.
* @param ty The Y translation.
* @since 0.1
*/
public void setTranslate(final double tx, final double ty) {
type = SVG_TRANSFORM_TRANSLATE;
matrix.initMatrix();
matrix.translate(tx, ty);
angle = Double.NaN;
}
/**
* The transformation will be set by the given values.
* @param a The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @param b The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @param c The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @param d The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @param e The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @param f The values of the matrix: [a, c, e, b, d, f, 0, 0, 1].
* @since 0.1
*/
public void setMatrix(final double a, final double b, final double c, final double d, final double e, final double f) {
type = SVG_TRANSFORM_MATRIX;
matrix.setMatrix(a, b, c, d, e, f);
angle = Double.NaN;
}
/**
* The transformation will be a scaling.
* @param sx The X scaling.
* @param sy The Y scaling.
* @since 0.1
*/
public void setScale(final double sx, final double sy) {
type = SVG_TRANSFORM_SCALE;
matrix.initMatrix();
matrix.scaleNonUniform(sx, sy);
angle = Double.NaN;
}
/**
* The transformation will be a rotation form the origin.
* @param angle The rotation angle in degree.
* @param cx The X centre of the rotation.
* @param cy The Y centre of the rotation.
* @since 0.1
*/
public void setRotate(final double angle, final double cx, final double cy) {
SVGMatrix m1 = new SVGMatrix();
final SVGMatrix m2 = new SVGMatrix();
type = SVG_TRANSFORM_ROTATE;
matrix.initMatrix();
m1.translate(cx, cy);
m2.rotate(Math.toRadians(angle));
m1 = m1.multiply(m2);
m2.translate(-cx, -cy);
m1.multiply(m2);
matrix.setMatrix(m1.a, m1.b, m1.c, m1.d, m1.e, m1.f);
this.angle = angle;
this.cx = cx;
this.cy = cy;
}
/**
* The transformation will be a X skew.
* @param angle The angle of the skew in degree.
* @since 0.1
*/
public void setSkewX(final double angle) {
type = SVG_TRANSFORM_SKEWX;
matrix.initMatrix();
matrix.skewX(Math.toRadians(angle));
this.angle = angle;
}
/**
* The transformation will be a Y skew.
* @param angle The angle of the skew in degree.
* @since 0.1
*/
public void setSkewY(final double angle) {
type = SVG_TRANSFORM_SKEWY;
matrix.initMatrix();
matrix.skewY(Math.toRadians(angle));
this.angle = angle;
}
/**
* @return the type of the transformation.
* @since 0.1
*/
public int getType() {
return type;
}
/**
* @return True if the transformation is a rotation.
* @since 0.1
*/
public boolean isRotation() {
return getType()==SVG_TRANSFORM_ROTATE;
}
/**
* @return True if the transformation is a translation.
* @since 0.1
*/
public boolean isTranslation() {
return getType()==SVG_TRANSFORM_TRANSLATE;
}
/**
* @return True if the transformation is a scale.
* @since 0.1
*/
public boolean isScale() {
return getType()==SVG_TRANSFORM_SCALE;
}
/**
* @return True if the transformation is a X skew.
* @since 0.1
*/
public boolean isXSkew() {
return getType()==SVG_TRANSFORM_SKEWX;
}
/**
* @return True if the transformation is a Y skew.
* @since 0.1
*/
public boolean isYSkew() {
return getType()==SVG_TRANSFORM_SKEWY;
}
/**
* @return The rotation angle in degree if the transformation is a rotation. NaN otherwise.
*/
public double getRotationAngle() {
return isRotation() ? angle : Double.NaN;
}
/**
* @return The skew angle in degree if the transformation is a X skew. NaN otherwise.
*/
public double getXSkewAngle() {
return isXSkew() ? angle : Double.NaN;
}
/**
* @return The skew angle in degree if the transformation is a Y skew. NaN otherwise.
*/
public double getYSkewAngle() {
return isYSkew() ? angle : Double.NaN;
}
/**
* @return The X scale factor if the transformation is a scaling. NaN otherwise.
*/
public double getXScaleFactor() {
return isScale() ? matrix.getA() : Double.NaN;
}
/**
* @return The Y scale factor if the transformation is a scaling. NaN otherwise.
*/
public double getYScaleFactor() {
return isScale() ? matrix.getD() : Double.NaN;
}
/**
* @return The X translation if the transformation is a translation. NaN otherwise.
*/
public double getTX() {
return isTranslation() ? matrix.getE() : Double.NaN;
}
/**
* @return The Y translation if the transformation is a translation. NaN otherwise.
*/
public double getTY() {
return isTranslation() ? matrix.getF() : Double.NaN;
}
/**
* @return the matrix.
* @since 0.1
*/
public SVGMatrix getMatrix() {
return matrix;
}
/**
* @return The rotation X-position or NaN is the transformation is not a rotation.
* @since 3.0
*/
public double getCx() {
return cx;
}
/**
* @return The rotation Y-position or NaN is the transformation is not a rotation.
* @since 3.0
*/
public double getCy() {
return cy;
}
/**
* Creates a translation.
* @param x The tx.
* @param y The ty.
* @return The created translation.
* @since 2.0.0
*/
public static SVGTransform createTranslation(final double x, final double y) {
final SVGTransform t = new SVGTransform();
t.setTranslate(x, y);
return t;
}
/**
* Creates a rotation.
* @param cx The X centre of the rotation.
* @param cy The Y centre of the rotation.
* @param angle The angle of rotation in radian.
* @return The created translation.
* @since 0.1
*/
public static SVGTransform createRotation(final double angle, final double cx, final double cy) {
final SVGTransform r = new SVGTransform();
r.setRotate(angle, cx, cy);
return r;
}
/**
* Tests if the given transformation cancels the calling one.
* @param transform The transformation to test.
* @return True if the given transformation cancels the calling one.
* @since 3.0
*/
public boolean cancels(final SVGTransform transform) {
if(transform==null)
return false;
if(isTranslation())
return cancelsTranslation(transform);
if(isRotation())
return cancelsRotation(transform);
if(isScale())
return cancelsScale(transform);
if(isXSkew())
return cancelsXSkew(transform);
return isYSkew() && cancelsYSkew(transform);
}
/**
* @param transform The transformation to test.
* @return True: if the given transformation is a rotation that cancels the calling one.
* @since 3.0
*/
private boolean cancelsRotation(final SVGTransform transform) {
return transform.isRotation() && MathUtils.INST.equalsDouble(-(getRotationAngle()%360), transform.getRotationAngle()%360) &&
MathUtils.INST.equalsDouble(getCx(), transform.getCx()) && MathUtils.INST.equalsDouble(getCy(), transform.getCy());
}
/**
* @param transform The transformation to test.
* @return True: if the given transformation is a translation that cancels the calling one.
* @since 3.0
*/
private boolean cancelsTranslation(final SVGTransform transform) {
return transform.isTranslation() && MathUtils.INST.equalsDouble(-getTX(), transform.getTX()) && MathUtils.INST.equalsDouble(-getTY(), transform.getTY());
}
/**
* @param transform The transformation to test.
* @return True: if the given transformation is a scaling that cancels the calling one.
* @since 3.0
*/
private boolean cancelsScale(final SVGTransform transform) {
return transform.isScale() && MathUtils.INST.equalsDouble(getXScaleFactor()*transform.getXScaleFactor(), 1.) &&
MathUtils.INST.equalsDouble(getYScaleFactor()*transform.getYScaleFactor(), 1.);
}
/**
* @param transform The transformation to test.
* @return True: if the given transformation is an X-skewing that cancels the calling one.
* @since 3.0
*/
private boolean cancelsXSkew(final SVGTransform transform) {
throw new IllegalArgumentException("Not yet implemented"); //$NON-NLS-1$
}
/**
* @param transform The transformation to test.
* @return True: if the given transformation is an Y-skewing that cancels the calling one.
* @since 3.0
*/
private boolean cancelsYSkew(final SVGTransform transform) {
throw new IllegalArgumentException("Not yet implemented"); //$NON-NLS-1$
}
@Override
public String toString() {
final StringBuilder code = new StringBuilder();
final SVGMatrix m = getMatrix();
switch(getType()) {
case SVG_TRANSFORM_MATRIX:
code.append(SVGAttributes.SVG_TRANSFORM_MATRIX).append('(').append(m).append(')');
break;
case SVG_TRANSFORM_ROTATE:
code.append(SVGAttributes.SVG_TRANSFORM_ROTATE).append('(').append(getRotationAngle());
if(!MathUtils.INST.equalsDouble(m.getE(), 0.) || !MathUtils.INST.equalsDouble(m.getF(), 0.))
code.append(' ').append(m.getE()).append(' ').append(m.getF());
code.append(')');
break;
case SVG_TRANSFORM_SCALE:
code.append(SVGAttributes.SVG_TRANSFORM_SCALE).append('(').append(getXScaleFactor()).append(' ').append(getYScaleFactor()).append(')');
break;
case SVG_TRANSFORM_SKEWX:
code.append(SVGAttributes.SVG_TRANSFORM_SKEW_X).append('(').append(getXSkewAngle()).append(')');
break;
case SVG_TRANSFORM_SKEWY:
code.append(SVGAttributes.SVG_TRANSFORM_SKEW_Y).append('(').append(getYSkewAngle()).append(')');
break;
case SVG_TRANSFORM_TRANSLATE:
code.append(SVGAttributes.SVG_TRANSFORM_TRANSLATE).append('(').append(getTX()).append(' ').append(getTY()).append(')');
break;
}
return code.toString();
}
}