]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / g2d / nodes / SVGNode.java
diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/SVGNode.java
new file mode 100644 (file)
index 0000000..e88c7b9
--- /dev/null
@@ -0,0 +1,447 @@
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ *     VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.scenegraph.g2d.nodes;
+\r
+import java.awt.Graphics2D;\r
+import java.awt.Point;\r
+import java.awt.geom.AffineTransform;\r
+import java.awt.geom.Rectangle2D;\r
+import java.io.ByteArrayInputStream;\r
+import java.io.IOException;\r
+import java.lang.ref.WeakReference;\r
+import java.math.BigInteger;\r
+import java.net.URI;\r
+import java.net.URL;\r
+import java.security.MessageDigest;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.util.ArrayList;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.WeakHashMap;\r
+\r
+import org.simantics.scenegraph.ExportableWidget.RasterOutputWidget;\r
+import org.simantics.scenegraph.LoaderNode;\r
+import org.simantics.scenegraph.ScenegraphUtils;\r
+import org.simantics.scenegraph.g2d.G2DNode;\r
+import org.simantics.scenegraph.utils.BufferedImage;\r
+import org.simantics.scenegraph.utils.G2DUtils;\r
+import org.simantics.scenegraph.utils.InitValueSupport;\r
+import org.simantics.scenegraph.utils.MipMapBufferedImage;\r
+import org.simantics.scenegraph.utils.MipMapVRamBufferedImage;\r
+import org.simantics.scenegraph.utils.VRamBufferedImage;\r
+import org.simantics.scl.runtime.function.Function1;\r
+import org.simantics.scl.runtime.function.Function2;\r
+import org.simantics.utils.threads.AWTThread;\r
+\r
+import com.kitfox.svg.SVGCache;\r
+import com.kitfox.svg.SVGDiagram;\r
+import com.kitfox.svg.SVGElement;\r
+import com.kitfox.svg.SVGException;\r
+import com.kitfox.svg.SVGRoot;\r
+import com.kitfox.svg.SVGUniverse;\r
+import com.kitfox.svg.Text;\r
+import com.kitfox.svg.Tspan;\r
+import com.kitfox.svg.animation.AnimationElement;\r
+\r
+@RasterOutputWidget
+public class SVGNode extends G2DNode implements InitValueSupport, LoaderNode {
+\r
+       public static class SVGNodeAssignment {\r
+               public String elementId;\r
+               public String attributeNameOrId;\r
+               public String value;\r
+               public SVGNodeAssignment(String elementId, String attributeNameOrId, String value) {\r
+                       this.elementId = elementId;\r
+                       this.attributeNameOrId = attributeNameOrId;\r
+                       this.value = value;\r
+               }\r
+               @Override\r
+               public int hashCode() {\r
+                       final int prime = 31;\r
+                       int result = 1;\r
+                       result = prime * result + ((attributeNameOrId == null) ? 0 : attributeNameOrId.hashCode());\r
+                       result = prime * result + ((elementId == null) ? 0 : elementId.hashCode());\r
+                       result = prime * result + ((value == null) ? 0 : value.hashCode());\r
+                       return result;\r
+               }\r
+               @Override\r
+               public boolean equals(Object obj) {\r
+                       if (this == obj)\r
+                               return true;\r
+                       if (obj == null)\r
+                               return false;\r
+                       if (getClass() != obj.getClass())\r
+                               return false;\r
+                       SVGNodeAssignment other = (SVGNodeAssignment) obj;\r
+                       if (attributeNameOrId == null) {\r
+                               if (other.attributeNameOrId != null)\r
+                                       return false;\r
+                       } else if (!attributeNameOrId.equals(other.attributeNameOrId))\r
+                               return false;\r
+                       if (elementId == null) {\r
+                               if (other.elementId != null)\r
+                                       return false;\r
+                       } else if (!elementId.equals(other.elementId))\r
+                               return false;\r
+                       if (value == null) {\r
+                               if (other.value != null)\r
+                                       return false;\r
+                       } else if (!value.equals(other.value))\r
+                               return false;\r
+                       return true;\r
+               }\r
+       }\r
+       
+    private static final long serialVersionUID = 8508750881358776559L;
+
+    protected String          data             = null;\r
+    protected String          defaultData      = null;\r
+    protected Point           targetSize       = null;\r
+    protected Boolean         useMipMap        = true;\r
+    protected Rectangle2D     bounds           = null;\r
+    \r
+    protected List<SVGNodeAssignment> assignments = new ArrayList<SVGNodeAssignment>();\r
+\r
+    transient BufferedImage buffer    = null;\r
+    transient String documentCache    = null;\r
+    transient SVGDiagram diagramCache = null;\r
+    transient String dataHash         = null;\r
+\r
+    static transient Map<String, WeakReference<BufferedImage>> bufferCache = new HashMap<String, WeakReference<BufferedImage>>();\r
+\r
+    @Override\r
+    public void cleanup() {\r
+        cleanDiagramCache();\r
+    }\r
+\r
+    public void setAssignments(List<SVGNodeAssignment> ass) {\r
+       assignments.clear();\r
+       assignments.addAll(ass);\r
+    }\r
+    \r
+    public void cleanDiagramCache() {\r
+        SVGDiagram d = diagramCache;\r
+        if (d != null) {\r
+            diagramCache = null;\r
+            SVGUniverse univ = SVGCache.getSVGUniverse();\r
+            if (univ.decRefCountAndClear(d.getXMLBase()) == 0) {\r
+                // Cleared!\r
+                //System.out.println("cleared: " + d.getXMLBase());\r
+            }\r
+        }\r
+    }\r
+\r
+    static WeakHashMap<String, String> dataCache = new WeakHashMap<String, String>();\r
+\r
+    @PropertySetter("SVG")
+    @SyncField("data")
+    public void setData(String data) {\r
+        String cached = dataCache.get(data);\r
+        if (cached == null) {\r
+            cached = data;\r
+            dataCache.put(data, data);\r
+        }\r
+        this.data = cached;\r
+        this.defaultData = cached;\r
+    }
+
+    @SyncField("targetSize")
+    public void setTargetSize(Point p) {
+        this.targetSize = p; // FIXME: Point doesn't serialize correctly for some reason
+    }
+
+    @SyncField("targetSize")
+    public void setTargetSize(int x, int y) {
+        this.targetSize = new Point(x, y); // FIXME: Point doesn't serialize correctly for some reason
+    }
+
+    @SyncField("useMipMap")
+    public void useMipMap(Boolean use) {
+        this.useMipMap = use;
+    }\r
+\r
+    @PropertySetter("Bounds")
+    @SyncField("bounds")
+    public void setBounds(Rectangle2D bounds) {
+        this.bounds = bounds;
+    }
+
+    @Override\r
+    public Rectangle2D getBoundsInLocal() {
+        if (bounds == null)\r
+            parseSVG();\r
+        return bounds;
+    }
+
+    @Override
+    public void render(Graphics2D g2d) {
+        if (data == null)\r
+            return; // Not initialized\r
+\r
+        if (!data.equals(documentCache) || diagramCache == null || buffer == null)\r
+            initBuffer(g2d);
+\r
+        AffineTransform ot = null;\r
+        if (!transform.isIdentity()) {\r
+            ot = g2d.getTransform();\r
+            g2d.transform(transform);\r
+        }
+
+        if (buffer != null)\r
+            buffer.paint(g2d);\r
+\r
+        if (ot != null)\r
+            g2d.setTransform(ot);\r
+    }
+
+    protected String parseSVG() {
+        if (data == null)\r
+            return null;\r
+\r
+        try {\r
+               SVGUniverse univ = SVGCache.getSVGUniverse();\r
+               synchronized(univ) {
+                       // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
+                       byte[] dataBytes = data.getBytes("UTF-8");
+                       dataHash = digest(dataBytes, assignments);\r
+                       if (diagramCache != null)\r
+                               univ.decRefCount(diagramCache.getXMLBase());\r
+                       URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), dataHash);
+                       diagramCache = univ.getDiagram(uri, false);\r
+                       if (diagramCache != null) {\r
+                               if (diagramCache.getRoot() == null) {\r
+                                       diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA), false);\r
+                                       dataHash = "broken";\r
+                               } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {\r
+                                       diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA), false);\r
+                                       dataHash = "empty";\r
+                               } else {\r
+                                       for(SVGNodeAssignment ass : assignments) {\r
+                                               SVGElement e = diagramCache.getElement(ass.elementId);\r
+                                               if(e != null) {\r
+                                                       if("$text".equals(ass.attributeNameOrId)) {\r
+                                                               Tspan t = (Tspan)e;\r
+                                                               t.setText(ass.value);\r
+                                                               Text text = (Text)t.getParent();\r
+                                                               text.rebuild();\r
+                                                       } else {\r
+                                                               e.setAttribute(ass.attributeNameOrId, AnimationElement.AT_AUTO, ass.value);\r
+                                                       }\r
+                                               }\r
+                                       }\r
+                                       if(!assignments.isEmpty())\r
+                                               diagramCache.updateTime(0);\r
+                               }\r
+                       }
+                       documentCache = data;\r
+                       if (diagramCache != null) {\r
+                               setBounds((Rectangle2D) diagramCache.getRoot().getBoundingBox().clone());\r
+                               univ.incRefCount(diagramCache.getXMLBase());\r
+                       } else {\r
+                               setBounds(new Rectangle2D.Double());\r
+                       }\r
+               }\r
+        } catch (SVGException e) {\r
+            setBounds((Rectangle2D) diagramCache.getViewRect().clone());\r
+        } catch (IOException e) {\r
+            diagramCache = null;
+        }
+
+        return dataHash;
+    }
+\r
+    public static Rectangle2D getBounds(String data) {\r
+       return getBounds(data, null);\r
+    }\r
+\r
+    public static Rectangle2D getBounds(String data, List<SVGNodeAssignment> assignments) {\r
+        if (data == null) {\r
+            new Exception("null SVG data").printStackTrace();\r
+            return null;\r
+        }\r
+\r
+        SVGDiagram diagramCache = null;\r
+        try {\r
+            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
+            byte[] dataBytes = data.getBytes("UTF-8");\r
+            String digest = digest(dataBytes, assignments);\r
+\r
+            SVGUniverse univ = SVGCache.getSVGUniverse();\r
+            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.\r
+            synchronized (univ) {\r
+                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);\r
+                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);\r
+                diagramCache = univ.getDiagram(uri, false);\r
+                if (diagramCache != null) {\r
+                    if (diagramCache.getRoot() == null) {\r
+                        diagramCache = univ.getDiagram(univ.loadSVG(BROKEN_SVG_DATA));\r
+                    } else if (diagramCache.getRoot().getBoundingBox().isEmpty()) {\r
+                        diagramCache = univ.getDiagram(univ.loadSVG(EMPTY_SVG_DATA));\r
+                    }\r
+                }\r
+            }\r
+\r
+            Rectangle2D rect = null;\r
+            if (diagramCache != null) {\r
+                SVGRoot root = diagramCache.getRoot();\r
+                Rectangle2D bbox = root.getBoundingBox();\r
+                rect = (Rectangle2D) bbox.clone();\r
+            } else {\r
+                rect = new Rectangle2D.Double();\r
+            }\r
+            return rect;\r
+        } catch (SVGException e) {\r
+            return ((Rectangle2D) diagramCache.getViewRect().clone());\r
+        } catch(IOException e) {\r
+        }\r
+        return null;\r
+    }\r
+\r
+    public static Rectangle2D getRealBounds(String data) {\r
+       return getRealBounds(data, null);\r
+    }\r
+
+    public static Rectangle2D getRealBounds(String data, List<SVGNodeAssignment> assignments) {\r
+        if (data == null) {\r
+            new Exception("null SVG data").printStackTrace();\r
+            return null;\r
+        }\r
+\r
+        SVGDiagram diagramCache = null;\r
+        try {\r
+            // NOTE: hard-coded to assume all SVG data is encoded in UTF-8\r
+            byte[] dataBytes = data.getBytes("UTF-8");\r
+            String digest = digest(dataBytes, assignments);\r
+\r
+            SVGUniverse univ = SVGCache.getSVGUniverse();\r
+            // TODO: this completely removes any parallel processing from the SVG loading which would be nice to have.\r
+            synchronized (univ) {\r
+                //System.out.println(Thread.currentThread() + ": LOADING SVG: " + digest);\r
+                URI uri = univ.loadSVG(new ByteArrayInputStream(dataBytes), digest);\r
+                diagramCache = univ.getDiagram(uri, false);\r
+                if (diagramCache != null) {\r
+                       SVGRoot root = diagramCache.getRoot(); \r
+                    if (root == null) return new Rectangle2D.Double();\r
+                    return (Rectangle2D)root.getBoundingBox().clone();\r
+                }\r
+            }\r
+        } catch (SVGException e) {\r
+            return ((Rectangle2D) diagramCache.getViewRect().clone());\r
+        } catch(IOException e) {\r
+        }\r
+        return null;\r
+    }\r
+\r
+    protected void initBuffer(Graphics2D g2d) {\r
+       \r
+        if (!data.equals(documentCache) || diagramCache == null) {\r
+               dataHash = parseSVG();
+               if (diagramCache == null) {
+                       System.err.println("UNABLE TO PARSE SVG:\n" + data);
+                       return;
+               }\r
+        }
+
+        if (buffer != null) {
+            buffer = null;
+        }
+        diagramCache.setIgnoringClipHeuristic(true); // FIXME
+        if(bufferCache.containsKey(dataHash) && bufferCache.get(dataHash).get() != null) {
+            buffer = bufferCache.get(dataHash).get();
+        } else if(diagramCache.getViewRect().getWidth()==0 || diagramCache.getViewRect().getHeight()==0) {
+            buffer = null;
+        } else if(useMipMap) {\r
+               if(G2DUtils.isAccelerated(g2d)) {
+                buffer = new MipMapVRamBufferedImage(diagramCache, bounds, targetSize);
+            } else {
+                buffer = new MipMapBufferedImage(diagramCache, bounds, targetSize);
+            }
+            bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
+        } else {
+               if(G2DUtils.isAccelerated(g2d)) {
+                buffer = new VRamBufferedImage(diagramCache, bounds, targetSize);
+            } else {
+                buffer = new BufferedImage(diagramCache, bounds, targetSize);
+            }
+            bufferCache.put(dataHash, new WeakReference<BufferedImage>(buffer));
+        }
+    }
+\r
+    public void setProperty(String field, Object value) {\r
+        if("data".equals(field)) {\r
+//             System.out.println("SVGNode data -> " + value);\r
+            this.data = (String)value;\r
+        } else if ("z".equals(field)) {\r
+//             System.out.println("SVGNode z -> " + value);\r
+            setZIndex((Integer)value);\r
+        } else if ("position".equals(field)) {\r
+//             System.out.println("SVGNode position -> " + value);\r
+            Point point = (Point)value;\r
+            setTransform(AffineTransform.getTranslateInstance(point.x, point.y));\r
+//             setPosition(point.x, point.y);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void initValues() {\r
+        data = defaultData;\r
+        dataHash = null;\r
+        buffer =  null;\r
+    }\r
+\r
+    static WeakHashMap<String, String> digestCache = new WeakHashMap<String, String>();\r
+    \r
+    static String digest(byte[] dataBytes, List<SVGNodeAssignment> assignments) {\r
+        try {\r
+            MessageDigest md = MessageDigest.getInstance("MD5");\r
+            byte[] messageDigest = md.digest(dataBytes);\r
+            BigInteger number = new BigInteger(1, messageDigest);\r
+            String dataHash = number.toString(16) + (assignments != null ? assignments.hashCode() : 0);\r
+            String result = digestCache.get(dataHash);\r
+            if(result == null) {\r
+               result = dataHash;\r
+               digestCache.put(dataHash,dataHash);\r
+            }\r
+            return result;\r
+        } catch (NoSuchAlgorithmException e) {\r
+            // Shouldn't happen\r
+            throw new Error("MD5 digest must exist.");\r
+        }\r
+    }\r
+\r
+    static URL BROKEN_SVG_DATA = SVGNode.class.getResource("broken.svg");\r
+    static URL EMPTY_SVG_DATA = SVGNode.class.getResource("empty.svg");\r
+\r
+       @Override\r
+       public Function1<Object, Boolean> getPropertyFunction(String propertyName) {\r
+               return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);\r
+       }\r
+       \r
+       @Override\r
+       public <T> T getProperty(String propertyName) {\r
+               return null;\r
+       }\r
+\r
+       @Override\r
+       public void setPropertyCallback(Function2<String, Object, Boolean> callback) {\r
+       }\r
+\r
+       public void synchronizeDocument(String document) {\r
+               setData(document);\r
+       }\r
+\r
+       public void synchronizeTransform(double[] data) {\r
+               this.setTransform(new AffineTransform(data));\r
+       }\r
+       
+}