1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
14 import java.awt.Graphics2D;
15 import java.awt.Point;
16 import java.awt.geom.AffineTransform;
17 import java.awt.geom.Rectangle2D;
18 import java.io.ByteArrayInputStream;
19 import java.io.IOException;
20 import java.lang.ref.WeakReference;
21 import java.math.BigInteger;
24 import java.security.MessageDigest;
25 import java.security.NoSuchAlgorithmException;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.List;
32 import java.util.WeakHashMap;
34 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
35 import org.simantics.scenegraph.LoaderNode;
36 import org.simantics.scenegraph.ScenegraphUtils;
37 import org.simantics.scenegraph.g2d.G2DNode;
38 import org.simantics.scenegraph.utils.BufferedImage;
39 import org.simantics.scenegraph.utils.G2DUtils;
40 import org.simantics.scenegraph.utils.InitValueSupport;
41 import org.simantics.scenegraph.utils.MipMapBufferedImage;
42 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
43 import org.simantics.scenegraph.utils.VRamBufferedImage;
44 import org.simantics.scl.runtime.function.Function1;
45 import org.simantics.scl.runtime.function.Function2;
46 import org.simantics.utils.threads.AWTThread;
48 import com.kitfox.svg.RenderableElement;
49 import com.kitfox.svg.SVGCache;
50 import com.kitfox.svg.SVGDiagram;
51 import com.kitfox.svg.SVGElement;
52 import com.kitfox.svg.SVGException;
53 import com.kitfox.svg.SVGRoot;
54 import com.kitfox.svg.SVGUniverse;
55 import com.kitfox.svg.Text;
56 import com.kitfox.svg.Tspan;
57 import com.kitfox.svg.animation.AnimationElement;
60 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
62 private static final long serialVersionUID = 8508750881358776559L;
64 protected String data = null;
65 protected String defaultData = null;
66 protected Point targetSize = null;
67 protected Boolean useMipMap = true;
68 protected Rectangle2D bounds = null;
70 protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
72 protected transient BufferedImage buffer = null;
73 protected transient String documentCache = null;
74 protected transient SVGDiagram diagramCache = null;
75 protected transient String dataHash = null;
77 static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
85 public void cleanup() {
89 public void setAssignments(List<SVGNodeAssignment> ass) {
91 assignments.addAll(ass);
94 public void cleanDiagramCache() {
95 SVGDiagram d = diagramCache;
98 SVGUniverse univ = SVGCache.getSVGUniverse();
99 if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
101 //System.out.println("cleared: " + d.getXMLBase());
106 static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
108 @PropertySetter("SVG")
110 public void setData(String data) {
111 String cached = dataCache.get(data);
112 if (cached == null) {
114 dataCache.put(data, data);
117 this.defaultData = cached;
120 @SyncField("targetSize")
121 public void setTargetSize(Point p) {
122 this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
125 @SyncField("targetSize")
126 public void setTargetSize(int x, int y) {
127 this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
130 @SyncField("useMipMap")
131 public void useMipMap(Boolean use) {
132 this.useMipMap = use;
135 @PropertySetter("Bounds")
137 public void setBounds(Rectangle2D bounds) {
138 this.bounds = bounds;
142 public Rectangle2D getBoundsInLocal() {
149 public void render(Graphics2D g2d) {
151 return; // Not initialized
153 if (!data.equals(documentCache) || diagramCache == null || buffer == null)
156 AffineTransform ot = null;
157 if (!transform.isIdentity()) {
158 ot = g2d.getTransform();
159 g2d.transform(transform);
166 g2d.setTransform(ot);
169 protected int dynamicHash() {
173 protected String parseSVG() {
177 SVGUniverse univ = SVGCache.getSVGUniverse();
179 Rectangle2D bbox = null;
180 synchronized (univ) {
181 // Relinquish reference to current element
182 if (diagramCache != null) {
183 univ.decRefCount(diagramCache.getXMLBase());
187 // Lets check for rootAssignment that contributes the whole SVG
188 SVGNodeAssignment rootAssignment = null;
189 if (!assignments.isEmpty()) {
190 for (SVGNodeAssignment ass : assignments) {
191 if (ass.attributeNameOrId.equals("$root")) {
192 rootAssignment = ass;
198 if (rootAssignment != null) {
199 dataBytes = rootAssignment.value.getBytes("UTF-8");
201 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
202 dataBytes = data.getBytes("UTF-8");
204 dataHash = digest(dataBytes, assignments, dynamicHash());
205 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
206 diagramCache = univ.getDiagram(uri, false);
208 if (diagramCache != null) {
209 univ.incRefCount(diagramCache.getXMLBase());
210 SVGRoot root = diagramCache.getRoot();
212 univ.decRefCount(diagramCache.getXMLBase());
213 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
215 univ.incRefCount(diagramCache.getXMLBase());
216 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
218 bbox = root.getBoundingBox();
219 if (bbox.isEmpty()) {
220 // Lets check if this should be visible or not
221 Set<?> presentationAttributes = root.getPresentationAttributes();
222 if (!presentationAttributes.contains("display")) {
223 // TODO: fix this - How can one read values of attributes in SVG salamander???
224 univ.decRefCount(diagramCache.getXMLBase());
225 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
227 univ.incRefCount(diagramCache.getXMLBase());
228 bbox = (Rectangle2D) root.getBoundingBox().clone();
230 bbox = new Rectangle2D.Double(0, 0, 0, 0);
233 if (applyAssignments(diagramCache, assignments)) {
234 bbox = (Rectangle2D) root.getBoundingBox().clone();
236 bbox = (Rectangle2D) bbox.clone();
241 bbox = new Rectangle2D.Double();
245 documentCache = data;
247 } catch (SVGException e) {
248 // This can only occur if diagramCache != null.
249 setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
250 univ.decRefCount(diagramCache.getXMLBase());
252 } catch (IOException e) {
259 protected boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
260 if (assignments.isEmpty())
263 boolean changed = false;
265 for (SVGNodeAssignment ass : assignments) {
266 SVGElement e = diagram.getElement(ass.elementId);
268 if ("$text".equals(ass.attributeNameOrId)) {
269 if (e instanceof Tspan) {
271 if (ass.value.trim().isEmpty()) {
274 t.setText(ass.value);
276 SVGElement parent = t.getParent();
277 if (parent instanceof Text)
278 ((Text) parent).rebuild();
281 } else if (ass.attributeNameOrId.startsWith("#")) {
282 e.setAttribute(ass.attributeNameOrId.substring(1), AnimationElement.AT_CSS, ass.value);
285 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
294 public static Rectangle2D getBounds(String data) {
295 return getBounds(data, 0);
298 public static Rectangle2D getBounds(String data, int dynamicHash) {
299 return getBounds(data, Collections.emptyList(), dynamicHash);
302 public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
304 new Exception("null SVG data").printStackTrace();
308 SVGDiagram diagramCache = null;
310 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
311 byte[] dataBytes = data.getBytes("UTF-8");
312 String digest = digest(dataBytes, assignments, dynamicHash);
314 SVGUniverse univ = SVGCache.getSVGUniverse();
315 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
316 synchronized (univ) {
317 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
318 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
319 diagramCache = univ.getDiagram(uri, false);
320 if (diagramCache != null) {
321 if (diagramCache.getRoot() == null) {
322 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
323 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
324 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
329 Rectangle2D rect = null;
330 if (diagramCache != null) {
331 SVGRoot root = diagramCache.getRoot();
332 Rectangle2D bbox = root.getBoundingBox();
333 rect = (Rectangle2D) bbox.clone();
335 rect = new Rectangle2D.Double();
338 } catch (SVGException e) {
339 return diagramCache.getViewRect(new Rectangle2D.Double());
340 } catch(IOException e) {
345 public static Rectangle2D getRealBounds(String data) {
346 return getRealBounds(data, Collections.emptyList(), 0);
349 public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
351 new Exception("null SVG data").printStackTrace();
355 SVGDiagram diagramCache = null;
357 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
358 byte[] dataBytes = data.getBytes("UTF-8");
359 String digest = digest(dataBytes, assignments, dynamicHash);
361 SVGUniverse univ = SVGCache.getSVGUniverse();
362 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
363 synchronized (univ) {
364 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
365 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
366 diagramCache = univ.getDiagram(uri, false);
367 if (diagramCache != null) {
368 SVGRoot root = diagramCache.getRoot();
369 if (root == null) return new Rectangle2D.Double();
370 return (Rectangle2D)root.getBoundingBox().clone();
373 } catch (SVGException e) {
374 return diagramCache.getViewRect(new Rectangle2D.Double());
375 } catch(IOException e) {
380 protected void initBuffer(Graphics2D g2d) {
381 if (!data.equals(documentCache) || diagramCache == null) {
382 dataHash = parseSVG();
383 if (diagramCache == null) {
384 System.err.println("UNABLE TO PARSE SVG:\n" + data);
389 if (buffer != null) {
392 diagramCache.setIgnoringClipHeuristic(true); // FIXME
393 if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
394 buffer = bufferCache.get(dataHash).get();
395 } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
397 } else if(useMipMap) {
398 if(G2DUtils.isAccelerated(g2d)) {
399 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
401 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
403 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
405 if(G2DUtils.isAccelerated(g2d)) {
406 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
408 buffer = new BufferedImage(diagramCache, bounds, targetSize);
410 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
414 public void setProperty(String field, Object value) {
415 if("data".equals(field)) {
416 // System.out.println("SVGNode data -> " + value);
417 this.data = (String)value;
418 } else if ("z".equals(field)) {
419 // System.out.println("SVGNode z -> " + value);
420 setZIndex((Integer)value);
421 } else if ("position".equals(field)) {
422 // System.out.println("SVGNode position -> " + value);
423 Point point = (Point)value;
424 setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
425 // setPosition(point.x, point.y);
430 public void initValues() {
436 static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
438 static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments, int dynamicHash) {
440 MessageDigest md = MessageDigest.getInstance("MD5");
441 byte[] messageDigest = md.digest(dataBytes);
442 BigInteger number = new BigInteger(1, messageDigest);
443 String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
444 String result = digestCache.get(dataHash);
447 digestCache.put(dataHash,dataHash);
450 } catch (NoSuchAlgorithmException e) {
452 throw new Error("MD5 digest must exist.");
456 static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
457 static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
460 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
461 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
465 public <T> T getProperty(String propertyName) {
470 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
473 public void synchronizeDocument(String document) {
477 public void synchronizeTransform(double[] data) {
478 this.setTransform(new AffineTransform(data));
481 public String getSVGText() {
482 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
483 //return diagramCache.toString();
484 //return data.replace("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"><svg xmlns=\"http://www.w3.org/2000/svg\" overflow=\"visible\" version=\"1.1\"", "<g").replaceAll("svg>", "/g>");
488 public Rectangle2D getElementBounds(String id) throws SVGException {
489 SVGElement e = diagramCache.getElement(id);
490 if (e instanceof RenderableElement) {
491 return ((RenderableElement)e).getBoundingBox();