/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.perseus.model;
import com.sun.perseus.util.SimpleTokenizer;
import com.sun.perseus.util.SVGConstants;
import org.w3c.dom.DOMException;
import java.util.Vector;
/**
* <code>AbstractAnimate</code> is used as a base class for various
* animation classes (see <code>Animate</code> and <code>AnimateMotion</code>).
*
* @version $Id: AbstractAnimate.java,v 1.5 2006/06/29 10:47:28 ln156897 Exp $
*/
public abstract class AbstractAnimate extends Animation {
/**
* calcMode value for linear interpolation.
*/
public static final int CALC_MODE_LINEAR = 1;
/**
* calcMode value for discrete interpolation
*/
public static final int CALC_MODE_DISCRETE = 2;
/**
* calcMode value for paced interpolation.
*/
public static final int CALC_MODE_PACED = 3;
/**
* calcMode for spline interpolation.
*/
public static final int CALC_MODE_SPLINE = 4;
/**
* Minimum required flatness for keySpline approximations by
* polylines.
*/
public static final float MIN_FLATNESS_SQUARE = 0.01f * 0.01f;
/**
* End value specification
*/
String to;
/**
* Starting value specification.
*/
String from;
/**
* Intermediate value specification.
*/
String by;
/**
* Complete values specification list.
*/
String values;
/**
* The interpolation mode, one of the CALC_MODE_XYZ values.
*/
int calcMode = CALC_MODE_LINEAR;
/**
* The actual calcMode. For types which do not support interpolation,
* the calcMode is forced to be discrete.
*/
int actualCalcMode = CALC_MODE_DISCRETE;
/**
* Key times, to control the animation pace. May be null or a list of
* values between 0 and 1.
* @see <a
* href="http://www.w3.org/TR/smil20/smil-timing.html#animation-adef-keyTimes>SMIL
* 2.0 Specification</a>
*/
float[] keyTimes;
/**
* Key splines, used to control the speed of animation on a particular
* time interval (interval pacing).
*
* May be null or an array of arrays of 4 floating point values.
* @see <a
* href="http://www.w3.org/TR/smil20/smil-timing.html#animation-animationNS-InterpolationKeysplines"/>SMIL
* 2.0 Specification</a>
*/
float[][] keySplines;
/**
* refSplines is an array of points which define a linear approximation of
* the keySplines. refSplines is computed in the validate() method and
* used in the curve() method. There is one float array per keySpline.
*/
float[][][] refSplines;
/**
* Controls whether the animation is additive or not. True maps to the
* SVG 'sum' value and false maps to the SVG 'replace' value.
*/
boolean additive;
/**
* Controls whether the animation has a cumulative behavior or not. This
* covers the behavior over multiple iterations of the simple duration.
*/
boolean accumulate;
/**
* The RefValues corresponding to this <set> element. A <set>
* element has a single segment with the same begin and end value.
*/
RefValues refValues = null;
/**
* Used, temporarily, to hold the refValues for the to attribute.
*/
RefValues toRefValues;
/**
* Used, temporarily, to hold the refValues for the from attribute.
*/
RefValues fromRefValues;
/**
* Used, temporarily, to hold the refValues for the by attribute.
*/
RefValues byRefValues;
/**
* Used, temporarily, to hold the refValues for the values attribute.
*/
RefValues valuesRefValues;
/**
* refTimes holds key times for the animation.
*
* refTimes is computed in the validate method.
*
* refTimes holds the key times for the <em>begining</em> of each segment.
* Therefore, there is as many refTimes as there are segments, in all cases.
*
* @see #validate
*/
float[] refTimes;
/**
* A working buffer for mapping segment index and progress
*/
float[] sisp = {0, 0};
/**
* Simple flag to check if we are dealing with a to-animation or
* not. This flag is needed to control the addition and cumulative
* behavior on to-animations, which is different than that on other
* animations (e.g., a values or a from-to animation).
*/
boolean isToAnimation;
/**
* Builds a new Animate element that belongs to the given
* document. This <code>Animate</code> will belong
* to the <code>DocumentNode</code>'s time container.
*
* @param ownerDocument the document this node belongs to.
* @param localName the animation element's local name.
* @throws IllegalArgumentException if the input ownerDocument is null
*/
public AbstractAnimate(final DocumentNode ownerDocument,
final String localName) {
super(ownerDocument, localName);
}
/**
* This is the Animate element's animation function.
*
* f(t) {
* a. Compute the 'simple duration penetration' p
* p = t / dur
* where dur is the simple duration.
*
* b. Compute the 'current time segment' i
* refTimes[i] <= p < refTimes[i+1]
*
* c. Compute the 'segment penetration' sp
* sp = (p - refTimes[i])
* /
* (refTimes[i+1] - refTimes[i])
* Note: 0 <= sp <= 1
*
* d. Compute the 'interpolated interval
* penetration' isp
*
* isp = calcMode(sp)
* Note: 0 <= isp <= 1
*
* e. Compute the animated value:
* v = refValues.compute(isp)
* v has the same number of components as refValues.
* }
*
* @param t the animation's simple time.
*/
Object[] f(final long t) {
// a. Compute the simple duration penetration p
// See: http://www.w3.org/TR/smil20/smil-timing.html#animation-animationNS-InterpolationAndIndefSimpleDur
float p = 0;
if (timedElementSupport.simpleDur != null
&&
timedElementSupport.simpleDur.isResolved()) {
p = t / (float) timedElementSupport.simpleDur.value;
}
// Iterate for each component.
int nc = refValues.getComponents();
float sp = 0;
int si = 0;
float endTime = 1;
float beginTime = 0;
int i = 0;
// b. Compute the 'current time segment' index si[ci]
// We iterate from 1 because the first value in refTimes
// is always 0.
for (i = 1; i < refTimes.length; i++) {
if (p < refTimes[i]) {
endTime = refTimes[i];
break;
}
beginTime = refTimes[i];
}
si = i - 1;
// c. Compute the segment penetration
if (endTime == beginTime) {
sp = 1;
} else {
sp = (p - beginTime) / (endTime - beginTime);
}
// d. Compute the 'interpolated segment penetration'
sp = calcMode(sp, si);
// At this point, we have computed:
// a. the array of time segment indices corresponding to 't'
// b. the array of penetration into the time segments.
//
// The following call lets the animate implementation map
// the time segment indices and the time segment penetration
// into refValues indices and penetration, in case these are
// different. Typically, these are the same, but they may be
// different, for example in the case of animateMotion with
// keyPoints.
sisp[0] = si;
sisp[1] = sp;
mapToSegmentProgress(si, sp, sisp);
si = (int) sisp[0];
sp = sisp[1];
// If we are dealing with a to-animation, we need to get the
// value for the single segment's start value.
if (isToAnimation) {
Object[] baseValue = baseVal.getBaseValue();
refValues.getSegment(0).setStart(baseValue);
return refValues.compute(si, sp);
}
// The reminder of this method is for non-additive animations
// Compute the simple function value
Object[] v = refValues.compute(si, sp);
// Account for cumulative behavior.
if (!isToAnimation && accumulate && timedElementSupport.curIter > 0) {
// Last simple time value, lv
Object[] lv =
refValues.getSegment(refValues.getSegments() - 1).getEnd();
Object[] r = traitAnim.multiply(lv, timedElementSupport.curIter);
v = traitAnim.sum(r, v);
}
// Now, account for additive behavior.
if (!additive) {
return v;
}
return traitAnim.sum(baseVal.getBaseValue(), v);
}
/**
* The following call lets the animate implementation map
* the time segment indices and the time segment penetration
* into refValues indices and penetration, in case these are
* different. Typically, these are the same, but they may be
* different, for example in the case of animateMotion with
* keyPoints.
*/
protected void mapToSegmentProgress(final int si,
final float sp,
final float[] sisp) {
}
/**
* Intepolates the input value depending on the calcMode attribute
* and, in the case of spline interpolation, depending on the
* keySplines value.
*
* @param p the value to interpolate. Should be in the [0, 1] range.
* @param si the time segment from which the computation is done. This
* is needed to identify the correct keySplines to use in
* case of spline animations.
*/
float calcMode(final float p, final int si) {
switch (actualCalcMode) {
case CALC_MODE_DISCRETE:
return 0;
case CALC_MODE_LINEAR:
// Linear does not modify the current penetration
// because we are on an x = y line.
case CALC_MODE_PACED:
// Paced does not modify the current penetration
// because we have computed the time intervals
// so as to have constant velocity. There is no
// further interpolation in this method.
return p;
case CALC_MODE_SPLINE:
default:
return curve(p, refSplines[si]);
}
}
/**
* Computes an array of points which do a linear approximation of the
* input splines.
*
* @param splines the array of splines to approximate with a polyline.
* The splines array should be an made of four element floats.
* if any of the elements is null, a NullPointerException will
* be thrown. If any of the element has a length less than four,
* an ArrayIndexOutOfBoundsException will be thrown.
* @return an array of point arrays. Each point array is a polyline
* approximation of the input spline.
*/
static float[][][] toRefSplines(final float[][] splines) {
float[][][] rSplines = new float[splines.length][][];
float[][] splineDef = new float[4][2];
for (int i = 0; i < splines.length; i++) {
Vector v = new Vector();
v.addElement(new float[] {0, 0});
splineDef[0][0] = 0;
splineDef[0][1] = 0;
splineDef[1][0] = splines[i][0];
splineDef[1][1] = splines[i][1];
splineDef[2][0] = splines[i][2];
splineDef[2][1] = splines[i][3];
splineDef[3][0] = 1;
splineDef[3][1] = 1;
toRefSpline(splineDef, v);
float[][] polyline = new float[v.size()][];
v.copyInto(polyline);
rSplines[i] = polyline;
}
return rSplines;
}
/**
* Converts the input spline curve (defined by its four control points)
* into a polyline approximation (i.e., an array of points). If the curve
* is flat enough (see the <code>isFlat</code> method), then the curve is
* approximated to a line between its two end control points and the
* last control point is added to the input <code>segment</code> vector.
* If the curve is not flat enough, then it is sub-divided and the
* subdivided curves are, recursively, tested for flatness.
*
* The flatness used for flatness test is controlled by the
* <code>MIN_FLATNESS_SQUARE</code> constant.
*
* @param curve the spline curve to approximate. Should not be null. Should
* be an array of four arrays of two elements. If there are less than
* four arrays or if there are less than two elements in these arrays
* then an ArrayIndexOutOfBoundsException is thrown. If any element
* in the curve array is null, a NullPointerException is thrown.
* @param segments the vector to which polyline points should be added.
* Should not be null.
*/
static void toRefSpline(final float[][] curve, final Vector segments) {
if (isFlat(curve, MIN_FLATNESS_SQUARE)) {
segments.addElement(new float[] {curve[3][0], curve[3][1]});
return;
}
float[][] lcurve = new float[4][2];
float[][] rcurve = new float[4][2];
// L1 = P1
lcurve[0] = curve[0];
// L2 = (P1 + P2) / 2
lcurve[1][0] = (curve[0][0] + curve[1][0]) / 2;
lcurve[1][1] = (curve[0][1] + curve[1][1]) / 2;
// H = (P2 + P3) / 2
float[] h = new float[2];
h[0] = (curve[1][0] + curve[2][0]) / 2;
h[1] = (curve[1][1] + curve[2][1]) / 2;
// L3 = (L2 + H) / 2
lcurve[2][0] = (lcurve[1][0] + h[0]) / 2;
lcurve[2][1] = (lcurve[1][1] + h[1]) / 2;
// R3 = (P3 + P4) / 2
rcurve[2][0] = (curve[2][0] + curve[3][0]) / 2;
rcurve[2][1] = (curve[2][1] + curve[3][1]) / 2;
// R2 = (H + R3) / 2
rcurve[1][0] = (h[0] + rcurve[2][0]) / 2;
rcurve[1][1] = (h[1] + rcurve[2][1]) / 2;
// L4 = R1 = (L3 + R2) / 2
lcurve[3][0] = (lcurve[2][0] + rcurve[1][0]) / 2;
lcurve[3][1] = (lcurve[2][1] + rcurve[1][1]) / 2;
rcurve[0] = lcurve[3];
// R4 = P4
rcurve[3] = curve[3];
toRefSpline(lcurve, segments);
toRefSpline(rcurve, segments);
}
/**
* @param curve the spline curve to test for flatness. The curve is
* considered flat if the distance from the two intermediate control
* points from the line between the first and the last control points
* is less than the desired flatness maximum. The input array must
* have at least four elements and each one must be at least
* 2 elements long. If not, an ArrayOutOfBoundsException is thrown.
* The curve array should not be null.
* @param flatness. The maximum distance allowed for the intermediate curve
* control points.
*/
static boolean isFlat(float[][] curve, float flatness) {
// Compute the square distance of P2 and P3 to the (P1, P4) line.
float dx = curve[3][0] - curve[0][0];
float dy = curve[3][1] - curve[0][1];
float div = dx * dx + dy * dy;
if (div == 0) {
return true;
}
float den = (dx * (curve[1][1] - curve[0][1])
-
dy * (curve[1][0] - curve[0][0]));
float dP2Sq = (den * den) / div;
den = (dx * (curve[2][1] - curve[0][1])
-
dy * (curve[2][0] - curve[0][0]));
float dP3Sq = (den * den) / div;
return (dP2Sq <= flatness && dP3Sq <= flatness);
}
/**
* Computes the curve value for the requested value on the specified
* curves, defined by a polyline approximation.
*
* The method assumes that the input polyline points are between 0 and 1 and
* in increasing order along the x axis. The method considers the p value to
* be on the polyline's x-axis and finds the two points between which it
* lies and returns a linear approximation for that segment.
*
* For degenerate cases (e.g., if the polyline x-axis values are not in the
* [0, 1] interval or if p is not in the [0, 1] interval either), the method
* returns 0 if the input penetration p is less than the first polyline
* point. The method returns 1 if the input penetration is more than the
* last polyline x-axis coordinate.
*
* @param p the value for which the curve polynomial should be computed.
* @param polyline the polyline curve approximation. Should not be null.
* each element in the polyline array should not be null. Each
* element in the array should be at least of length 2.
*/
static float curve(final float p,
final float[][] polyline) {
int i = 0;
for (; i < polyline.length; i++) {
if (p < polyline[i][0]) {
break;
}
}
if (i == polyline.length || i == 0) {
// Degenerate cases.
//
// This should never happen:
//
// Should not be polyline.length because the last entry in polyline
// should be '1' and the maximum input value for p should be '1'.
//
// Should not get 0 because the first entry should be zero and the
// input parameter should be greater or equal to zero.
//
return i / (float) polyline.length; // returns 0 for 0 and 1 for 1
}
float[] from = polyline[i - 1];
float[] to = polyline[i];
// Compute the progress between from and to:
//
// p = (1 - t) * from[0] + t.to[0]
// p = t * (t[0] - from[0]) + from[0]
// t = (p - from[0]) / (to[0] - from[0])
//
// So, the interpolated progress is:
//
// ip = (1 - t) * from[1] + t.to[1]
// ip = (1 - (p - from[0]) / (to[0] - from[0])) * from[1]
// + ((p - from[0]) / (to[0] - from[0])) * to[1]
// ip = ((to[0] - from[0] - p + from[0]) / (to[0] - from[0])) * from[1]
// + (to[1] * p - to[1] * from[0]) / (to[0] - from[0])
// ip = ((to[0] - p) / (to[0] - from[0]) * from[1]
// + (to[1] * p - to[1] * from[0]) / (to[0] - from[0])
// ip = (to[0] * from[1] - p * from[1]) / (to[0] - from[0])
// + (to[1] * p - to[1] * from[0]) / (to[0] - from[0])
// ip = (to[0] * from[1] - p * from[1] + to[1] * p - to[1] * from[0])
// / (to[0] - from[0])
// ip = (to[0] * from[1] - to[1] * from[0] + p * (to[1] - from[1]))
// / (to[0] - from[0])
return (to[0] * from[1] - to[1] * from[0] + p * (to[1] - from[1]))
/ (to[0] - from[0]);
}
/**
* Supported traits: to, attributeName
*
* @param traitName the name of the trait which the element may support.
* @return true if this element supports the given trait in one of the
* trait accessor methods.
*/
boolean supportsTrait(final String traitName) {
if (SVGConstants.SVG_TO_ATTRIBUTE == traitName
||
SVGConstants.SVG_FROM_ATTRIBUTE == traitName
||
SVGConstants.SVG_BY_ATTRIBUTE == traitName
||
SVGConstants.SVG_VALUES_ATTRIBUTE == traitName
||
SVGConstants.SVG_CALC_MODE_ATTRIBUTE == traitName
||
SVGConstants.SVG_KEY_TIMES_ATTRIBUTE == traitName
||
SVGConstants.SVG_KEY_SPLINES_ATTRIBUTE == traitName
||
SVGConstants.SVG_ADDITIVE_ATTRIBUTE == traitName
||
SVGConstants.SVG_ACCUMULATE_ATTRIBUTE == traitName) {
return true;
} else {
return super.supportsTrait(traitName);
}
}
// JAVADOC COMMENT ELIDED
public String getTraitImpl(final String name)
throws DOMException {
if (SVGConstants.SVG_TO_ATTRIBUTE == name) {
return to;
} else if (SVGConstants.SVG_FROM_ATTRIBUTE == name) {
return from;
} else if (SVGConstants.SVG_BY_ATTRIBUTE == name) {
return by;
} else if (SVGConstants.SVG_VALUES_ATTRIBUTE == name) {
return values;
} else if (SVGConstants.SVG_CALC_MODE_ATTRIBUTE == name) {
switch (calcMode) {
case CALC_MODE_DISCRETE:
return SVGConstants.SVG_DISCRETE_VALUE;
case CALC_MODE_LINEAR:
return SVGConstants.SVG_LINEAR_VALUE;
case CALC_MODE_PACED:
return SVGConstants.SVG_PACED_VALUE;
case CALC_MODE_SPLINE:
default:
return SVGConstants.SVG_SPLINE_VALUE;
}
} else if (SVGConstants.SVG_KEY_TIMES_ATTRIBUTE == name) {
return toStringTrait(keyTimes);
} else if (SVGConstants.SVG_KEY_SPLINES_ATTRIBUTE == name) {
return toStringTrait(keySplines);
} else if (SVGConstants.SVG_ADDITIVE_ATTRIBUTE == name) {
if (additive) {
return SVGConstants.SVG_SUM_VALUE;
}
return SVGConstants.SVG_REPLACE_VALUE;
} else if (SVGConstants.SVG_ACCUMULATE_ATTRIBUTE == name) {
if (accumulate) {
return SVGConstants.SVG_SUM_VALUE;
}
return SVGConstants.SVG_NONE_VALUE;
} else {
return super.getTraitImpl(name);
}
}
// JAVADOC COMMENT ELIDED
public void setTraitImpl(final String name,
final String value)
throws DOMException {
if (SVGConstants.SVG_TO_ATTRIBUTE == name) {
checkWriteLoading(name);
to = value;
} else if (SVGConstants.SVG_FROM_ATTRIBUTE == name) {
checkWriteLoading(name);
from = value;
} else if (SVGConstants.SVG_BY_ATTRIBUTE == name) {
checkWriteLoading(name);
by = value;
} else if (SVGConstants.SVG_VALUES_ATTRIBUTE == name) {
checkWriteLoading(name);
values = value;
} else if (SVGConstants.SVG_KEY_TIMES_ATTRIBUTE == name) {
checkWriteLoading(name);
// Generic float array parsing
keyTimes = parseFloatArrayTrait(name, value.replace(';', ','));
// Now, check that the keyTimes values are from 0 to 1 and
// in increasing order. Note that this only performs the
// validations which can be made prior to a call to validate.
if (keyTimes[0] < 0 || keyTimes[0] > 1) {
throw illegalTraitValue(name, value);
}
for (int i = 1; i < keyTimes.length; i++) {
if (keyTimes[i] < 0 || keyTimes[i] > 1) {
throw illegalTraitValue(name, value);
}
if (keyTimes[i] < keyTimes[i - 1]) {
throw illegalTraitValue(name, value);
}
}
} else if (SVGConstants.SVG_KEY_SPLINES_ATTRIBUTE == name) {
checkWriteLoading(name);
SimpleTokenizer st = new SimpleTokenizer(value, ";");
int n = st.countTokens();
keySplines = new float[n][];
for (int i = 0; i < n; i++) {
String splineDef = st.nextToken();
keySplines[i] = parseFloatArrayTrait(name, splineDef);
if (keySplines[i].length != 4 ||
keySplines[i][0] < 0 ||
keySplines[i][0] > 1 ||
keySplines[i][1] < 0 ||
keySplines[i][1] > 1 ||
keySplines[i][2] < 0 ||
keySplines[i][2] > 1 ||
keySplines[i][3] < 0 ||
keySplines[i][3] > 1) {
throw illegalTraitValue(name, value);
}
}
} else if (SVGConstants.SVG_ADDITIVE_ATTRIBUTE == name) {
checkWriteLoading(name);
if (SVGConstants.SVG_REPLACE_VALUE.equals(value)) {
additive = false;
} else if (SVGConstants.SVG_SUM_VALUE.equals(value)) {
additive = true;
} else {
throw illegalTraitValue(name, value);
}
} else if (SVGConstants.SVG_ACCUMULATE_ATTRIBUTE == name) {
checkWriteLoading(name);
if (SVGConstants.SVG_NONE_VALUE.equals(value)) {
accumulate = false;
} else if (SVGConstants.SVG_SUM_VALUE.equals(value)) {
accumulate = true;
} else {
throw illegalTraitValue(name, value);
}
} else if (SVGConstants.SVG_CALC_MODE_ATTRIBUTE == name) {
if (SVGConstants.SVG_DISCRETE_VALUE.equals(value)) {
calcMode = CALC_MODE_DISCRETE;
} else if (SVGConstants.SVG_LINEAR_VALUE.equals(value)) {
calcMode = CALC_MODE_LINEAR;
} else if (SVGConstants.SVG_PACED_VALUE.equals(value)) {
calcMode = CALC_MODE_PACED;
} else if (SVGConstants.SVG_SPLINE_VALUE.equals(value)) {
calcMode = CALC_MODE_SPLINE;
} else {
throw illegalTraitValue(name, value);
}
} else {
super.setTraitImpl(name, value);
}
}
/**
* Computes refTimes so that each animation segment lasts the same length
* of time.
*/
float[] getDefaultTiming(RefValues refValues) {
int ns = refValues.getSegments();
float[] refTimes = new float[ns];
float startTime = 0;
float segLength = 1 / (float) ns;
for (int i = 0; i < ns; i++) {
refTimes[i] = startTime;
startTime += segLength;
}
return refTimes;
}
/**
* Computes refTimes so that there is a paced speed on each values segment.
*/
float[] getPacedTiming(RefValues refValues) {
// a) Compute the refValues length, D
// D = sum(from 0 to n; seg(i).length());
//
// b) Compute the overall paced velocity, V
// V = D / dur;
//
// c) For each segment i, compute its refTime:
// refTime[j][i] = refTime[j][i-1] + (seg(i).length(j) / V(j)) / dur;
float D = refValues.getLength();
int ns = refValues.getSegments();
refTimes = new float[ns];
float prevRefTime = 0;
if (D > 0) {
// For each segment index si
for (int si = 1; si < ns; si++) {
refTimes[si] = prevRefTime + refValues.getLength(si -1) / D;
prevRefTime = refTimes[si];
}
} else {
// Segments are all of zero length, they should be evenly spaced
// in time.
float sl = 1f / ns;
for (int si = 1; si < ns; si++) {
refTimes[si] = si * sl;
}
}
return refTimes;
}
/**
* Validating an Animate consists in:
* a) Setting its target element. If there was no idRef, then targetElement
* is still null and will be positioned to the parent node.
*
* b) Validating the from, to, by and values traits with the targetElement,
* using the target trait name, namespace and value.
*
* c) Validating the keyTimes and the keySplines trait values to check they
* are compatible with the values specification.
*
* @throws DOMException if there is a validation error, for example if the
* to value is incompatible with the target trait or if the target
* trait is not animatable.
*/
void validate() throws DOMException {
// =====================================================================
// a) Set the target element.
if (targetElement == null) {
targetElement = (ElementNode) parent;
}
// =====================================================================
// Check that the traitName attribute was specified.
if (traitName == null) {
throw illegalTraitValue(SVGConstants.SVG_ATTRIBUTE_NAME_ATTRIBUTE,
traitName);
}
// =====================================================================
// b) Validate the to/from/by/values traits with the target element.
// Note that traitName should _never_ be null when validate() is
// invoked. It is either required (e.g., for Animate) or fixed (e.g.,
// for AnimateMotion. If, for example in some unit test configutations,
// this method is called without a specified traitName, the method
// generates a NullPointerException.
traitAnim = targetElement.getSafeTraitAnimNS(traitNamespace, traitName);
// If the traitAnim type does not support interpolation, force the
// calcMode to be discrete.
if (traitAnim.supportsInterpolation()) {
actualCalcMode = calcMode;
} else {
actualCalcMode = CALC_MODE_DISCRETE;
}
validateValues();
// Now, apply precedence rules to compute the actual RefValues.
selectRefValues();
// If the animation has no effect, stop here.
if (hasNoEffect) {
return;
}
// =====================================================================
// If we are dealing with discrete timing, we need to add a last time
// segment so that we implement the desired behavior for discrete
// timing. With a single interval with start/end values, only the start
// value will be shown. If we add a new interval with the end value,
// then the end value will be shown for the last time interval.
if (actualCalcMode == CALC_MODE_DISCRETE) {
refValues.makeDiscrete();
}
// Initialize the refValues
refValues.initialize();
// =====================================================================
// c. Validate that keyTimes and keySplines trait values are compatible
// with the values specification.
computeRefTimes();
// c.1 Validate that keySplines is compatible with the animation set-up
//
// See:
// http://www.w3.org/TR/2001/REC-smil20-20010807/smil20.html#animation-animationNS-InterpolationKeysplines
//
// The attribute is ignored unless the CALC_MODE is spline. For spline
// interpolation, there must be one fewer sets of control points in the
// keySplines attribute than there are keyTimes.
//
if (actualCalcMode == CALC_MODE_SPLINE) {
if (keySplines == null
||
refTimes.length != keySplines.length) {
throw animationError(idRef,
traitNamespace,
traitName,
targetElement.getNamespaceURI(),
targetElement.getLocalName(),
getId(),
getNamespaceURI(),
getLocalName(),
Messages.formatMessage(
Messages.ERROR_INVALID_ANIMATION_KEY_SPLINES,
new Object[] {
getTrait(
SVGConstants.SVG_KEY_SPLINES_ATTRIBUTE),
getTrait(SVGConstants.SVG_KEY_TIMES_ATTRIBUTE)
}));
}
// Turn the keySplines attribute into a set of (x, y) points
// which can be interpolated between.
refSplines = toRefSplines(keySplines);
}
}
/**
* Computes refTimes from the calcMode and keyTimes attributes. Validates
* that the keyTimes attribute is compatible with the animate set up. This
* may be overridden by subclasses (e.g., animateMotion), when there are
* special rules for checking keyTimes compatiblity.
*/
protected void computeRefTimes() throws DOMException {
// c.1 Validate that keyTimes is compatible with the animation set-up
//
// if (calcMode == paced)
// refTimes such that change velocity is constant
// refTimes = getPacedTiming(refValues) // see follow on slides
//
// else if (keyTimes defined)
// refTimes = keyTimes
// if (calcMode == discreet)
// refTimes[n] = 1 // Accounts for the added nth value
// // in refValues
//
// else refTimes = f(refValues)
// the simple duration is divided into n-1 intervals,
// where n = refValues.length
// refTimes = defaultTiming(refValues)
if (actualCalcMode == CALC_MODE_PACED) {
// keyTimes are ignored
// See: http://www.w3.org/TR/smil20/smil-timing.html#animation-adef-keyTimes
refTimes = getPacedTiming(refValues);
} else if (keyTimes != null) {
// Check keyTimes is compatible with the animation specification.
//
// a) In all cases, the first keyTime must be zero.
//
// b) For non-discrete animations,
// b.1) the last keyTime must be one.
// b.2) there should be one more keyTimes than there are
// segments.
//
// c) For discrete animations,
// c.1) there should be as many keyTimes as there are segments.
//
if (/* a) */ keyTimes.length < 1 || keyTimes[0] != 0
||
/* b) */ (actualCalcMode != CALC_MODE_DISCRETE
&&
(/* b.1) */ keyTimes[keyTimes.length - 1] != 1
||
/* b.2) */ keyTimes.length !=
refValues.getSegments() + 1))
||
/* c) */ (actualCalcMode == CALC_MODE_DISCRETE
&&
/* c.1) */ keyTimes.length !=
refValues.getSegments())) {
throw animationError(idRef,
traitNamespace,
traitName,
targetElement.getNamespaceURI(),
targetElement.getLocalName(),
getId(),
getNamespaceURI(),
getLocalName(),
Messages.formatMessage(
Messages.ERROR_INVALID_ANIMATION_KEY_TIMES,
new Object[] {
getTrait(SVGConstants.SVG_KEY_TIMES_ATTRIBUTE)
}));
}
// If the calcMode is _not_ discrete, we trim the last '1'
// value.
if (actualCalcMode != CALC_MODE_DISCRETE) {
refTimes = new float[keyTimes.length - 1];
System.arraycopy(keyTimes, 0, refTimes, 0, refTimes.length);
} else {
refTimes = keyTimes;
}
// Validate that there are as many refTimes as there are refValues.
// We do the check using the first component, as we know there is at
// least one component no matter what value type we are dealing
// with.
if (refTimes.length != refValues.getSegments()) {
throw animationError(idRef,
traitNamespace,
traitName,
targetElement.getNamespaceURI(),
targetElement.getLocalName(),
getId(),
getNamespaceURI(),
getLocalName(),
Messages.formatMessage(
Messages.ERROR_INVALID_ANIMATION_KEY_TIMES,
new Object[] {
getTrait(SVGConstants.SVG_KEY_TIMES_ATTRIBUTE)
}));
}
} else {
// Simple case, give the same time length to each segment.
refTimes = getDefaultTiming(refValues);
}
}
/**
* Validates the different so-called values attributes, such as the
* to/from/by and values attributes. Derived classes may have more 'values'
* attributes, and may override this method to validate these additional
* attributes.
*
* @throws DOMException if there is a validation error, for example if the
* to value is incompatible with the target trait or if the target
* trait is not animatable.
*/
final void validateValues() throws DOMException {
// b.1) Validate the to traits value
toRefValues = null;
if (to != null) {
toRefValues = traitAnim.toRefValues(this,
new String[] {to},
null,
SVGConstants.SVG_TO_ATTRIBUTE);
}
// b.2) Validate the by traits value
byRefValues = null;
if (by != null) {
byRefValues = traitAnim.toRefValues(this,
new String[] {by},
null,
SVGConstants.SVG_BY_ATTRIBUTE);
}
// b.3) Validate the from traits value
fromRefValues = null;
if (from != null) {
fromRefValues = traitAnim.toRefValues(this,
new String[] {from},
null,
SVGConstants.SVG_FROM_ATTRIBUTE);
}
// b.4) Validate the values trait value
valuesRefValues = null;
if (values != null) {
String[] v = parseStringArrayTrait(
SVGConstants.SVG_VALUES_ATTRIBUTE,
values,
";");
valuesRefValues = traitAnim.toRefValues(this,
v,
null,
SVGConstants.SVG_VALUES_ATTRIBUTE);
}
validateValuesExtra();
}
/**
* Allows extension classes to validate addition values sources.
*
* @throws DOMException if there is a validation error, for example if the
* to value is incompatible with the target trait or if the target
* trait is not animatable.
*/
void validateValuesExtra() throws DOMException {
}
/**
* Allows extensions to select a different source for refValues, in case
* the extension has addition values sources that have higher precedence
* than the default. For example, animateMotion has the path attribute and
* the mpath children which have higher precedence.
*
* @throws DOMException if there is no way to compute a set of reference
* values, for example if none of the values sources is specified.
*/
void selectRefValuesExtra() throws DOMException {
}
/**
* Computes the 'right' source for reference values, depending on the
* precedence rules for the different values sources.
*
* @throws DOMException if there is no way to compute a set of reference
* values, for example if none of the values sources is specified.
*/
final void selectRefValues() throws DOMException {
refValues = null;
selectRefValuesExtra();
if (refValues != null) {
return;
}
// Pseudo-code for accounting for precedence rules.
//
// If (values defined)
// // values anim
// refValues = values;
// else if (from defined)
// if (to defined)
// // from-to anim
// refValues = (from; to)
// else if (by defined)
// // from-by anim
// refValues = (from; from+by)
// else
// // no effect, there is nothing like a from anim
// else if (by defined)
// // by anim
// refValues = (0; by) and force additive anim
// else if (to defined)
// // to anim
// refValues = (baseVal; to)
// else
// // no effect
//
isToAnimation = false;
if (values != null) {
refValues = valuesRefValues;
} else if (from != null) {
refValues = fromRefValues;
if (to != null) {
// from-to animatin
refValues.getSegment(0).collapse(toRefValues.getSegment(0),
this);
} else if (by != null) {
// from-by animation
if (refValues.getSegment(0).isAdditive()) {
try {
refValues.getSegment(0).addToEnd(
byRefValues.getSegment(0).getStart());
} catch (IllegalArgumentException iae) {
// Incompatible by value
throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
Messages.formatMessage(
Messages.ERROR_INCOMPATIBLE_FROM_BY,
new Object[] {
traitName,
traitNamespace,
getLocalName(),
getNamespaceURI(),
from,
to
}));
}
} else {
throw new DOMException(
DOMException.NOT_SUPPORTED_ERR,
Messages.formatMessage(
Messages.ERROR_ATTRIBUTE_NOT_ADDITIVE_FROM_BY,
new Object[] {
traitName,
traitNamespace,
getLocalName(),
getNamespaceURI()
}));
}
} else {
throw animationError(idRef,
traitNamespace,
traitName,
targetElement.getNamespaceURI(),
targetElement.getLocalName(),
getId(),
getNamespaceURI(),
getLocalName(),
Messages.formatMessage(
Messages.ERROR_INVALID_ANIMATION_FROM_ANIM,
null));
}
} else if (by != null) {
// by animation
if (!byRefValues.getSegment(0).isAdditive()) {
throw new DOMException(
DOMException.NOT_SUPPORTED_ERR,
Messages.formatMessage(
Messages.ERROR_ATTRIBUTE_NOT_ADDITIVE_BY,
new Object[] {
traitName,
traitNamespace,
getLocalName(),
getNamespaceURI()
}));
}
refValues = byRefValues;
refValues.getSegment(0).setZeroStart();
additive = true;
} else if (to != null) {
// to animation
// We cannot compute the base value yet so we can only set the
// refValues to the toRefValues and delay computing the segment's
// begin until we actually have a baseValue. This will have to be
// updated on each computing cycle because the base value may change
// over time.
isToAnimation = true;
refValues = toRefValues;
} else {
// SMIL Animation
// specification:
// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#AnimFuncValues
// "similarly, if none of the from, to, by or values attributes are
// specified, the animation will have no effect."
hasNoEffect = true;
}
}
}
|