source: josm/trunk/src/com/kitfox/svg/SVGElement.java@ 12713

Last change on this file since 12713 was 11525, checked in by Don-vip, 7 years ago

see #14319 - update to latest version of svgSalamander (2017-01-07, patched)

  • Property svn:eol-style set to native
File size: 24.8 KB
Line 
1/*
2 * SVG Salamander
3 * Copyright (c) 2004, Mark McKay
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
9 *
10 * - Redistributions of source code must retain the above
11 * copyright notice, this list of conditions and the following
12 * disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials
16 * provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 *
31 * Mark McKay can be contacted at mark@kitfox.com. Salamander and other
32 * projects can be found at http://www.kitfox.com
33 *
34 * Created on January 26, 2004, 1:59 AM
35 */
36package com.kitfox.svg;
37
38import java.awt.geom.AffineTransform;
39import java.awt.geom.GeneralPath;
40import java.io.Serializable;
41import java.net.URI;
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.HashMap;
45import java.util.HashSet;
46import java.util.Iterator;
47import java.util.LinkedList;
48import java.util.List;
49import java.util.Set;
50import java.util.regex.Matcher;
51import java.util.regex.Pattern;
52
53import org.xml.sax.Attributes;
54import org.xml.sax.SAXException;
55
56import com.kitfox.svg.pathcmd.Arc;
57import com.kitfox.svg.pathcmd.BuildHistory;
58import com.kitfox.svg.pathcmd.Cubic;
59import com.kitfox.svg.pathcmd.CubicSmooth;
60import com.kitfox.svg.pathcmd.Horizontal;
61import com.kitfox.svg.pathcmd.LineTo;
62import com.kitfox.svg.pathcmd.MoveTo;
63import com.kitfox.svg.pathcmd.PathCommand;
64import com.kitfox.svg.pathcmd.Quadratic;
65import com.kitfox.svg.pathcmd.QuadraticSmooth;
66import com.kitfox.svg.pathcmd.Terminal;
67import com.kitfox.svg.pathcmd.Vertical;
68import com.kitfox.svg.xml.StyleAttribute;
69import com.kitfox.svg.xml.StyleSheet;
70import com.kitfox.svg.xml.XMLParseUtil;
71
72/**
73 * @author Mark McKay
74 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
75 */
76abstract public class SVGElement implements Serializable
77{
78
79 public static final long serialVersionUID = 0;
80 public static final String SVG_NS = "http://www.w3.org/2000/svg";
81 protected SVGElement parent = null;
82 protected final ArrayList<SVGElement> children = new ArrayList<>();
83 protected String id = null;
84 /**
85 * CSS class. Used for applying style sheet information.
86 */
87 protected String cssClass = null;
88 /**
89 * Styles defined for this elemnt via the <b>style</b> attribute.
90 */
91 protected final HashMap<String, StyleAttribute> inlineStyles = new HashMap<>();
92 /**
93 * Presentation attributes set for this element. Ie, any attribute other
94 * than the <b>style</b> attribute.
95 */
96 protected final HashMap<String, StyleAttribute> presAttribs = new HashMap<>();
97 /**
98 * A list of presentation attributes to not include in the presentation
99 * attribute set.
100 */
101 protected static final Set<String> ignorePresAttrib;
102
103 static
104 {
105 HashSet<String> set = new HashSet<>();
106// set.add("id");
107// set.add("class");
108// set.add("style");
109// set.add("xml:base");
110
111 ignorePresAttrib = Collections.unmodifiableSet(set);
112 }
113 /**
114 * This element may override the URI we resolve against with an xml:base
115 * attribute. If so, a copy is placed here. Otherwise, we defer to our
116 * parent for the reolution base
117 */
118 protected URI xmlBase = null;
119 /**
120 * The diagram this element belongs to
121 */
122 protected SVGDiagram diagram;
123 boolean dirty = true;
124
125 /**
126 * Creates a new instance of SVGElement
127 */
128 public SVGElement()
129 {
130 this(null, null, null);
131 }
132
133 public SVGElement(String id, SVGElement parent)
134 {
135 this(id, null, parent);
136 }
137
138 public SVGElement(String id, String cssClass, SVGElement parent)
139 {
140 this.id = id;
141 this.cssClass = cssClass;
142 this.parent = parent;
143 }
144
145 abstract public String getTagName();
146
147 public SVGElement getParent()
148 {
149 return parent;
150 }
151
152 void setParent(SVGElement parent)
153 {
154 this.parent = parent;
155 }
156
157 /**
158 * @return an ordered list of nodes from the root of the tree to this node
159 */
160 public List<SVGElement> getPath(List<SVGElement> retVec)
161 {
162 if (retVec == null)
163 {
164 retVec = new ArrayList<>();
165 }
166
167 if (parent != null)
168 {
169 parent.getPath(retVec);
170 }
171 retVec.add(this);
172
173 return retVec;
174 }
175
176 /**
177 * @param retVec - A list to add all children to. If null, a new list is
178 * created and children of this group are added.
179 *
180 * @return The list containing the children of this group
181 */
182 public List<SVGElement> getChildren(List<SVGElement> retVec)
183 {
184 if (retVec == null)
185 {
186 retVec = new ArrayList<>();
187 }
188
189 retVec.addAll(children);
190
191 return retVec;
192 }
193
194 /**
195 * @param id - Id of svg element to return
196 * @return the child of the given id, or null if no such child exists.
197 */
198 public SVGElement getChild(String id)
199 {
200 for (SVGElement ele : children) {
201 String eleId = ele.getId();
202 if (eleId != null && eleId.equals(id))
203 {
204 return ele;
205 }
206 }
207
208 return null;
209 }
210
211 /**
212 * Searches children for given element. If found, returns index of child.
213 * Otherwise returns -1.
214 */
215 public int indexOfChild(SVGElement child)
216 {
217 return children.indexOf(child);
218 }
219
220 /**
221 * Swaps 2 elements in children.
222 *
223 * @i index of first
224 * @j index of second
225 *
226 * @return true if successful, false otherwise
227 */
228 public void swapChildren(int i, int j) throws SVGException
229 {
230 if ((children == null) || (i < 0) || (i >= children.size()) || (j < 0) || (j >= children.size()))
231 {
232 return;
233 }
234
235 SVGElement temp = children.get(i);
236 children.set(i, children.get(j));
237 children.set(j, temp);
238 build();
239 }
240
241 /**
242 * Called during SAX load process to notify that this tag has begun the
243 * process of being loaded
244 *
245 * @param attrs - Attributes of this tag
246 * @param helper - An object passed to all SVG elements involved in this
247 * build process to aid in sharing information.
248 */
249 public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent) throws SAXException
250 {
251 //Set identification info
252 this.parent = parent;
253 this.diagram = helper.diagram;
254
255 this.id = attrs.getValue("id");
256 if (this.id != null && !this.id.equals(""))
257 {
258 diagram.setElement(this.id, this);
259 }
260
261 String className = attrs.getValue("class");
262 this.cssClass = (className == null || className.equals("")) ? null : className;
263 //docRoot = helper.docRoot;
264 //universe = helper.universe;
265
266 //Parse style string, if any
267 String style = attrs.getValue("style");
268 if (style != null)
269 {
270 HashMap<?, ?> map = XMLParseUtil.parseStyle(style, inlineStyles);
271 }
272
273 String base = attrs.getValue("xml:base");
274 if (base != null && !base.equals(""))
275 {
276 try
277 {
278 xmlBase = new URI(base);
279 } catch (Exception e)
280 {
281 throw new SAXException(e);
282 }
283 }
284
285 //Place all other attributes into the presentation attribute list
286 int numAttrs = attrs.getLength();
287 for (int i = 0; i < numAttrs; i++)
288 {
289 String name = attrs.getQName(i);
290 if (ignorePresAttrib.contains(name))
291 {
292 continue;
293 }
294 String value = attrs.getValue(i);
295
296 presAttribs.put(name, new StyleAttribute(name, value));
297 }
298 }
299
300 /**
301 * @return a set of Strings that corespond to CSS attributes on this element
302 */
303 public Set<String> getInlineAttributes()
304 {
305 return inlineStyles.keySet();
306 }
307
308 /**
309 * @return a set of Strings that corespond to XML attributes on this element
310 */
311 public Set<String> getPresentationAttributes()
312 {
313 return presAttribs.keySet();
314 }
315
316 /**
317 * Called after the start element but before the end element to indicate
318 * each child tag that has been processed
319 */
320 public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
321 {
322 children.add(child);
323 child.parent = this;
324 child.setDiagram(diagram);
325 }
326
327 protected void setDiagram(SVGDiagram diagram)
328 {
329 this.diagram = diagram;
330 diagram.setElement(id, this);
331 for (SVGElement ele : children) {
332 ele.setDiagram(diagram);
333 }
334 }
335
336 public void removeChild(SVGElement child) throws SVGElementException
337 {
338 if (!children.contains(child))
339 {
340 throw new SVGElementException(this, "Element does not contain child " + child);
341 }
342
343 children.remove(child);
344 }
345
346 /**
347 * Called during load process to add text scanned within a tag
348 */
349 public void loaderAddText(SVGLoaderHelper helper, String text)
350 {
351 }
352
353 /**
354 * Called to indicate that this tag and the tags it contains have been
355 * completely processed, and that it should finish any load processes.
356 */
357 public void loaderEndElement(SVGLoaderHelper helper) throws SVGParseException
358 {
359// try
360// {
361// build();
362// }
363// catch (SVGException se)
364// {
365// throw new SVGParseException(se);
366// }
367 }
368
369 /**
370 * Called by internal processes to rebuild the geometry of this node from
371 * it's presentation attributes, style attributes and animated tracks.
372 */
373 protected void build() throws SVGException
374 {
375 StyleAttribute sty = new StyleAttribute();
376
377 if (getPres(sty.setName("id")))
378 {
379 String newId = sty.getStringValue();
380 if (!newId.equals(id))
381 {
382 diagram.removeElement(id);
383 id = newId;
384 diagram.setElement(this.id, this);
385 }
386 }
387 if (getPres(sty.setName("class")))
388 {
389 cssClass = sty.getStringValue();
390 }
391 if (getPres(sty.setName("xml:base")))
392 {
393 xmlBase = sty.getURIValue();
394 }
395
396 //Build children
397 for (int i = 0; i < children.size(); ++i)
398 {
399 SVGElement ele = children.get(i);
400 ele.build();
401 }
402 }
403
404 public URI getXMLBase()
405 {
406 return xmlBase != null ? xmlBase
407 : (parent != null ? parent.getXMLBase() : diagram.getXMLBase());
408 }
409
410 /**
411 * @return the id assigned to this node. Null if no id explicitly set.
412 */
413 public String getId()
414 {
415 return id;
416 }
417 LinkedList<SVGElement> contexts = new LinkedList<>();
418
419 /**
420 * Hack to allow nodes to temporarily change their parents. The Use tag will
421 * need this so it can alter the attributes that a particular node uses.
422 */
423 protected void pushParentContext(SVGElement context)
424 {
425 contexts.addLast(context);
426 }
427
428 protected SVGElement popParentContext()
429 {
430 return contexts.removeLast();
431 }
432
433 protected SVGElement getParentContext()
434 {
435 return contexts.isEmpty() ? null : (SVGElement) contexts.getLast();
436 }
437
438 public SVGRoot getRoot()
439 {
440 return parent == null ? null : parent.getRoot();
441 }
442
443 /*
444 * Returns the named style attribute. Checks for inline styles first, then
445 * internal and extranal style sheets, and finally checks for presentation
446 * attributes.
447 * @param styleName - Name of attribute to return
448 * @param recursive - If true and this object does not contain the
449 * named style attribute, checks attributes of parents abck to root until
450 * one found.
451 */
452 public boolean getStyle(StyleAttribute attrib) throws SVGException
453 {
454 return getStyle(attrib, true);
455 }
456
457 /**
458 * Copies the current style into the passed style attribute. Checks for
459 * inline styles first, then internal and extranal style sheets, and finally
460 * checks for presentation attributes. Recursively checks parents.
461 *
462 * @param attrib - Attribute to write style data to. Must have it's name set
463 * to the name of the style being queried.
464 * @param recursive - If true and this object does not contain the named
465 * style attribute, checks attributes of parents back to root until one
466 * found.
467 */
468 public boolean getStyle(StyleAttribute attrib, boolean recursive) throws SVGException
469 {
470 String styName = attrib.getName();
471
472 //Check for local inline styles
473 StyleAttribute styAttr = inlineStyles.get(styName);
474
475 attrib.setStringValue(styAttr == null ? "" : styAttr.getStringValue());
476
477 //Return if we've found a non animated style
478 if (styAttr != null)
479 {
480 return true;
481 }
482
483
484 //Check for presentation attribute
485 StyleAttribute presAttr = presAttribs.get(styName);
486
487 attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
488
489 //Return if we've found a presentation attribute instead
490 if (presAttr != null)
491 {
492 return true;
493 }
494
495 //Check for style sheet
496 SVGRoot root = getRoot();
497 if (root != null)
498 {
499 StyleSheet ss = root.getStyleSheet();
500 if (ss != null)
501 {
502 return ss.getStyle(attrib, getTagName(), cssClass);
503 }
504 }
505
506 //If we're recursive, check parents
507 if (recursive)
508 {
509 SVGElement parentContext = getParentContext();
510 if (parentContext != null)
511 {
512 return parentContext.getStyle(attrib, true);
513 }
514 if (parent != null)
515 {
516 return parent.getStyle(attrib, true);
517 }
518 }
519
520 //Unsuccessful reading style attribute
521 return false;
522 }
523
524 /**
525 * @return the raw style value of this attribute. Does not take the
526 * presentation value or animation into consideration. Used by animations to
527 * determine the base to animate from.
528 */
529 public StyleAttribute getStyleAbsolute(String styName)
530 {
531 //Check for local inline styles
532 return inlineStyles.get(styName);
533 }
534
535 /**
536 * Copies the presentation attribute into the passed one.
537 *
538 * @return - True if attribute was read successfully
539 */
540 public boolean getPres(StyleAttribute attrib) throws SVGException
541 {
542 String presName = attrib.getName();
543
544 //Make sure we have a coresponding presentation attribute
545 StyleAttribute presAttr = presAttribs.get(presName);
546
547 //Copy presentation value directly
548 attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
549
550 //Return if we found presentation attribute
551 if (presAttr != null)
552 {
553 return true;
554 }
555
556 return false;
557 }
558
559 /**
560 * @return the raw presentation value of this attribute. Ignores any
561 * modifications applied by style attributes or animation. Used by
562 * animations to determine the starting point to animate from
563 */
564 public StyleAttribute getPresAbsolute(String styName)
565 {
566 //Check for local inline styles
567 return presAttribs.get(styName);
568 }
569
570 static protected AffineTransform parseTransform(String val) throws SVGException
571 {
572 final Matcher matchExpression = Pattern.compile("\\w+\\([^)]*\\)").matcher("");
573
574 AffineTransform retXform = new AffineTransform();
575
576 matchExpression.reset(val);
577 while (matchExpression.find())
578 {
579 retXform.concatenate(parseSingleTransform(matchExpression.group()));
580 }
581
582 return retXform;
583 }
584
585 static public AffineTransform parseSingleTransform(String val) throws SVGException
586 {
587 final Matcher matchWord = Pattern.compile("([a-zA-Z]+|-?\\d+(\\.\\d+)?(e-?\\d+)?|-?\\.\\d+(e-?\\d+)?)").matcher("");
588
589 AffineTransform retXform = new AffineTransform();
590
591 matchWord.reset(val);
592 if (!matchWord.find())
593 {
594 //Return identity transformation if no data present (eg, empty string)
595 return retXform;
596 }
597
598 String function = matchWord.group().toLowerCase();
599
600 LinkedList<String> termList = new LinkedList<>();
601 while (matchWord.find())
602 {
603 termList.add(matchWord.group());
604 }
605
606
607 double[] terms = new double[termList.size()];
608 Iterator<String> it = termList.iterator();
609 int count = 0;
610 while (it.hasNext())
611 {
612 terms[count++] = XMLParseUtil.parseDouble(it.next());
613 }
614
615 //Calculate transformation
616 if (function.equals("matrix"))
617 {
618 retXform.setTransform(terms[0], terms[1], terms[2], terms[3], terms[4], terms[5]);
619 } else if (function.equals("translate"))
620 {
621 if (terms.length == 1)
622 {
623 retXform.setToTranslation(terms[0], 0);
624 } else
625 {
626 retXform.setToTranslation(terms[0], terms[1]);
627 }
628 } else if (function.equals("scale"))
629 {
630 if (terms.length > 1)
631 {
632 retXform.setToScale(terms[0], terms[1]);
633 } else
634 {
635 retXform.setToScale(terms[0], terms[0]);
636 }
637 } else if (function.equals("rotate"))
638 {
639 if (terms.length > 2)
640 {
641 retXform.setToRotation(Math.toRadians(terms[0]), terms[1], terms[2]);
642 } else
643 {
644 retXform.setToRotation(Math.toRadians(terms[0]));
645 }
646 } else if (function.equals("skewx"))
647 {
648 retXform.setToShear(Math.toRadians(terms[0]), 0.0);
649 } else if (function.equals("skewy"))
650 {
651 retXform.setToShear(0.0, Math.toRadians(terms[0]));
652 } else
653 {
654 throw new SVGException("Unknown transform type");
655 }
656
657 return retXform;
658 }
659
660 static protected float nextFloat(LinkedList<String> l)
661 {
662 String s = l.removeFirst();
663 return Float.parseFloat(s);
664 }
665
666 static protected PathCommand[] parsePathList(String list)
667 {
668 final Matcher matchPathCmd = Pattern.compile("([MmLlHhVvAaQqTtCcSsZz])|([-+]?((\\d*\\.\\d+)|(\\d+))([eE][-+]?\\d+)?)").matcher(list);
669
670 //Tokenize
671 LinkedList<String> tokens = new LinkedList<>();
672 while (matchPathCmd.find())
673 {
674 tokens.addLast(matchPathCmd.group());
675 }
676
677
678 boolean defaultRelative = false;
679 LinkedList<PathCommand> cmdList = new LinkedList<>();
680 char curCmd = 'Z';
681 while (tokens.size() != 0)
682 {
683 String curToken = tokens.removeFirst();
684 char initChar = curToken.charAt(0);
685 if ((initChar >= 'A' && initChar <= 'Z') || (initChar >= 'a' && initChar <= 'z'))
686 {
687 curCmd = initChar;
688 } else
689 {
690 tokens.addFirst(curToken);
691 }
692
693 PathCommand cmd = null;
694
695 switch (curCmd)
696 {
697 case 'M':
698 cmd = new MoveTo(false, nextFloat(tokens), nextFloat(tokens));
699 curCmd = 'L';
700 break;
701 case 'm':
702 cmd = new MoveTo(true, nextFloat(tokens), nextFloat(tokens));
703 curCmd = 'l';
704 break;
705 case 'L':
706 cmd = new LineTo(false, nextFloat(tokens), nextFloat(tokens));
707 break;
708 case 'l':
709 cmd = new LineTo(true, nextFloat(tokens), nextFloat(tokens));
710 break;
711 case 'H':
712 cmd = new Horizontal(false, nextFloat(tokens));
713 break;
714 case 'h':
715 cmd = new Horizontal(true, nextFloat(tokens));
716 break;
717 case 'V':
718 cmd = new Vertical(false, nextFloat(tokens));
719 break;
720 case 'v':
721 cmd = new Vertical(true, nextFloat(tokens));
722 break;
723 case 'A':
724 cmd = new Arc(false, nextFloat(tokens), nextFloat(tokens),
725 nextFloat(tokens),
726 nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
727 nextFloat(tokens), nextFloat(tokens));
728 break;
729 case 'a':
730 cmd = new Arc(true, nextFloat(tokens), nextFloat(tokens),
731 nextFloat(tokens),
732 nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
733 nextFloat(tokens), nextFloat(tokens));
734 break;
735 case 'Q':
736 cmd = new Quadratic(false, nextFloat(tokens), nextFloat(tokens),
737 nextFloat(tokens), nextFloat(tokens));
738 break;
739 case 'q':
740 cmd = new Quadratic(true, nextFloat(tokens), nextFloat(tokens),
741 nextFloat(tokens), nextFloat(tokens));
742 break;
743 case 'T':
744 cmd = new QuadraticSmooth(false, nextFloat(tokens), nextFloat(tokens));
745 break;
746 case 't':
747 cmd = new QuadraticSmooth(true, nextFloat(tokens), nextFloat(tokens));
748 break;
749 case 'C':
750 cmd = new Cubic(false, nextFloat(tokens), nextFloat(tokens),
751 nextFloat(tokens), nextFloat(tokens),
752 nextFloat(tokens), nextFloat(tokens));
753 break;
754 case 'c':
755 cmd = new Cubic(true, nextFloat(tokens), nextFloat(tokens),
756 nextFloat(tokens), nextFloat(tokens),
757 nextFloat(tokens), nextFloat(tokens));
758 break;
759 case 'S':
760 cmd = new CubicSmooth(false, nextFloat(tokens), nextFloat(tokens),
761 nextFloat(tokens), nextFloat(tokens));
762 break;
763 case 's':
764 cmd = new CubicSmooth(true, nextFloat(tokens), nextFloat(tokens),
765 nextFloat(tokens), nextFloat(tokens));
766 break;
767 case 'Z':
768 case 'z':
769 cmd = new Terminal();
770 break;
771 default:
772 throw new RuntimeException("Invalid path element");
773 }
774
775 cmdList.add(cmd);
776 defaultRelative = cmd.isRelative;
777 }
778
779 PathCommand[] retArr = new PathCommand[cmdList.size()];
780 cmdList.toArray(retArr);
781 return retArr;
782 }
783
784 static protected GeneralPath buildPath(String text, int windingRule)
785 {
786 PathCommand[] commands = parsePathList(text);
787
788 int numKnots = 2;
789 for (int i = 0; i < commands.length; i++)
790 {
791 numKnots += commands[i].getNumKnotsAdded();
792 }
793
794
795 GeneralPath path = new GeneralPath(windingRule, numKnots);
796
797 BuildHistory hist = new BuildHistory();
798
799 for (int i = 0; i < commands.length; i++)
800 {
801 PathCommand cmd = commands[i];
802 cmd.appendPath(path, hist);
803 }
804
805 return path;
806 }
807
808 /**
809 * Updates all attributes in this diagram associated with a time event. Ie,
810 * all attributes with track information.
811 *
812 * @return - true if this node has changed state as a result of the time
813 * update
814 */
815 abstract public boolean updateTime(double curTime) throws SVGException;
816
817 public int getNumChildren()
818 {
819 return children.size();
820 }
821
822 public SVGElement getChild(int i)
823 {
824 return children.get(i);
825 }
826
827 public double lerp(double t0, double t1, double alpha)
828 {
829 return (1 - alpha) * t0 + alpha * t1;
830 }
831}
Note: See TracBrowser for help on using the repository browser.