1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.scenegraph.g2d.nodes;
14 import java.awt.Graphics2D;
\r
15 import java.awt.Point;
\r
16 import java.awt.geom.AffineTransform;
\r
17 import java.awt.geom.Rectangle2D;
\r
18 import java.io.ByteArrayInputStream;
\r
19 import java.io.IOException;
\r
20 import java.lang.ref.WeakReference;
\r
21 import java.math.BigInteger;
\r
22 import java.net.URI;
\r
23 import java.net.URL;
\r
24 import java.security.MessageDigest;
\r
25 import java.security.NoSuchAlgorithmException;
\r
26 import java.util.ArrayList;
\r
27 import java.util.HashMap;
\r
28 import java.util.List;
\r
29 import java.util.Map;
\r
30 import java.util.WeakHashMap;
\r
32 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
\r
33 import org.simantics.scenegraph.LoaderNode;
\r
34 import org.simantics.scenegraph.ScenegraphUtils;
\r
35 import org.simantics.scenegraph.g2d.G2DNode;
\r
36 import org.simantics.scenegraph.utils.BufferedImage;
\r
37 import org.simantics.scenegraph.utils.G2DUtils;
\r
38 import org.simantics.scenegraph.utils.InitValueSupport;
\r
39 import org.simantics.scenegraph.utils.MipMapBufferedImage;
\r
40 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
\r
41 import org.simantics.scenegraph.utils.VRamBufferedImage;
\r
42 import org.simantics.scl.runtime.function.Function1;
\r
43 import org.simantics.scl.runtime.function.Function2;
\r
44 import org.simantics.utils.threads.AWTThread;
\r
46 import com.kitfox.svg.SVGCache;
\r
47 import com.kitfox.svg.SVGDiagram;
\r
48 import com.kitfox.svg.SVGElement;
\r
49 import com.kitfox.svg.SVGException;
\r
50 import com.kitfox.svg.SVGRoot;
\r
51 import com.kitfox.svg.SVGUniverse;
\r
52 import com.kitfox.svg.Text;
\r
53 import com.kitfox.svg.Tspan;
\r
54 import com.kitfox.svg.animation.AnimationElement;
\r
57 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
59 public static class SVGNodeAssignment {
\r
60 public String elementId;
\r
61 public String attributeNameOrId;
\r
62 public String value;
\r
63 public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) {
\r
64 this.elementId = elementId;
\r
65 this.attributeNameOrId = attributeNameOrId;
\r
69 public int hashCode() {
\r
70 final int prime = 31;
\r
72 result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode());
\r
73 result = prime * result + ((elementId == null) ? 0 : elementId.hashCode());
\r
74 result = prime * result + ((value == null) ? 0 : value.hashCode());
\r
78 public boolean equals(Object obj) {
\r
83 if (getClass() != obj.getClass())
\r
85 SVGNodeAssignment other = (SVGNodeAssignment) obj;
\r
86 if (attributeNameOrId == null) {
\r
87 if (other.attributeNameOrId != null)
\r
89 } else if (!attributeNameOrId.equals(other.attributeNameOrId))
\r
91 if (elementId == null) {
\r
92 if (other.elementId != null)
\r
94 } else if (!elementId.equals(other.elementId))
\r
96 if (value == null) {
\r
97 if (other.value != null)
\r
99 } else if (!value.equals(other.value))
\r
105 private static final long serialVersionUID = 8508750881358776559L;
107 protected String data = null;
\r
108 protected String defaultData = null;
\r
109 protected Point targetSize = null;
\r
110 protected Boolean useMipMap = true;
\r
111 protected Rectangle2D bounds = null;
\r
113 protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
\r
115 transient BufferedImage buffer = null;
\r
116 transient String documentCache = null;
\r
117 transient SVGDiagram diagramCache = null;
\r
118 transient String dataHash = null;
\r
120 static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
\r
123 public void cleanup() {
\r
124 cleanDiagramCache();
\r
127 public void setAssignments(List<SVGNodeAssignment> ass) {
\r
128 assignments.clear();
\r
129 assignments.addAll(ass);
\r
132 public void cleanDiagramCache() {
\r
133 SVGDiagram d = diagramCache;
\r
135 diagramCache = null;
\r
136 SVGUniverse univ = SVGCache.getSVGUniverse();
\r
137 if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
\r
139 //System.out.println("cleared: " + d.getXMLBase());
\r
144 static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
\r
146 @PropertySetter("SVG")
148 public void setData(String data) {
\r
149 String cached = dataCache.get(data);
\r
150 if (cached == null) {
\r
152 dataCache.put(data, data);
\r
154 this.data = cached;
\r
155 this.defaultData = cached;
\r
158 @SyncField("targetSize")
159 public void setTargetSize(Point p) {
160 this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
163 @SyncField("targetSize")
164 public void setTargetSize(int x, int y) {
165 this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
168 @SyncField("useMipMap")
169 public void useMipMap(Boolean use) {
170 this.useMipMap = use;
173 @PropertySetter("Bounds")
175 public void setBounds(Rectangle2D bounds) {
176 this.bounds = bounds;
180 public Rectangle2D getBoundsInLocal() {
181 if (bounds == null)
\r
187 public void render(Graphics2D g2d) {
189 return; // Not initialized
\r
191 if (!data.equals(documentCache) || diagramCache == null || buffer == null)
\r
194 AffineTransform ot = null;
\r
195 if (!transform.isIdentity()) {
\r
196 ot = g2d.getTransform();
\r
197 g2d.transform(transform);
\r
200 if (buffer != null)
\r
204 g2d.setTransform(ot);
\r
207 protected String parseSVG() {
212 SVGUniverse univ = SVGCache.getSVGUniverse();
\r
214 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
\r
215 byte[] dataBytes = data.getBytes("UTF-8");
216 dataHash = digest(dataBytes, assignments);
\r
217 if (diagramCache != null)
\r
218 univ.decRefCount(diagramCache.getXMLBase());
\r
219 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
220 diagramCache = univ.getDiagram(uri, false);
\r
221 if (diagramCache != null) {
\r
222 if (diagramCache.getRoot() == null) {
\r
223 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
\r
224 dataHash = "broken";
\r
225 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
\r
226 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
\r
227 dataHash = "empty";
\r
229 for(SVGNodeAssignment ass : assignments) {
\r
230 SVGElement e = diagramCache.getElement(ass.elementId);
\r
232 if("$text".equals(ass.attributeNameOrId)) {
\r
233 Tspan t = (Tspan)e;
\r
234 t.setText(ass.value);
\r
235 Text text = (Text)t.getParent();
\r
238 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
\r
242 if(!assignments.isEmpty())
\r
243 diagramCache.updateTime(0);
\r
246 documentCache = data;
\r
247 if (diagramCache != null) {
\r
248 setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone());
\r
249 univ.incRefCount(diagramCache.getXMLBase());
\r
251 setBounds(new Rectangle2D.Double());
\r
254 } catch (SVGException e) {
\r
255 setBounds((Rectangle2D) diagramCache.getViewRect().clone());
\r
256 } catch (IOException e) {
\r
263 public static Rectangle2D getBounds(String data) {
\r
264 return getBounds(data, null);
\r
267 public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
\r
268 if (data == null) {
\r
269 new Exception("null SVG data").printStackTrace();
\r
273 SVGDiagram diagramCache = null;
\r
275 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
\r
276 byte[] dataBytes = data.getBytes("UTF-8");
\r
277 String digest = digest(dataBytes, assignments);
\r
279 SVGUniverse univ = SVGCache.getSVGUniverse();
\r
280 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
\r
281 synchronized (univ) {
\r
282 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
\r
283 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
\r
284 diagramCache = univ.getDiagram(uri, false);
\r
285 if (diagramCache != null) {
\r
286 if (diagramCache.getRoot() == null) {
\r
287 diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
\r
288 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
\r
289 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
\r
294 Rectangle2D rect = null;
\r
295 if (diagramCache != null) {
\r
296 SVGRoot root = diagramCache.getRoot();
\r
297 Rectangle2D bbox = root.getBoundingBox();
\r
298 rect = (Rectangle2D) bbox.clone();
\r
300 rect = new Rectangle2D.Double();
\r
303 } catch (SVGException e) {
\r
304 return ((Rectangle2D) diagramCache.getViewRect().clone());
\r
305 } catch(IOException e) {
\r
310 public static Rectangle2D getRealBounds(String data) {
\r
311 return getRealBounds(data, null);
\r
314 public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
\r
315 if (data == null) {
\r
316 new Exception("null SVG data").printStackTrace();
\r
320 SVGDiagram diagramCache = null;
\r
322 // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
\r
323 byte[] dataBytes = data.getBytes("UTF-8");
\r
324 String digest = digest(dataBytes, assignments);
\r
326 SVGUniverse univ = SVGCache.getSVGUniverse();
\r
327 // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
\r
328 synchronized (univ) {
\r
329 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
\r
330 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
\r
331 diagramCache = univ.getDiagram(uri, false);
\r
332 if (diagramCache != null) {
\r
333 SVGRoot root = diagramCache.getRoot();
\r
334 if (root == null) return new Rectangle2D.Double();
\r
335 return (Rectangle2D)root.getBoundingBox().clone();
\r
338 } catch (SVGException e) {
\r
339 return ((Rectangle2D) diagramCache.getViewRect().clone());
\r
340 } catch(IOException e) {
\r
345 protected void initBuffer(Graphics2D g2d) {
\r
347 if (!data.equals(documentCache) || diagramCache == null) {
\r
348 dataHash = parseSVG();
349 if (diagramCache == null) {
350 System.err.println("UNABLE TO PARSE SVG:\n" + data);
355 if (buffer != null) {
358 diagramCache.setIgnoringClipHeuristic(true); // FIXME
359 if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
360 buffer = bufferCache.get(dataHash).get();
361 } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
363 } else if(useMipMap) {
\r
364 if(G2DUtils.isAccelerated(g2d)) {
365 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
367 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
369 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
371 if(G2DUtils.isAccelerated(g2d)) {
372 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
374 buffer = new BufferedImage(diagramCache, bounds, targetSize);
376 bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
380 public void setProperty(String field, Object value) {
\r
381 if("data".equals(field)) {
\r
382 // System.out.println("SVGNode data -> " + value);
\r
383 this.data = (String)value;
\r
384 } else if ("z".equals(field)) {
\r
385 // System.out.println("SVGNode z -> " + value);
\r
386 setZIndex((Integer)value);
\r
387 } else if ("position".equals(field)) {
\r
388 // System.out.println("SVGNode position -> " + value);
\r
389 Point point = (Point)value;
\r
390 setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
\r
391 // setPosition(point.x, point.y);
\r
396 public void initValues() {
\r
397 data = defaultData;
\r
402 static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
\r
404 static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
\r
406 MessageDigest md = MessageDigest.getInstance("MD5");
\r
407 byte[] messageDigest = md.digest(dataBytes);
\r
408 BigInteger number = new BigInteger(1, messageDigest);
\r
409 String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
\r
410 String result = digestCache.get(dataHash);
\r
411 if(result == null) {
\r
413 digestCache.put(dataHash,dataHash);
\r
416 } catch (NoSuchAlgorithmException e) {
\r
417 // Shouldn't happen
\r
418 throw new Error("MD5 digest must exist.");
\r
422 static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
\r
423 static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
\r
426 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
\r
427 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
\r
431 public <T> T getProperty(String propertyName) {
\r
436 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
\r
439 public void synchronizeDocument(String document) {
\r
443 public void synchronizeTransform(double[] data) {
\r
444 this.setTransform(new AffineTransform(data));
\r