]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Sync git svn branch with SVN repository r33269.
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / SVGNode.java
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
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.scenegraph.g2d.nodes;
13 \r
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
31 \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
45 \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
55 \r
56 @RasterOutputWidget
57 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
58 \r
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
66                         this.value = value;\r
67                 }\r
68                 @Override\r
69                 public int hashCode() {\r
70                         final int prime = 31;\r
71                         int result = 1;\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
75                         return result;\r
76                 }\r
77                 @Override\r
78                 public boolean equals(Object obj) {\r
79                         if (this == obj)\r
80                                 return true;\r
81                         if (obj == null)\r
82                                 return false;\r
83                         if (getClass() != obj.getClass())\r
84                                 return false;\r
85                         SVGNodeAssignment other = (SVGNodeAssignment) obj;\r
86                         if (attributeNameOrId == null) {\r
87                                 if (other.attributeNameOrId != null)\r
88                                         return false;\r
89                         } else if (!attributeNameOrId.equals(other.attributeNameOrId))\r
90                                 return false;\r
91                         if (elementId == null) {\r
92                                 if (other.elementId != null)\r
93                                         return false;\r
94                         } else if (!elementId.equals(other.elementId))\r
95                                 return false;\r
96                         if (value == null) {\r
97                                 if (other.value != null)\r
98                                         return false;\r
99                         } else if (!value.equals(other.value))\r
100                                 return false;\r
101                         return true;\r
102                 }\r
103         }\r
104         
105     private static final long serialVersionUID = 8508750881358776559L;
106
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
112     \r
113     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();\r
114 \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
119 \r
120     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();\r
121 \r
122     @Override\r
123     public void cleanup() {\r
124         cleanDiagramCache();\r
125     }\r
126 \r
127     public void setAssignments(List<SVGNodeAssignment> ass) {\r
128         assignments.clear();\r
129         assignments.addAll(ass);\r
130     }\r
131     \r
132     public void cleanDiagramCache() {\r
133         SVGDiagram d = diagramCache;\r
134         if (d != null) {\r
135             diagramCache = null;\r
136             SVGUniverse univ = SVGCache.getSVGUniverse();\r
137             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {\r
138                 // Cleared!\r
139                 //System.out.println("cleared: " + d.getXMLBase());\r
140             }\r
141         }\r
142     }\r
143 \r
144     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();\r
145 \r
146     @PropertySetter("SVG")
147     @SyncField("data")
148     public void setData(String data) {\r
149         String cached = dataCache.get(data);\r
150         if (cached == null) {\r
151             cached = data;\r
152             dataCache.put(data, data);\r
153         }\r
154         this.data = cached;\r
155         this.defaultData = cached;\r
156     }
157
158     @SyncField("targetSize")
159     public void setTargetSize(Point p) {
160         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
161     }
162
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
166     }
167
168     @SyncField("useMipMap")
169     public void useMipMap(Boolean use) {
170         this.useMipMap = use;
171     }\r
172 \r
173     @PropertySetter("Bounds")
174     @SyncField("bounds")
175     public void setBounds(Rectangle2D bounds) {
176         this.bounds = bounds;
177     }
178
179     @Override\r
180     public Rectangle2D getBoundsInLocal() {
181         if (bounds == null)\r
182             parseSVG();\r
183         return bounds;
184     }
185
186     @Override
187     public void render(Graphics2D g2d) {
188         if (data == null)\r
189             return; // Not initialized\r
190 \r
191         if (!data.equals(documentCache) || diagramCache == null || buffer == null)\r
192             initBuffer(g2d);
193 \r
194         AffineTransform ot = null;\r
195         if (!transform.isIdentity()) {\r
196             ot = g2d.getTransform();\r
197             g2d.transform(transform);\r
198         }
199
200         if (buffer != null)\r
201             buffer.paint(g2d);\r
202 \r
203         if (ot != null)\r
204             g2d.setTransform(ot);\r
205     }
206
207     protected String parseSVG() {
208         if (data == null)\r
209             return null;\r
210 \r
211         try {\r
212                 SVGUniverse univ = SVGCache.getSVGUniverse();\r
213                 synchronized(univ) {
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
228                                 } else {\r
229                                         for(SVGNodeAssignment ass : assignments) {\r
230                                                 SVGElement e = diagramCache.getElement(ass.elementId);\r
231                                                 if(e != null) {\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
236                                                                 text.rebuild();\r
237                                                         } else {\r
238                                                                 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);\r
239                                                         }\r
240                                                 }\r
241                                         }\r
242                                         if(!assignments.isEmpty())\r
243                                                 diagramCache.updateTime(0);\r
244                                 }\r
245                         }
246                         documentCache = data;\r
247                         if (diagramCache != null) {\r
248                                 setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone());\r
249                                 univ.incRefCount(diagramCache.getXMLBase());\r
250                         } else {\r
251                                 setBounds(new Rectangle2D.Double());\r
252                         }\r
253                 }\r
254         } catch (SVGException e) {\r
255             setBounds((Rectangle2D) diagramCache.getViewRect().clone());\r
256         } catch (IOException e) {\r
257             diagramCache = null;
258         }
259
260         return dataHash;
261     }
262 \r
263     public static Rectangle2D getBounds(String data) {\r
264         return getBounds(data, null);\r
265     }\r
266 \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
270             return null;\r
271         }\r
272 \r
273         SVGDiagram diagramCache = null;\r
274         try {\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
278 \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
290                     }\r
291                 }\r
292             }\r
293 \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
299             } else {\r
300                 rect = new Rectangle2D.Double();\r
301             }\r
302             return rect;\r
303         } catch (SVGException e) {\r
304             return ((Rectangle2D) diagramCache.getViewRect().clone());\r
305         } catch(IOException e) {\r
306         }\r
307         return null;\r
308     }\r
309 \r
310     public static Rectangle2D getRealBounds(String data) {\r
311         return getRealBounds(data, null);\r
312     }\r
313
314     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {\r
315         if (data == null) {\r
316             new Exception("null SVG data").printStackTrace();\r
317             return null;\r
318         }\r
319 \r
320         SVGDiagram diagramCache = null;\r
321         try {\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
325 \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
336                 }\r
337             }\r
338         } catch (SVGException e) {\r
339             return ((Rectangle2D) diagramCache.getViewRect().clone());\r
340         } catch(IOException e) {\r
341         }\r
342         return null;\r
343     }\r
344 \r
345     protected void initBuffer(Graphics2D g2d) {\r
346         \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);
351                         return;
352                 }\r
353         }
354
355         if (buffer != null) {
356             buffer = null;
357         }
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) {
362             buffer = null;
363         } else if(useMipMap) {\r
364                 if(G2DUtils.isAccelerated(g2d)) {
365                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
366             } else {
367                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
368             }
369             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
370         } else {
371                 if(G2DUtils.isAccelerated(g2d)) {
372                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
373             } else {
374                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
375             }
376             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
377         }
378     }
379 \r
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
392         }\r
393     }\r
394 \r
395     @Override\r
396     public void initValues() {\r
397         data = defaultData;\r
398         dataHash = null;\r
399         buffer =  null;\r
400     }\r
401 \r
402     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();\r
403     \r
404     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {\r
405         try {\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
412                 result = dataHash;\r
413                 digestCache.put(dataHash,dataHash);\r
414             }\r
415             return result;\r
416         } catch (NoSuchAlgorithmException e) {\r
417             // Shouldn't happen\r
418             throw new Error("MD5 digest must exist.");\r
419         }\r
420     }\r
421 \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
424 \r
425         @Override\r
426         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {\r
427                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);\r
428         }\r
429         \r
430         @Override\r
431         public <T> T getProperty(String propertyName) {\r
432                 return null;\r
433         }\r
434 \r
435         @Override\r
436         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {\r
437         }\r
438 \r
439         public void synchronizeDocument(String document) {\r
440                 setData(document);\r
441         }\r
442 \r
443         public void synchronizeTransform(double[] data) {\r
444                 this.setTransform(new AffineTransform(data));\r
445         }\r
446         
447 }