OTS2DSet.java

  1. package org.opentrafficsim.core.geometry;

  2. import java.awt.geom.Rectangle2D;
  3. import java.io.Serializable;
  4. import java.util.Collection;
  5. import java.util.HashSet;
  6. import java.util.Iterator;
  7. import java.util.Set;

  8. import com.vividsolutions.jts.geom.Envelope;

  9. import nl.tudelft.simulation.language.Throw;

  10. /**
  11.  * Set of OTSShape objects and provides methods for fast selection of those objects that intersect an OTSShape. <br>
  12.  * An OTS2DSet internally stores the OTSShapes in a quad tree. At time of construction the minimum cell size is defined. Node
  13.  * expansion is never performed on nodes that are smaller than this limit. <br>
  14.  * Each node (even the non-leaf nodes) store a set of OTSShape. Non-leaf nodes locally store those shapes that completely cover
  15.  * the rectangular area of the node. Such shapes are <b>not</b> also stored in leaf nodes below that node. OTSShapes that
  16.  * partially cover a non-leaf node are stored in each of the leaf nodes below that node that those OTSShapes (partially) cover.
  17.  * Leaf nodes that cannot be expanded (because they are too small) also store all OTSShapes that partially cover the area of the
  18.  * node. <br>
  19.  * If removal of an OTSShape objects results in a leaf becoming empty, that leaf is removed from its parent (which may then
  20.  * itself become empty and removed in turn).
  21.  * <p>
  22.  * Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  23.  * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
  24.  * <p>
  25.  * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jun 20, 2016 <br>
  26.  * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
  27.  * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
  28.  * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
  29.  */
  30. public class OTS2DSet implements Set<OTSShape>, Serializable
  31. {
  32.     /** */
  33.     private static final long serialVersionUID = 20170400L;

  34.     /** Set of all shapes used for iterators, etc. */
  35.     private final Set<OTSShape> allShapes = new HashSet<OTSShape>();

  36.     /** How fine will this quad tree divide. This one is copied to each sub-node which is somewhat inefficient. */
  37.     private final double minimumCellSize;

  38.     /** Spatial storage for the OTSShapes. */
  39.     private QuadTreeNode quadTree;

  40.     /**
  41.      * Construct an empty OTS2DSet for a rectangular region. Objects that do not intersect this region will never be stored in
  42.      * this OTS2DSet. (Trying to add such an OTSShape is <b>not</b> an error; the <code>add</code> method will return false,
  43.      * indicating that the set has not been modified.)
  44.      * @param boundingBox Rectangle2D; the region
  45.      * @param minimumCellSize double; resolution of the underlying quad tree
  46.      * @throws OTSGeometryException when the bounding box covers no surface
  47.      */
  48.     public OTS2DSet(final Rectangle2D boundingBox, final double minimumCellSize) throws OTSGeometryException
  49.     {
  50.         Throw.when(null == boundingBox, NullPointerException.class, "The boundingBox may not be null");
  51.         Throw.when(boundingBox.getWidth() <= 0 || boundingBox.getHeight() <= 0, OTSGeometryException.class,
  52.                 "The boundingBox must have nonzero surface (got %s", boundingBox);
  53.         Throw.when(minimumCellSize <= 0, OTSGeometryException.class, "The minimumCellSize must be > 0 (got %f)",
  54.                 minimumCellSize);
  55.         this.quadTree = new QuadTreeNode(boundingBox);
  56.         this.minimumCellSize = minimumCellSize;
  57.     }

  58.     /** {@inheritDoc} */
  59.     @Override
  60.     public final int size()
  61.     {
  62.         return this.allShapes.size();
  63.     }

  64.     /** {@inheritDoc} */
  65.     @Override
  66.     public final boolean isEmpty()
  67.     {
  68.         return this.allShapes.isEmpty();
  69.     }

  70.     /** {@inheritDoc} */
  71.     @Override
  72.     public final boolean contains(final Object o)
  73.     {
  74.         return this.allShapes.contains(o);
  75.     }

  76.     /** {@inheritDoc} */
  77.     @Override
  78.     public final Iterator<OTSShape> iterator()
  79.     {
  80.         return new QuadTreeIterator();
  81.     }

  82.     /** {@inheritDoc} */
  83.     @Override
  84.     public final Object[] toArray()
  85.     {
  86.         return this.allShapes.toArray();
  87.     }

  88.     /** {@inheritDoc} */
  89.     @Override
  90.     public final <T> T[] toArray(final T[] a)
  91.     {
  92.         return this.allShapes.toArray(a);
  93.     }

  94.     /** {@inheritDoc} */
  95.     @Override
  96.     public final boolean add(final OTSShape e)
  97.     {
  98.         if (!this.quadTree.intersects(e))
  99.         {
  100.             return false;
  101.         }
  102.         if (this.allShapes.contains(e))
  103.         {
  104.             return false;
  105.         }
  106.         if (!this.quadTree.add(e))
  107.         {
  108.             System.err.println("add: ERROR object could not be added to the quad tree");
  109.         }
  110.         return this.allShapes.add(e);
  111.     }

  112.     /** {@inheritDoc} */
  113.     @Override
  114.     public final boolean remove(final Object o)
  115.     {
  116.         if (!this.allShapes.remove(o))
  117.         {
  118.             return false;
  119.         }
  120.         if (!this.quadTree.remove((OTSShape) o))
  121.         {
  122.             System.err.println("remove: ERROR object could not be removed from the quad tree");
  123.         }
  124.         return true;
  125.     }

  126.     /** {@inheritDoc} */
  127.     @Override
  128.     public final boolean containsAll(final Collection<?> c)
  129.     {
  130.         for (Object o : c)
  131.         {
  132.             if (!contains(o))
  133.             {
  134.                 return false;
  135.             }
  136.         }
  137.         return true;
  138.     }

  139.     /** {@inheritDoc} */
  140.     @Override
  141.     public final boolean addAll(final Collection<? extends OTSShape> c)
  142.     {
  143.         boolean result = false;
  144.         for (OTSShape s : c)
  145.         {
  146.             if (add(s))
  147.             {
  148.                 result = true;
  149.             }
  150.         }
  151.         return result;
  152.     }

  153.     /** {@inheritDoc} */
  154.     @Override
  155.     public final boolean retainAll(final Collection<?> c)
  156.     {
  157.         boolean result = false;
  158.         for (Iterator<OTSShape> it = iterator(); it.hasNext();)
  159.         {
  160.             OTSShape shape = it.next();
  161.             if (!c.contains(shape))
  162.             {
  163.                 it.remove();
  164.                 result = true;
  165.             }
  166.         }
  167.         return result;
  168.     }

  169.     /** {@inheritDoc} */
  170.     @Override
  171.     public final boolean removeAll(final Collection<?> c)
  172.     {
  173.         boolean result = false;
  174.         for (Iterator<OTSShape> it = iterator(); it.hasNext();)
  175.         {
  176.             OTSShape shape = it.next();
  177.             if (c.contains(shape))
  178.             {
  179.                 it.remove();
  180.                 result = true;
  181.             }
  182.         }
  183.         return result;
  184.     }

  185.     /** {@inheritDoc} */
  186.     @Override
  187.     public final void clear()
  188.     {
  189.         this.quadTree.clear();
  190.         this.allShapes.clear();
  191.     }

  192.     /**
  193.      * Return the set of all shapes in this OTS2DSet that intersect the given rectangle.
  194.      * @param rectangle Rectangle2D; the rectangle
  195.      * @return Set&lt;OTSShape&gt;; the shapes that intersect the rectangle
  196.      */
  197.     public final Set<OTSShape> intersectingShapes(final Rectangle2D rectangle)
  198.     {
  199.         return this.quadTree.intersectingShapes(rectangle);
  200.     }

  201.     /**
  202.      * Recursively print this OTS2DSet.
  203.      * @param recursionDepth int; maximum depth to recurse
  204.      * @return String
  205.      */
  206.     final String toString(final int recursionDepth)
  207.     {
  208.         return "OTS2DSet [contains " + size() + (1 == this.allShapes.size() ? "shape" : "shapes") + ", minimumCellSize="
  209.                 + this.minimumCellSize + ", quadTree=" + this.quadTree.toString(recursionDepth) + "]";
  210.     }

  211.     /** {@inheritDoc} */
  212.     @Override
  213.     public final String toString()
  214.     {
  215.         return toString(0);
  216.     }

  217.     /**
  218.      * Return all OTSShapes in this OTS2DSet that intersect a given OTSShape.
  219.      * @param shape OTSShape; the given OTSShape
  220.      * @return Set&lt;OTSShape&gt;; all OTSShapes in this OTS2DSet that intersect <code>shape</code>
  221.      */
  222.     public final Set<OTSShape> intersectingShapes(final OTSShape shape)
  223.     {
  224.         Envelope envelope = shape.getEnvelope();
  225.         Set<OTSShape> result = intersectingShapes(
  226.                 new Rectangle2D.Double(envelope.getMinX(), envelope.getMinY(), envelope.getWidth(), envelope.getHeight()));
  227.         for (Iterator<OTSShape> it = result.iterator(); it.hasNext();)
  228.         {
  229.             if (!it.next().intersects(shape))
  230.             {
  231.                 it.remove();
  232.             }
  233.         }
  234.         return result;
  235.     }

  236.     /**
  237.      * Return an ASCII art rendering of this OTS2DSet.
  238.      * @param recursionDepth int; maximum recursion depth
  239.      * @return String; a somewhat human readable rendering of this OTS2DSet
  240.      */
  241.     public final String toStringGraphic(final int recursionDepth)
  242.     {
  243.         return this.quadTree.toStringGraphic(recursionDepth);
  244.     }

  245.     /**
  246.      * Iterator for quad tree. Shall iterate over the local set of shapes and the (up to four) non-null leave nodes.
  247.      */
  248.     class QuadTreeIterator implements Iterator<OTSShape>, Serializable
  249.     {
  250.         /** */
  251.         private static final long serialVersionUID = 20170400L;

  252.         /** Underlying iterator that traverses the allShapes Set. */
  253.         @SuppressWarnings("synthetic-access")
  254.         private final Iterator<OTSShape> theIterator = OTS2DSet.this.allShapes.iterator();

  255.         /** Remember the last returned result so we can remove it when requested. */
  256.         private OTSShape lastResult = null;

  257.         /** {@inheritDoc} */
  258.         @Override
  259.         public final boolean hasNext()
  260.         {
  261.             return this.theIterator.hasNext();
  262.         }

  263.         /** {@inheritDoc} */
  264.         @Override
  265.         public final OTSShape next()
  266.         {
  267.             this.lastResult = this.theIterator.next();
  268.             return this.lastResult;
  269.         }

  270.         /** {@inheritDoc} */
  271.         @SuppressWarnings("synthetic-access")
  272.         @Override
  273.         public final void remove()
  274.         {
  275.             this.theIterator.remove();
  276.             if (!OTS2DSet.this.quadTree.remove(this.lastResult))
  277.             {
  278.                 System.err.println("iterator.remove: ERROR: could not remove " + this.lastResult + " from the quad tree");
  279.             }
  280.         }

  281.         /** {@inheritDoc} */
  282.         @Override
  283.         public String toString()
  284.         {
  285.             return "QuadTreeIterator [theIterator=" + this.theIterator + ", lastResult=" + this.lastResult + "]";
  286.         }

  287.     }

  288.     /**
  289.      * Spatial-aware storage for a set of OTSShape objects.
  290.      */
  291.     class QuadTreeNode implements Serializable
  292.     {
  293.         /** */
  294.         private static final long serialVersionUID = 20170400L;

  295.         /** The OTSShapes stored at this node. */
  296.         private Set<OTSShape> shapes = new HashSet<OTSShape>();

  297.         /** The bounding box of this QuadTreeNode. */
  298.         private final Rectangle2D boundingBox;

  299.         /** The bounding box of this QuadTreeNode as an OTSShape. */
  300.         private final OTSShape boundingShape;

  301.         /**
  302.          * The four leaves of this node in the quad tree. An empty sub tree may be represented by null. If this field is
  303.          * initialized to null; this node may not expand by adding sub-nodes.
  304.          */
  305.         private final QuadTreeNode[] leaves;

  306.         /**
  307.          * Construct a new QuadTreeNode.
  308.          * @param boundingBox Rectangle2D; the bounding box of the area of the new QuadTreeNode
  309.          */
  310.         @SuppressWarnings("synthetic-access")
  311.         QuadTreeNode(final Rectangle2D boundingBox)
  312.         {
  313.             this.boundingBox = boundingBox;
  314.             this.boundingShape = rectangleShape(boundingBox);
  315.             this.leaves = boundingBox.getWidth() > OTS2DSet.this.minimumCellSize
  316.                     || boundingBox.getHeight() > OTS2DSet.this.minimumCellSize ? new QuadTreeNode[4] : null;
  317.         }

  318.         /**
  319.          * Return a Set containing all OTSShapes in this QuadTreeNode that intersect a rectangular area.
  320.          * @param rectangle Rectangle2D; the area
  321.          * @return Set&lt;OTSShape&gt;; the set
  322.          */
  323.         public Set<OTSShape> intersectingShapes(final Rectangle2D rectangle)
  324.         {
  325.             Set<OTSShape> result = new HashSet<OTSShape>();
  326.             if (!this.boundingBox.intersects(rectangle))
  327.             {
  328.                 return result;
  329.             }
  330.             if (null == this.leaves)
  331.             {
  332.                 return result;
  333.             }
  334.             for (QuadTreeNode leaf : this.leaves)
  335.             {
  336.                 if (null != leaf && leaf.intersects(rectangle))
  337.                 {
  338.                     result.addAll(leaf.intersectingShapes(rectangle));
  339.                 }
  340.             }
  341.             for (OTSShape shape : this.shapes)
  342.             {
  343.                 OTSShape rectangleShape = rectangleShape(rectangle);
  344.                 if (rectangleShape.intersects(shape))
  345.                 {
  346.                     result.add(shape);
  347.                 }
  348.             }
  349.             return result;
  350.         }

  351.         /**
  352.          * Test if this QuadTreeNode intersects a rectangular area.
  353.          * @param rectangle Rectangle2D; the rectangular area
  354.          * @return boolean; true if the rectangular area intersects this QuadTreeNode; false otherwise
  355.          */
  356.         private boolean intersects(final Rectangle2D rectangle)
  357.         {
  358.             return this.boundingBox.intersects(rectangle);
  359.         }

  360.         /**
  361.          * Remove all OTSShapes from this QuadTreeNode and cut off all leaves.
  362.          */
  363.         public void clear()
  364.         {
  365.             this.shapes.clear();
  366.             for (int index = 0; index < this.leaves.length; index++)
  367.             {
  368.                 this.leaves[index] = null;
  369.             }
  370.         }

  371.         /**
  372.          * Remove an OTSShape from this QuadTreeNode.
  373.          * @param shape OTSShape; the shape that must be removed.
  374.          * @return boolean; true if this node (or a sub-node) was altered; false otherwise
  375.          */
  376.         public boolean remove(final OTSShape shape)
  377.         {
  378.             if (!this.boundingShape.intersects(shape))
  379.             {
  380.                 return false;
  381.             }
  382.             for (OTSShape s : this.shapes)
  383.             {
  384.                 if (shape.equals(s))
  385.                 {
  386.                     this.shapes.remove(shape);
  387.                     return true;
  388.                 }
  389.             }
  390.             boolean result = false;
  391.             for (int index = 0; index < this.leaves.length; index++)
  392.             {
  393.                 QuadTreeNode qtn = this.leaves[index];
  394.                 if (null != qtn)
  395.                 {
  396.                     if (qtn.remove(shape))
  397.                     {
  398.                         result = true;
  399.                         if (qtn.isEmpty())
  400.                         {
  401.                             this.leaves[index] = null; // Cut off empty leaf node
  402.                         }
  403.                     }
  404.                 }
  405.             }
  406.             return result;
  407.         }

  408.         /**
  409.          * Check if this QuadTreeNode is empty.
  410.          * @return boolean; true if this QuadTreeNode is empty
  411.          */
  412.         private boolean isEmpty()
  413.         {
  414.             if (!this.shapes.isEmpty())
  415.             {
  416.                 return false;
  417.             }
  418.             if (null == this.leaves)
  419.             {
  420.                 return true;
  421.             }
  422.             for (QuadTreeNode qtn : this.leaves)
  423.             {
  424.                 if (null != qtn)
  425.                 {
  426.                     return false;
  427.                 }
  428.             }
  429.             return true;
  430.         }

  431.         /**
  432.          * Test if the area of this QuadTree intersects an OTSShape.
  433.          * @param shape OTSShape; the shape
  434.          * @return boolean; true if the area of this QuadTree intersects the shape; false otherwise
  435.          */
  436.         public boolean intersects(final OTSShape shape)
  437.         {
  438.             return this.boundingShape.intersects(shape);
  439.         }

  440.         /**
  441.          * Construct a OTSShape from a Rectangle2D.
  442.          * @param rectangle Rectangle3D; the rectangle
  443.          * @return OTSShape; a new OTSShape
  444.          */
  445.         private OTSShape rectangleShape(final Rectangle2D rectangle)
  446.         {
  447.             double left = rectangle.getMinX();
  448.             double bottom = rectangle.getMinY();
  449.             double right = rectangle.getMaxX();
  450.             double top = rectangle.getMaxY();
  451.             try
  452.             {
  453.                 return new OTSShape(new OTSPoint3D(left, bottom), new OTSPoint3D(right, bottom), new OTSPoint3D(right, top),
  454.                         new OTSPoint3D(left, top));
  455.             }
  456.             catch (OTSGeometryException exception)
  457.             {
  458.                 exception.printStackTrace();
  459.                 return null;
  460.             }
  461.         }

  462.         /**
  463.          * Add an OTSShape to this QuadTreeNode.
  464.          * @param shape OTSShape; the shape
  465.          * @return boolean; true if this QuadTreeNode changed as a result of this operation
  466.          */
  467.         public final boolean add(final OTSShape shape)
  468.         {
  469.             if (!this.boundingShape.intersects(shape))
  470.             {
  471.                 return false;
  472.             }
  473.             if ((null == this.leaves) || shape.contains(this.boundingBox))
  474.             {
  475.                 // shape belongs in the set of shapes of this node.
  476.                 return this.shapes.add(shape);
  477.             }
  478.             // This node may have leaves and shape does not entirely contain this node. Add shape to all applicable leaves.
  479.             boolean result = false;
  480.             for (int index = 0; index < this.leaves.length; index++)
  481.             {
  482.                 if (null == this.leaves[index])
  483.                 {
  484.                     double subWidth = this.boundingBox.getWidth() / 2;
  485.                     double subHeight = this.boundingBox.getHeight() / 2;
  486.                     if (0 == subWidth)
  487.                     {
  488.                         // loss of precision; degenerate into a binary tree
  489.                         subWidth = this.boundingBox.getWidth();
  490.                     }
  491.                     if (0 == subHeight)
  492.                     {
  493.                         // loss of precision; degenerate into a binary tree
  494.                         subHeight = this.boundingBox.getHeight();
  495.                     }
  496.                     double left = this.boundingBox.getMinX();
  497.                     if (0 != index / 2)
  498.                     {
  499.                         left += subWidth;
  500.                     }
  501.                     double bottom = this.boundingBox.getMinY();
  502.                     if (0 != index % 2)
  503.                     {
  504.                         bottom += subHeight;
  505.                     }
  506.                     Rectangle2D subBox = new Rectangle2D.Double(left, bottom, subWidth, subHeight);
  507.                     if (rectangleShape(subBox).intersects(shape))
  508.                     {
  509.                         // Expand this node by adding a sub node.
  510.                         this.leaves[index] = new QuadTreeNode(subBox);
  511.                         if (this.leaves[index].add(shape))
  512.                         {
  513.                             result = true;
  514.                         }
  515.                         else
  516.                         {
  517.                             throw new Error("Cannot happen: new QuadTreeNode refused to add shape that intersects it");
  518.                         }
  519.                     }
  520.                 }
  521.                 else
  522.                 {
  523.                     // Leaf node already exists. Let the leaf determine if shape should be stored (somewhere) in it.
  524.                     if (this.leaves[index].add(shape))
  525.                     {
  526.                         result = true;
  527.                     }
  528.                 }
  529.             }
  530.             return result;
  531.         }

  532.         /**
  533.          * Helper function for toString.
  534.          * @param recursionDepth int; maximum number of levels to print recursively
  535.          * @param index int; index in leaves
  536.          * @return String
  537.          */
  538.         private String printLeaf(final int recursionDepth, final int index)
  539.         {
  540.             QuadTreeNode leaf = this.leaves[index];
  541.             if (null == leaf)
  542.             {
  543.                 return "null";
  544.             }
  545.             if (recursionDepth > 0)
  546.             {
  547.                 return leaf.toString(recursionDepth - 1);
  548.             }
  549.             int leafSize = leaf.shapes.size();
  550.             return leafSize + " shape" + (1 == leafSize ? "" : "s");
  551.         }

  552.         /**
  553.          * Recursively print this QuadTreeNode.
  554.          * @param recursionDepth int; maximum depth to recurse
  555.          * @return String
  556.          */
  557.         final String toString(final int recursionDepth)
  558.         {
  559.             return "QuadTreeNode [" + this.shapes.size() + ", bounds=[LB: " + this.boundingBox.getMinX() + ","
  560.                     + this.boundingBox.getMinY() + ", RT: " + this.boundingBox.getMaxX() + "," + this.boundingBox.getMaxY()
  561.                     + "], " + subNodes(recursionDepth) + ", local " + this.shapes.size()
  562.                     + (1 == this.shapes.size() ? " shape" : " shapes") + "]";
  563.         }

  564.         /**
  565.          * Print the leaves of this QuadTreeNode.
  566.          * @param recursionDepth int; maximum depth to recurse
  567.          * @return String
  568.          */
  569.         private String subNodes(final int recursionDepth)
  570.         {
  571.             if (null == this.leaves)
  572.             {
  573.                 return "cannot have leaves";
  574.             }
  575.             return "leaves=[LB: " + printLeaf(recursionDepth, 0) + ", RB: " + printLeaf(recursionDepth, 1) + ", LT: "
  576.                     + printLeaf(recursionDepth, 2) + ", RT: " + printLeaf(recursionDepth, 3) + "]";
  577.         }

  578.         /** {@inheritDoc} */
  579.         @Override
  580.         public final String toString()
  581.         {
  582.             return toString(0);
  583.         }

  584.         /**
  585.          * Return concatenation of a number of copies of a string.
  586.          * @param count int; number of copies to concatenate
  587.          * @param string String; the string to repeat
  588.          * @return String
  589.          */
  590.         private String repeat(final int count, final String string)
  591.         {
  592.             StringBuilder result = new StringBuilder();
  593.             for (int i = 0; i < count; i++)
  594.             {
  595.                 result.append(string);
  596.             }
  597.             return result.toString();
  598.         }

  599.         /** Graphic to draw a vertical line. */
  600.         private static final String VLINE = "|";

  601.         /** Graphic to draw a horizontal line. */
  602.         private static final String HLINE = "-";

  603.         /** Graphic to draw a space. */
  604.         private static final String SPACE = " ";

  605.         /** Number of digits to print. */
  606.         private static final int NUMBERSIZE = 6;

  607.         /**
  608.          * Similar to toStringGraphic, but with QuadTreeNode argument which can be null. <br>
  609.          * This code is <b>not</b> optimized for performance; the repeated use of String.split is probably expensive.
  610.          * @param qtn QuadTreeNode; the QuadTreeNode to render. Can be null.
  611.          * @param recursionDepth int; levels to recurse
  612.          * @return String
  613.          */
  614.         private String subStringGraphic(final QuadTreeNode qtn, final int recursionDepth)
  615.         {
  616.             StringBuffer result = new StringBuffer();
  617.             if (0 == recursionDepth)
  618.             {
  619.                 if (null == qtn)
  620.                 {
  621.                     result.append(repeat(NUMBERSIZE, SPACE));
  622.                 }
  623.                 else
  624.                 {
  625.                     String numberBuf = String.format("%d", size());
  626.                     int spare = NUMBERSIZE - numberBuf.length();
  627.                     int filled = 0;
  628.                     while (filled < spare / 2)
  629.                     {
  630.                         result.append(SPACE);
  631.                         filled++;
  632.                     }
  633.                     result.append(numberBuf);
  634.                     while (filled < spare)
  635.                     {
  636.                         result.append(SPACE);
  637.                         filled++;
  638.                     }
  639.                     result.append("\n");
  640.                     return result.toString();
  641.                 }
  642.             }
  643.             else
  644.             {
  645.                 String[] left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[1], recursionDepth - 1)
  646.                         .split("\\n");
  647.                 String[] right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[3], recursionDepth - 1)
  648.                         .split("\\n");
  649.                 String horizontalLine = null;
  650.                 for (int i = 0; i < left.length; i++)
  651.                 {
  652.                     if (0 == i)
  653.                     {
  654.                         StringBuilder line = new StringBuilder();
  655.                         int width = left[0].length() + 1 + right[0].length();
  656.                         if (null == qtn)
  657.                         {
  658.                             line.append(repeat(width, SPACE));
  659.                         }
  660.                         else
  661.                         {
  662.                             String numberBuf = String.format("%d", qtn.shapes.size());
  663.                             int spare = width - numberBuf.length();
  664.                             line.append(repeat(spare / 2, HLINE));
  665.                             line.append(numberBuf);
  666.                             line.append(repeat(spare - spare / 2, HLINE));
  667.                         }
  668.                         horizontalLine = line.toString();
  669.                     }
  670.                     result.append(left[i]);
  671.                     result.append(null == qtn ? SPACE : VLINE);
  672.                     result.append(right[i]);
  673.                     result.append("\n");
  674.                 }
  675.                 result.append(horizontalLine);
  676.                 result.append("\n");
  677.                 left = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[0], recursionDepth - 1)
  678.                         .split("\\n");
  679.                 right = subStringGraphic(null == qtn || null == qtn.leaves ? null : qtn.leaves[2], recursionDepth - 1)
  680.                         .split("\\n");
  681.                 for (int i = 0; i < left.length; i++)
  682.                 {
  683.                     result.append(left[i]);
  684.                     result.append(null == qtn ? SPACE : VLINE);
  685.                     result.append(right[i]);
  686.                     result.append("\n");
  687.                 }
  688.                 result.append("\n");
  689.             }
  690.             return result.toString();
  691.         }

  692.         /**
  693.          * Return a String depicting this QuadTreeNode.
  694.          * @param recursionDepth int; levels to recurse
  695.          * @return String
  696.          */
  697.         public final String toStringGraphic(final int recursionDepth)
  698.         {
  699.             return subStringGraphic(this, recursionDepth);
  700.         }

  701.     }

  702. }