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.HashMap;
28 import java.util.List;
30 import java.util.WeakHashMap;
32 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
33 import org.simantics.scenegraph.LoaderNode;
34 import org.simantics.scenegraph.ScenegraphUtils;
35 import org.simantics.scenegraph.g2d.G2DNode;
36 import org.simantics.scenegraph.utils.BufferedImage;
37 import org.simantics.scenegraph.utils.G2DUtils;
38 import org.simantics.scenegraph.utils.InitValueSupport;
39 import org.simantics.scenegraph.utils.MipMapBufferedImage;
40 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
41 import org.simantics.scenegraph.utils.VRamBufferedImage;
42 import org.simantics.scl.runtime.function.Function1;
43 import org.simantics.scl.runtime.function.Function2;
44 import org.simantics.utils.threads.AWTThread;
46 import com.kitfox.svg.SVGCache;
47 import com.kitfox.svg.SVGDiagram;
48 import com.kitfox.svg.SVGElement;
49 import com.kitfox.svg.SVGException;
50 import com.kitfox.svg.SVGRoot;
51 import com.kitfox.svg.SVGUniverse;
52 import com.kitfox.svg.Text;
53 import com.kitfox.svg.Tspan;
54 import com.kitfox.svg.animation.AnimationElement;
57 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
59 private static final long serialVersionUID = 8508750881358776559L;
61 protected String data = null;
62 protected String defaultData = null;
63 protected Point targetSize = null;
64 protected Boolean useMipMap = true;
65 protected Rectangle2D bounds = null;
67 protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
69 transient BufferedImage buffer = null;
70 transient String documentCache = null;
71 transient SVGDiagram diagramCache = null;
72 transient String dataHash = null;
74 static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
77 public void cleanup() {
81 public void setAssignments(List<SVGNodeAssignment> ass) {
83 assignments.addAll(ass);
86 public void cleanDiagramCache() {
87 SVGDiagram d = diagramCache;
90 SVGUniverse univ = SVGCache.getSVGUniverse();
91 if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
93 //System.out.println("cleared: " + d.getXMLBase());
98 static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
100 @PropertySetter("SVG")
102 public void setData(String data) {
103 String cached = dataCache.get(data);
104 if (cached == null) {
106 dataCache.put(data, data);
109 this.defaultData = cached;
112 @SyncField("targetSize")
113 public void setTargetSize(Point p) {
114 this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
117 @SyncField("targetSize")
118 public void setTargetSize(int x, int y) {
119 this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
122 @SyncField("useMipMap")
123 public void useMipMap(Boolean use) {
124 this.useMipMap = use;
127 @PropertySetter("Bounds")
129 public void setBounds(Rectangle2D bounds) {
130 this.bounds = bounds;
134 public Rectangle2D getBoundsInLocal() {
141 public void render(Graphics2D g2d) {
143 return; // Not initialized
145 if (!data.equals(documentCache) || diagramCache == null || buffer == null)
148 AffineTransform ot = null;
149 if (!transform.isIdentity()) {
150 ot = g2d.getTransform();
151 g2d.transform(transform);
158 g2d.setTransform(ot);
161 protected String parseSVG() {
165 SVGUniverse univ = SVGCache.getSVGUniverse();
167 Rectangle2D bbox = null;
168 synchronized (univ) {
169 // Relinquish reference to current element
170 if (diagramCache != null) {
171 univ.decRefCount(diagramCache.getXMLBase());
175 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
176 byte[] dataBytes = data.getBytes("UTF-8");
177 dataHash = digest(dataBytes, assignments);
178 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
179 diagramCache = univ.getDiagram(uri, false);
181 if (diagramCache != null) {
182 univ.incRefCount(diagramCache.getXMLBase());
184 if (diagramCache.getRoot() == null) {
185 univ.decRefCount(diagramCache.getXMLBase());
186 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
188 univ.incRefCount(diagramCache.getXMLBase());
189 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
191 bbox = diagramCache.getRoot().getBoundingBox();
192 if (bbox.isEmpty()) {
193 univ.decRefCount(diagramCache.getXMLBase());
194 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
196 univ.incRefCount(diagramCache.getXMLBase());
197 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
199 if (applyAssignments(diagramCache, assignments)) {
200 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
202 bbox = (Rectangle2D) bbox.clone();
207 bbox = new Rectangle2D.Double();
211 documentCache = data;
213 } catch (SVGException e) {
214 // This can only occur if diagramCache != null.
215 setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
216 univ.decRefCount(diagramCache.getXMLBase());
218 } catch (IOException e) {
225 private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
226 if (assignments.isEmpty())
228 boolean changed = false;
229 for (SVGNodeAssignment ass : assignments) {
230 SVGElement e = diagram.getElement(ass.elementId);
232 if ("$text".equals(ass.attributeNameOrId)) {
233 if (e instanceof Tspan) {
235 if (ass.value.trim().isEmpty()) {
238 t.setText(ass.value);
240 SVGElement parent = t.getParent();
241 if (parent instanceof Text)
242 ((Text) parent).rebuild();
246 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
251 diagram.updateTime(0);
255 public static Rectangle2D getBounds(String data) {
256 return getBounds(data, null);
259 public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
261 new Exception("null SVG data").printStackTrace();
265 SVGDiagram diagramCache = null;
267 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
268 byte[] dataBytes = data.getBytes("UTF-8");
269 String digest = digest(dataBytes, assignments);
271 SVGUniverse univ = SVGCache.getSVGUniverse();
272 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
273 synchronized (univ) {
274 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
275 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
276 diagramCache = univ.getDiagram(uri, false);
277 if (diagramCache != null) {
278 if (diagramCache.getRoot() == null) {
279 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
280 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
281 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
286 Rectangle2D rect = null;
287 if (diagramCache != null) {
288 SVGRoot root = diagramCache.getRoot();
289 Rectangle2D bbox = root.getBoundingBox();
290 rect = (Rectangle2D) bbox.clone();
292 rect = new Rectangle2D.Double();
295 } catch (SVGException e) {
296 return diagramCache.getViewRect(new Rectangle2D.Double());
297 } catch(IOException e) {
302 public static Rectangle2D getRealBounds(String data) {
303 return getRealBounds(data, null);
306 public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
308 new Exception("null SVG data").printStackTrace();
312 SVGDiagram diagramCache = null;
314 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
315 byte[] dataBytes = data.getBytes("UTF-8");
316 String digest = digest(dataBytes, assignments);
318 SVGUniverse univ = SVGCache.getSVGUniverse();
319 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
320 synchronized (univ) {
321 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
322 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
323 diagramCache = univ.getDiagram(uri, false);
324 if (diagramCache != null) {
325 SVGRoot root = diagramCache.getRoot();
326 if (root == null) return new Rectangle2D.Double();
327 return (Rectangle2D)root.getBoundingBox().clone();
330 } catch (SVGException e) {
331 return diagramCache.getViewRect(new Rectangle2D.Double());
332 } catch(IOException e) {
337 protected void initBuffer(Graphics2D g2d) {
339 if (!data.equals(documentCache) || diagramCache == null) {
340 dataHash = parseSVG();
341 if (diagramCache == null) {
342 System.err.println("UNABLE TO PARSE SVG:\n" + data);
347 if (buffer != null) {
350 diagramCache.setIgnoringClipHeuristic(true); // FIXME
351 if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
352 buffer = bufferCache.get(dataHash).get();
353 } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
355 } else if(useMipMap) {
356 if(G2DUtils.isAccelerated(g2d)) {
357 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
359 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
361 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
363 if(G2DUtils.isAccelerated(g2d)) {
364 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
366 buffer = new BufferedImage(diagramCache, bounds, targetSize);
368 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
372 public void setProperty(String field, Object value) {
373 if("data".equals(field)) {
374 // System.out.println("SVGNode data -> " + value);
375 this.data = (String)value;
376 } else if ("z".equals(field)) {
377 // System.out.println("SVGNode z -> " + value);
378 setZIndex((Integer)value);
379 } else if ("position".equals(field)) {
380 // System.out.println("SVGNode position -> " + value);
381 Point point = (Point)value;
382 setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
383 // setPosition(point.x, point.y);
388 public void initValues() {
394 static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
396 static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
398 MessageDigest md = MessageDigest.getInstance("MD5");
399 byte[] messageDigest = md.digest(dataBytes);
400 BigInteger number = new BigInteger(1, messageDigest);
401 String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
402 String result = digestCache.get(dataHash);
405 digestCache.put(dataHash,dataHash);
408 } catch (NoSuchAlgorithmException e) {
410 throw new Error("MD5 digest must exist.");
414 static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
415 static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
418 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
419 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
423 public <T> T getProperty(String propertyName) {
428 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
431 public void synchronizeDocument(String document) {
435 public void synchronizeTransform(double[] data) {
436 this.setTransform(new AffineTransform(data));
439 public String getSVGText() {
440 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
441 //return diagramCache.toString();
442 //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>");