--- /dev/null
+/*******************************************************************************\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
+
+}