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;
31 import java.util.WeakHashMap;
33 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
34 import org.simantics.scenegraph.LoaderNode;
35 import org.simantics.scenegraph.ScenegraphUtils;
36 import org.simantics.scenegraph.g2d.G2DNode;
37 import org.simantics.scenegraph.utils.BufferedImage;
38 import org.simantics.scenegraph.utils.G2DUtils;
39 import org.simantics.scenegraph.utils.InitValueSupport;
40 import org.simantics.scenegraph.utils.MipMapBufferedImage;
41 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
42 import org.simantics.scenegraph.utils.VRamBufferedImage;
43 import org.simantics.scl.runtime.function.Function1;
44 import org.simantics.scl.runtime.function.Function2;
45 import org.simantics.utils.threads.AWTThread;
47 import com.kitfox.svg.SVGCache;
48 import com.kitfox.svg.SVGDiagram;
49 import com.kitfox.svg.SVGElement;
50 import com.kitfox.svg.SVGException;
51 import com.kitfox.svg.SVGRoot;
52 import com.kitfox.svg.SVGUniverse;
53 import com.kitfox.svg.Text;
54 import com.kitfox.svg.Tspan;
55 import com.kitfox.svg.animation.AnimationElement;
58 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
60 private static final long serialVersionUID = 8508750881358776559L;
62 protected String data = null;
63 protected String defaultData = null;
64 protected Point targetSize = null;
65 protected Boolean useMipMap = true;
66 protected Rectangle2D bounds = null;
68 protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
70 transient BufferedImage buffer = null;
71 transient String documentCache = null;
72 transient SVGDiagram diagramCache = null;
73 transient String dataHash = null;
75 static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
78 public void cleanup() {
82 public void setAssignments(List<SVGNodeAssignment> ass) {
84 assignments.addAll(ass);
87 public void cleanDiagramCache() {
88 SVGDiagram d = diagramCache;
91 SVGUniverse univ = SVGCache.getSVGUniverse();
92 if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
94 //System.out.println("cleared: " + d.getXMLBase());
99 static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
101 @PropertySetter("SVG")
103 public void setData(String data) {
104 String cached = dataCache.get(data);
105 if (cached == null) {
107 dataCache.put(data, data);
110 this.defaultData = cached;
113 @SyncField("targetSize")
114 public void setTargetSize(Point p) {
115 this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
118 @SyncField("targetSize")
119 public void setTargetSize(int x, int y) {
120 this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
123 @SyncField("useMipMap")
124 public void useMipMap(Boolean use) {
125 this.useMipMap = use;
128 @PropertySetter("Bounds")
130 public void setBounds(Rectangle2D bounds) {
131 this.bounds = bounds;
135 public Rectangle2D getBoundsInLocal() {
142 public void render(Graphics2D g2d) {
144 return; // Not initialized
146 if (!data.equals(documentCache) || diagramCache == null || buffer == null)
149 AffineTransform ot = null;
150 if (!transform.isIdentity()) {
151 ot = g2d.getTransform();
152 g2d.transform(transform);
159 g2d.setTransform(ot);
162 protected String parseSVG() {
166 SVGUniverse univ = SVGCache.getSVGUniverse();
168 Rectangle2D bbox = null;
169 synchronized (univ) {
170 // Relinquish reference to current element
171 if (diagramCache != null) {
172 univ.decRefCount(diagramCache.getXMLBase());
177 // Lets check for rootAssignment that contributes the whole SVG
178 SVGNodeAssignment rootAssignment = null;
179 if (!assignments.isEmpty()) {
180 for (SVGNodeAssignment ass : assignments) {
181 if (ass.attributeNameOrId.equals("$root")) {
182 rootAssignment = ass;
188 if (rootAssignment != null) {
189 dataBytes = rootAssignment.value.getBytes("UTF-8");
191 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
192 dataBytes = data.getBytes("UTF-8");
194 dataHash = digest(dataBytes, assignments);
195 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
196 diagramCache = univ.getDiagram(uri, false);
198 if (diagramCache != null) {
199 univ.incRefCount(diagramCache.getXMLBase());
200 SVGRoot root = diagramCache.getRoot();
202 univ.decRefCount(diagramCache.getXMLBase());
203 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
205 univ.incRefCount(diagramCache.getXMLBase());
206 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
208 bbox = root.getBoundingBox();
209 if (bbox.isEmpty()) {
210 // Lets check if this should be visible or not
211 Set presentationAttributes = root.getPresentationAttributes();
212 if (!presentationAttributes.contains("display")) {
213 // TODO: fix this - How can one read values of attributes in SVG salamander???
214 univ.decRefCount(diagramCache.getXMLBase());
215 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
217 univ.incRefCount(diagramCache.getXMLBase());
218 bbox = (Rectangle2D) root.getBoundingBox().clone();
220 bbox = new Rectangle2D.Double(0, 0, 0, 0);
223 if (applyAssignments(diagramCache, assignments)) {
224 bbox = (Rectangle2D) root.getBoundingBox().clone();
226 bbox = (Rectangle2D) bbox.clone();
231 bbox = new Rectangle2D.Double();
235 documentCache = data;
237 } catch (SVGException e) {
238 // This can only occur if diagramCache != null.
239 setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
240 univ.decRefCount(diagramCache.getXMLBase());
242 } catch (IOException e) {
249 private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
250 if (assignments.isEmpty())
252 boolean changed = false;
253 for (SVGNodeAssignment ass : assignments) {
254 // System.err.println("assign: " + ass.elementId + " " + ass.attributeNameOrId + " " + ass.value);
255 // if("opacity".equals(ass.attributeNameOrId))
256 // System.err.println("faaf");
257 SVGElement e = diagram.getElement(ass.elementId);
259 if ("$text".equals(ass.attributeNameOrId)) {
260 if (e instanceof Tspan) {
262 if (ass.value.trim().isEmpty()) {
265 t.setText(ass.value);
267 SVGElement parent = t.getParent();
268 if (parent instanceof Text)
269 ((Text) parent).rebuild();
273 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
278 diagram.updateTime(0);
282 public static Rectangle2D getBounds(String data) {
283 return getBounds(data, null);
286 public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
288 new Exception("null SVG data").printStackTrace();
292 SVGDiagram diagramCache = null;
294 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
295 byte[] dataBytes = data.getBytes("UTF-8");
296 String digest = digest(dataBytes, assignments);
298 SVGUniverse univ = SVGCache.getSVGUniverse();
299 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
300 synchronized (univ) {
301 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
302 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
303 diagramCache = univ.getDiagram(uri, false);
304 if (diagramCache != null) {
305 if (diagramCache.getRoot() == null) {
306 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
307 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
308 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
313 Rectangle2D rect = null;
314 if (diagramCache != null) {
315 SVGRoot root = diagramCache.getRoot();
316 Rectangle2D bbox = root.getBoundingBox();
317 rect = (Rectangle2D) bbox.clone();
319 rect = new Rectangle2D.Double();
322 } catch (SVGException e) {
323 return diagramCache.getViewRect(new Rectangle2D.Double());
324 } catch(IOException e) {
329 public static Rectangle2D getRealBounds(String data) {
330 return getRealBounds(data, null);
333 public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
335 new Exception("null SVG data").printStackTrace();
339 SVGDiagram diagramCache = null;
341 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
342 byte[] dataBytes = data.getBytes("UTF-8");
343 String digest = digest(dataBytes, assignments);
345 SVGUniverse univ = SVGCache.getSVGUniverse();
346 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
347 synchronized (univ) {
348 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
349 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
350 diagramCache = univ.getDiagram(uri, false);
351 if (diagramCache != null) {
352 SVGRoot root = diagramCache.getRoot();
353 if (root == null) return new Rectangle2D.Double();
354 return (Rectangle2D)root.getBoundingBox().clone();
357 } catch (SVGException e) {
358 return diagramCache.getViewRect(new Rectangle2D.Double());
359 } catch(IOException e) {
364 protected void initBuffer(Graphics2D g2d) {
366 if (!data.equals(documentCache) || diagramCache == null) {
367 dataHash = parseSVG();
368 if (diagramCache == null) {
369 System.err.println("UNABLE TO PARSE SVG:\n" + data);
374 if (buffer != null) {
377 diagramCache.setIgnoringClipHeuristic(true); // FIXME
378 if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
379 buffer = bufferCache.get(dataHash).get();
380 } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
382 } else if(useMipMap) {
383 if(G2DUtils.isAccelerated(g2d)) {
384 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
386 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
388 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
390 if(G2DUtils.isAccelerated(g2d)) {
391 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
393 buffer = new BufferedImage(diagramCache, bounds, targetSize);
395 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
399 public void setProperty(String field, Object value) {
400 if("data".equals(field)) {
401 // System.out.println("SVGNode data -> " + value);
402 this.data = (String)value;
403 } else if ("z".equals(field)) {
404 // System.out.println("SVGNode z -> " + value);
405 setZIndex((Integer)value);
406 } else if ("position".equals(field)) {
407 // System.out.println("SVGNode position -> " + value);
408 Point point = (Point)value;
409 setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
410 // setPosition(point.x, point.y);
415 public void initValues() {
421 static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
423 static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
425 MessageDigest md = MessageDigest.getInstance("MD5");
426 byte[] messageDigest = md.digest(dataBytes);
427 BigInteger number = new BigInteger(1, messageDigest);
428 String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
429 String result = digestCache.get(dataHash);
432 digestCache.put(dataHash,dataHash);
435 } catch (NoSuchAlgorithmException e) {
437 throw new Error("MD5 digest must exist.");
441 static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
442 static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
445 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
446 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
450 public <T> T getProperty(String propertyName) {
455 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
458 public void synchronizeDocument(String document) {
462 public void synchronizeTransform(double[] data) {
463 this.setTransform(new AffineTransform(data));
466 public String getSVGText() {
467 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
468 //return diagramCache.toString();
469 //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>");