How to serialize GeneralPath using GraphML

Tips & Tricks

Summary

This article and the corresponding sample code explain how to serialize a GeneralPath object with GraphML.
For a better user experience, please go to the integrated documentation viewer to read this article.

Description

java.awt.geom.GeneralPath as well as the newer java.awt.geom.Path2D subclasses Path2D.Float and Path2D.Double support serialization by implementing the java.io.Serializable interface. It is, of course, possible to leverage Serializable when storing data in GraphML, too. There is, however, a significant downside to this approach: Serializable generates binary data which then has to be Base64 encoded to be included in GraphML. Fortunately, for the above mentioned paths it is easily possible to generate human-readable data by iterating over path segments and querying for segment type and coordinates. The attached sample code demonstrates this approach by means of a simple GeneralPath based node realizer and corresponding GraphML serializer implementation.

package demo.io.graphml;

import demo.view.DemoBase;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import y.io.GraphMLIOHandler;
import y.io.graphml.graph2d.AbstractNodeRealizerSerializer;
import y.io.graphml.input.GraphMLParseContext;
import y.io.graphml.input.GraphMLParseException;
import y.io.graphml.output.GraphMLWriteContext;
import y.io.graphml.output.GraphMLWriteException;
import y.io.graphml.output.XmlWriter;
import y.view.Graph2D;
import y.view.LineType;
import y.view.NodeRealizer;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;

/**
 * Demonstrates how to write GraphML serialization for <code>GeneralPath</code>
 * segments.
 */
public class GeneralPathSerializationDemo extends DemoBase {
  protected void configureDefaultRealizers() {
    super.configureDefaultRealizers();

    final GeneralPath path = new GeneralPath();
    path.moveTo(0, 0);
    path.lineTo(10, 0);
    path.curveTo(15, 3, 15, 6, 10, 10);
    path.quadTo(5, 15, 0, 10);
    path.closePath();

    final Graph2D graph = view.getGraph2D();
    graph.setDefaultNodeRealizer(new GeneralPathNodeRealizer(path));
  }

  protected GraphMLIOHandler createGraphMLIOHandler() {
    final GraphMLIOHandler ioHandler = super.createGraphMLIOHandler();
    ioHandler.addNodeRealizerSerializer(new GeneralPathNodeRealizerSerializer());
    return ioHandler;
  }


  public static void main( String[] args ) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        initLnF();
        (new GeneralPathSerializationDemo()).start();
      }
    });
  }


  /**
   * A simple <code>NodeRealizer</code> that draws a general path.
   * This class is public to allow GraphML's default reflection based
   * instantiation mechanism to work. (The alternative would be to overwrite
   * {@link AbstractNodeRealizerSerializer#createRealizerInstance(org.w3c.dom.Node, y.io.graphml.input.GraphMLParseContext)}).
   */
  public static final class GeneralPathNodeRealizer extends NodeRealizer {
    private GeneralPath path;

    /**
     * Default constructor to allow the default reflection based instantiation
     * mechanism of GraphML to work.
     */
    public GeneralPathNodeRealizer() {
      this((GeneralPath) null);
    }

    public GeneralPathNodeRealizer( final GeneralPath path ) {
      this.path = path;
    }

    /**
     * Copy constructor to satisfy yFiles' node copying requirements.
     * @param prototype the protoptype realizer to be copied.
     */
    public GeneralPathNodeRealizer( final NodeRealizer prototype ) {
      super(prototype);
      if (prototype instanceof GeneralPathNodeRealizer) {
        path = ((GeneralPathNodeRealizer) prototype).path;
      } else {
        path = new GeneralPath();
      }
    }

    /**
     * Overwritten to ensure yFiles' node copying methods work properly.
     * @param nr the protoptype realizer to be copied.
     * @return a closest-match copy of the specified prototype realizer.
     */
    public NodeRealizer createCopy( final NodeRealizer nr ) {
      return new GeneralPathNodeRealizer(nr);
    }

    /**
     * Paints the realizer's general path.
     * @param g the graphics context to paint upon.
     */
    protected void paintNode( final Graphics2D g ) {
      if (isSelected()) {
        paintHotSpots(g);
      }

      if (path != null) {
        final Color lc = getLineColor();
        final LineType lt = getLineType();
        if (lc != null && lt != null) {
          final Color oldColor = g.getColor();
          final Stroke oldStroke = g.getStroke();
          final AffineTransform oldTransform = g.getTransform();

          g.setColor(lc);
          g.setStroke(lt);
          g.translate(getX(), getY());

          final Rectangle2D bnds = path.getBounds2D();
          final AffineTransform transform = new AffineTransform();
          transform.scale(
                  bnds.getWidth() > 0 ? getWidth() / bnds.getWidth() : 1,
                  bnds.getHeight() > 0 ? getHeight() / bnds.getHeight() : 1);
          transform.translate(-bnds.getX(), -bnds.getY());
          g.draw(path.createTransformedShape(transform));

          g.setTransform(oldTransform);
          g.setStroke(oldStroke);
          g.setColor(oldColor);
        }
      }

      paintText(g);
    }
  }

  /**
   * GraphML serializer for <code>GeneralPathNodeRealizer</code> instances.
   */
  public static final class GeneralPathNodeRealizerSerializer extends AbstractNodeRealizerSerializer {
    public String getName() {
      return "GeneralPathNode";
    }

    public String getNamespaceURI() {
      return "demo.io.graphml.GeneralPathNodeRealizer";
    }

    public Class getRealizerClass() {
      return GeneralPathNodeRealizer.class;
    }


    /**
     * Overwritten to write <code>GeneralPath</code> segment information.
     */
    public void write(
            NodeRealizer realizer,
            XmlWriter writer,
            GraphMLWriteContext context
    ) throws GraphMLWriteException {
      super.write(realizer, writer, context);

      final String nsuri = getNamespaceURI();
      writer.writeStartElement("GeneralPath", nsuri);

      final GeneralPath path = ((GeneralPathNodeRealizer) realizer).path;
      if (path != null) {
        final float[] buffer = new float[6];
        for (PathIterator it = path.getPathIterator(new AffineTransform()); !it.isDone(); it.next()) {
          switch (it.currentSegment(buffer)) {
            case PathIterator.SEG_MOVETO:
              writer.writeStartElement("MoveTo", nsuri);
              writer.writeAttribute("x", buffer[0]);
              writer.writeAttribute("y", buffer[1]);
              writer.writeEndElement();
              break;

            case PathIterator.SEG_LINETO:
              writer.writeStartElement("LineTo", nsuri);
              writer.writeAttribute("x", buffer[0]);
              writer.writeAttribute("y", buffer[1]);
              writer.writeEndElement();
              break;

            case PathIterator.SEG_QUADTO:
              writer.writeStartElement("QuadTo", nsuri);
              writer.writeAttribute("cx", buffer[0]);
              writer.writeAttribute("cy", buffer[1]);
              writer.writeAttribute("x", buffer[2]);
              writer.writeAttribute("y", buffer[3]);
              writer.writeEndElement();
              break;

            case PathIterator.SEG_CUBICTO:
              writer.writeStartElement("CubicTo", nsuri);
              writer.writeAttribute("cx1", buffer[0]);
              writer.writeAttribute("cy1", buffer[1]);
              writer.writeAttribute("cx2", buffer[2]);
              writer.writeAttribute("cy2", buffer[3]);
              writer.writeAttribute("x", buffer[4]);
              writer.writeAttribute("y", buffer[5]);
              writer.writeEndElement();
              break;

            case PathIterator.SEG_CLOSE:
              writer.writeStartElement("Close", nsuri);
              writer.writeEndElement();
              break;
          }
        }
      }

      writer.writeEndElement();
    }

    /**
     * Overwritten to parse <code>GeneralPath</code> segment information.
     */
    public void parse(
            NodeRealizer realizer,
            Node domNode,
            GraphMLParseContext context
    ) throws GraphMLParseException {
      super.parse(realizer, domNode, context);

      for (Node child = domNode.getFirstChild(); child != null; child = child.getNextSibling()) {
        if (child.getNodeType() == Node.ELEMENT_NODE &&
            "GeneralPath".equals(child.getLocalName())) {
          parseGeneralPath(realizer, child, context);
          break;
        }
      }
    }

    private void parseGeneralPath(
            NodeRealizer realizer,
            Node domNode,
            GraphMLParseContext context
    ) {
      final GeneralPath path = new GeneralPath();
      boolean foundInstructions = false;
      for (Node child = domNode.getFirstChild(); child != null; child = child.getNextSibling()) {
        if (child.getNodeType() == Node.ELEMENT_NODE) {
          final Element element = (Element) child;

          final String name = child.getLocalName();
          if ("MoveTo".equals(name)) {
            final float x = Float.parseFloat(element.getAttribute("x"));
            final float y = Float.parseFloat(element.getAttribute("y"));
            path.moveTo(x, y);
            foundInstructions = true;
          } else if ("LineTo".equals(name)) {
            final float x = Float.parseFloat(element.getAttribute("x"));
            final float y = Float.parseFloat(element.getAttribute("y"));
            path.lineTo(x, y);
            foundInstructions = true;
          } else if ("QuadTo".equals(name)) {
            final float cx = Float.parseFloat(element.getAttribute("cx"));
            final float cy = Float.parseFloat(element.getAttribute("cy"));
            final float x = Float.parseFloat(element.getAttribute("x"));
            final float y = Float.parseFloat(element.getAttribute("y"));
            path.quadTo(cx, cy, x, y);
            foundInstructions = true;
          } else if ("CubicTo".equals(name)) {
            final float cx1 = Float.parseFloat(element.getAttribute("cx1"));
            final float cy1 = Float.parseFloat(element.getAttribute("cy1"));
            final float cx2 = Float.parseFloat(element.getAttribute("cx2"));
            final float cy2 = Float.parseFloat(element.getAttribute("cy2"));
            final float x = Float.parseFloat(element.getAttribute("x"));
            final float y = Float.parseFloat(element.getAttribute("y"));
            path.curveTo(cx1, cy1, cx2, cy2, x, y);
            foundInstructions = true;
          } else if ("Close".equals(name)) {
            path.closePath();
            foundInstructions = true;
          }
        }
      }

      if (foundInstructions) {
        ((GeneralPathNodeRealizer) realizer).path = path;
      }
    }
  }
}

To compile and run the above sample code, copy the attached file into src/demo/io/graphml of your yFiles installation directory.

Resources

Categories this article belongs to:
yFiles for Java > yFiles Viewer > Displaying and Editing Graphs > Writing Customized Realizers
yFiles for Java > yFiles Viewer > GraphML Extension Package > Using the yFiles GraphML Extension Package
Applies to:
yFiles for Java 2: 2.7, 2.8, 2.9, 2.10, 2.11, 2.12, 2.13, 2.14, 2.15, 2.16, 2.17, 2.18
Keywords:
GraphML - GeneralPath - serializer