2fc37b7d59ab6bc9179943ab80b60246118e08e3
[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         private static final long serialVersionUID = 8508750881358776559L;
60
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;
66     
67     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
68
69     transient BufferedImage buffer    = null;
70     transient String documentCache    = null;
71     transient SVGDiagram diagramCache = null;
72     transient String dataHash         = null;
73
74     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
75
76     @Override
77     public void cleanup() {
78         cleanDiagramCache();
79     }
80
81     public void setAssignments(List<SVGNodeAssignment> ass) {
82         assignments.clear();
83         assignments.addAll(ass);
84     }
85     
86     public void cleanDiagramCache() {
87         SVGDiagram d = diagramCache;
88         if (d != null) {
89             diagramCache = null;
90             SVGUniverse univ = SVGCache.getSVGUniverse();
91             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
92                 // Cleared!
93                 //System.out.println("cleared: " + d.getXMLBase());
94             }
95         }
96     }
97
98     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
99
100     @PropertySetter("SVG")
101     @SyncField("data")
102     public void setData(String data) {
103         String cached = dataCache.get(data);
104         if (cached == null) {
105             cached = data;
106             dataCache.put(data, data);
107         }
108         this.data = cached;
109         this.defaultData = cached;
110     }
111
112     @SyncField("targetSize")
113     public void setTargetSize(Point p) {
114         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
115     }
116
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
120     }
121
122     @SyncField("useMipMap")
123     public void useMipMap(Boolean use) {
124         this.useMipMap = use;
125     }
126
127     @PropertySetter("Bounds")
128     @SyncField("bounds")
129     public void setBounds(Rectangle2D bounds) {
130         this.bounds = bounds;
131     }
132
133     @Override
134     public Rectangle2D getBoundsInLocal() {
135         if (bounds == null)
136             parseSVG();
137         return bounds;
138     }
139
140     @Override
141     public void render(Graphics2D g2d) {
142         if (data == null)
143             return; // Not initialized
144
145         if (!data.equals(documentCache) || diagramCache == null || buffer == null)
146             initBuffer(g2d);
147
148         AffineTransform ot = null;
149         if (!transform.isIdentity()) {
150             ot = g2d.getTransform();
151             g2d.transform(transform);
152         }
153
154         if (buffer != null)
155             buffer.paint(g2d);
156
157         if (ot != null)
158             g2d.setTransform(ot);
159     }
160
161     protected String parseSVG() {
162         if (data == null)
163             return null;
164
165         SVGUniverse univ = SVGCache.getSVGUniverse();
166         try {
167             Rectangle2D bbox = null;
168             synchronized (univ) {
169                 // Relinquish reference to current element
170                 if (diagramCache != null) {
171                     univ.decRefCount(diagramCache.getXMLBase());
172                     diagramCache = null;
173                 }
174
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);
180
181                 if (diagramCache != null) {
182                     univ.incRefCount(diagramCache.getXMLBase());
183
184                     if (diagramCache.getRoot() == null) {
185                         univ.decRefCount(diagramCache.getXMLBase());
186                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
187                         dataHash = "broken";
188                         univ.incRefCount(diagramCache.getXMLBase());
189                         bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
190                     } else {
191                         bbox = diagramCache.getRoot().getBoundingBox();
192                         if (bbox.isEmpty()) {
193                             univ.decRefCount(diagramCache.getXMLBase());
194                             diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
195                             dataHash = "empty";
196                             univ.incRefCount(diagramCache.getXMLBase());
197                             bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
198                         } else {
199                             if (applyAssignments(diagramCache, assignments)) {
200                                 bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
201                             } else {
202                                 bbox = (Rectangle2D) bbox.clone();
203                             }
204                         }
205                     }
206                 } else {
207                     bbox = new Rectangle2D.Double();
208                 }
209             }
210
211             documentCache = data;
212             setBounds(bbox);
213         } catch (SVGException e) {
214             // This can only occur if diagramCache != null.
215             setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
216             univ.decRefCount(diagramCache.getXMLBase());
217             diagramCache = null;
218         } catch (IOException e) {
219             diagramCache = null;
220         }
221
222         return dataHash;
223     }
224
225     private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
226         if (assignments.isEmpty())
227             return false;
228         boolean changed = false;
229         for (SVGNodeAssignment ass : assignments) {
230             SVGElement e = diagram.getElement(ass.elementId);
231             if (e != null) {
232                 if ("$text".equals(ass.attributeNameOrId)) {
233                     if (e instanceof Tspan) {
234                         Tspan t = (Tspan) e;
235                         if (ass.value.trim().isEmpty()) {
236                                 t.setText("-");
237                         } else {
238                                 t.setText(ass.value);
239                         }
240                         SVGElement parent = t.getParent();
241                         if (parent instanceof Text)
242                             ((Text) parent).rebuild();
243                         changed = true;
244                     }
245                 } else {
246                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
247                     changed = true;
248                 }
249             }
250         }
251         diagram.updateTime(0);
252         return changed;
253     }
254
255     public static Rectangle2D getBounds(String data) {
256         return getBounds(data, null);
257     }
258
259     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
260         if (data == null) {
261             new Exception("null SVG data").printStackTrace();
262             return null;
263         }
264
265         SVGDiagram diagramCache = null;
266         try {
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);
270
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));
282                     }
283                 }
284             }
285
286             Rectangle2D rect = null;
287             if (diagramCache != null) {
288                 SVGRoot root = diagramCache.getRoot();
289                 Rectangle2D bbox = root.getBoundingBox();
290                 rect = (Rectangle2D) bbox.clone();
291             } else {
292                 rect = new Rectangle2D.Double();
293             }
294             return rect;
295         } catch (SVGException e) {
296             return diagramCache.getViewRect(new Rectangle2D.Double());
297         } catch(IOException e) {
298         }
299         return null;
300     }
301
302     public static Rectangle2D getRealBounds(String data) {
303         return getRealBounds(data, null);
304     }
305
306     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
307         if (data == null) {
308             new Exception("null SVG data").printStackTrace();
309             return null;
310         }
311
312         SVGDiagram diagramCache = null;
313         try {
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);
317
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();
328                 }
329             }
330         } catch (SVGException e) {
331             return diagramCache.getViewRect(new Rectangle2D.Double());
332         } catch(IOException e) {
333         }
334         return null;
335     }
336
337     protected void initBuffer(Graphics2D g2d) {
338         
339         if (!data.equals(documentCache) || diagramCache == null) {
340                 dataHash = parseSVG();
341                 if (diagramCache == null) {
342                         System.err.println("UNABLE TO PARSE SVG:\n" + data);
343                         return;
344                 }
345         }
346
347         if (buffer != null) {
348             buffer = null;
349         }
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) {
354             buffer = null;
355         } else if(useMipMap) {
356                 if(G2DUtils.isAccelerated(g2d)) {
357                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
358             } else {
359                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
360             }
361             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
362         } else {
363                 if(G2DUtils.isAccelerated(g2d)) {
364                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
365             } else {
366                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
367             }
368             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
369         }
370     }
371
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);
384         }
385     }
386
387     @Override
388     public void initValues() {
389         data = defaultData;
390         dataHash = null;
391         buffer =  null;
392     }
393
394     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
395     
396     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
397         try {
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);
403             if(result == null) {
404                 result = dataHash;
405                 digestCache.put(dataHash,dataHash);
406             }
407             return result;
408         } catch (NoSuchAlgorithmException e) {
409             // Shouldn't happen
410             throw new Error("MD5 digest must exist.");
411         }
412     }
413
414     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
415     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
416
417         @Override
418         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
419                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
420         }
421         
422         @Override
423         public <T> T getProperty(String propertyName) {
424                 return null;
425         }
426
427         @Override
428         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
429         }
430
431         public void synchronizeDocument(String document) {
432                 setData(document);
433         }
434
435         public void synchronizeTransform(double[] data) {
436                 this.setTransform(new AffineTransform(data));
437         }
438         
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>");
443                 return ret;
444         }
445         
446 }