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

Last change on this file since 4656 was 4256, checked in by bastiK, 13 years ago

see #6560 - basic svg support, includes kitfox svgsalamander, r 98, patched

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