]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / SVGNode.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.scenegraph.g2d.nodes;
13
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;
22 import java.net.URI;
23 import java.net.URL;
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;
29 import java.util.Map;
30 import java.util.WeakHashMap;
31
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;
45
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;
55
56 @RasterOutputWidget
57 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
58
59         public static class SVGNodeAssignment {
60                 public String elementId;
61                 public String attributeNameOrId;
62                 public String value;
63                 public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) {
64                         this.elementId = elementId;
65                         this.attributeNameOrId = attributeNameOrId;
66                         this.value = value;
67                 }
68                 @Override
69                 public int hashCode() {
70                         final int prime = 31;
71                         int result = 1;
72                         result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode());
73                         result = prime * result + ((elementId == null) ? 0 : elementId.hashCode());
74                         result = prime * result + ((value == null) ? 0 : value.hashCode());
75                         return result;
76                 }
77                 @Override
78                 public boolean equals(Object obj) {
79                         if (this == obj)
80                                 return true;
81                         if (obj == null)
82                                 return false;
83                         if (getClass() != obj.getClass())
84                                 return false;
85                         SVGNodeAssignment other = (SVGNodeAssignment) obj;
86                         if (attributeNameOrId == null) {
87                                 if (other.attributeNameOrId != null)
88                                         return false;
89                         } else if (!attributeNameOrId.equals(other.attributeNameOrId))
90                                 return false;
91                         if (elementId == null) {
92                                 if (other.elementId != null)
93                                         return false;
94                         } else if (!elementId.equals(other.elementId))
95                                 return false;
96                         if (value == null) {
97                                 if (other.value != null)
98                                         return false;
99                         } else if (!value.equals(other.value))
100                                 return false;
101                         return true;
102                 }
103         }
104         
105     private static final long serialVersionUID = 8508750881358776559L;
106
107     protected String          data             = null;
108     protected String          defaultData      = null;
109     protected Point           targetSize       = null;
110     protected Boolean         useMipMap        = true;
111     protected Rectangle2D     bounds           = null;
112     
113     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
114
115     transient BufferedImage buffer    = null;
116     transient String documentCache    = null;
117     transient SVGDiagram diagramCache = null;
118     transient String dataHash         = null;
119
120     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
121
122     @Override
123     public void cleanup() {
124         cleanDiagramCache();
125     }
126
127     public void setAssignments(List<SVGNodeAssignment> ass) {
128         assignments.clear();
129         assignments.addAll(ass);
130     }
131     
132     public void cleanDiagramCache() {
133         SVGDiagram d = diagramCache;
134         if (d != null) {
135             diagramCache = null;
136             SVGUniverse univ = SVGCache.getSVGUniverse();
137             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
138                 // Cleared!
139                 //System.out.println("cleared: " + d.getXMLBase());
140             }
141         }
142     }
143
144     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
145
146     @PropertySetter("SVG")
147     @SyncField("data")
148     public void setData(String data) {
149         String cached = dataCache.get(data);
150         if (cached == null) {
151             cached = data;
152             dataCache.put(data, data);
153         }
154         this.data = cached;
155         this.defaultData = cached;
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     }
172
173     @PropertySetter("Bounds")
174     @SyncField("bounds")
175     public void setBounds(Rectangle2D bounds) {
176         this.bounds = bounds;
177     }
178
179     @Override
180     public Rectangle2D getBoundsInLocal() {
181         if (bounds == null)
182             parseSVG();
183         return bounds;
184     }
185
186     @Override
187     public void render(Graphics2D g2d) {
188         if (data == null)
189             return; // Not initialized
190
191         if (!data.equals(documentCache) || diagramCache == null || buffer == null)
192             initBuffer(g2d);
193
194         AffineTransform ot = null;
195         if (!transform.isIdentity()) {
196             ot = g2d.getTransform();
197             g2d.transform(transform);
198         }
199
200         if (buffer != null)
201             buffer.paint(g2d);
202
203         if (ot != null)
204             g2d.setTransform(ot);
205     }
206
207     protected String parseSVG() {
208         if (data == null)
209             return null;
210
211         try {
212                 SVGUniverse univ = SVGCache.getSVGUniverse();
213                 synchronized(univ) {
214                         // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
215                         byte[] dataBytes = data.getBytes("UTF-8");
216                         dataHash = digest(dataBytes, assignments);
217                         if (diagramCache != null)
218                                 univ.decRefCount(diagramCache.getXMLBase());
219                         URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
220                         diagramCache = univ.getDiagram(uri, false);
221                         if (diagramCache != null) {
222                                 if (diagramCache.getRoot() == null) {
223                                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
224                                         dataHash = "broken";
225                                 } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
226                                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
227                                         dataHash = "empty";
228                                 } else {
229                                         for(SVGNodeAssignment ass : assignments) {
230                                                 SVGElement e = diagramCache.getElement(ass.elementId);
231                                                 if(e != null) {
232                                                         if("$text".equals(ass.attributeNameOrId)) {
233                                                                 Tspan t = (Tspan)e;
234                                                                 t.setText(ass.value);
235                                                                 Text text = (Text)t.getParent();
236                                                                 text.rebuild();
237                                                         } else {
238                                                                 e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
239                                                         }
240                                                 }
241                                         }
242                                         if(!assignments.isEmpty())
243                                                 diagramCache.updateTime(0);
244                                 }
245                         }
246                         documentCache = data;
247                         if (diagramCache != null) {
248                                 setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone());
249                                 univ.incRefCount(diagramCache.getXMLBase());
250                         } else {
251                                 setBounds(new Rectangle2D.Double());
252                         }
253                 }
254         } catch (SVGException e) {
255             setBounds((Rectangle2D) diagramCache.getViewRect().clone());
256         } catch (IOException e) {
257             diagramCache = null;
258         }
259
260         return dataHash;
261     }
262
263     public static Rectangle2D getBounds(String data) {
264         return getBounds(data, null);
265     }
266
267     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
268         if (data == null) {
269             new Exception("null SVG data").printStackTrace();
270             return null;
271         }
272
273         SVGDiagram diagramCache = null;
274         try {
275             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
276             byte[] dataBytes = data.getBytes("UTF-8");
277             String digest = digest(dataBytes, assignments);
278
279             SVGUniverse univ = SVGCache.getSVGUniverse();
280             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
281             synchronized (univ) {
282                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
283                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
284                 diagramCache = univ.getDiagram(uri, false);
285                 if (diagramCache != null) {
286                     if (diagramCache.getRoot() == null) {
287                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
288                     } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
289                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
290                     }
291                 }
292             }
293
294             Rectangle2D rect = null;
295             if (diagramCache != null) {
296                 SVGRoot root = diagramCache.getRoot();
297                 Rectangle2D bbox = root.getBoundingBox();
298                 rect = (Rectangle2D) bbox.clone();
299             } else {
300                 rect = new Rectangle2D.Double();
301             }
302             return rect;
303         } catch (SVGException e) {
304             return ((Rectangle2D) diagramCache.getViewRect().clone());
305         } catch(IOException e) {
306         }
307         return null;
308     }
309
310     public static Rectangle2D getRealBounds(String data) {
311         return getRealBounds(data, null);
312     }
313
314     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
315         if (data == null) {
316             new Exception("null SVG data").printStackTrace();
317             return null;
318         }
319
320         SVGDiagram diagramCache = null;
321         try {
322             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
323             byte[] dataBytes = data.getBytes("UTF-8");
324             String digest = digest(dataBytes, assignments);
325
326             SVGUniverse univ = SVGCache.getSVGUniverse();
327             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
328             synchronized (univ) {
329                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
330                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
331                 diagramCache = univ.getDiagram(uri, false);
332                 if (diagramCache != null) {
333                         SVGRoot root = diagramCache.getRoot(); 
334                     if (root == null) return new Rectangle2D.Double();
335                     return (Rectangle2D)root.getBoundingBox().clone();
336                 }
337             }
338         } catch (SVGException e) {
339             return ((Rectangle2D) diagramCache.getViewRect().clone());
340         } catch(IOException e) {
341         }
342         return null;
343     }
344
345     protected void initBuffer(Graphics2D g2d) {
346         
347         if (!data.equals(documentCache) || diagramCache == null) {
348                 dataHash = parseSVG();
349                 if (diagramCache == null) {
350                         System.err.println("UNABLE TO PARSE SVG:\n" + data);
351                         return;
352                 }
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) {
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
380     public void setProperty(String field, Object value) {
381         if("data".equals(field)) {
382 //              System.out.println("SVGNode data -> " + value);
383             this.data = (String)value;
384         } else if ("z".equals(field)) {
385 //              System.out.println("SVGNode z -> " + value);
386             setZIndex((Integer)value);
387         } else if ("position".equals(field)) {
388 //              System.out.println("SVGNode position -> " + value);
389             Point point = (Point)value;
390             setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
391 //              setPosition(point.x, point.y);
392         }
393     }
394
395     @Override
396     public void initValues() {
397         data = defaultData;
398         dataHash = null;
399         buffer =  null;
400     }
401
402     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
403     
404     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
405         try {
406             MessageDigest md = MessageDigest.getInstance("MD5");
407             byte[] messageDigest = md.digest(dataBytes);
408             BigInteger number = new BigInteger(1, messageDigest);
409             String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);
410             String result = digestCache.get(dataHash);
411             if(result == null) {
412                 result = dataHash;
413                 digestCache.put(dataHash,dataHash);
414             }
415             return result;
416         } catch (NoSuchAlgorithmException e) {
417             // Shouldn't happen
418             throw new Error("MD5 digest must exist.");
419         }
420     }
421
422     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
423     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
424
425         @Override
426         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
427                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
428         }
429         
430         @Override
431         public <T> T getProperty(String propertyName) {
432                 return null;
433         }
434
435         @Override
436         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
437         }
438
439         public void synchronizeDocument(String document) {
440                 setData(document);
441         }
442
443         public void synchronizeTransform(double[] data) {
444                 this.setTransform(new AffineTransform(data));
445         }
446         
447 }