Dynamic terminals and connections
[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.Set;
31 import java.util.WeakHashMap;
32
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;
46
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;
56
57 @RasterOutputWidget
58 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
59
60         private static final long serialVersionUID = 8508750881358776559L;
61
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;
67     
68     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
69
70     transient BufferedImage buffer    = null;
71     transient String documentCache    = null;
72     transient SVGDiagram diagramCache = null;
73     transient String dataHash         = null;
74
75     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
76
77     @Override
78     public void cleanup() {
79         cleanDiagramCache();
80     }
81
82     public void setAssignments(List<SVGNodeAssignment> ass) {
83         assignments.clear();
84         assignments.addAll(ass);
85     }
86     
87     public void cleanDiagramCache() {
88         SVGDiagram d = diagramCache;
89         if (d != null) {
90             diagramCache = null;
91             SVGUniverse univ = SVGCache.getSVGUniverse();
92             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
93                 // Cleared!
94                 //System.out.println("cleared: " + d.getXMLBase());
95             }
96         }
97     }
98
99     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
100
101     @PropertySetter("SVG")
102     @SyncField("data")
103     public void setData(String data) {
104         String cached = dataCache.get(data);
105         if (cached == null) {
106             cached = data;
107             dataCache.put(data, data);
108         }
109         this.data = cached;
110         this.defaultData = cached;
111     }
112
113     @SyncField("targetSize")
114     public void setTargetSize(Point p) {
115         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
116     }
117
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
121     }
122
123     @SyncField("useMipMap")
124     public void useMipMap(Boolean use) {
125         this.useMipMap = use;
126     }
127
128     @PropertySetter("Bounds")
129     @SyncField("bounds")
130     public void setBounds(Rectangle2D bounds) {
131         this.bounds = bounds;
132     }
133
134     @Override
135     public Rectangle2D getBoundsInLocal() {
136         if (bounds == null)
137             parseSVG();
138         return bounds;
139     }
140
141     @Override
142     public void render(Graphics2D g2d) {
143         if (data == null)
144             return; // Not initialized
145
146         if (!data.equals(documentCache) || diagramCache == null || buffer == null)
147             initBuffer(g2d);
148
149         AffineTransform ot = null;
150         if (!transform.isIdentity()) {
151             ot = g2d.getTransform();
152             g2d.transform(transform);
153         }
154
155         if (buffer != null)
156             buffer.paint(g2d);
157
158         if (ot != null)
159             g2d.setTransform(ot);
160     }
161
162     protected String parseSVG() {
163         if (data == null)
164             return null;
165
166         SVGUniverse univ = SVGCache.getSVGUniverse();
167         try {
168             Rectangle2D bbox = null;
169             synchronized (univ) {
170                 // Relinquish reference to current element
171                 if (diagramCache != null) {
172                     univ.decRefCount(diagramCache.getXMLBase());
173                     diagramCache = null;
174                 }
175
176                 
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;
183                             break;
184                         }
185                     }
186                 }
187                 byte[] dataBytes;
188                 if (rootAssignment != null) {
189                     dataBytes = rootAssignment.value.getBytes("UTF-8");
190                 } else {
191                     // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
192                     dataBytes = data.getBytes("UTF-8");
193                 }
194                 dataHash = digest(dataBytes, assignments);
195                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
196                 diagramCache = univ.getDiagram(uri, false);
197
198                 if (diagramCache != null) {
199                     univ.incRefCount(diagramCache.getXMLBase());
200                     SVGRoot root = diagramCache.getRoot();
201                     if (root == null) {
202                         univ.decRefCount(diagramCache.getXMLBase());
203                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
204                         dataHash = "broken";
205                         univ.incRefCount(diagramCache.getXMLBase());
206                         bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
207                     } else {
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);
216                                 dataHash = "empty";
217                                 univ.incRefCount(diagramCache.getXMLBase());
218                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
219                             } else {
220                                 bbox = new Rectangle2D.Double(0, 0, 0, 0);
221                             }
222                         } else {
223                             if (applyAssignments(diagramCache, assignments)) {
224                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
225                             } else {
226                                 bbox = (Rectangle2D) bbox.clone();
227                             }
228                         }
229                     }
230                 } else {
231                     bbox = new Rectangle2D.Double();
232                 }
233             }
234
235             documentCache = data;
236             setBounds(bbox);
237         } catch (SVGException e) {
238             // This can only occur if diagramCache != null.
239             setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
240             univ.decRefCount(diagramCache.getXMLBase());
241             diagramCache = null;
242         } catch (IOException e) {
243             diagramCache = null;
244         }
245
246         return dataHash;
247     }
248
249     private static boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
250         if (assignments.isEmpty())
251             return false;
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);
258             if (e != null) {
259                 if ("$text".equals(ass.attributeNameOrId)) {
260                     if (e instanceof Tspan) {
261                         Tspan t = (Tspan) e;
262                         if (ass.value.trim().isEmpty()) {
263                                 t.setText("-");
264                         } else {
265                                 t.setText(ass.value);
266                         }
267                         SVGElement parent = t.getParent();
268                         if (parent instanceof Text)
269                             ((Text) parent).rebuild();
270                         changed = true;
271                     }
272                 } else {
273                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
274                     changed = true;
275                 }
276             }
277         }
278         diagram.updateTime(0);
279         return changed;
280     }
281
282     public static Rectangle2D getBounds(String data) {
283         return getBounds(data, null);
284     }
285
286     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {
287         if (data == null) {
288             new Exception("null SVG data").printStackTrace();
289             return null;
290         }
291
292         SVGDiagram diagramCache = null;
293         try {
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);
297
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));
309                     }
310                 }
311             }
312
313             Rectangle2D rect = null;
314             if (diagramCache != null) {
315                 SVGRoot root = diagramCache.getRoot();
316                 Rectangle2D bbox = root.getBoundingBox();
317                 rect = (Rectangle2D) bbox.clone();
318             } else {
319                 rect = new Rectangle2D.Double();
320             }
321             return rect;
322         } catch (SVGException e) {
323             return diagramCache.getViewRect(new Rectangle2D.Double());
324         } catch(IOException e) {
325         }
326         return null;
327     }
328
329     public static Rectangle2D getRealBounds(String data) {
330         return getRealBounds(data, null);
331     }
332
333     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {
334         if (data == null) {
335             new Exception("null SVG data").printStackTrace();
336             return null;
337         }
338
339         SVGDiagram diagramCache = null;
340         try {
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);
344
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();
355                 }
356             }
357         } catch (SVGException e) {
358             return diagramCache.getViewRect(new Rectangle2D.Double());
359         } catch(IOException e) {
360         }
361         return null;
362     }
363
364     protected void initBuffer(Graphics2D g2d) {
365         
366         if (!data.equals(documentCache) || diagramCache == null) {
367                 dataHash = parseSVG();
368                 if (diagramCache == null) {
369                         System.err.println("UNABLE TO PARSE SVG:\n" + data);
370                         return;
371                 }
372         }
373
374         if (buffer != null) {
375             buffer = null;
376         }
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) {
381             buffer = null;
382         } else if(useMipMap) {
383                 if(G2DUtils.isAccelerated(g2d)) {
384                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
385             } else {
386                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
387             }
388             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
389         } else {
390                 if(G2DUtils.isAccelerated(g2d)) {
391                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
392             } else {
393                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
394             }
395             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
396         }
397     }
398
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);
411         }
412     }
413
414     @Override
415     public void initValues() {
416         data = defaultData;
417         dataHash = null;
418         buffer =  null;
419     }
420
421     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
422     
423     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {
424         try {
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);
430             if(result == null) {
431                 result = dataHash;
432                 digestCache.put(dataHash,dataHash);
433             }
434             return result;
435         } catch (NoSuchAlgorithmException e) {
436             // Shouldn't happen
437             throw new Error("MD5 digest must exist.");
438         }
439     }
440
441     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
442     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
443
444         @Override
445         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
446                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
447         }
448         
449         @Override
450         public <T> T getProperty(String propertyName) {
451                 return null;
452         }
453
454         @Override
455         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
456         }
457
458         public void synchronizeDocument(String document) {
459                 setData(document);
460         }
461
462         public void synchronizeTransform(double[] data) {
463                 this.setTransform(new AffineTransform(data));
464         }
465         
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>");
470                 return ret;
471         }
472         
473 }