]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Merge "Supply SVG text editor with element measurement context"
[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.Collections;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.WeakHashMap;
33
34 import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;
35 import org.simantics.scenegraph.LoaderNode;
36 import org.simantics.scenegraph.ScenegraphUtils;
37 import org.simantics.scenegraph.g2d.G2DNode;
38 import org.simantics.scenegraph.utils.BufferedImage;
39 import org.simantics.scenegraph.utils.G2DUtils;
40 import org.simantics.scenegraph.utils.InitValueSupport;
41 import org.simantics.scenegraph.utils.MipMapBufferedImage;
42 import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;
43 import org.simantics.scenegraph.utils.VRamBufferedImage;
44 import org.simantics.scl.runtime.function.Function1;
45 import org.simantics.scl.runtime.function.Function2;
46 import org.simantics.utils.threads.AWTThread;
47
48 import com.kitfox.svg.RenderableElement;
49 import com.kitfox.svg.SVGCache;
50 import com.kitfox.svg.SVGDiagram;
51 import com.kitfox.svg.SVGElement;
52 import com.kitfox.svg.SVGException;
53 import com.kitfox.svg.SVGRoot;
54 import com.kitfox.svg.SVGUniverse;
55 import com.kitfox.svg.Text;
56 import com.kitfox.svg.Tspan;
57 import com.kitfox.svg.animation.AnimationElement;
58
59 @RasterOutputWidget
60 public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
61
62         private static final long serialVersionUID = 8508750881358776559L;
63
64     protected String          data             = null;
65     protected String          defaultData      = null;
66     protected Point           targetSize       = null;
67     protected Boolean         useMipMap        = true;
68     protected Rectangle2D     bounds           = null;
69
70     protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();
71
72     protected transient BufferedImage buffer    = null;
73     protected transient String documentCache    = null;
74     protected transient SVGDiagram diagramCache = null;
75     protected transient String dataHash         = null;
76
77     static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();
78
79     @Override
80     public void init() {
81         super.init();
82     }
83
84     @Override
85     public void cleanup() {
86         cleanDiagramCache();
87     }
88
89     public void setAssignments(List<SVGNodeAssignment> ass) {
90         assignments.clear();
91         assignments.addAll(ass);
92     }
93     
94     public void cleanDiagramCache() {
95         SVGDiagram d = diagramCache;
96         if (d != null) {
97             diagramCache = null;
98             SVGUniverse univ = SVGCache.getSVGUniverse();
99             if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {
100                 // Cleared!
101                 //System.out.println("cleared: " + d.getXMLBase());
102             }
103         }
104     }
105
106     static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();
107
108     @PropertySetter("SVG")
109     @SyncField("data")
110     public void setData(String data) {
111         String cached = dataCache.get(data);
112         if (cached == null) {
113             cached = data;
114             dataCache.put(data, data);
115         }
116         this.data = cached;
117         this.defaultData = cached;
118     }
119
120     @SyncField("targetSize")
121     public void setTargetSize(Point p) {
122         this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
123     }
124
125     @SyncField("targetSize")
126     public void setTargetSize(int x, int y) {
127         this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
128     }
129
130     @SyncField("useMipMap")
131     public void useMipMap(Boolean use) {
132         this.useMipMap = use;
133     }
134
135     @PropertySetter("Bounds")
136     @SyncField("bounds")
137     public void setBounds(Rectangle2D bounds) {
138         this.bounds = bounds;
139     }
140
141     @Override
142     public Rectangle2D getBoundsInLocal() {
143         if (bounds == null)
144             parseSVG();
145         return bounds;
146     }
147
148     @Override
149     public void render(Graphics2D g2d) {
150         if (data == null)
151             return; // Not initialized
152
153         if (!data.equals(documentCache) || diagramCache == null || buffer == null)
154             initBuffer(g2d);
155
156         AffineTransform ot = null;
157         if (!transform.isIdentity()) {
158             ot = g2d.getTransform();
159             g2d.transform(transform);
160         }
161
162         if (buffer != null)
163             buffer.paint(g2d);
164
165         if (ot != null)
166             g2d.setTransform(ot);
167     }
168
169     protected int dynamicHash() {
170         return 0;
171     }
172
173     protected String parseSVG() {
174         if (data == null)
175             return null;
176
177         SVGUniverse univ = SVGCache.getSVGUniverse();
178         try {
179             Rectangle2D bbox = null;
180             synchronized (univ) {
181                 // Relinquish reference to current element
182                 if (diagramCache != null) {
183                     univ.decRefCount(diagramCache.getXMLBase());
184                     diagramCache = null;
185                 }
186
187                 // Lets check for rootAssignment that contributes the whole SVG 
188                 SVGNodeAssignment rootAssignment = null;
189                 if (!assignments.isEmpty()) {
190                     for (SVGNodeAssignment ass : assignments) {
191                         if (ass.attributeNameOrId.equals("$root")) {
192                             rootAssignment = ass;
193                             break;
194                         }
195                     }
196                 }
197                 byte[] dataBytes;
198                 if (rootAssignment != null) {
199                     dataBytes = rootAssignment.value.getBytes("UTF-8");
200                 } else {
201                     // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
202                     dataBytes = data.getBytes("UTF-8");
203                 }
204                 dataHash = digest(dataBytes, assignments, dynamicHash());
205                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
206                 diagramCache = univ.getDiagram(uri, false);
207
208                 if (diagramCache != null) {
209                     univ.incRefCount(diagramCache.getXMLBase());
210                     SVGRoot root = diagramCache.getRoot();
211                     if (root == null) {
212                         univ.decRefCount(diagramCache.getXMLBase());
213                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);
214                         dataHash = "broken";
215                         univ.incRefCount(diagramCache.getXMLBase());
216                         bbox = (Rectangle2D) diagramCache.getRoot().getBoundingBox().clone();
217                     } else {
218                         bbox = root.getBoundingBox();
219                         if (bbox.isEmpty()) {
220                             // Lets check if this should be visible or not
221                             Set<?> presentationAttributes = root.getPresentationAttributes();
222                             if (!presentationAttributes.contains("display")) {
223                                 // TODO: fix this - How can one read values of attributes in SVG salamander???
224                                 univ.decRefCount(diagramCache.getXMLBase());
225                                 diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);
226                                 dataHash = "empty";
227                                 univ.incRefCount(diagramCache.getXMLBase());
228                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
229                             } else {
230                                 bbox = new Rectangle2D.Double(0, 0, 0, 0);
231                             }
232                         } else {
233                             if (applyAssignments(diagramCache, assignments)) {
234                                 bbox = (Rectangle2D) root.getBoundingBox().clone();
235                             } else {
236                                 bbox = (Rectangle2D) bbox.clone();
237                             }
238                         }
239                     }
240                 } else {
241                     bbox = new Rectangle2D.Double();
242                 }
243             }
244
245             documentCache = data;
246             setBounds(bbox);
247         } catch (SVGException e) {
248             // This can only occur if diagramCache != null.
249             setBounds(diagramCache.getViewRect(new Rectangle2D.Double()));
250             univ.decRefCount(diagramCache.getXMLBase());
251             diagramCache = null;
252         } catch (IOException e) {
253             diagramCache = null;
254         }
255
256         return dataHash;
257     }
258
259     protected boolean applyAssignments(SVGDiagram diagram, List<SVGNodeAssignment> assignments) throws SVGException {
260         if (assignments.isEmpty())
261             return false;
262
263         boolean changed = false;
264
265         for (SVGNodeAssignment ass : assignments) {
266             SVGElement e = diagram.getElement(ass.elementId);
267             if (e != null) {
268                 if ("$text".equals(ass.attributeNameOrId)) {
269                     if (e instanceof Tspan) {
270                         Tspan t = (Tspan) e;
271                         if (ass.value.trim().isEmpty()) {
272                                 t.setText("-");
273                         } else {
274                                 t.setText(ass.value);
275                         }
276                         SVGElement parent = t.getParent();
277                         if (parent instanceof Text)
278                             ((Text) parent).rebuild();
279                         changed = true;
280                     }
281                 } else if (ass.attributeNameOrId.startsWith("#")) {
282                     e.setAttribute(ass.attributeNameOrId.substring(1), AnimationElement.AT_CSS, ass.value);
283                     changed = true;
284                 } else {
285                     e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);
286                     changed = true;
287                 }
288             }
289         }
290
291         return changed;
292     }
293
294     public static Rectangle2D getBounds(String data) {
295         return getBounds(data, 0);
296     }
297
298     public static Rectangle2D getBounds(String data, int dynamicHash) {
299         return getBounds(data, Collections.emptyList(), dynamicHash);
300     }
301
302     public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
303         if (data == null) {
304             new Exception("null SVG data").printStackTrace();
305             return null;
306         }
307
308         SVGDiagram diagramCache = null;
309         try {
310             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
311             byte[] dataBytes = data.getBytes("UTF-8");
312             String digest = digest(dataBytes, assignments, dynamicHash);
313
314             SVGUniverse univ = SVGCache.getSVGUniverse();
315             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
316             synchronized (univ) {
317                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
318                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
319                 diagramCache = univ.getDiagram(uri, false);
320                 if (diagramCache != null) {
321                     if (diagramCache.getRoot() == null) {
322                         diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));
323                     } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {
324                         diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));
325                     }
326                 }
327             }
328
329             Rectangle2D rect = null;
330             if (diagramCache != null) {
331                 SVGRoot root = diagramCache.getRoot();
332                 Rectangle2D bbox = root.getBoundingBox();
333                 rect = (Rectangle2D) bbox.clone();
334             } else {
335                 rect = new Rectangle2D.Double();
336             }
337             return rect;
338         } catch (SVGException e) {
339             return diagramCache.getViewRect(new Rectangle2D.Double());
340         } catch(IOException e) {
341         }
342         return null;
343     }
344
345     public static Rectangle2D getRealBounds(String data) {
346         return getRealBounds(data, Collections.emptyList(), 0);
347     }
348
349     public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments, int dynamicHash) {
350         if (data == null) {
351             new Exception("null SVG data").printStackTrace();
352             return null;
353         }
354
355         SVGDiagram diagramCache = null;
356         try {
357             // NOTE: hard-coded to assume all SVG data is encoded in UTF-8
358             byte[] dataBytes = data.getBytes("UTF-8");
359             String digest = digest(dataBytes, assignments, dynamicHash);
360
361             SVGUniverse univ = SVGCache.getSVGUniverse();
362             // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.
363             synchronized (univ) {
364                 //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);
365                 URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);
366                 diagramCache = univ.getDiagram(uri, false);
367                 if (diagramCache != null) {
368                     SVGRoot root = diagramCache.getRoot(); 
369                     if (root == null) return new Rectangle2D.Double();
370                     return (Rectangle2D)root.getBoundingBox().clone();
371                 }
372             }
373         } catch (SVGException e) {
374             return diagramCache.getViewRect(new Rectangle2D.Double());
375         } catch(IOException e) {
376         }
377         return null;
378     }
379
380     protected void initBuffer(Graphics2D g2d) {
381         if (!data.equals(documentCache) || diagramCache == null) {
382             dataHash = parseSVG();
383             if (diagramCache == null) {
384                 System.err.println("UNABLE TO PARSE SVG:\n" + data);
385                 return;
386             }
387         }
388
389         if (buffer != null) {
390             buffer = null;
391         }
392         diagramCache.setIgnoringClipHeuristic(true); // FIXME
393         if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
394             buffer = bufferCache.get(dataHash).get();
395         } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
396             buffer = null;
397         } else if(useMipMap) {
398             if(G2DUtils.isAccelerated(g2d)) {
399                 buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
400             } else {
401                 buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
402             }
403             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
404         } else {
405             if(G2DUtils.isAccelerated(g2d)) {
406                 buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
407             } else {
408                 buffer = new BufferedImage(diagramCache, bounds, targetSize);
409             }
410             bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
411         }
412     }
413
414     public void setProperty(String field, Object value) {
415         if("data".equals(field)) {
416 //              System.out.println("SVGNode data -> " + value);
417             this.data = (String)value;
418         } else if ("z".equals(field)) {
419 //              System.out.println("SVGNode z -> " + value);
420             setZIndex((Integer)value);
421         } else if ("position".equals(field)) {
422 //              System.out.println("SVGNode position -> " + value);
423             Point point = (Point)value;
424             setTransform(AffineTransform.getTranslateInstance(point.x, point.y));
425 //              setPosition(point.x, point.y);
426         }
427     }
428
429     @Override
430     public void initValues() {
431         data = defaultData;
432         dataHash = null;
433         buffer =  null;
434     }
435
436     static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();
437
438     static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments, int dynamicHash) {
439         try {
440             MessageDigest md = MessageDigest.getInstance("MD5");
441             byte[] messageDigest = md.digest(dataBytes);
442             BigInteger number = new BigInteger(1, messageDigest);
443             String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0) + 31 * dynamicHash;
444             String result = digestCache.get(dataHash);
445             if(result == null) {
446                 result = dataHash;
447                 digestCache.put(dataHash,dataHash);
448             }
449             return result;
450         } catch (NoSuchAlgorithmException e) {
451             // Shouldn't happen
452             throw new Error("MD5 digest must exist.");
453         }
454     }
455
456     static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");
457     static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");
458
459         @Override
460         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
461                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
462         }
463         
464         @Override
465         public <T> T getProperty(String propertyName) {
466                 return null;
467         }
468
469         @Override
470         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
471         }
472
473         public void synchronizeDocument(String document) {
474                 setData(document);
475         }
476
477         public void synchronizeTransform(double[] data) {
478                 this.setTransform(new AffineTransform(data));
479         }
480
481         public String getSVGText() {
482                 String ret = data.replace("<svg", "<g").replaceAll("svg>", "g>");
483                 //return diagramCache.toString();
484                 //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>");
485                 return ret;
486         }
487
488     public Rectangle2D getElementBounds(String id) throws SVGException {
489         SVGElement e = diagramCache.getElement(id);
490         if (e instanceof RenderableElement) {
491             return ((RenderableElement)e).getBoundingBox();
492         } else {
493            return null;
494         }
495     }
496
497 }