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.Iterator;
  6. import java.util.LinkedHashSet;
  7. import java.util.Set;

  8. import org.djutils.exceptions.Throw;
  9. import org.djutils.logger.CategoryLogger;
  10. import org.locationtech.jts.geom.Envelope;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  289.     }

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

  297.         /** The OTSShapes stored at this node. */
  298.         private Set<OTSShape> shapes = new LinkedHashSet<OTSShape>();

  299.         /** The bounding box of this QuadTreeNode. */
  300.         private final Rectangle2D boundingBox;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  703.     }

  704. }